Skip to content

The Plugin Mechanism

Audience: Tier 2/3 (team adopter, framework contributor).

Path convention. <harness-dir>/ below = the harness's runtime dir (.claude / .codex / .kiro / .aidlc / .cursor); plugins/<name>/ = the authored plugin source; dist/plugins/<name>/<harness>/ = the ignored local host-plugin projection emitted by bun scripts/package.ts.

This chapter is the canonical reference for the AIDLC plugin system: an optional, owned, versioned set of contributions - new stages, agents, scopes, sensors, doctor checks, and additive modifications to existing core stages - authored once as a harness-neutral tree and emitted as a real host plugin for each harness. A plugin never edits core/; a disabled plugin's files stay installed, but its stages and scopes are filtered out of the graph and its merged contributions are stripped from core stages, so the workflow runs as bare core. The system generalizes the one proven edit-free seam (phase rules composed additively) to every surface, and delivers it through each host's own plugin machinery rather than a bespoke installer. Cross-link to Stage Definition (the stage frontmatter a plugin authors, including plugin/number/when), Engine and Skill System (the graph the composer feeds and the orchestrator routes off), Artifact Vocabulary (the namespacing rule), and the authoring walkthrough Authoring a Plugin.


1. What a plugin is

A plugin is a directory (and a git repository) with a declarative manifest and core-shaped subtrees. It can:

  • add new stages (numbered by the engine on first compile - a plugin claims no display-number range), agents, scopes, and sensors (method/rules for the space memory seed are deferred: the validator rejects a memory contribution); and
  • modify existing core stages additively via the contribution seam (§6) — enriching what a stage produces, consumes, checks, and instructs, without editing it.

First-party plugins (shipped by the AIDLC team) and third-party plugins (anyone else) are mechanically identical — same structure, same seams, same composer. They differ in provenance (whose repository the plugin lives in and who reviewed it) and in how the host lists them: AI-DLC's own show as aidlc-<name> in the one shared aidlc-plugins catalogue, anyone else's under their author's names in a catalogue of their own (section 9). plugins/test-pro/ is the reference fixture.

The design principles the mechanism holds to:

  • Strict-additive, never override. Contributions only add. Set-union over structural surfaces is commutative, which is what makes independent authors safe; a genuine conflict is a compose error with plugin attribution, never a silent last-writer-wins.
  • Core immutability. No plugin edits a core/ file; everything a plugin ships lives in the plugin's own tree.
  • Reversible & inert when off. Disabling a plugin recomposes to exactly the install without it.
  • Slug is identity. Stages are identified by slug everywhere that matters (edges, jumps, resolution). number is display/ordering only, so an inserted plugin stage never renumbers or destabilizes core.

2. Why install-time, delivered as a host plugin

