agent-assembly is a build system for agent sessions. It assembles behavior context, hooks, and skills into a target project directory, then launches the CLI.
This SPEC describes mechanisms, not instances. It defines how assemble_modules works, not which modules a specific project uses. Project-specific configurations belong in their launcher scripts and launcher test files, not here.
Every launcher picks from the same pipeline steps as needed:
assemble_modules → assemble_hooks → assemble_skills → ensure_gitignore → launch
Not every launcher uses every step. The launcher itself is the configuration — it calls only the steps it needs.
| Step | What it does | Generated artifact | CLI pick-up mechanism |
|---|---|---|---|
assemble_modules |
Concatenates selected module files into a single behavior context file | TARGET/CLAUDE.local.md |
CLI auto-loads CLAUDE.local.md from working directory |
assemble_hooks |
Builds hook JSON from selected hook scripts, merges into settings file | TARGET/.claude/settings.local.json |
CLI auto-loads settings.local.json, merges with settings.json |
assemble_skills |
Symlinks selected skills into target project | TARGET/.claude/skills/<name> (symlink) |
CLI discovers skills from .claude/skills/ |
ensure_gitignore |
Ensures all generated artifacts are in .gitignore |
Appends to TARGET/.gitignore |
— |
launch |
cd TARGET && exec CLI [args...] |
— | Caller passes --add-dir, flags, etc. |
A launcher script is just: source lib.sh, choose what to assemble, call these functions in order.
- Pre: TARGET directory exists; each mod is a filename in
modules/ - Post:
TARGET/CLAUDE.local.mdcontains concatenation of all found modules, separated by---, with{{ASSEMBLY_DIR}}placeholders replaced by actual path - Missing module: warning to stderr, skip, continue
- Bad target: return 1, error to stderr (never
exit— must not kill the caller) - Stdout:
Assembled N modules → <path>
- Pre: each hook is a filename in
hooks/ - Post:
TARGET/.claude/settings.local.jsonis overwritten with hooks JSON. No merge — previous content is discarded. - Missing hook: warning to stderr, skip
- No valid hooks: writes
{"hooks": {}} - Hook event type: currently all hooks are injected as
UserPromptSubmit. When other event types are needed, extend the interface.
- Pre: each skill is a directory name in
.claude/skills/ - Post: symlinks created at
TARGET/.claude/skills/<name>pointing to source - Missing skill: warning to stderr, skip
- Post:
TARGET/.gitignorecontains entries forCLAUDE.local.md,.claude/settings.local.json,.claude/skills/ - Idempotent: no duplicate entries on repeated calls
- If inside tmux: opens a new tmux window named after TARGET's basename, runs
cd TARGET && CLI [args...]in it - If not inside tmux:
cd TARGET && exec CLI [args...](replaces current process) - Caller's responsibility:
--add-dir,--dangerously-skip-permissions, and all other flags are passed by the launcher, not decided by this function
Everything the agent needs to know (behavior rules, profile, goals) lives here as markdown files. This is the single source of truth — no other copy of this content should exist.
Rules:
- Each module is self-contained: understandable without reading other modules
- No duplication across modules: one concept lives in one file only
- Granularity = one independently toggleable capability
- Modules may use
{{ASSEMBLY_DIR}}placeholder, replaced at assembly time with actual path - Project-specific modules (workflows, directory maps) belong in the project, not here
Naming: <slug>.md, lowercase, hyphens. No enforced prefix taxonomy — profile-* is descriptive, not a classification rule.
Shell scripts that the CLI invokes at specific lifecycle events. This directory provides the mechanism to inject them into target projects.
Rules:
- Each hook is a standalone executable script
- Hooks receive input from CLI on stdin (JSON) and output JSON to stdout, or exit silently
- Hook scripts must handle their own path resolution (they may be invoked via absolute path from any working directory)
Naming: <descriptive-slug>.sh.
launchers/lib.sh implements the 5 pipeline functions (assemble_modules, assemble_hooks, assemble_skills, ensure_gitignore, launch). All other files in this directory are project launcher scripts that source lib.sh and call these functions.
Rules:
lib.shis the shared library — all pipeline functions live here- Every launcher must
source "$(dirname "$0")/lib.sh"as first action - Every launcher follows the pipeline: assemble_modules → assemble_hooks → assemble_skills → ensure_gitignore → launch
- The launcher itself is the configuration — no separate config file needed
Naming: <project>-<platform>-<cli>.sh for launchers. Platform defaults to mac. Examples: myapp-mac-claude.sh, docs-mac-claude.sh. lib.sh is reserved for the shared library.
Skill directories that can be symlinked into target projects via assemble_skills.
Rules:
- Each skill is a directory containing at minimum
SKILL.md - Skills are discovered by CLI from
.claude/skills/in the working directory
Naming: <skill-name>/ directory with SKILL.md inside.
These files are produced by the pipeline and written into the target project directory. They must never be hand-edited — they are overwritten on every launch.
| Artifact | Generated by | Must be gitignored |
|---|---|---|
CLAUDE.local.md |
assemble_modules |
Yes |
.claude/settings.local.json |
assemble_hooks |
Yes |
.claude/skills/* (symlinks) |
assemble_skills |
Yes |
ensure_gitignore enforces the gitignore entries. The target project's CLAUDE.md and .claude/settings.json are not generated — they belong to the project.
| ID | Statement |
|---|---|
| INV-01 | Every launcher sources lib.sh and calls pipeline steps as needed |
| INV-02 | Generated artifacts are overwritten on every launch — never hand-edited |
| INV-03 | assemble_hooks overwrites settings.local.json entirely — no merge with prior content |
| INV-04 | settings.json (tracked) must NOT contain hooks — hooks are managed exclusively by launchers |
| INV-05 | Generated artifacts are gitignored in the target project |
| INV-06 | Hook commands in generated settings.local.json use absolute paths |
| INV-07 | launch() is generic — all flags and --add-dir are the launcher's decision |
| INV-08 | {{ASSEMBLY_DIR}} in module content is replaced at assembly time |
- Create
modules/<slug>.md— self-contained, no duplication with existing modules - Add the filename to the desired launcher(s)'
assemble_modulescall - Run
bash tests/run.sh
- Create
hooks/<slug>.sh(receives JSON on stdin, outputs JSON to stdout), add the filename to your launcher'sassemble_hookscall. - Run
bash tests/run.sh
- Create
launchers/<environment>-<platform>-<cli>.sh source "$(dirname "$0")/lib.sh"- Set
CLIandTARGET - Call the pipeline functions, selecting modules/hooks/skills as needed
- Run
bash tests/run.sh
- Create
.claude/skills/<name>/SKILL.md - Add to desired launcher(s)'
assemble_skillscall - Run
bash tests/run.sh
- Make the change
- Update contracts in this SPEC if the interface changed
- Run
bash tests/run.sh— all must pass
bash tests/run.shTests are split into two levels:
tests/run.sh— Core tests (lib.sh functions, hooks, data consistency, documentation). Environment-independent.tests/test-<name>.sh— Per-launcher assembly tests. Each launcher can have its own test file verifying its module/hook/skill combination.
run.sh auto-discovers and runs all tests/test-*.sh files. Adding a new launcher? Create tests/test-<name>.sh.
Unit tests verify pipeline functions in isolation. End-to-end testing verifies the full launcher → CLI → agent experience in a real session.
When to do it: after any change to modules, hooks, or launcher configuration. Unit tests alone cannot catch issues like hook double-firing, missing context in agent responses, or incorrect module content.
How:
- Launch via the launcher:
bash launchers/<launcher>.sh. This opens a new tmux window with the CLI running. - Send a message to the tmux window. CLI input requires two Enters — first Enter is a newline, second Enter submits:
tmux send-keys -t "<window>" "<message>" Enter && sleep 1 && tmux send-keys -t "<window>" Enter
- Wait for the agent to respond, then read output with
tmux capture-pane -t "<window>" -p -S -<lines>. - Verify: profile recognized, hook content received (Action Check / Memory alarm), agent behavior matches module instructions.