assemble
Skill2 starsChanged 6 months agoMITView on GitHub
Tool concierge — scan installed Claude Code skills/plugins/agents, recommend a per-stage workflow, and let the user pick which tool to use at each stage. Use when the user types `/assemble <task description>` or `/assemble resume` or `/assemble list`.
3 other projects use a version of this.
Adopted by
Most recently taken up by
- Mharbulous/probanda-devtheir own version, first seen Nov 8, 2026
- lucasacoutinho/ddstheir own version, first seen Oct 13, 2026
- mrdainami/omni-video-directortheir own version, first seen Jul 9, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- mrdainami/omni-video-director★ 16same name
- lucasacoutinho/dds★ 2same name
- Mharbulous/probanda-dev★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
11 rules. 0 are common, 4 are distinctive, and 7 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
When invoked, follow this 6-phase loop.
- Very specific: line 17
All Python snippets must use from server import … — never reference submodules (server.progress, server.inventory, etc.) directly.
- Distinctive: line 39
After §1 Boot extracts the task token, additionally check for sub-command keywords.
- Very specific: line 49
If no sub-command keyword matches, proceed to §1 Boot's existing resume/list/task-string routing.
- Very specific: line 74
Current approach: every skill self-describes via its own SKILL.md frontmatter (description + first 500 chars of body).
- Distinctive: line 109
Everything else is left alone: If a tie or zero-score set means nothing qualified, tell the user once:
- Very specific: line 113
Use the localized version via t("notices.unclassified_hint", n=<n>) (the bash snippet below prints it for you).
- Distinctive: line 156
Never bundle 3+ stages. Selection quality beats perceived speed.
- Very specific: line 178
When the tool finishes: mark_stage('<rid>','<stage>','done',tool_used='<name>')
- Very specific: line 195
If <run_id> given → use it directly.
- Very specific: line 213
After the last stage hits done (or the user picks done):
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
/assemble — Tool Concierge
When invoked, follow this 6-phase loop. Every step is mandatory unless explicitly marked optional.
0. Prerequisites
~/.claude/skills/assemble/server/{state_store,inventory,classify,sequence,menu,progress,i18n}.pymust exist~/.claude/skills/assemble/server/__init__.pyexposes the full facade — always import fromserver, never from submodules- Python 3 (stdlib only)
Facade convention (important)
All Python snippets must use from server import … — never reference submodules (server.progress, server.inventory, etc.) directly. This removes guesswork about where each function lives and keeps the next LLM from tripping over ImportError when a module gets refactored.
Exported symbols:
scan, apply_classification,
unclassified_names, unclassified_entries,
parse_skill_frontmatter, load_stages, load_stage_roles,
create_run, load_progress, mark_stage, list_runs, find_resumable,
build_stage_options, tools_for_stage, contextual_helpers,
build_classify_prompt, parse_classify_response,
build_sequence_prompt, parse_sequence_response,unclassified_entries() returns a flat list of unclassified items — each entry is {'kind': 'skill'|'agent', 'name': ..., 'path': ...}. It is not bucketed by kind; the kind field lives on each row. unclassified_names() is kept for back-compat as skills-only (returns a list of names).
Language
User-facing text is picked via ASSEMBLE_LOCALE (default en; ko ships in-tree). When you address the user directly, match their conversation language. The menu labels and stage descriptions rendered by build_stage_options / load_stages already respect the locale — don't translate those manually.
Sub-commands
After §1 Boot extracts the task token, additionally check for sub-command keywords. If matched, route to the corresponding flow doc and STOP §2~§7 default concierge flow:
| Keyword (first arg token) | Flow doc | Spike |
eject | ~/.claude/skills/assemble/docs/eject-flow.md | XII |
(future) roles | (deferred) | — |
(future) import | (deferred) | V5 |
Behavior: if first token after /assemble is a sub-command keyword, read the flow doc and follow it line-by-line. Default concierge flow (§2 inventory refresh through §7 list) is bypassed.
If no sub-command keyword matches, proceed to §1 Boot's existing resume/list/task-string routing.
1. Boot
Strip the trailing assemble/leading /assemble token to extract the task.
Routing:
/assemble resume [<run_id>]→ §5 Resume/assemble list→ §7 List- otherwise → §2
2. Inventory refresh
cd ~/.claude/skills/assemble && python3 -c "
from server import scan, unclassified_names
inv = scan()
print(f'INV_OK skills={len(inv[\"skills\"])} agents={len(inv[\"agents\"])} unclassified={len(unclassified_names())}')
"Classification policy (pre_mapping retired, 2026-04-21 rework):
Earlier builds shipped config/pre_mapping.json — a name→stage table. That approach had a fundamental flaw: you can't pre-map tools you don't know are installed. A fresh user's Mac dumped most skills into unclassified because the table only covered the original author's toolchain.
Current approach: every skill self-describes via its own SKILL.md frontmatter (description + first 500 chars of body). scan() reads that and classifies.
- Heuristic (first pass) —
_classify_heuristicininventory.pyrequires ≥2 stage-keyword hits in the description to mark a skillheuristic-classified. Safety/meta skills also get a role hint socontextual_helperspicks them up. - Heuristic miss →
unclassified— the skill isn't visible in any stage menu yet. Task-relevance inline (LLM, second pass) — score unclassified entries against the
/assemble <task>string by token overlap on name/description. For the top 2 only, build the classify prompt, reply with JSON yourself (you are the LLM — no external call), then validate and apply.Canonical invocation (works for both skills and agents — no
kindargument; the facade keys onname):from pathlib import Path from server import ( parse_skill_frontmatter, build_classify_prompt, parse_classify_response, apply_classification, ) fm = parse_skill_frontmatter(Path(entry['path'])) # entry from unclassified_entries() prompt = build_classify_prompt( name=fm['name'], description=fm.get('description', ''), body_excerpt=fm.get('body_excerpt', ''), ) # Reply to `prompt` yourself with the JSON format it specifies. parsed = parse_classify_response(your_json_reply) apply_classification( name=fm['name'], mappings=parsed['mappings'], confidence=parsed['confidence'], reasoning=parsed['reasoning'], )confidence == high→ apply directly as shown aboveconfidence in {medium, low}→ oneAskUserQuestionto confirm before applying
Everything else is left alone. If a tie or zero-score set means nothing qualified, tell the user once:
"{n} skill(s) are unclassified and won't appear in this run's menus. To bulk-classify, run:\n
~/.claude/skills/assemble/bin/classify-inventory"Use the localized version via
t("notices.unclassified_hint", n=<n>)(the bash snippet below prints it for you).- User override wins — anything stored as
llm-classifiedviaapply_classification()is preserved across scans, and heuristic cannot overwrite it.
Outcome: classification is driven entirely by what's installed in front of you. No global table to maintain.
3. Sequence recommendation
cd ~/.claude/skills/assemble && python3 -c "
from server import build_sequence_prompt
import sys
print(build_sequence_prompt(task=sys.argv[1]))
" "<task>"Reply with a JSON sequence yourself. Validate with parse_sequence_response. Then ask via AskUserQuestion:
question: "Does this sequence look right? <stage1> → <stage2> → ..."
options: ["approve", "edit", "cancel"]- approve → continue
- edit → follow-up
AskUserQuestionto add/remove stages (multi-select, options = 8 sequential stages) - cancel → STOP
Then create the run:
python3 -c "
from server import create_run
print(create_run(task='<task>', sequence=<approved sequence>))
"4. Stage loop
Bundling policy (revised after 2026-04-21 review). An earlier version batched up to 4 stages into a single AskUserQuestion. Problem: stages aren't really independent (plan shapes design; execute failure drives debug), so pre-selected later-stage picks got stale. Current rules are conservative:
4a. When to bundle
- Default: surface exactly one stage at a time. User answers → tool runs → mark done → advance → open next menu.
- Preload exception: bundle at most 2 stages into one
AskUserQuestion(2 questions) iff those stages are clearly independent. Independence check:- Stage A's outcome doesn't affect Stage B's tool choice (e.g.
verify+shipis often independent;plan+designis not) - Both stages' tool candidates are already
heuristic-classifiedorllm-classified(nounclassifiedmixed in)
- Stage A's outcome doesn't affect Stage B's tool choice (e.g.
- Never bundle 3+ stages. Selection quality beats perceived speed.
4b. Build the menu
python3 -c "
import json
from server import build_stage_options
for s in ['<s1>','<s2>','<s3>']:
print(s, json.dumps(build_stage_options(s), ensure_ascii=False))
"Each stage becomes one AskUserQuestion question:
header: stage name (lowercase, ≤12 chars)options: up to 3 tools + whichever ofskip / manual / donefits the context. If there are more than 4, paginate: show top-3 recommended + one "more options" entry that reveals the rest on the next turn.
4c. Handle each response
As soon as a stage answer arrives:
- Invoke the picked tool (Skill/Agent, or invoke the helper directly)
mark_stage('<rid>','<stage>','in_progress',tool_used='<name>')- When the tool finishes:
mark_stage('<rid>','<stage>','done',tool_used='<name>')
Note: done alone auto-backfills started_at if missing (2026-04-21 guard). Still, calling in_progress before done is the canonical pattern.
4d. Menu option semantics
- tool → invoke the skill/agent;
donewhen the tool returns. - helper → a meta/safety assist. Invoke it, mark only the helper, then re-open the same stage menu.
- ask (
물어보기inko) → reply with a paragraph recommending one of the listed tools given the task context, then re-render the menu. - skip →
status='skipped', advance. - manual (
직접inko) →status='manual', advance. - back →
mark_stage(rid, cur_stage, status='back'). This moves the cursor only; it does not overwrite the stage's recorded status (pending/in_progress/done stay intact). Earlier versions persistedstatus='back', which broke wrap-up counts andfind_resumable. - done → break out of the stage loop.
5. Resume
When /assemble resume:
- If
<run_id>given → use it directly. - Else:
find_resumable(). Empty → say so and STOP. 1 → use it. Many →AskUserQuestionto pick. - Load progress, then announce using
progress.resume_announce(see below for the localized template). Example output (en):"Resuming: <task>. Current stage: <s> (<idx+1>/<total>)". - Re-enter §4 from
current_stage_index.
python3 -c "
from server import load_progress
from server.i18n import t
p = load_progress('<rid>')
idx = p['current_stage_index']
print(t('progress.resume_announce', task=p['task'],
stage=p['stages'][idx]['stage'], idx=idx+1, total=len(p['stages'])))
"6. Wrap-up
After the last stage hits done (or the user picks done):
python3 -c "
from server import load_progress
from server.i18n import t
from datetime import datetime
p = load_progress('<rid>')
created = datetime.fromisoformat(p['created_at'])
updated = datetime.fromisoformat(p['updated_at'])
duration = int((updated - created).total_seconds())
done = sum(1 for s in p['stages'] if s['status'] == 'done')
skipped = sum(1 for s in p['stages'] if s['status'] == 'skipped')
manual = sum(1 for s in p['stages'] if s['status'] == 'manual')
tools = sorted({s['tool_used'] for s in p['stages'] if s['tool_used']})
m, s = divmod(duration, 60)
tools_out = ', '.join(tools) or t('progress.no_tools')
print(t('progress.wrap_up', rid=p['run_id'], m=m, s=s,
done=done, skipped=skipped, manual=manual, tools=tools_out))
print(t('progress.progress_path',
path=f'~/.claude/channels/assemble/runs/{p[\"run_id\"]}/progress.json'))
"(Optional) If the learn skill is installed, offer a retro.
7. List (past runs)
When /assemble list:
python3 -c "
from server import list_runs, load_progress
for rid in list_runs()[-10:]:
p = load_progress(rid) or {}
done = sum(1 for s in p.get('stages',[]) if s.get('status')=='done')
total = len(p.get('stages',[]))
status = 'done' if done == total else f'{done}/{total}'
print(f'{rid} [{status}] {p.get(\"task\",\"\")[:60]}')
"Show the output verbatim, then mention they can resume any line with /assemble resume <run_id>.
8. Troubleshooting
ImportError: cannot import 'X' from 'server.Y'— don't reach into submodules. Usefrom server import X. See §0.started_at: NULL— pre-2026-04-21 build. Currentmark_stageguards against this.- Classifications keep disappearing — the scan merge preserves
llm-classifiedentries;heuristic-classifiedrecomputes on every scan when the description changes (by design). - Lots of unclassified on a fresh install — expected after pre_mapping retirement. The heuristic catches descriptions with enough stage keywords; the rest land in
unclassifieduntil the user runsbin/classify-inventory --applyor the inline LLM pass covers them. - I've seen a
pre-mappedsource — legacy label. Current sources areheuristic-classified,llm-classified,unclassified. inventory.json.bad-<ts>appeared — cache was corrupt, scan auto-quarantined + rebuilt.llm-classifiedentries survive unless the cache was destroyed mid-write; check withbin/classify-inventory --status.- User skill was overwritten by a plugin skill — pre-2026-04-21 precedence bug. Current
enumerate_skill_pathsreturns user → plugin, so first-wins dedupe always keeps user. - Concurrent
classify-inventory+/assemblelost a classification — pre-2026-04-21 race. Currentapply_classificationandscanshareupdate_json_locked. illegal transition: stage 'X' already 'done'— trying to drivedoneback intoin_progress. Usebackto move the cursor; don't callmark_stageagain.
9. Internals: lazy-load policy
- Each option returned by
build_stage_options()is{label, kind, description, tool_path}. Tool-kind options additionally carrybundled: bool(V4 Phase A —Truefor entries under~/.claude/skills/assemble/bundled/; bundled labels are prefixed with★and sorted before user/plugin tools). Description is capped at 80 chars, except bundled-only fallback options append a localized hint that may extend slightly past that cap. The skill body is never loaded until the user picks and the appropriate invocation path runs (see next bullet for slash-skill vs bundled distinction). - ★-prefixed bundled options are NOT slash-skill registry entries. When the user picks one, load it by
Readon<tool_path>/SKILL.mdand follow the body in-place. Do NOT callSkill(<name>)— that resolves slash-skills only and will returnUnknown skill: <name>for bundled bundles. Thetool_pathfield on each option is the absolute directory path to the bundle's SKILL.md (verified byenumerate_skill_paths). Example:Skilltool is for/loop//assemble/etc.;Readis for★ plan-pack. - The cache at
~/.claude/channels/assemble/inventory.jsonstores a 500-charbody_excerptper skill, used for classification only — it never reaches the user menu. - Audit: confirmed 2026-04-21. Skill bodies don't leak into context.
10. Coexisting with other plugins
Other installed plugins (Vercel, gstack, context7, etc.) register their own SessionStart and file-pattern hooks. These hooks fire independently of /assemble — they're part of Claude Code's runtime, not something this skill triggers or controls.
Policy:
- Ignore the hook output. Session-start preambles and file-pattern notices from other plugins aren't errors and don't affect the stage loop. Continue with §1–§7 as if they weren't there.
- Those plugins' skills are still first-class tool candidates.
scan()inventories everySKILL.mdon disk regardless of which plugin owns it. After classification, they show up under the relevant stages (e.g.vercel-cliundership,vercel-agentunderreview/debug,vercel-storageunderexecute). That's a feature —/assembleis a concierge over whatever the user has installed. - Don't suggest disabling other plugins as a workaround. The user installed them for reasons unrelated to
/assemble.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