A recurring design question was whether a plugin is a build-time artifact (pre-composed centrally, copied in) or an install-time one (composed on the consumer's machine over their chosen set). The mechanism is install-time, for two structural reasons the wider ecosystem (npm, Cargo, Helm, VS Code, Nix) has already converged on:

  • Combinatorial explosion. N plugins yield up to 2ⁿ enabled subsets; a central build cannot pre-compose every combination. The only artifact worth pre-building is bare core (the empty set), identical for everyone.
  • Late resolution. The correct plugin set — and how two plugins' contributions to the same stage merge — is only knowable on the install that chose them.

The delivery vehicle is the host's own plugin system, not a bespoke AIDLC installer. Every harness the framework targets already ships a manifest-first, git-distributed plugin model (Claude Code .claude-plugin/, Codex .codex-plugin/); an AIDLC plugin is one of those. This is the hybrid: real host plugins where a store exists (Claude, Codex, opencode, and Copilot project as "store" layouts), a folder-drop for the one that has none (Kiro). The consequences:

  • We run no distribution infrastructure. Customers host their own plugin repos (git + semver tags + a marketplace.json). One marketplace entry lists a plugin for a mixed fleet.
  • Trust is host-native. Org restrictions use the host's managed allowlist (Claude strictKnownMarketplaces, unoverridable by users; Codex hash-pinned trust). AIDLC builds no trust layer.
  • The composer runs on install, triggered by a host hook. No pre-built per-combination tree; the SessionStart hook composes the chosen set locally.

Security note - Kiro's folder-drop has no install-time trust gate. Claude and Codex mediate a plugin through their own trust prompts (managed marketplace / hash-pinned approval) before its hooks can run. Kiro has no plugin store, so the folder-drop path copies executable plugin files with no equivalent gate: dropping the tree is the trust decision. The Kiro IDE projection includes a v1 SessionStart registration that runs aidlc engine plugin sync, or the Bun launcher at hooks/aidlc-plugin-compose.ts where aidlc is not installed; the Kiro CLI projection leaves composition to the command you run explicitly. Treat either Kiro plugin drop like git clone && run: only install plugins from a source you would run code from, review the diff the drop introduces, and pin the plugin repo to a reviewed tag rather than tracking a moving branch. The composer itself is additive and never edits core/, but it executes with the user's process privileges.

The contribution seam (§6) is why this matters: it is structurally VS Code's contributes + Cargo's additive feature-union — the best-composing model in the field — and it is available to every plugin, first- and third-party alike, with no gatekeeping.

3. Plugin structure and manifest

A plugin's tree mirrors core/'s shape, so the packager can project it into every harness — authored once, harness-neutral:

The tree mirrors core/'s full shape as the designed surface; ✅ marks the subtrees the packager projects today, ⏳ marks designed-but-not-yet-projected ones (§7):

plugins/<name>/
  .aidlc-plugin/plugin.json              # the manifest
  stages/<phase>/<slug>.md               # ✅ NEW stages (slug identity; number is display-only)
  sensors/aidlc-<id>.md                  # ✅ NEW sensor manifests
  tools/<id>.ts                          # ✅ sensor scripts (so a sensor can run)
  tools/<plugin>-doctor.ts               # ✅ optional /aidlc --doctor checks
  contributions/<phase>/<slug>.md        # ✅ ADDITIVE modifications to core stages (§6)
  contributions/agents/<agent>.md        # ✅ PROSE fragments into core personas (§6)
  agents/<plugin>-<role>-agent.md        # ✅ NEW agents (stem == frontmatter name)
  scopes/<plugin>-<name>.md              # ✅ NEW scopes (stem == frontmatter name)
  knowledge/<agent-slug>/…               # ✅ per-agent METHODOLOGY knowledge
  tests/                                 # the plugin's own content validation (integration tier)
  memory/{org,team,project}.md           # ⏳ method/rule additions → default-space seed (§7)
  memory/phases/<phase>.md               # ⏳

Core scope files use the aidlc- filename prefix; plugin scope files replace it with the plugin prefix (<plugin>-<name>.md), and the filename stem equals the frontmatter name. Plugin agent files follow the same rule: <plugin>-<role>-agent.md, with frontmatter name equal to the stem.

.aidlc-plugin/plugin.json is a declarative manifest. Its top level mirrors the common host plugin-manifest shape (so a marketplace or host tooling can list/version/trust it); AIDLC-specific config is isolated in a nested aidlc block:

{
  "name": "test-pro",                 // == dir name; "core", "aidlc", and "aidlc-*" are reserved; kebab-case
  "version": "0.1.0",                 // semver
  "description": "…",
  "author": { "name": "AWS AIDLC" },
  "dependencies": ["core", "compliance@^1.2.0"],  // core plus at most one base plugin (§8)
  "aidlc": {
    // "marketplace": "acme-aidlc-plugins",  // a plugin someone else builds: its catalogue (section 9)
    "contributes": {                  // which subtrees this plugin ships
      "stages": "stages/", "agents": "agents/", "scopes": "scopes/",
      "sensors": "sensors/", "knowledge": "knowledge/",
      "tools": "tools/", "overlays": "contributions/"
    }
  }
}

Create a deterministic minimal authored plugin repository:

bun <tools-dir>/aidlc-plugin-create.ts <name> [targetDir]

CREATE refuses non-empty targets and emits a manifest plus one namespaced example stage, scope, agent, and tests README. The result is immediately valid; hooks/compose.ts is absent by design because BUILD injects the bundled hook.

Validate an authored plugin repository before building it:

bun <tools-dir>/aidlc-plugin-validate.ts <plugin-root> [--with <base-root>]

The shipped validator runs offline without an AIDLC project or framework checkout. It checks the manifest, stage schema, scope and agent identity, plugin-local artifact collisions, accidental test/fixture payloads under tools/, and any vendored hooks/compose.ts against the template bundled with the tool. A stage agent only the plugin's base could supply is a warning unless --with names the author's checkout of that base; in a plugin that declares no base, a stage or agent that is neither core's nor the plugin's own is an error naming dependencies. Top-level aidlc plugin validate|build routes delegate to these same implementations; create|test routes remain deferred to RFC #723 §2e.

Build one host projection from the same external repository:

bun <tools-dir>/aidlc-plugin-build.ts <plugin-root> <harness> [outDir]

The builder validates before writing and defaults to <plugin-root>/dist/<harness>/. The checkout packager and shipped builder call the same emitter. Harness manifests remain the source of truth: packaging emits their projection records to tools/data/plugin-targets.json, which the standalone builder consumes offline.

Test composition without mutating the selected install:

bun <tools-dir>/aidlc-plugin-test.ts <plugin-root> \
  --install <project-root> [--harness <name>] [--with <base-root>]

The tool builds a projection, copies only the manifest-derived install roots into a disposable candidate, runs the real compose hook twice, and requires a drop-free first pass, a clean post-compose graph containing the plugin stages and scopes, and a byte-stable second pass. Shared harness leaves require --harness. --with composes the base's checkout into the candidate first. --dist is not supported; the tool refuses it.

Contribution paths are plugin-relative and may not escape the plugin root. The top level is lenient (unknown keys preserved, for forward-compat and cross-tool tolerance); the aidlc block is strict (unknown keys rejected, to catch authoring typos). Projection still discovers content by directory convention (stages/, sensors/, tools/, contributions/, scopes/, agents/, knowledge/), so each declared value must equal its canonical directory; configurable routing is deferred. memory declarations are rejected until that subtree can be projected. Stage numbers are display-only, so a plugin claims no number range in the manifest. overlays is the manifest name for canonical contributions/, consumed by the merge rather than copied.

4. Composition model

The composer runs once over bare core + {chosen plugins} and writes the effective install. The same composer runs regardless of how it is triggered:

Host Trigger Trust
Claude SessionStart hook (fires eagerly on session spawn) managed allowlist (strictKnownMarketplaces)
Codex SessionStart hook (fires lazily on first interaction) one-time trust prompt, content-hash-pinned
Kiro CLI aidlc engine plugin sync when the binary is on PATH, or manual bun <plugin>/hooks/compose.ts after the folder-drop n/a - folder-drop distribution
Kiro IDE SessionStart hook registered by the folder-dropped projection (.kiro/hooks/aidlc-<plugin>-compose.json, running the launcher in the plugin's folder .kiro/aidlc-plugins/<name>/) n/a - folder-drop distribution

The steps (identical regardless of trigger):

  1. Discover and validate the host-provided installed roots, manifest identities/versions, and project selection, and put the bases first: a plugin composes after the one base plugin it builds on (§8).
  2. Copy new primitives — each plugin's stages/agents/scopes/knowledge/sensors/tools subtrees into the corresponding harness roots, substituting the {{HARNESS_DIR}} token to the harness's actual dir; memory remains deferred (§7).
  3. Merge contributions — every active contribution to a stage is folded into the target stage's source (§6): structural surfaces set-unioned, prose fragments spliced at their anchors.
  4. Compile — aidlc-graph compile regenerates stage-graph.json + scope-grid.json; the orchestrator routes entirely off those, so a plugin stage runs the moment it is composed — no prose or skill edit needed.

Because composition is one N-way merge (not a sequence of independent overlays), two plugins that both contribute to the same stage are genuinely merged — structural additions set-union, prose fragments order deterministically — rather than one silently overwriting the other. The runtime stays read-only with respect to composition: all merging happens at compose time, never per session. The merge edits stage source (not the compiled JSON), so it is durable across any later aidlc-graph compile (e.g. the rebuild-stage-graph hook) and idempotent — re-running on every SessionStart composes nothing new.

Re-composing a newer version of the same plugin replaces the files that plugin installed when they are unchanged since it installed them. The proof is the hash-proven record tools/data/plugin-owned-<key>.json, written by aidlc engine plugin sync and by the compose hook alike. A file the person changed is never overwritten: the compose drops log names it and the step (move the change elsewhere, remove the file, re-run compose). A file the record does not cover (core, another plugin, a copy composed before the record existed, or a local edit) is kept and reported the same way, unless it differs from the plugin's copy only in the knowledge preflight paragraph the plugin builder writes (an agent 2.11.0 reshaped at install and never recorded): that is the plugin's own older copy, and compose replaces and records it. A prose fragment the new version no longer ships at its old anchor and order is removed from the stage and from the contribution sidecar, so a moved fragment appears once.

An engine reinstall is different from a graph compile: copying a fresh dist/<harness>/ overwrites the compiled graph and core stage sources, so plugin-owned files and sidecars may remain while graph entries and contribution merges disappear. Re-run /aidlc plugin sync after every engine upgrade; hosts with a compose hook also repair the surface on the next session start. The doctor's Composed plugin surface check detects both missing plugin graph entries and stale structural or prose contributions, and sync reads the same check: a plugin whose recorded addition is gone (a structural value, a fragment's markers, or a fragment nested inside another of the same plugin's) composes again. A fragment whose markers are in place but whose text was changed is the person's edit. When that is the plugin's only gap, sync leaves it and doctor keeps naming it; when another of the plugin's additions is gone, sync composes the whole plugin again, which rewrites its fragments with the plugin's text, as a plugin update does. Unreadable or malformed enabled-plugin sidecars and records that target a missing stage also fail the check rather than being skipped. Because an already-composed structural value cannot be attributed safely after its provenance is corrupted, recover an invalid sidecar by refreshing the stock engine, removing that sidecar, and then running plugin sync.

5. Selection

Plugins add; the install selects. Composing a plugin copies its files into the install and merges its additive contributions, but the users of that install see only the plugins named by <harness-dir>/tools/data/harness.json:

{
  "harnessDir": ".claude",
  "rulesSubdir": "rules",
  "plugins": ["aidlc", "test-pro"]
}

The plugins key is optional. If it is absent, every installed plugin is enabled, preserving existing installs and keeping shipped core byte-identical. When it is present, the list is the enabled set. aidlc is the implicit core plugin; omitting it disables core stages/scopes/runners while leaving the files installed and re-enableable. The three Initialization stages are the exception: bootstrap has no plugin identity, so those stages are always enabled for every enabled scope.

Use the deterministic utility command to inspect or change the selection:

aidlc engine plugin select
aidlc engine plugin select test-pro
aidlc engine plugin select aidlc,test-pro

select-plugins validates names against the known set (aidlc plus plugin names found on compiled nodes and scope files) while holding the workspace mutation lock. It copies the project surfaces to staging, strips disabled contributions there, writes harness.json, recompiles the graph/grid, regenerates stage/scope runners, and refreshes the generated tables in tables.md, beside the orchestrator skill. Only then does it diff staging against the live project and submit the complete set of changed files to aidlc-transaction.ts. The audit append is the transaction's committed validator: if it fails, the engine restores every selection, graph, runner, table, contribution, and sidecar byte. This replaces the older three-file snapshot convention. /aidlc --doctor reports the enabled plugins, per-plugin enabled-stage counts, and hard-fails if the graph's enabled:false flags disagree with harness.json.

aidlc engine plugin list is a separate installed-versus-composed status command; it does not print or change the project selection.

A plugin whose base is left out is paused. The selection is the whole list, so a person who leaves a base plugin out and keeps a plugin built on it typed that plugin themselves; select-plugins (and aidlc config project --plugins) writes the list exactly as typed and says one line, once, at that change: zeta is off. alpha builds on zeta, so alpha is paused until zeta is on again. To turn alpha off as well: /aidlc plugin select aidlc. While paused, nothing the plugin adds runs: its stages, scopes and runners are filtered and its merged contributions stripped, as for a plugin turned off, and sync leaves it alone and says nothing. The rule has one owner, pluginsEnabled() in aidlc-lib.ts (the selection less the paused plugins; the base comes from the plugin's composition stamp), which every runtime reader and doctor's selection-agreement check read; pluginsSelected() is the list as typed. plugin list shows the plugin as paused: needs zeta, which is off and doctor lists it as information. Turning the base back on brings the plugin back at the next session start (on Kiro CLI and opencode, when the person runs aidlc engine plugin sync --plugin-root <folder>); the select or config project --plugins that turns it on says which, once (resumedLine() beside pausedLine() in aidlc-lib.ts, with the words from nextCompose()). A pause that would strand open work is refused like any turn-off, naming the plugin and the base. Through aidlc config project --plugins, a plugin turned off or paused loses its merged contributions the same way: every config refresh runs the same rule, stripPluginsTurnedOff() in aidlc-utility.ts, in its staged tree.

5a. Installed inventory, composition stamps, and sync

The host owns published-versus-installed state. AIDLC compares that installed state with project-local composition state, entirely offline:

  • Claude reads schema-v2 ~/.claude/plugins/installed_plugins.json and enabledPlugins from ~/.claude/settings.json. A Claude local or project install counts only in its own project, the one that contains the record's projectPath (the folder the install ran in, which can be a subdirectory), except that a project install also counts in a clone or worktree that has no record of its own when its .claude/settings.json enables the plugin, as Claude Code loads it there. When the registry exists but that settings file cannot be read, Claude falls back to the current root and plugin list and doctor flag the file as needing attention. Without a registry the settings file is not read (see the fallback below).
  • Codex reads only plugin IDs declared in ~/.codex/config.toml, then inspects their exact cache paths under ~/.codex/plugins/cache/<marketplace>/<plugin>/<version-or-local>/.
  • On Kiro, Cursor, Copilot, and OpenCode AI-DLC reads no host plugin list. They accept only the plugin root injected into the current hook. Outside that invocation a plugin composed into the project is listed as not compared (no host plugin list), never as missing, and doctor passes with the composed plugins and their versions. Claude and Codex use the same fallback if their registry source disappears.

Each adapter reads one host-native manifest: .claude-plugin/plugin.json, .codex-plugin/plugin.json, .kiro-plugin/plugin.json (Kiro and Kiro IDE), .cursor-plugin/plugin.json, Copilot's .plugin/plugin.json, or .opencode-plugin/plugin.json. An installed plugin is AI-DLC's when its root carries the plugin's own .aidlc-plugin/plugin.json, whose name is its key; a build from before that file travelled with the plugin (every released test-pro) is still known by its aidlc-<key> host name. Another vendor's plugins in the same host list are left alone, broken entries included. The key must be safe and the version semver. Duplicate identities are rejected with every source path; no adapter recursively scans a home or cache directory.

After composition, AIDLC writes <harness-dir>/tools/data/plugin-compose-<key>.json with the plugin name, version, and a deterministic source hash. The hash covers sorted compose input paths plus LF-normalized bytes before {{HARNESS_DIR}} substitution. Host wrappers and generated project output are excluded, so same-version vendored edits and path-only renames are visible.

aidlc engine plugin list [--verbose|--json] compares the host inventory with those stamps. Default output deliberately has only three actions: current, run: aidlc engine plugin sync, or needs attention: <remediation>. On a host with no plugin list, a composed plugin reads not compared: no host plugin list and needs nothing. Verbose and JSON output retain the internal reason: version differs, source changed, not composed, legacy unstamped, disabled, missing, invalid/ambiguous, inventory unavailable, held for its base (dependency-unmet, whose STATUS is needs attention: and the line sync said; §8), or paused because its base is off (paused, a plain state; §5). A plugin installed twice, an old aidlc-<name> copy beside its rebuilt one, is installed-twice: the rebuilt copy is used and its STATUS names the old copy's remove command.

aidlc engine plugin sync composes every enabled installed plugin in a staged project, bases first (§8), regenerates graph and runner surfaces, writes composition and ownership records, diffs the staged project, and submits one project transaction. Expected-state checks reject concurrent live edits; a commit failure rolls back all bytes, modes, stamps, and ownership records. The staged copy holds no links: a link on the way to or inside AI-DLC's own folders, or on the way to a file sync would write, stops sync before anything changes and names the link, or the AI-DLC folder that holds it when the link sits deeper inside. A supported host hook with an injected current root uses the same implementation for only that plugin. Plain sync never deletes content for a missing installed source. Explicit aidlc engine plugin sync --prune-missing requires a proved full inventory, --yes when non-interactive, and hash-proven ownership; it refuses locally modified or unowned paths. At a terminal it asks nothing: it names the plugins it prunes and how to get them back, then prunes. A plugin file's identity is its committed text: a checkout that turns line endings (Git for Windows' core.autocrlf) is no change to the plugin's files, for sync and for the compose hook alike (the rule in docs/reference/04-stage-protocol.md, "Line endings").

Neither list, doctor, nor sync checks a remote plugin registry. The host remains responsible for published-version discovery.

For source-tree installs, the plugin-sync utility verb (the compose hooks' fallback front) runs discovered plugin roots' hooks/compose.ts files, from the environment or --plugin-root <dir>, and exits cleanly when no plugin roots are configured: with no installed plugins; nothing to sync on Claude Code and Codex, and elsewhere with the line plugin sync prints there, which names the command for each composed plugin (noPluginListLine() and syncCommand() in aidlc-plugin.ts). If roots are configured but none carries hooks/compose.ts, it exits 1 and names each root and reason; with a mixed set it warns for each skipped root, composes the valid roots, and exits 0.

Plugin doctor checks

An enabled plugin may ship tools/<plugin>-doctor.ts. /aidlc --doctor discovers that script in the composed harness tools directory and runs it directly with Bun, no shell, with AIDLC_PROJECT_DIR, AIDLC_HARNESS_DIR, and AIDLC_PLUGIN_NAME set. On a native install that Bun is the aidlc binary run with BUN_BE_BUN=1, which the script inherits, so an aidlc command the script runs would start Bun instead: check with Bun APIs, not by running aidlc. A disabled plugin's script remains inert. When harness.json has no plugins selection, every installed plugin is eligible: one that owns a stage or scope, or one whose composition left a sidecar under tools/data/ (plugin-contrib-, plugin-owned-, or plugin-compose-<plugin>.json). A plugin that ships only sensors or overlays is therefore discoverable and selectable once it is composed.

The script writes one JSON object to stdout:

{
  "checks": [
    {
      "pass": false,
      "label": "required connector is installed",
      "fix": "install the connector and re-run doctor",
      "severity": "error"
    }
  ]
}

severity defaults to error; a failing error check fails doctor, while a failing advisory check is displayed and exported without changing the exit code. Passing checks render normally. Doctor treats the installed plugin as the code trust boundary, but contains failures: a spawn error, timeout (five minutes by default, with AIDLC_PLUGIN_DOCTOR_TIMEOUT_MS as a positive-integer override), non-zero exit, invalid JSON/shape, or malformed entries becomes a bounded finding instead of crashing doctor. Output is capped at 50 check rows per plugin, stdout at 256 KiB, and labels/fixes at 300 characters.

The compiled stage-graph.json persists the full installed stage set. Disabled nodes carry "enabled": false; enabled nodes omit the key. Runtime loaders filter disabled nodes, so runners, state rows, scope tables, and orchestration see only the selected graph. loadStageGraphAll() is reserved for doctor and selection tooling. Stage numbers are assigned from the full graph, so disabling and later re-enabling a plugin preserves the exact numbers. The selection filter covers stages, scopes, and runners; a disabled plugin's agents/ and knowledge/ files stay on disk AND stay loadable (the agent roster and knowledge lookups are not selection-filtered) - inert unless something references them, since the stages that would dispatch those agents are filtered out.

The compiled scope grid contains only enabled scope identities. Scope files for disabled plugins remain on disk, but they are not valid runtime scopes until the plugin is selected again. An enabled scope whose frontmatter says freeform_default: true is the freeform/default fallback first, in any install, mixed ones included. Otherwise, if core is disabled and exactly one plugin scope owner is enabled, the fallback uses that plugin's first scope alphabetically. If multiple plugin scope owners are enabled and core's preferred classic fallback is unavailable, the orchestrator errors and asks for an explicit --scope.

Disabling a plugin also removes what it merged into core stages, not just its own files. Compose records the structural adds it actually applied (produces / sensors / full consume entries including required and optional conditional_on / scopes / required_sections, per target stage) and each successfully applied fragment's anchor/order/hash in a per-plugin sidecar at tools/data/plugin-contrib-<key>.json; spliced prose fragments also carry their own sentinel markers. On disable, select-plugins and every config refresh (aidlc config project --plugins among them) strip both from the installed stage source inside their own rollback transaction, through one rule (stripPluginsTurnedOff()), so a disabled or paused plugin's contributions stop steering enabled stages. Re-enabling restores them on the next session start (on Kiro CLI and opencode, which run no compose hook, when the person runs aidlc engine plugin sync --plugin-root <folder> with the plugin's folder): the plugin's compose re-merges, byte-identical. The select line says which, and so does a held plugin's line, from one rule (nextCompose() in aidlc-plugin.ts); the held line names the folder sync ran with, and the select line, which no record tells the folder, says <plugin's folder>.

Compose hooks and select-plugins serialize planning on the same realpath-keyed workspace lock; live project changes then commit through the transaction engine's root lock and expected-state checks. The protected span covers installed stage edits, per-plugin sidecars, selection writes, graph/grid compilation, runner/table generation, audit validation, and rollback, so concurrent plugin hooks cannot lose one another's set-union updates and a disable cannot race with compose to leave an untracked contribution active.

Selection is closure-checked at compile time: an enabled stage may not require an artifact whose only producer stages are disabled. The error names the consuming stage, the artifact, the disabled producer stage(s), and the plugin(s) that provide them, then tells you to enable those plugins or disable the consumer. This catches plugin-only selections that would otherwise route a stage with a starved required input. A requires_stage edge pointing at a disabled stage is NOT an error (the ordering edge is vacuous when the dependency never runs - a plugin-only install legitimately orders plugin stages after core ones), but doctor lists such dropped edges as an advisory.

select-plugins also refuses a change that would strand an active workflow: disabling the plugin that owns a running workflow's scope, or one that owns a pending EXECUTE stage in its plan, is rejected naming each dependency (complete or archive the workflow first, since a parked workflow still needs its plugin when it resumes, or keep the plugin enabled). Doctor hard-fails on a selection that already strands one.

Composing a plugin does not auto-enable it when a selection already exists. The compose hook still copies the plugin's own files (stages, scopes, agents, knowledge, sensors, tools; its stages, scopes and runners are runtime-filtered, while its agents and knowledge stay loadable as above) and records an advisory drop naming the select-plugins command to expose that plugin, but it does NOT merge contributions into core stage source while disabled - merged contributions bypass the selection filter, and merging them would undo the disable-time strip on every session start. With no selection key, composed plugins are active immediately, preserving the original status quo.

bundle is deliberately unused today. The word is reserved for a possible future collection-of-plugins concept; plugin ownership is always expressed with plugin:.

6. The contribution seam

A contribution is a file a plugin ships to additively modify a named existing stage, at contributions/<phase>/<slug>.md. It never edits the target:

---
target: build-and-test        # the existing core stage being enriched
plugin: test-pro
adds:                         # STRUCTURAL — set-unioned into the stage node
  produces:
    - test-pro-regression-suite            # plugin-namespaced (§8)
  consumes:
    - artifact: test-pro-testability-requirements
      required: false
  sensors:
    - coverage-threshold
  required_sections:
    - "Branch Coverage"        # declared H2 (merged into the stage; not machine-enforced yet — see §9)
fragments:                    # PROSE — spliced into the stage body
  - anchor: after-step:8
    order: 100
---

## fragment: after-step:8

### Step 8a (test-pro): Branch + coverage enrichment
…prose the agent reads, inserted after the target stage's Step 8…

bundle: is reserved and unused; write plugin: for plugin ownership.

Merge semantics:

  • Structural surfaces — set union into the target stage's source frontmatter. Commutative, order-independent, safe across uncoordinated authors. Implemented today: produces, consumes (artifact + required + conditional_on, each preserved), sensors, scopes, requires_stage, required_sections. adds.scopes carries two guard rails: the scope's identity file must already be installed (a name with no scopes/<name>.md would resolve as an all-SKIP phantom), and that installed file's plugin: frontmatter must name the contributing plugin exactly — putting a core stage under a core or foreign-plugin scope changes selection semantics the other owner never agreed to, and ownership comes from the file's declared owner, not a name-prefix rule (dash prefixes overlap across plugin names; a core scope declares no plugin: and never merges); a violating entry is dropped-with-log, never merged. adds.requires_stage set-unions ordering edges; an edge that cannot hold is dropped-with-log, never merged. The dependency must be an installed stage (core, or a plugin stage already on disk) other than the target, and it must compile before the target by the compiler's own rule: full pinned numbers compare (prefix, then index), a pinned row keeps its number when its stage moves phase directory, and only an unpinned stage takes its prefix from its phase directory. A stage the same compose adds is unpinned and seeds past its phase max, so an installed stage cannot require it within the same phase (ordering a plugin stage before a core one is RFC #1100); between two new same-phase stages the edge holds unless it would close a cycle. An edge to a stage a scope skips is vacuous there, exactly as core's own cross-scope edges are. Recorded edges are re-checked on every compose, runtime refresh and Cursor reinstall: one that no longer holds is removed from the stage and from the contribution sidecar (a still-declared edge merges again once it holds), and one the incoming runtime now declares itself passes to core, so a later disable never strips a core edge.
  • Prose fragments (fragments of step/question prose) — spliced into the stage body at the declared anchor, ordered deterministically by (order, plugin). Each spliced block is wrapped in a content-hashed sentinel and its anchor/order/hash is persisted in the contribution sidecar, so re-composing is idempotent, an upgraded fragment replaces its prior block, prose-only plugins retain verifiable provenance, and blocks from separate plugins interleave by (order, plugin) regardless of hook-firing order. The agent reads base body + ordered fragments at runtime.
  • No override, ever. A contribution can only add. It cannot change a stage's lead_agent, relax a consumes[].required, remove a field, or replace existing step prose. A genuine need to change upstream behavior is a framework-level decision, never a quiet patch inside a plugin.

Fragment anchors:

Anchor Inserts the fragment…
after-step:<n> right after ### Step <n>'s content (before the next heading)
before-step:<n> immediately before ### Step <n>
after-questions not supported yet: compose drop-logs it as an unknown anchor; use after-step:<n>
end-of-steps at the end of the ## Steps block
in:<Compartment> at the end of the named ## <Compartment> block
after-preflight personas: right after the delegated-knowledge preflight the packager injects
end-of-body at the end of the authored body (before knowledge absorbed into a reviewer persona)

Surface-by-surface — what a plugin uses for each kind of upstream change. "Status" marks what the compose hook merges today vs. what is designed-but-deferred:

Change Mechanism Status
Stage asks new questions fragments of question prose ✅ implemented
Stage produces an extra artifact adds.produces + a fragments step that emits it ✅ implemented
Stage requires new sections adds.required_sections ✅ merged (⚠️ declarative — not machine-enforced yet, §9)
Add a verification to a stage adds.sensors (+ ship the manifest and tools/ script) ✅ implemented
Add a consume edge adds.consumes ✅ implemented
Add a requires_stage edge adds.requires_stage ✅ implemented (the dependency must compile before the target — a lower full pinned number, or an earlier phase for a stage not yet pinned; violations are dropped-with-log)
Put an existing stage under a plugin scope adds.scopes for a core stage (own-plugin scopes only; installed identity file required); include_stages: on the scope for a stage of the plugin's base (section 7) ✅ implemented
Inject phase policy / guardrails ship memory/phases/<p>.md into the default-space seed (§7) ⏳ deferred (not yet projected)
Enrich a core persona (a collaboration bullet, a mandatory anchor) contributions/agents/<agent>.md with fragments at in:<H2>, after-preflight or end-of-body ✅ implemented (prose only; adds.* is refused with an advisory drop)

7. Method/rules, agents, knowledge, scopes, and activation

This section describes the surfaces beyond new stages + the contribution seam, with status called out so an author is not misled:

Method/rules → the memory seed (⏳ deferred). The framework's rule layer is the per-space memory tree (aidlc/spaces/<space>/memory/{org,team,project}.md, phases/<phase>.md), seeded from core/memory/. The design is that a plugin contributes a memory/phases/<p>.md that set-unions into that seed. The packager does not yet project a plugin's memory/ tree, so this has no effect today.

Agents (✅ projected + composed). A plugin ships new personas under agents/<plugin>-<role>-agent.md, with frontmatter name equal to the filename stem and plugin: <plugin>. Compose copies them into <harness>/agents/ without clobbering core or another plugin; an identical file is an idempotent skip, and different content at the same destination is drop-logged. On OpenCode, compose also emits a native .opencode/agents/ twin with mode: subagent, permission.task: deny, and OpenCode-valid model/memory frontmatter. On Kiro, compose removes the unsupported disallowedTools: Task line from the .kiro/agents/ persona while Kiro's native agent tool configuration keeps nested delegation unavailable; a different disallowedTools value is drop-logged and the persona is not copied. Re-compose also migrates an existing persona only when it is an exact, unchanged, same-plugin copy from the pre-projection composer; edited or foreign files retain no-clobber behavior. An already-composed unsupported value is left in place with a degraded diagnostic that names the file to remove before re-compose.

A plugin may also enrich a core persona through contributions/agents/<agent>.md (§6): prose fragments only, spliced into <harness>/agents/<agent>.md at in:<H2>, after-preflight or end-of-body, recorded in the contribution sidecar under the agent slug and stripped on disable exactly like stage fragments. The same fragments reach every file a harness dispatches the persona from, so the instruction reaches the agent on every harness: the Markdown persona (the engine roster, and the native agent on Claude Code, Cursor and Kiro IDE; Kiro CLI's agent JSON loads its prompt from it), plus the harness-native twin the packager builds from it: the Codex agent TOML's developer_instructions string (the fragment text escaped for TOML), the OpenCode .opencode/agents/ and Copilot .github/agents/ native agents, and on Kiro IDE the persona without its tool permissions (.kiro/aidlc-common/personas/<agent>.md, the file the engine roster names there for an agent a stage reads inline). Doctor checks each of these files, a refresh replays the fragments into each, and disable and prune strip them from each.

On Kiro CLI, Codex, and OpenCode, a Markdown persona in the engine roster is available only for mode: inline. Native dispatch also requires a per-harness dispatch surface — a hand-authored agent-v1 JSON plus registration in the conductor's trustedAgents list on Kiro CLI, an agent config TOML (the shipped aidlc-*-agent.toml shape) on Codex, or a native .opencode/agents/ subagent file on OpenCode. The kiro-ide row (Kiro IDE and Kiro CLI v3) instead dispatches the installed agent Markdown itself, but only when tools: is non-empty and permissions.rules is a non-empty list of well-formed capability/effect/match entries whose effect is allow, ask, or deny (core's own personas carry all three); empty permissions, missing or empty rules, malformed entries, and any other effect are rejected. Compose therefore rejects a plugin stage whose dispatched topology (mob, pipeline, or subagent for the lead and supports; a reviewer: on any gated stage regardless of mode) names an agent without the complete installed dispatch surface, and records the stage, agent, and remediation in the compose drops log. On Kiro CLI, the JSON and trustedAgents registration are checked independently: having only one still rejects the stage. On Kiro IDE, author the installed .md with both required blocks or change the stage to mode: inline; the IDE path never reads aidlc.json. On OpenCode — the one harness whose native surface compose itself emits — a plugin-shipped persona counts as the surface when it would survive the native-twin emission (closed frontmatter, no un-projectable disallowedTools); Kiro/Codex surfaces are always hand-authored, so a plugin's own files never satisfy those checks. Hand-authoring the missing surface and re-running compose accepts the stage. The Markdown persona remains composed for any accepted inline stage that also uses it.

agent-team is schema-reserved but has no runtime consumer, so compose rejects plugin stages that select it on every harness instead of silently treating them as inline. If the installed stage parser is unavailable, Kiro/Codex/OpenCode compose fails closed: only a stage with an explicit mode: inline scalar and no reviewer: is accepted (quoted scalar forms are recognized). A no-clobber upgrade cannot remove a stage composed by an older hook, so an existing stage that fails these dispatch checks remains on disk but emits a degraded health row naming the remediation.

Knowledge = methodology only (✅ projected + composed). A plugin ships per-agent methodology knowledge into knowledge/<agent-slug>/, composed into <harness>/knowledge/<agent-slug>/. Domain/space knowledge (aidlc/spaces/<space>/knowledge/) is empty-at-bootstrap user runtime state a plugin neither ships nor seeds.

Scopes (✅ projected + composed). A plugin scope's identity is one file under scopes/<plugin>-<name>.md, with frontmatter name equal to the filename stem and plugin: <plugin>. Compose copies it into <harness>/scopes/ without clobbering. Membership on plugin-authored stages works through those stages' scopes: frontmatter. A plugin scope may set freeform_default: true to nominate itself when the preferred core classic default is disabled; at most one enabled scope may claim the nomination, and graph compilation rejects an ambiguous selected set. Adding a plugin scope to an existing core stage works through a contribution's adds.scopes (§6) — own-plugin scopes only, and the scope file must be installed before the merge. A plugin built on a base (section 8) runs the base's stages in its own scope by naming them in the scope's include_stages: list: compile adds each to that scope's column (the stage's compiled row lists the scope), and the stage files and every other scope stay as they are. An include no installed stage has is left out of the column and never fails the compile; sync holds the plugin and says which stage is missing (section 8). Validate refuses a core or own stage there and a name that cannot be the base's (a plugin's stage slugs start with its name, which compose enforces), and checks the base's against the --with checkout of the base.

Activation (when:) (⚠️ parsed, not evaluated). A stage may carry a structured when: predicate; {producer-in-plan: X} is schema-validated and parsed, but no engine consumer evaluates it yet — aidlc-graph names itself the future home. So a stage carrying when: is today EXECUTE under its declared scopes unconditionally. A plugin's own stages exist only when the plugin is in the chosen set, so "is this plugin active" is already a compose-time fact.

Composer screening (ars:). The adaptive composer's expected-value screen reads its per-stage cost priors from tools/data/ars-priors.json, which names every core stage and nothing else, so a plugin stage used to land in aidlc-graph ars output as a no-prior row ("not screenable"). A plugin stage declares the same four facts in an ars: block in its own frontmatter (targets and cost, optional role and project_types), validated by the stage schema (and therefore by aidlc-plugin-validate), compiled onto its graph node verbatim, and read by the ars subcommand whenever the priors file has no entry for the slug. A shipped entry always wins, so core screening is unchanged; each screen row carries a priorSource (shipped | stage | null). See Stage Definition § ars.

plugin.json aidlc.contributes. VALIDATE reads the block, rejects unknown keys, and requires exact canonical values for every currently projected subtree. BUILD and TEST stop on the same findings, and direct emission asserts the path contract again before replacing output. The emitter still discovers bytes by directory convention rather than routing through arbitrary manifest paths; configurable routing remains deferred. memory is rejected until its projection exists. There is no aidlc.lock.json read either; the composer resolves nothing from a lockfile today.

8. Multi-tenant guards

Independent authors who never coordinate are kept safe by:

  • Namespacing. Contributed artifact logical names are <plugin>-prefixed; core-* is reserved. A plugin's stages, agents, scopes, and sensors should be unique across the chosen set and against core. Primitive file collisions are no-clobber and drop-logged with attribution (no silent shadowing); the one exception is the plugin's own file, unchanged since the plugin installed it, which a re-compose replaces with the plugin's newer copy.
  • Shallow layering. A plugin builds on core and at most one base plugin, named in its manifest's dependencies as "<plugin>" or "<plugin>@<range>" (npm's range grammar); a base builds on core alone, so the longest chain is core, base, team plugin. The build copies the plugin's own .aidlc-plugin/plugin.json into every projection (a host manifest cannot carry it: Claude Code reads a dependencies key there as host plugins to enable), and validate rejects a second base, a self-dependency or a range npm cannot read. Sync composes the bases first, then the plugins built on them. AI-DLC never installs or fetches a base; it reads the one installed version (on a host with no plugin list, the version composed in the project), and there is no solver and no lock file. A plugin whose base is not installed (or not in the project yet), is disabled in the host, is outside the range, builds on another plugin or lacks a stage the plugin's scopes include (include_stages, section 7), or whose manifest names two bases or cannot be read, is held and never resolved: sync says one line once, writes tools/data/plugin-held-<key>.json, composes every other plugin and exits 0 (--json lists data.held); plugin list and doctor show the line until a sync composes the plugin and removes the record. A held plugin composed before stays as it was, because plain sync never deletes, and its line says so. A session-start hook names only its own plugin; on Claude Code and Codex, whose plugin lists sync reads, the hook also composes the installed base first, so the hosts' hook order does not matter. The composition stamp records base, so a host with no plugin list keeps the one-level rule too. A projection built before the manifest travelled with it builds on core alone.
  • Deterministic ordering. The one non-commutative surface (prose fragments) is ordered by explicit (order, plugin), never by load order.
  • Conflicts are visible. A genuinely non-commutative collision — the same stage's same fragment anchor at the same order, an unsatisfiable cross-plugin edge, or a duplicate primitive path — is dropped or rejected with attribution, rather than resolving by overlay order.

9. As-built: emission, install, and the worked example

The shared plugin emitter projects one authored root into one host plugin. bun scripts/package.ts calls it for discovered first-party plugins/<name>/ roots; aidlc-plugin-build.ts calls it from an external plugin repository. Each projection carries .aidlc-plugin-projection.json, binding replacement authority to the logical plugin and exact harness, plus the host-native manifest (.claude-plugin/ / .codex-plugin/ / Copilot .plugin/ / .kiro-plugin/ / .opencode-plugin/ / .cursor-plugin/), for a plugin someone else builds a marketplace.json of its own where the host reads one (beside the host manifest; Codex reads only .agents/plugins/marketplace.json), the compose hook, and the plugin's content (stages with full number/plugin/when frontmatter — the schema accepts them natively). A non-empty output without a valid matching marker is never cleaned; there is no force bypass. Cursor keeps plugin-agent compose inputs under aidlc/agents/, outside Cursor's auto-discovered root agents/; compose projects the single native copy into the installed .cursor/agents/ roster with harness tokens resolved and named model pins removed. The compose hook is a single portable compose.ts (bun — no GNU-specific shell) that is harness-agnostic: plugin root resolves from CLAUDE_PLUGIN_ROOT | PLUGIN_ROOT | AIDLC_PLUGIN_ROOT and falls back to the emitted hook location, project dir from CLAUDE_PROJECT_DIR | AIDLC_PROJECT_DIR | PWD (Codex leaves the project-dir var unset — PWD is the fallback), and the harness leaf from AIDLC_HARNESS_DIR, which each host command or Cursor launcher supplies. Cursor's launcher additionally parses SessionStart workspace_roots, chooses the only root carrying an AI-DLC Cursor install, and refuses multiple matching roots unless AIDLC_PROJECT_DIR selects one. It copies new stages/scopes/agents/knowledge/sensors/tools without clobbering, merges the seam idempotently (content-hashed sentinel splices, compare-before-write), and records any contribution it has to drop (missing target, malformed anchor, a key the installed engine won't accept) to per-plugin <hooksHealthDir>/plugin-compose-<key>.drops files — the same per-space health dir core hooks write to and /aidlc --doctor scans — rather than failing the session. The plugins this repository ships carry no catalogue of their own: the packager writes one aidlc-plugins catalogue per host format at the root of dist/plugins/ (Claude Code .claude-plugin/, Codex .agents/plugins/, Copilot .plugin/, Cursor .cursor-plugin/; none for Kiro, which has no store, or opencode, which installs npm modules), listing each plugin with a ./<name>/<harness> source, and the release runtime carries the same plugins/ folder, so a person adds it once and installs any of them. The plugin's stage and scope runners land in the harness's skill-discovery root (<harness-dir>/skills/, Copilot's .github/skills/, Codex's .agents/skills/) with the host's explicit-only guard (disable-model-invocation: true in the frontmatter, or Codex's agents/openai.yaml), exactly as the shipped runners do. Installed test/fixture payloads are audited separately in a per-harness plugin-compose-installed-tool-payloads-<harness>.drops record keyed by the composing harness leaf: the scan walks that harness's installed tools tree even when the corrected plugin projection no longer contains the path, so a clean compose on one harness never erases another harness's advisory, and legacy files are reported without attributing them to whichever plugin happens to compose next because older compose versions stored no tool-file provenance. Sensor manifests carry an extra copy-time guard: discovery flatly scans sensors/ and indexes only basenames matching aidlc-<id>.md, so a plugin manifest under any other name (or nested in a subdirectory) would compose but never fire. Compose rejects such a manifest and records a degraded drop naming the file and the required shape, and reports one an older compose hook already landed the same way on the next run, so a mis-named sensor is never silently dead on disk.

The host shows a plugin under its author's names. The plugins this repository ships are AI-DLC's own: the packager names them aidlc-<name> in the aidlc-plugins catalogue. A plugin anyone else builds with aidlc-plugin-build.ts keeps its manifest name as its host name, in the catalogue aidlc.marketplace names, or one named after the plugin; aidlc and names starting aidlc- (in any letter case) are refused there, since a second aidlc-plugins catalogue replaces ours in Claude Code. The plugin's own manifest in the projection is authoritative for plugin identity: compose reads <name> from it (or, for an older build, from the host package ID aidlc-<name>) and rejects owned stage, scope, agent, or contribution content whose plugin: field differs. Incoming scope/agent names are reserved as each file is accepted, so duplicate identities within one plugin tree are dropped before compilation. Structural list comparisons canonicalize quoted and unquoted YAML scalars. Compose also holds the realpath-keyed workspace lock for the full transaction; if graph compilation fails, newly copied files and contribution writes are restored before the retry marker is written.

The emitted SessionStart command probes for aidlc on PATH first and runs aidlc engine plugin sync when it is available. Without it, the hooks run the project's own <harness-dir>/tools/aidlc-plugin.ts sync with Bun, so the copy channel keeps the same composition record, and fall back to the direct bun hooks/compose.ts invocation when that tool is missing (the Cursor and Kiro IDE launcher also when it refuses, so an older project still composes).

Cursor and Kiro run a plugin hook in the platform's shell (Cursor: the person's shell, or PowerShell on Windows; Kiro: /bin/sh, or cmd.exe on Windows), so their hooks carry no sh -c. Cursor's emitted hook uses Cursor's flat camelCase schema (hooks.sessionStart[].command) with two entries: aidlc engine plugin sync --harness-dir .cursor, which reads the plugin root and workspace Cursor puts in the hook's environment, and bun ./hooks/aidlc-plugin-compose.ts .cursor for an install without aidlc. Cursor starts each entry on its own, so the one whose runtime is absent fails without stopping the other. That Bun launcher uses Bun.which and process.execPath to probe aidlc, then run the project's own sync tool or the sibling compose.ts. On Kiro the whole plugin sits in its own folder, .kiro/aidlc-plugins/<name>/, so a second plugin dropped into the same project never lands on the first. Kiro IDE's v1 SessionStart registration is one command that reads the same in /bin/sh and cmd.exe: aidlc engine plugin sync --harness-dir .kiro --plugin-root ./.kiro/aidlc-plugins/<name> || bun ./.kiro/aidlc-plugins/<name>/hooks/aidlc-plugin-compose.ts .kiro kiro-ide. Kiro gives a hook no plugin root, but runs it in the workspace root. Both name their harness, so a project with Claude Code installed beside Cursor or Kiro IDE composes into the right one.

Install, per host:

# First materialize the ignored local plugin projections:
bun scripts/package.ts

# Claude Code (dist/plugins is the one aidlc-plugins catalogue; the copy
# runtime's plugins/ folder is the same. A native install keeps it at
# <install root>/plugins, refreshed on install, update, use and rollback.)
/plugin marketplace add <repo-or-path>/dist/plugins   # native: <install root>/plugins
/plugin install aidlc-<name>@aidlc-plugins        # SessionStart hook composes on next session

# Codex CLI (in a git repo)
codex plugin marketplace add <repo-or-path>/dist/plugins   # native: <install root>/plugins
codex plugin add aidlc-<name>@aidlc-plugins       # approve the one-time hook trust

# Kiro (no store — folder-drop, then run the composer explicitly).
# The drop is one copy of the projection's .kiro folder; PLUGIN_ROOT is the
# plugin's folder it lands in (it carries hooks/compose.ts + the plugin content).
cp -r dist/plugins/<name>/kiro/.kiro <project>/
PLUGIN_ROOT="<project>/.kiro/aidlc-plugins/aidlc-<name>"
AIDLC_PLUGIN_ROOT="$PLUGIN_ROOT" AIDLC_PROJECT_DIR="<project>" \
  AIDLC_HARNESS_DIR=.kiro aidlc engine plugin sync
# fallback when aidlc is not installed:
AIDLC_PLUGIN_ROOT="$PLUGIN_ROOT" AIDLC_PROJECT_DIR="<project>" \
  AIDLC_HARNESS_DIR=.kiro bun "$PLUGIN_ROOT/hooks/compose.ts"

Then /aidlc plugin list (or aidlc engine plugin list in a terminal) and /aidlc --doctor compare installed and composed plugin versions and source hashes. The selection diagnostics remain in doctor, and a scoped run (/aidlc --scope enterprise) routes enabled plugin stages wherever their scopes put them on-path.

Worked example - test-pro across a mixed fleet. A platform team publishes test-pro once (validate its repository, build each supported harness projection with aidlc-plugin-build.ts, test each projection against a disposable install candidate with aidlc-plugin-test.ts, push a <plugin>--v<version> tag, publish the generated marketplace metadata). Claude teams /plugin install; Codex teams codex plugin add (approve trust once); Kiro teams git pull + run the composer explicitly (above). In every case the composer merges test-pro's two new stages and its contributions to build-and-test/nfr-requirements/nfr-design/performance-validation - the same enriched, 35-stage (33 core plus two test-pro), doctor-clean install. Validated across all seven harness projections (Claude, Codex, Cursor, Kiro CLI, Kiro IDE, opencode, GitHub Copilot).

Status. Implemented and validated: schema support for number/name/plugin/when/ars (aidlc-stage-schema.ts); stage-authored composer priors (ars: compiled onto the node and read by aidlc-graph ars for slugs the shipped priors file does not name); compile-side carry-through of authored plugin ownership into compiled stage nodes (core omits the field); install-time selection through harness.json + select-plugins, including staged regeneration, shared-transaction commit/rollback, audit validation, full-graph persistence, filtered runtime loading, closure checks, runner pruning, doctor rows, and compose advisory drops; selection-aware tools/<plugin>-doctor.ts checks with bounded fail-loud execution; the standalone offline aidlc-plugin-create.ts, aidlc-plugin-validate.ts, aidlc-plugin-build.ts, and aidlc-plugin-test.ts authoring tools with deterministic scaffolds, bundled compose-hook templates, manifest-derived harness target data, canonical contribution-path validation, and plugin/harness-bound output markers; top-level aidlc plugin validate|build routes that delegate to those same tools; evidence-backed Claude/Codex inventories with Kiro current-root fallback; deterministic composition stamps, the full installed-versus-composed comparator, transactional aggregate sync, and ownership-safe explicit prune; plugin-namespaced stage/scope runner generation; the shared packager/standalone emitter (every discovered harness projection); projection and no-clobber compose for plugin stages/, scopes/, agents/, knowledge/, sensors/, and tools/; the harness-agnostic compose hook (scripts/plugin-hooks-template/compose.ts); the reusable tests/harness/plugin-kit.ts build, shared compose-subprocess/drop-reading, delegated content-validation, and live-invocation helpers; the contribution seam for produces / consumes / sensors / scopes (own-plugin, installed-file-guarded) / requires_stage (ordering-guarded, re-checked on compose, refresh and Cursor reinstall) / required_sections + prose fragments (content-hashed, idempotent, order-deterministic). Guarded by tests/unit/t242-plugin-state.test.ts (inventory fixtures, hashes, comparator, rollback, and prune), tests/integration/t188-plugin-compose.serial.test.ts (the compose mechanism), tests/integration/t224-plugin-selection.test.ts (selection), tests/integration/t300-plugin-kit.test.ts (the reusable kit), tests/integration/t327-plugin-author-routes.test.ts (top-level authoring routes), tests/unit/t314-plugin-validate.test.ts (offline validator), tests/unit/t315-plugin-build.test.ts (isolated byte-parity builder), tests/unit/t316-plugin-test.test.ts (isolated candidate compose tier), tests/unit/t317-plugin-create.test.ts (isolated whole-toolchain scaffold), tests/unit/t313-plugin-doctor-checks.test.ts (doctor runner), tests/unit/t354-plugin-dependencies.test.ts (bases first, held lines, hooks in any order), and each plugin's own tests/ (content; wired into the integration tier). Top-level aidlc plugin create|test routes remain deferred to RFC #723 section 2e, and aidlc-plugin-test --dist is not supported. Deferred / not yet wired: projection/merge of a plugin's memory/ subtree and configurable aidlc.contributes routing; when: predicate evaluation (parsed, no engine consumer); machine-enforcement of merged required_sections (the field merges + validates but does not reach the compiled node, and the shipped required-sections sensor derives its expectations from templates - nothing fails a stage for a missing declared section yet); the after-questions fragment anchor (locateAnchor has no case - it drop-logs "unknown anchor"; use after-step:<n>); and reading any lockfile (a base plugin's version is read from what is installed; §8). Number seeding for NEW slugs is edge-aware: a first compile orders each phase's batch of new stages by their own requires_stage edges (ties break by the authored number: hint, then slug) and assigns next-free contiguous indices in that order - the engine owns all number values (authors claim none, so uncoordinated plugins cannot collide), a multi-stage plugin's sub-DAG seeds in flow order regardless of filenames, and an already-pinned row keeps its JSON values. Authored name: seeds the display name for a new slug.

10. Invariants

  • Core is immutable. No plugin ever edits core/.
  • Additive-only. Contributions add; they never override or remove.
  • Inert when off. With every plugin disabled the workflow runs as bare core: plugin stages and scopes are filtered out and merged contributions are stripped, while the plugin files stay installed for re-enabling.
  • One composer, host-triggered. The same code composes wherever it runs; the only centrally pre-built artifact is bare core.
  • A plugin IS a host plugin. The packager emits real .claude-plugin/ / .codex-plugin/ / Copilot .plugin/ / .kiro-plugin/ / .opencode-plugin/ / .cursor-plugin/ manifests, installed through the host's native commands. AIDLC runs no distribution infrastructure.
  • Slug identity, display-only numbers. Inserting a plugin stage never renumbers core.
  • Trust is host-native. Org restrictions use the host's managed allowlist; AIDLC builds no trust layer.
  • No gatekeeping. First- and third-party plugins are mechanically equal; provenance is the only difference.

Cross-references

  • Authoring a Plugin — the author-facing walkthrough (build the fixture end to end).
  • Stage Definition — the stage frontmatter contract, including plugin/number/when/ars.
  • Artifact Vocabulary — logical-name namespacing.
  • Engine and Skill System — the compiled graph the composer feeds and the orchestrator routes off.
  • Example config docs (marketplace.json, managed-settings.json, aidlc.lock.json) under examples/test-pro/; the composition-timing evidence and the sequenced build history are preserved in this repo's git log.