diff --git a/.claude/rules/learned.md b/.claude/rules/learned.md index 38dadb99..d17f6570 100644 --- a/.claude/rules/learned.md +++ b/.claude/rules/learned.md @@ -2,13 +2,13 @@ - `git worktree add -b origin/develop` sets the new branch to track `origin/develop`, so a bare `git push` goes to `develop`, and the Archon pre-push guard does not refuse it. Add `--no-track`, or run `git branch --unset-upstream`, then push with `-u origin `. (2026-09-28) - Subagent frontmatter takes `tools` and `disallowedTools`. `allowed-tools` is not a subagent field, so an agent that declares only `allowed-tools` inherits every tool. (2026-09-28) -- A skill that runs through `/loop` or the Skill tool must not set `disable-model-invocation: true`. The Skill tool refuses such a skill, which is how `afk-loop` broke under `/loop`. (2026-09-28) +- A skill that runs through `/loop` or the Skill tool must not set `disable-model-invocation: true`. The Skill tool refuses such a skill, which is how `afk-loop` broke under `/loop`. Claude Code still asks for confirmation before it deletes some files, such as `*.visual-check.*`, even in a session started with `--dangerously-skip-permissions`, so an unattended `/afk-loop` stops there until someone answers. (2026-09-28) - A new plugin must be added to the `tag_if_changed` list in `.github/workflows/release.yml` as well as to `ci.yml`. Without that line, the release workflow on `main` never creates the plugin's tag. (2026-09-28) - When a repo skill depends on a plugin, such as `afk-loop` on `pr-review-toolkit`, enable that plugin under `enabledPlugins` in the committed `.claude/settings.json`, so every developer who clones the repo has it. (2026-09-28) -- Session opener comments on issues start with `**Opener`, in bold. A filter for comments that start with `Opener` finds nothing, so match the bold prefix. (2026-09-28) +- Session opener comments on issues start with `**Opener`, in bold. A filter for comments that start with `Opener` finds nothing, so match the bold prefix. GitHub counts only open issues in `issue_dependencies_summary.blocked_by`, so an issue closed when it is created cannot block another issue. (2026-09-28) - In Archify diagrams, a node's `tag` renders with `data-detail="fine"`, so it is hidden at default zoom and in exports. `mainPath` refuses any step to a lower column, and `col` is capped at 0 to 5, so a chain of more than six nodes cannot keep `mainPath`. (2026-09-28) - `.claude/scheduled_tasks.lock` is Claude Code runtime state and is untracked on purpose, so never commit it. (2026-09-28) -- Before Archon 0.11.0, a `loop:` node ignores `allowed_tools` and `denied_tools` without a warning. From 0.11.0 it honours both, but `hooks`, `mcp`, `skills` and `sandbox` stay ignored on `loop:`, so check the installed Archon version before you rely on a tool rule inside a loop. `.claude/skills/archon/` is repo-authored, not vendored, so fix its stale `references/parameter-matrix.md` by hand. (2026-10-01) -- An Archon node does not get a plugin that only the committed `.claude/settings.json` declares. The plugin reaches a node after someone installs it, and a node whose Skill or agent call fails still finishes green. By default a node also loads user-scope plugins, and `settingSources: [project]` on the node removes them. (2026-10-01) +- Before Archon 0.11.0, a `loop:` node ignores `allowed_tools` and `denied_tools` without a warning. From 0.11.0 it honours both, but `hooks`, `mcp`, `skills` and `sandbox` stay ignored on `loop:`, so check the installed Archon version before you rely on a tool rule inside a loop. From 0.12, a completed run removes its own worktree when the worktree is clean, and keeps it when it holds uncommitted or untracked files. `$ARTIFACTS_DIR` sits outside the worktree and survives, so copy anything a run leaves inside its worktree out before the run ends. `.claude/skills/archon/` is repo-authored, not vendored, so fix its stale `references/parameter-matrix.md` by hand. (2026-10-01) +- Up to Archon 0.11, a node does not get a plugin that only the committed `.claude/settings.json` declares: the plugin reaches a node after someone installs it, a node loads user-scope plugins by default, and `settingSources: [project]` on the node removes them. From Archon 0.12.0, a Claude node loads no installed plugin unless it names it in `plugins: [name@marketplace]`, and it fails before its first turn if a named plugin is missing. Measured on 0.13.0, a node with no `plugins:` still loads the `@builtin` plugins, reaches no MCP server and does not fail, and a named plugin's skills reach the node only when `skills:` names them too. Also on 0.13.0, a declared MCP server that cannot connect fails the node before its first turn, even with `optional: true`. On every version, a node whose Skill or agent call fails still finishes green. (2026-10-21) - `/pr-review-toolkit:review-pr` takes its scope from `git diff --name-only`, which reads the working tree, not a merge-base. It finds the PR with `gh pr view`, so it works on GitHub only, and a caller must pass the scope explicitly. (2026-10-01) -- The `evidence` node in `apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-build.yaml` sets `verdictState` to `pass`, `fail` or `unresolved`. Never write `green` or `red` in a criterion, even where an issue says "red verdict" in prose. (2026-10-01) +- When you rename a GitHub issue or PR, the old title stays in a renamed-title event on the timeline, and neither the API nor "Delete revision from history" removes it. An edited body or comment keeps its old text as a revision until someone deletes that revision by hand, so check a text for NDA terms before you post it for the first time. (2026-10-30) diff --git a/.claude/settings.json b/.claude/settings.json index 005e8224..281c60ea 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,7 +9,8 @@ }, "enabledPlugins": { "ponytail@ponytail": true, - "pr-review-toolkit@claude-plugins-official": true + "pr-review-toolkit@claude-plugins-official": true, + "unic-learning-loop@unic-agent-plugins": true }, "permissions": { "allow": [ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f102308e..659096df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,6 @@ jobs: tag_if_changed "apps/claude-code/pr-review" tag_if_changed "apps/claude-code/auto-format" tag_if_changed "apps/claude-code/unic-confluence" - tag_if_changed "apps/claude-code/unic-archon-dlc" tag_if_changed "apps/claude-code/unic-pr-review" tag_if_changed "apps/claude-code/unic-spec-review" tag_if_changed "apps/claude-code/unic-learning-loop" diff --git a/README.md b/README.md index e54ba1d4..bce4da42 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,13 @@ The `unic-archon-dlc` plugin was withdrawn from the catalogue on 2026-10-30. See Add the Unic marketplace once (run inside `claude`): ``` -/plugin marketplace add https://github.com/unic/unic-agents-plugins +/plugin marketplace add unic/unic-agents-plugins#main ``` +`main` holds the released versions, and `develop` is the development branch, which an add without a ref installs. + +If you added the marketplace without a ref, run `/plugin marketplace remove unic-agent-plugins` and add it again with the command above. + Then install individual plugins: ``` diff --git a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-build.yaml b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-build.yaml index 40ec6551..95c2f6e9 100644 --- a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-build.yaml +++ b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-build.yaml @@ -4,27 +4,32 @@ description: > implements every slice test-first with structural anti-cheating: a single generic loop advances each slice through two FRESH-context phases — RED (write a provably-failing test) and GREEN (minimum implementation) — serially in dependency order. The baton between phases is artefacts on - disk (committed test, committed impl), never session memory (ADR-0012). Procedure comes from the + disk (committed test, committed impl), never session memory. Procedure comes from the `tdd` and `implement` Methods, read by path from `.archon/methods/`; refactoring is deliberately NOT in the loop — `tdd` puts it in the review stage, so it lives in /pr-review's `code-review` - Fowler smells (#281, amending ADR-0023). Then a local non-posting code-review pre-check, verify, - goals-check, report, and gate a PR to develop (ADR-0011/0022/0023/0030/0031). + Fowler smells. Then a local non-posting code-review pre-check, verify, + goals-check, evidence, report, and a commit and push of the work on every verdict. Only a "pass" + verdict opens a PR to the run's base branch, Archon's $BASE_BRANCH, and gates it; any other + verdict cancels the run with the work on the pushed branch. -# Required at the workflow level so the build-pr-gate approval message reaches the user (ADR-0011). +# Required at the workflow level so the build-pr-gate approval message reaches the user. interactive: true -# The engine refuses terminal `completed` unless $ARTIFACTS_DIR/evidence.json exists — the -# `evidence` node below is the only writer; the engine gates on presence only, the content -# contract is this workflow's own (ADR-0034). +# The engine refuses terminal `completed` unless $ARTIFACTS_DIR/evidence.json exists. The +# `evidence` node below is the only writer, and it writes the file on every verdict, so this +# policy no longer refuses a red run. Two guards cover `evidence` ending without writing. +# push-branch refuses a Session mirror that is missing at HEAD, so the run ends `failed`. This +# policy catches the one case left: the mirror is committed but the $ARTIFACTS_DIR copy is absent. +# The verdict decides how the run ends, at guard-not-pass and open-pr. evidence_policy: required: true nodes: # --------------------------------------------------------------------------------------------- # bootstrap — parse the slug from $ARGUMENTS, read config, confirm issues.json exists. - # A prompt node (NOT a script) so it needs no plugin-lib import and no $CLAUDE_PLUGIN_ROOT - # (ADR-0023 §5): the agent reads .archon/unic-dlc.config.yaml with its own tools. It also passes the - # whole `sdlc_needs` block downstream and runs the declared install once for the run (#430). + # A prompt node (NOT a script) so it needs no plugin-lib import and no $CLAUDE_PLUGIN_ROOT: + # the agent reads .archon/unic-dlc.config.yaml with its own tools. It also passes the + # whole `sdlc_needs` block downstream and runs the declared install once for the run. # --------------------------------------------------------------------------------------------- - id: bootstrap depends_on: [] @@ -38,15 +43,16 @@ nodes: 2. Read `.archon/unic-dlc.config.yaml` at the repo root. - If it is absent or unreadable → status "no-config". - Otherwise read: `artifacts_dir` (default "workflows"), `gates.build` (default "hitl"), - `build.coverage_threshold` (may be null), `project.branching` (default "gitflow"), and the - whole `sdlc_needs` block. + `build.coverage_threshold` (may be null), and the whole `sdlc_needs` block. 3. Confirm the build baton exists: `//issues.json`. - If it is absent → status "no-issues". 4. If slug, config, and issues.json are all present → status "ready". - Compute `expected_base` from branching: "develop" when branching == "gitflow", else "main". - Emit it in EVERY branch (fall back to "develop" when config cannot be read) — the - implement-review-precheck node pins its code-review fixed point to it. + This run's base branch is `$BASE_BRANCH`. Archon resolves it before this node starts, from + `--base`, then `worktree.baseBranch` in `.archon/config.yaml`, then the codebase default. It is + the code-review fixed point and the PR target. Archon cuts the worktree from it unless `--from` + names another branch. Read no base branch from the config. In EVERY branch, start `message` with + "Base branch: $BASE_BRANCH." and follow it with the reason for any status other than "ready". ## The `sdlc_needs` block — a key names a need, never a tool Every key in it names a NEED of this project's development process. `test` names the need; @@ -105,12 +111,11 @@ nodes: coverage: { type: [string, 'null'] } required: [install, build, test, e2e, lint, format, typecheck, dev, coverage] install_report: { type: string } - expected_base: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, sdlc_needs, install_report, expected_base, message] + required: [status, slug, artifacts_dir, gate, sdlc_needs, install_report, message] # --------------------------------------------------------------------------------------------- - # guard — expected precondition failures cancel cleanly (ADR-0011 / schema: cancel vs fail). One + # guard — expected precondition failures cancel cleanly. One # guard, because there is no repository to resolve: `docs/agents/issue-tracker.md` names it. # --------------------------------------------------------------------------------------------- - id: guard-not-ready @@ -123,15 +128,13 @@ nodes: # --------------------------------------------------------------------------------------------- # slopcheck — verify every NEW package against the npm registry before any code runs. - # Self-contained (ADR-0023 §5): imports nothing from the plugin. It once mirrored a tested - # lib/slopcheck.mjs in the unic-archon-dlc Plugin's own tree; #381 deleted that module, so the path - # is history and resolves nowhere in either repository, and this node is the only copy of the gate. + # Self-contained: imports nothing from the plugin. This node is the only copy of the gate. # --------------------------------------------------------------------------------------------- - id: slopcheck depends_on: [bootstrap] when: "$bootstrap.output.status == 'ready'" # always_run: true — a resume must re-verify package.json's CURRENT contents; exit 0 says - # nothing about packages added since the last completed attempt (ADR-0033). + # nothing about packages added since the last completed attempt. always_run: true runtime: bun timeout: 120000 @@ -207,11 +210,11 @@ nodes: # --------------------------------------------------------------------------------------------- # run-build — THE keystone. One fresh session per iteration advances ONE slice by ONE phase. - # Red and green never share a session; the baton is committed artefacts + build-state.json - # (ADR-0012 / ADR-0023). Serial in the dependency order already baked into issues.json. + # Red and green never share a session; the baton is committed artefacts + build-state.json. + # Serial in the dependency order already baked into issues.json. # # The loop's PROCEDURE is not written here: `tdd` and `implement` own it, read by path from - # `.archon/methods/` (ADR-0030/0031). What stays below is Harness-only — the exit-code anti-cheat + # `.archon/methods/`. What stays below is Harness-only — the exit-code anti-cheat # proof, the per-phase commit convention and build-state.json, none of which a Method can supply. # --------------------------------------------------------------------------------------------- - id: run-build @@ -238,8 +241,8 @@ nodes: `.archon/methods/tdd/mocking.md` - `.archon/methods/implement/SKILL.md` Read them at those literal paths and import nothing: an Archon node cannot import anything - from the Plugin (ADR-0023 §5), and the config / `.local` override tiers are retired - (ADR-0031, amended). If a file is missing, that is FATAL for this + from the Plugin, and the config / `.local` override tiers are retired. + If a file is missing, that is FATAL for this node — print "tdd/implement Method not found under .archon/methods/ — run /unic-archon-dlc:setup" and stop. Do not proceed from memory of what these Methods say. @@ -261,11 +264,11 @@ nodes: `docs/agents/issue-tracker.md` is absent. `/unic-archon-dlc:setup` owns that file and `docs/agents/triage-labels.md`, and `setup-matt-pocock-skills` is not a Plugin dependency — a run writes another host's template over the first and reverts the second to a five-role - vocabulary (ADR-0024). Never run it. If either file is missing, say so and stop. + vocabulary. Never run it. If either file is missing, say so and stop. Note what `tdd` says about refactoring: it is NOT part of the red → green loop, it belongs to the review stage. This loop therefore has no REFACTOR phase — /pr-review's `code-review` axis - carries the Fowler smell baseline instead (#281). + carries the Fowler smell baseline instead. ## Step 1 — Load state Read issues.json (an ordered array; blockers come first — honour that order). @@ -299,9 +302,10 @@ nodes: above is a different thing; see the next rule for what happens to it.) NEVER stage or commit build-state.json in a loop iteration. You rewrite it on every one of up to 60 iterations; committing it here would put 60 revisions of a scratch-looking file in the - PR. The `open-pr` node commits it ONCE, at the end — that single commit is the durable proof - of this loop's anti-cheat record (`red_exit`, `red_unexpected_pass`, the per-slice phase - order), which otherwise dies when `/cleanup` prunes this worktree (ADR-0012). + PR. The node that commits the Session artefacts after `report` commits it ONCE. That single + commit is the durable proof of this loop's anti-cheat record (`red_exit`, + `red_unexpected_pass`, the per-slice phase order), which otherwise dies when `/cleanup` + prunes this worktree. After each `git add`, run `git status --porcelain` and confirm the staged set is exactly the file(s) you named. Unstage anything else with `git restore --staged ` before committing. @@ -353,9 +357,11 @@ nodes: # --------------------------------------------------------------------------------------------- # implement-review-precheck — realises `implement`'s closing "once done, use /code-review" step, - # once, after the whole loop. A LOCAL PRE-CHECK THAT POSTS NOTHING: its findings go into report.md - # and nowhere else, so /pr-review keeps sole posting authority (#281 AC 4). Runs `code-review`'s own - # two-axis parallelism internally rather than the Harness re-implementing it (ADR-0030). + # once, after the whole loop. A LOCAL PRE-CHECK THAT POSTS NOTHING, so /pr-review keeps sole posting + # authority. Its prose `findings` go into report.md. Its `status` and `hard_findings` + # are gate inputs that `evidence` reads, and its `notes` reach evidence.json and the pull request + # description. Runs `code-review`'s own two-axis parallelism internally rather than the + # Harness re-implementing it. # --------------------------------------------------------------------------------------------- - id: implement-review-precheck depends_on: [run-build] @@ -365,8 +371,8 @@ nodes: You are the implement-review-precheck node of /build for slug "$bootstrap.output.slug". Read `.archon/methods/code-review/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If the file is missing that + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If the file is missing that is FATAL for this node: print "code-review Method not found at .archon/methods/code-review/SKILL.md — run /unic-archon-dlc:setup" and stop. @@ -374,14 +380,19 @@ nodes: It is a LOCAL PRE-CHECK. Follow the Method's Process steps 1–5 with these answers supplied — the Method asks a live human for two of them and there is no human here, so do NOT ask: - - **Step 1, the fixed point:** the branch point of this build branch — - `git merge-base origin/$bootstrap.output.expected_base HEAD`, falling back to - `git merge-base $bootstrap.output.expected_base HEAD` if `origin/` doesn't resolve. If + - **Step 1, the fixed point:** the branch point of this build branch. A worktree cut with + `--from ` may never have fetched the base, so run + `git fetch origin +refs/heads/$BASE_BRANCH:refs/remotes/origin/$BASE_BRANCH` first. The + explicit destination creates `origin/$BASE_BRANCH` even in a single-branch clone. A failed fetch is not fatal on its own. Then take + `git merge-base origin/$BASE_BRANCH HEAD`, falling back to + `git merge-base $BASE_BRANCH HEAD` if `origin/` doesn't resolve. If NEITHER resolves, do NOT fall back to the branch's first commit — that guarantees a "non-empty" diff that is not the Method's fixed point. Instead print "PRECHECK BLOCKED: - cannot resolve expected_base '$bootstrap.output.expected_base' via origin or local — no - merge-base found", emit that sentence as `findings`, and stop. Otherwise confirm the diff is - non-empty before spawning anything, as the Method requires. + cannot resolve base branch '$BASE_BRANCH' via origin or local — no + merge-base found", emit it as a blocked result (§ Blocked, below), and stop. Otherwise + confirm the diff is non-empty before spawning anything, as the Method requires. If the diff + is empty, print "PRECHECK BLOCKED: the diff against $BASE_BRANCH is empty, so there is + nothing to review", emit it as a blocked result, and stop. - **Step 2, the spec source:** already on disk for this slug — read `$bootstrap.output.artifacts_dir/$bootstrap.output.slug/PRD.md` and `$bootstrap.output.artifacts_dir/$bootstrap.output.slug/issues.json`, plus the tracker item @@ -391,14 +402,14 @@ nodes: - **The "/setup-matt-pocock-skills if `docs/agents/issue-tracker.md` is missing" fallback never applies.** `/unic-archon-dlc:setup` owns that file and `docs/agents/triage-labels.md`, and `setup-matt-pocock-skills` is not a Plugin dependency — a run writes another host's template over - the first and reverts the second to a five-role vocabulary (ADR-0024). Never run it. + the first and reverts the second to a five-role vocabulary. Never run it. ## The tracker contract `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` in this repository are the tracker contract. This node needs the first. Read it before the first tracker call. - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Write no command, subcommand or - flag here and name no provider: the DLC owns the what and none of the how (ADR-0016), and a + flag here and name no provider: the DLC owns the what and none of the how, and a flag table frozen in this file is stale the moment the tool changes. - **Addressing** — its § Addressing names the repository this run acts on. Name that repository explicitly in every call, and derive nothing from a remote URL. @@ -417,29 +428,87 @@ nodes: Run no tracker or PR mutation of any kind: no discussion on a PR, no edit to one, no write call against the tracker's API by any route, no item creation, no role change, no discussion. The read-only issue queries in step 2 are the only tracker traffic allowed here. /pr-review is the - only Box with posting authority; this axis pair is here to inform the build report and the human - at build-pr-gate. Emit the aggregated `## Standards` / `## Spec` text as `findings` for the - report node to fold in. + only Box with posting authority. Emit the aggregated `## Standards` / `## Spec` text as + `findings` for the report node to fold in. + + ## What your fields decide + This node is a gate input. The `evidence` node reads `status`, `hard_findings` and `notes`. + A non-empty `hard_findings` makes the build verdict `fail`, so the run opens no pull request. + A `status` other than `ran` makes the verdict `unresolved` when nothing else failed. `notes` + change no verdict. They reach evidence.json and the pull request description. Your prose + `findings` decides nothing; the report folds it in for the human. + + ## Classify every finding as hard or a note + The Method already separates the classes. Step 4 asks the Standards sub-agent to tell hard + violations from judgement calls, and the Spec sub-agent reports (a) missing or partial + requirements, (b) scope creep and (c) requirements implemented wrong. Put each finding in + exactly one place: + - A Standards finding that breaches a documented standard goes in `hard_findings` with + `category: documented-standard`. Its `citation` quotes the standard's file and the rule. + - A Spec finding of kind (a) goes in `hard_findings` with `category: spec-missing`, and one of + kind (c) with `category: spec-wrong`. Its `citation` quotes the spec line it breaks. + - A baseline smell, any other judgement call, and a Spec finding of kind (b) go in `notes`, + one string each, and never in `hard_findings`. + Each `hard_findings` entry is `{ axis, category, citation, summary }`. `axis` is `standards` + or `spec`, and `summary` says in one sentence what is wrong and where. A hard finding you + cannot cite still goes in `hard_findings`, with an empty `citation`; never move it to `notes`. + Set `status` to `ran` and `blocked_reason` to "" when the review ran. + + ## Blocked + Report `blocked` in each of the three cases this prompt names where the review cannot run: no + merge-base resolves, the `Agent` tool is unavailable, and the diff against the base is empty. + In those three cases emit every field: `status: blocked`, `blocked_reason` set to the sentence + you printed, `findings` set to the same sentence, and `hard_findings` and `notes` as empty + arrays. Every field is required, so omitting one aborts the run instead of reporting the block. + + Report `blocked` too when one of the two sub-agents returns no result, because `evidence` must + not certify a build that one axis never read. Print "PRECHECK BLOCKED: the sub-agent + returned no result, so only one axis was reviewed", with set to `Standards` or `Spec`, + and set `blocked_reason` to that sentence. Keep everything the other axis reported: its + findings in `findings` under its heading, its hard findings in `hard_findings` and its notes + in `notes`. A hard finding from that axis still makes the verdict `fail`. + + Two other stops also review nothing, and they stay as they are. A missing Method prints the + FATAL message above and stops, and a missing tracker contract prints the BLOCKED message above + and stops. Neither emits a blocked result. If the `Agent` tool is unavailable in this node and you cannot spawn the two sub-agents, do NOT silently restructure the review into something else: print "PRECHECK BLOCKED: Agent tool - unavailable in an Archon prompt node — code-review step 4 cannot run as written" and emit that - same sentence as `findings`. That is a real finding about the container, not a detail to route + unavailable in an Archon prompt node — code-review step 4 cannot run as written" and emit it + as a blocked result. That is a real finding about the container, not a detail to route around. output_format: type: object properties: findings: { type: string } - required: [findings] + status: + type: string + enum: [ran, blocked] + blocked_reason: { type: string } + hard_findings: + type: array + items: + type: object + properties: + axis: { type: string } + category: + type: string + enum: [documented-standard, spec-missing, spec-wrong] + citation: { type: string } + summary: { type: string } + required: [axis, category, citation, summary] + notes: + type: array + items: { type: string } + required: [findings, status, blocked_reason, hard_findings, notes] # --------------------------------------------------------------------------------------------- - # verification — full suite + stub scan on the diff. Self-contained prompt (ADR-0023 §5). + # verification — full suite + stub scan on the diff. Self-contained prompt. # --------------------------------------------------------------------------------------------- - id: verification depends_on: [implement-review-precheck] # always_run: true — the exit code is a prose report, not a promise; a resume must re-run the - # suite against the CURRENT tree, never replay a cached PASSED from before a since-applied fix - # (ADR-0033). + # suite against the CURRENT tree, never replay a cached PASSED from before a since-applied fix. always_run: true context: fresh allowed_tools: [Read, Bash, Glob, Grep] @@ -468,8 +537,10 @@ nodes: `e2e` is null this node wants no e2e run at all and reports nothing about it — silence here means "not wanted", which is not the same fact as "could not run". 2. Stub scan — inspect the diff against the base branch - (`git diff --name-only $BASE_BRANCH...HEAD`, source files only: - .mjs/.js/.ts/.py/.rb/.go/.rs). Flag any of these left in changed source: + (`git diff --name-only origin/$BASE_BRANCH...HEAD`, falling back to + `git diff --name-only $BASE_BRANCH...HEAD` if `origin/$BASE_BRANCH` doesn't resolve; source + files only: .mjs/.js/.ts/.py/.rb/.go/.rs). If neither ref resolves, put the stub scan in + `unresolved_needs` and name the ref that failed. Flag any of these left in changed source: - `TODO` / `FIXME` comments - Python `pass` as a whole statement - bare `return;` / `return null` / `return undefined` / `return None` used as a stub @@ -519,10 +590,11 @@ nodes: `unresolved_needs` in every case**, as an empty array when nothing was unresolved — the schema requires the field, so omitting it aborts the run instead of reporting a clean result. The prose is for the human at build-pr-gate. The `evidence` node reads `passed` and `failures` from both - of us, plus `verification`'s three-state `test_outcome`, and withholds unless both verdicts - are green AND that test outcome is "pass"; its own verdict keeps `unresolved` and `fail` - apart rather than calling both red, and - `evidence_policy` then gates on the file that node writes. + of us, plus `verification`'s three-state `test_outcome` and the pre-check's `status` and + `hard_findings`. It records the verdict "pass" only when both verdicts are green, that test + outcome is "pass", and the pre-check ran with no hard finding; its verdict keeps + `unresolved` and `fail` apart rather than calling both red. It writes evidence.json on + every verdict, and only a "pass" verdict opens a pull request. Do not fix anything here; just report. The report and gate nodes consume this verdict. output_format: type: object @@ -544,7 +616,7 @@ nodes: - id: goals-check depends_on: [verification] # always_run: true — same reason as verification: the coverage matrix is a fact about the - # CURRENT diff, and a resume must recompute it rather than reuse a cached verdict (ADR-0033). + # CURRENT diff, and a resume must recompute it rather than reuse a cached verdict. always_run: true context: fresh allowed_tools: [Read, Bash, Glob, Grep] @@ -566,10 +638,11 @@ nodes: `passed` boolean (true only when every criterion is COVERED) and a `failures` array, one short string per MISSING criterion — blockers ONLY, never a non-blocking note. The prose is for the human at build-pr-gate. The `evidence` node reads `passed` and `failures` from both - of us, plus `verification`'s three-state `test_outcome`, and withholds unless both verdicts - are green AND that test outcome is "pass"; its own verdict keeps `unresolved` and `fail` - apart rather than calling both red, and - `evidence_policy` then gates on the file that node writes. + of us, plus `verification`'s three-state `test_outcome` and the pre-check's `status` and + `hard_findings`. It records the verdict "pass" only when both verdicts are green, that test + outcome is "pass", and the pre-check ran with no hard finding; its verdict keeps + `unresolved` and `fail` apart rather than calling both red. It writes evidence.json on + every verdict, and only a "pass" verdict opens a pull request. output_format: type: object properties: @@ -581,18 +654,32 @@ nodes: required: [verdict, passed, failures] # --------------------------------------------------------------------------------------------- - # evidence — writes $ARTIFACTS_DIR/evidence.json ONLY when both verdicts are green; its mere - # PRESENCE is what workflow-level evidence_policy checks before allowing terminal `completed` - # (the engine gates on presence only — the content contract is this node's, ADR-0034). A SCRIPT, - # never a prompt: a prompt that both judges and certifies grades its own homework. - # always_run: true — its exit code is a write op, not a verdict; a resumed run must re-derive - # presence from the CURRENT verification/goals-check output, never trust a stale file (also why - # it deletes any existing evidence.json first, below) (ADR-0033/0034). + # evidence — writes $ARTIFACTS_DIR/evidence.json on EVERY verdict, with the verdict and the + # failures that produced it, and prints the verdict as JSON on stdout. It reads verification, + # goals-check and implement-review-precheck. A pre-check hard finding is a `fail`. A pre-check + # that did not run is `unresolved` when nothing else failed. guard-not-pass and open-pr branch on + # that printed verdict; workflow-level evidence_policy checks only that the file exists. + # A SCRIPT, never a prompt, because a prompt that both judges and certifies grades its + # own homework. + # always_run: true, because its exit code is a write op, not a verdict. A resumed run must + # re-derive the verdict from the CURRENT verification, goals-check and pre-check output, never + # trust a stale file (also why it deletes any existing evidence.json first, below). + # The pre-check arrives through `with:`, never through the script source. A node-output reference + # in the source substitutes raw, so a pre-check string holding a quote would end this script with + # a syntax error. Each `with:` binding reaches the script as an INPUTS_ environment + # variable, so no pre-check text is ever parsed as code. This node's `with:` binds only the four + # fields it reads, never `findings`, so the prose `findings` never counts against the environment + # size limit (measured on 0.12.1). `with:` on a script node needs Archon >= 0.10.0. # --------------------------------------------------------------------------------------------- - id: evidence - depends_on: [verification, goals-check] + depends_on: [implement-review-precheck, verification, goals-check] always_run: true runtime: bun + with: + precheck_status: $implement-review-precheck.output.status + precheck_blocked_reason: $implement-review-precheck.output.blocked_reason + precheck_hard_findings: $implement-review-precheck.output.hard_findings + precheck_notes: $implement-review-precheck.output.notes script: | import { existsSync, rmSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' @@ -604,12 +691,12 @@ nodes: // not throw, so this node logs "wrote evidence.json" while the engine's presence gate reads // the real ARTIFACTS_DIR and finds nothing — a run failing closed with a success line in its // log. Do NOT fall back to the Session dir: the gate reads only this path, so a fallback - // would make that silent case permanent (ADR-0033/0034). + // would make that silent case permanent. const artifactsDir = process.env.ARTIFACTS_DIR if (!artifactsDir) { console.error( 'evidence: ARTIFACTS_DIR is unset or empty — the engine did not export it to this script node. ' + - 'The evidence gate requires Archon >= 0.7.0 (ADR-0033/0034).' + 'The evidence gate requires Archon >= 0.7.0.' ) process.exit(1) } @@ -620,8 +707,8 @@ nodes: // Delete BOTH copies first: a resumed run reuses the same $ARTIFACTS_DIR and the same // Session dir, so a prior attempt's file would certify a red tree if this attempt fails // before writing a fresh one. The mirror needs this most — the engine's presence gate reads - // only the $ARTIFACTS_DIR copy, so a stale mirror survives an engine refusal and is what - // open-pr stages and a reviewer reads. + // only the $ARTIFACTS_DIR copy, so a stale mirror would survive a failed attempt and be what + // commit-session stages and a reviewer reads. if (existsSync(evidencePath)) rmSync(evidencePath) if (existsSync(mirrorPath)) rmSync(mirrorPath) @@ -632,7 +719,7 @@ nodes: // (Never write a placeholder node id anywhere in this body, comments included. The DAG // loader reads every dollar-prefixed node-output token here as a real edge, so a placeholder // makes the whole Box fail to load with "references unknown node". A comment on this very - // spot once did exactly that. Nothing guards it now — read for it (#381).) + // spot once did exactly that. Nothing guards it now — read for it.) const verificationPassed = $verification.output.passed const goalsCheckPassed = $goals-check.output.passed const verificationFailures = $verification.output.failures @@ -643,26 +730,81 @@ nodes: // `passed: true` with a non-empty `failures` array write a certificate whose own body listed // the failures. Both prompts define `failures` as blockers only, so a non-empty list is a // red verdict whatever the boolean says. This does not make the gate independent of the - // judge — only re-running the suite would, and ADR-0023 §5 rules that out (ADR-0034). + // judge. Only re-running the suite would do that. const verificationGreen = verificationPassed === true && verificationFailures.length === 0 const goalsCheckGreen = goalsCheckPassed === true && goalsCheckFailures.length === 0 // A check that could not run is `unresolved`, and unresolved is never a pass: an absent result - // and a green result are not the same fact (#430). `test` is the only need whose outcome is + // and a green result are not the same fact. `test` is the only need whose outcome is // mandatory here — every other need reports and none of them holds this gate. const testOutcome = '$verification.output.test_outcome' const unresolvedNeeds = $verification.output.unresolved_needs - // Three states in precedence order: a real failure is `fail`; a floor check that could not run - // is `unresolved`; only two green verdicts with a passing test are `pass`. The unresolved branch - // is tested FIRST because `verificationGreen` is already false whenever the test is unresolved — - // verification sets `passed` false for it — so testing green first would report an absent result - // as a failure. + // The pre-check's four fields, from the environment (see the header comment). A string field + // arrives as its raw text and an array field as JSON text (measured on 0.12.1). Read them + // defensively, because what this node cannot read is never a pass. Each read that is unset, + // does not parse, or gives a value of the wrong type adds its cause to `readErrors`, which goes + // to stderr and to `precheck_read_error`. `status` and `hard_findings` are self-reported by a + // prompt, exactly as `passed` is above. + const readErrors = [] + const readJson = (name, text) => { + if (text === undefined) { + readErrors.push(`${name} unset`) + return undefined + } + try { + return JSON.parse(text) + } catch (err) { + readErrors.push(`${name} is not JSON (${err.message}). Its text is ${text}`) + return undefined + } + } + const typeName = (value) => + value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value === 'object' ? 'an object' : `a ${typeof value}` + const statusText = process.env.INPUTS_PRECHECK_STATUS + if (statusText === undefined) readErrors.push('INPUTS_PRECHECK_STATUS unset') + else if (statusText === '') readErrors.push('INPUTS_PRECHECK_STATUS empty') + else if (statusText !== 'ran' && statusText !== 'blocked') + readErrors.push(`INPUTS_PRECHECK_STATUS is ${JSON.stringify(statusText)}, which is neither "ran" nor "blocked"`) + const precheckStatus = statusText ? statusText : null + const blockedReasonText = process.env.INPUTS_PRECHECK_BLOCKED_REASON + if (blockedReasonText === undefined) readErrors.push('INPUTS_PRECHECK_BLOCKED_REASON unset') + const precheckBlockedReason = blockedReasonText ?? '' + const hardFindingsText = process.env.INPUTS_PRECHECK_HARD_FINDINGS + const hardFindingsField = readJson('INPUTS_PRECHECK_HARD_FINDINGS', hardFindingsText) + if (hardFindingsField !== undefined && !Array.isArray(hardFindingsField)) + readErrors.push(`INPUTS_PRECHECK_HARD_FINDINGS is ${typeName(hardFindingsField)}, not an array. Its text is ${hardFindingsText}`) + const notesText = process.env.INPUTS_PRECHECK_NOTES + const notesField = readJson('INPUTS_PRECHECK_NOTES', notesText) + if (notesField !== undefined && !Array.isArray(notesField)) + readErrors.push(`INPUTS_PRECHECK_NOTES is ${typeName(notesField)}, not an array. Its text is ${notesText}`) + const precheckReadError = readErrors.join('; ') + // Every entry counts. An entry with an empty `citation` or a `category` outside the enum is + // still a hard finding, never dropped, because when in doubt the verdict stays red. A value + // that is not an array, but is present, counts as one entry for the same reason. + const hardFindings = Array.isArray(hardFindingsField) + ? hardFindingsField + : hardFindingsField === undefined || hardFindingsField === null + ? [] + : [hardFindingsField] + const precheckNotes = Array.isArray(notesField) ? notesField : [] + // The pre-check reviewed the code only when it says `ran` and both arrays it must emit are + // arrays. Anything else, `blocked` included, is a review that did not happen. + const precheckRan = precheckStatus === 'ran' && Array.isArray(hardFindingsField) && Array.isArray(notesField) + + // Three states in precedence order: a real failure is `fail`; a check that could not run is + // `unresolved`; only two green verdicts with a passing test and a pre-check that ran clean are + // `pass`. A pre-check hard finding is a real failure, so it is tested first. The unresolved + // test branch comes before the green one because `verificationGreen` is already false whenever + // the test is unresolved. Verification sets `passed` false for it, so testing green first + // would report an absent result as a failure. let verdictState - if (testOutcome === 'unresolved' && verificationFailures.length === 0 && goalsCheckGreen) { + if (hardFindings.length > 0) { + verdictState = 'fail' + } else if (testOutcome === 'unresolved' && verificationFailures.length === 0 && goalsCheckGreen) { verdictState = 'unresolved' } else if (verificationGreen && goalsCheckGreen && testOutcome === 'pass') { - verdictState = 'pass' + verdictState = precheckRan ? 'pass' : 'unresolved' } else { verdictState = 'fail' } @@ -671,20 +813,21 @@ nodes: // to STDOUT is its whole output. That makes stdout the machine channel and nothing else may go // there — every human line below uses console.error, which the run log still shows. Mixing one // progress line into stdout leaves a reader a string that is not JSON, and the reader is a - // `when:` expression with nowhere to put a parse failure. Nothing reads it yet: the `when:` that - // will consume it belongs to the ticket that makes this file unconditional, and a field with no - // reader here is that hand-off, not an oversight (#430 criterion 8). + // `when:` expression with nowhere to put a parse failure. Three readers: the `when:` of + // guard-not-pass, the `when:` of open-pr, and the cancel message of guard-not-pass, which names + // the verdict. Both `when:` expressions compare the `verdict` field with 'pass' and nothing + // else, so `fail` and `unresolved` take the same branch. console.log(JSON.stringify({ verdict: verdictState, test_outcome: testOutcome, unresolved_needs: unresolvedNeeds })) - if (verdictState !== 'pass') { - console.error(`evidence: withholding — verdict=${verdictState} verification(passed=${verificationPassed}, failures=${verificationFailures.length}, test=${testOutcome}) goals-check(passed=${goalsCheckPassed}, failures=${goalsCheckFailures.length})`) - if (unresolvedNeeds.length > 0) console.error(`evidence: unresolved needs — ${unresolvedNeeds.join(', ')}`) - process.exit(0) - } + console.error(`evidence: verdict=${verdictState} verification(passed=${verificationPassed}, failures=${verificationFailures.length}, test=${testOutcome}) goals-check(passed=${goalsCheckPassed}, failures=${goalsCheckFailures.length}) precheck(status=${precheckStatus}, hard=${hardFindings.length}, notes=${precheckNotes.length})`) + if (unresolvedNeeds.length > 0) console.error(`evidence: unresolved needs — ${unresolvedNeeds.join(', ')}`) + if (precheckReadError) console.error(`evidence: could not read the pre-check. ${precheckReadError}`) - // Both arrays are empty by construction here — the guard above returns on either being - // non-empty. They stay in the artefact so a reviewer reading evidence.json sees the two - // fields the gate checked rather than having to know which fields it consulted. + // Written on EVERY verdict, so the file the engine demands says what failed. The two failure + // arrays and `hard_findings` are the reason for a `fail`. `test_outcome` with + // `unresolved_needs`, and `precheck_status` with `precheck_blocked_reason`, are the reason for + // an `unresolved`; a null `precheck_status` means this node could not read the pre-check. + // Never return before this write. const evidence = { slug: '$bootstrap.output.slug', verdict: verdictState, @@ -692,12 +835,18 @@ nodes: unresolved_needs: unresolvedNeeds, verification_failures: verificationFailures, goals_check_failures: goalsCheckFailures, + precheck_status: precheckStatus, + precheck_blocked_reason: precheckBlockedReason, + hard_findings: hardFindings, + precheck_notes: precheckNotes, + precheck_read_error: precheckReadError, } writeFileSync(evidencePath, JSON.stringify(evidence, null, 2)) console.error(`evidence: wrote ${evidencePath} ✓`) - // Mirror into the repo-relative Session dir so a reviewer (and open-pr) can see it — - // $ARTIFACTS_DIR resolves OUTSIDE the repo tree and is gone once /cleanup prunes the worktree. + // Mirror into the repo-relative Session dir so a reviewer, and commit-session, which stages it, + // can see it. $ARTIFACTS_DIR resolves OUTSIDE the repo tree and is gone once /cleanup prunes + // the worktree. mkdirSync(sessionDir, { recursive: true }) copyFileSync(evidencePath, mirrorPath) console.error(`evidence: mirrored to ${mirrorPath} ✓`) @@ -716,13 +865,26 @@ nodes: list, never as prose in another section: - one line per need in `$verification.output.unresolved_needs`, naming the need; - `$bootstrap.output.install_report`, verbatim. - Write "every declared check ran" when `unresolved_needs` is empty. A check that could not + - when `precheck_status` in + `$bootstrap.output.artifacts_dir/$bootstrap.output.slug/evidence.json` is not `ran`, + or `precheck_read_error` there is not empty, one line for the code-review pre-check. + Name the status that `evidence` read, or write "no status" when it is null. Then add + `precheck_blocked_reason` when it is not empty, and `precheck_read_error` when it is + not empty. + Write "every declared check ran" only when `unresolved_needs` is empty, `precheck_status` + is `ran` and `precheck_read_error` is empty. A check that could not run is the fact a reader skims past when it is a sentence inside a paragraph, which is why it is a block of its own and why it goes above what was built. 2. What Was Built — 3-5 bullets referencing each issue id + title implemented this cycle. - If goals-check reported MISSING criteria, lead with "⚠ goals-check found N uncovered - criteria — see matrix below." - 3. Goals-Check Coverage Matrix — paste $goals-check.output.verdict verbatim. + Read `hard_findings` from + `$bootstrap.output.artifacts_dir/$bootstrap.output.slug/evidence.json`. If it holds any + entry, lead with "⚠ the code-review pre-check found N hard findings, so the verdict + is fail", then list each entry's `category`, `citation` and `summary`. If goals-check + reported MISSING criteria, write "⚠ goals-check found N uncovered criteria. See the + matrix below." after the pre-check warning, or first when there is none. + 3. Goals-Check Coverage Matrix — paste $goals-check.output.verdict verbatim. Then add this + sentence. "COVERED means a test and an implementation exist, not that either is correct. + Any hard findings from the code-review pre-check are listed in section 2." 4. Test Outcomes — open with `test: $verification.output.test_outcome`, then the counts, the coverage figure and any e2e result from $verification.output.verdict; note skipped/flaky. A need reported "unresolved" is named in section 1; this section carries what ran. @@ -738,30 +900,22 @@ nodes: Print: "report written to $bootstrap.output.artifacts_dir/$bootstrap.output.slug/report.md". # --------------------------------------------------------------------------------------------- - # open-pr — always runs (even in AFK mode); the gate below only PAUSES in hitl mode. + # commit-session — runs on EVERY verdict. Stages the named paths and commits them, so the work and + # the Session artefacts are committed before the verdict decides anything. A prompt, + # because telling the loop's uncommitted source and test files from scratch files is a judgement. # --------------------------------------------------------------------------------------------- - - id: open-pr + - id: commit-session depends_on: [report] context: fresh allowed_tools: [Read, Bash] prompt: | - You are the open-pr node of /build for slug "$bootstrap.output.slug". + You are the commit-session node of /build for slug "$bootstrap.output.slug". You stage and + commit. You do not push and you do not open a pull request. push-branch pushes, and open-pr + opens the pull request only on a "pass" verdict. Constants: SESSION = $bootstrap.output.artifacts_dir/$bootstrap.output.slug/ - ## The tracker contract - `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` in this repository are the - tracker contract. This node needs the first. Read it before the first tracker call. - - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that - server's own current tool list and build every call from it. Write no command, subcommand or - flag here and name no provider: the DLC owns the what and none of the how (ADR-0016), and a - flag table frozen in this file is stale the moment the tool changes. - - **Addressing** — its § Addressing names the repository this run acts on. Name that repository - explicitly in every call, and derive nothing from a remote URL. - If the file is absent, STOP and print "BLOCKED: this repository has no tracker contract at - docs/agents/issue-tracker.md", and act on nothing. - 1. Build the stage list. It is EXACTLY these named paths — an explicit list, never "everything changed by the build": - each source file the loop wrote that is still uncommitted, by name @@ -770,9 +924,9 @@ nodes: - SESSION/issues.json - SESSION/report.md - SESSION/build-state.json - - SESSION/evidence.json — ONLY if that file exists. The `evidence` node withholds it on - a red verdict, and `git add` on a path that does not exist fails with exit 128 and - takes the whole staging step with it. Check first; if it is absent, skip it and say so. + - SESSION/evidence.json. The `evidence` node writes it on every verdict, so it is + always there. If it is absent, STOP and print "BLOCKED: SESSION/evidence.json is + missing", and commit nothing. - each new `docs/adr/NNNN-*.md` the report node drafted, by name Derive the source/test entries by reading `git status --porcelain` and picking out the source and test files — then add them one at a time by name. A path already committed by a @@ -781,7 +935,7 @@ nodes: SESSION/build-state.json is committed HERE and ONLY here — the loop never commits it. This one commit is the durable proof of the loop's anti-cheat record (`red_exit`, `red_unexpected_pass`, the per-slice phase order), which otherwise dies when `/cleanup` - prunes this worktree (ADR-0012). + prunes this worktree. ## Staging — named paths only Run one `git add ` per path in the list above. NEVER `git add -A`, `git add .` or @@ -789,15 +943,113 @@ nodes: sweeps in scratch files it cannot see the origin of. NEVER stage `pr-body.md`, `*.tmp.md`, `*.scratch.md`, or anything under `$ARTIFACTS_DIR` — Archon's per-run directory, which resolves OUTSIDE the repo tree under - `~/.archon/workspaces//artifacts/`. (SESSION above is repo-relative and IS staged; write - the PR body inline rather than to a file, so there is no body file to leak.) + `~/.archon/workspaces//artifacts/`. (SESSION above is repo-relative and IS staged.) Then run `git status --porcelain` and confirm every staged entry is one of the named paths. Unstage anything else with `git restore --staged ` and say what you unstaged. 2. Commit the staged set: `build($bootstrap.output.slug): session artefacts`. - 3. Open a PR against the repository the tracker contract above names, targeting - `$bootstrap.output.expected_base`: + 3. Print the commit hash and the list of paths it holds. + + # --------------------------------------------------------------------------------------------- + # push-branch — runs on EVERY verdict, and pushes the branch commit-session committed on, to + # `origin`. A bash node, so a refusal or a failed push exits non-zero. The node fails, + # guard-not-pass and open-pr are both skipped, and the run ends `failed`. That is a different fact + # from a red verdict, because no work reached the remote. Before the push it refuses four cases: + # an empty $BASE_BRANCH, HEAD detached or on the run's base branch, a branch name that cannot go + # into the JSON below, and a Session artefact that is missing at HEAD or differs from the working + # tree. It checks the five Session paths commit-session stages: PRD.md, issues.json, report.md, + # build-state.json and evidence.json. A miss has two causes. commit-session did not commit, which a + # prompt node cannot report by failing. Or a resumed run re-ran `evidence` after the commit and the + # rewritten evidence.json differs, which needs a new run. It prints `git status --porcelain` to stderr before the push. Archon 0.12.1 + # substitutes $BASE_BRANCH in a bash body. Its stdout is JSON only, read field + # by field by guard-not-pass and open-pr. git's own lines go to stderr. + # --------------------------------------------------------------------------------------------- + - id: push-branch + depends_on: [commit-session] + bash: | + set -e + if [ -z "$BASE_BRANCH" ]; then + echo "push-branch: the run's base branch is empty, so this node cannot tell whether HEAD is on it" >&2 + exit 1 + fi + branch=$(git rev-parse --abbrev-ref HEAD) + if [ "$branch" = "HEAD" ]; then + echo "push-branch: HEAD is detached, so there is no branch to push" >&2 + exit 1 + fi + if [ "$branch" = "$BASE_BRANCH" ]; then + echo "push-branch: HEAD is on the base branch $branch, so a push would put the work on it directly" >&2 + exit 1 + fi + case "$branch" in + *'"'* | *'\'*) + echo "push-branch: the branch name holds a double quote or a backslash, so it cannot be reported as JSON" >&2 + exit 1 + ;; + esac + session=$bootstrap.output.artifacts_dir/$bootstrap.output.slug + for name in PRD.md issues.json report.md build-state.json evidence.json; do + file=$(printf '%s' "$session/$name" | tr -s /) + if ! git cat-file -e "HEAD:$file" 2>/dev/null || ! git diff --quiet HEAD -- "$file"; then + echo "push-branch: $file at HEAD is missing or differs from the working tree. Either commit-session did not commit it, or this is a resumed run that re-ran evidence after the commit, which needs a new run" >&2 + exit 1 + fi + done + git status --porcelain >&2 + git push -u origin "$branch" >&2 + printf '{"branch":"%s","commit":"%s"}\n' "$branch" "$(git rev-parse HEAD)" + + # --------------------------------------------------------------------------------------------- + # guard-not-pass — the sibling of open-pr. Its `when` is the exact negation of open-pr's, and both + # compare the verdict with 'pass' only, so `fail` and `unresolved` both end here and no third + # branch exists. It opens no pull request and writes nothing to the tracker. It ends the run + # `cancelled`, with the work already on the branch push-branch pushed. + # --------------------------------------------------------------------------------------------- + - id: guard-not-pass + depends_on: [push-branch] + when: "$evidence.output.verdict != 'pass'" + cancel: > + /build verdict is $evidence.output.verdict, so /build opened no pull request. The work is on + branch $push-branch.output.branch at commit $push-branch.output.commit. On that branch, read + $bootstrap.output.artifacts_dir/$bootstrap.output.slug/report.md for what failed and which + checks could not run, and $bootstrap.output.artifacts_dir/$bootstrap.output.slug/evidence.json + for the failures the verdict rests on. + + # --------------------------------------------------------------------------------------------- + # open-pr — the sibling of guard-not-pass. It runs only on a "pass" verdict. It sets no trigger + # rule. On any other verdict it is skipped, and so is build-pr-gate below. It stages and commits + # nothing. It opens the pull request from the pushed branch and links it. + # --------------------------------------------------------------------------------------------- + - id: open-pr + depends_on: [push-branch] + when: "$evidence.output.verdict == 'pass'" + context: fresh + allowed_tools: [Read, Bash] + prompt: | + You are the open-pr node of /build for slug "$bootstrap.output.slug". + + Constants: + SESSION = $bootstrap.output.artifacts_dir/$bootstrap.output.slug/ + BRANCH = $push-branch.output.branch + + commit-session committed the work and push-branch pushed BRANCH. Stage nothing, commit nothing + and push nothing here. + + ## The tracker contract + `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` in this repository are the + tracker contract. This node needs the first. Read it before the first tracker call. + - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that + server's own current tool list and build every call from it. Write no command, subcommand or + flag here and name no provider: the DLC owns the what and none of the how, and a + flag table frozen in this file is stale the moment the tool changes. + - **Addressing** — its § Addressing names the repository this run acts on. Name that repository + explicitly in every call, and derive nothing from a remote URL. + If the file is absent, STOP and print "BLOCKED: this repository has no tracker contract at + docs/agents/issue-tracker.md", and act on nothing. + + 1. Open a PR against the repository the tracker contract above names, from BRANCH, targeting + `$BASE_BRANCH`: Title: "build($bootstrap.output.slug): implementation" Body: ## Summary (TDD implementation for the slug; goals-check status; report link) ## Review Checklist @@ -806,8 +1058,14 @@ nodes: - [ ] Coverage meets the configured threshold - [ ] Tech-debt section in report.md is accurate - [ ] Drafted ADRs (if any) are ready to record - - 4. Link the PR to its work. SESSION/issues.json carries a `tracker_id` for each slice + ## Pre-check notes + One bullet per entry of `precheck_notes` in SESSION/evidence.json, or "none" + when the array is empty. Read the file, not the pre-check's prose. List no + hard finding here. A run with one has the verdict `fail` and never reaches + this node. + Write the PR body inline rather than to a file, so there is no body file to leak. + + 2. Link the PR to its work. SESSION/issues.json carries a `tracker_id` for each slice `/tickets` published. Link every one of those items to this PR through the registered skill's own link capability — a pull request and a tracker item each carry a links collection, and that collection is discoverable. Name no text token: a token written here @@ -815,20 +1073,27 @@ nodes: the ids in the PR body under `## Work items` and say the links were not created. A slice with no `tracker_id` is reported, never invented. - 5. Print the PR URL and the repository it was opened against. + If the tracker tool cannot open the PR, STOP and print "BLOCKED: the pull request could not be + opened" followed by the tool's error, verbatim. push-branch pushed BRANCH to `origin`, so a + contract repository that is not `origin`, such as a fork setup, is one cause. Open nothing else. + + 3. Print the PR URL and the repository it was opened against. # --------------------------------------------------------------------------------------------- - # build-pr-gate — HITL by default; skipped when gates.build == afk (PR already open above). + # build-pr-gate — runs only after open-pr, so only on a "pass" verdict. HITL by default; skipped + # when gates.build == afk, and the pull request open-pr opened stays open. # On reject: re-verify-and-fix from the reviewer's feedback, then re-pause (not a full rebuild). # --------------------------------------------------------------------------------------------- - id: build-pr-gate depends_on: [open-pr] when: "$bootstrap.output.gate == 'hitl'" + # Archon 0.12.1 substitutes node outputs in an approval message but not $BASE_BRANCH, so + # this message names the base through bootstrap's message, which starts with it. approval: message: > - Build implementation, goals-check matrix, and report are ready. Review the PR opened against - $bootstrap.output.expected_base. Approve to finish, or reject with feedback to trigger a - verify-and-fix pass. + Build implementation, goals-check matrix, and report are ready. $bootstrap.output.message + Review the PR opened against that base branch. Approve to finish, or reject with feedback to + trigger a verify-and-fix pass. on_reject: prompt: | The reviewer rejected the build PR with this feedback: diff --git a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-explore.yaml b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-explore.yaml index 1f3c633d..cb2e1e17 100644 --- a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-explore.yaml +++ b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-explore.yaml @@ -4,19 +4,19 @@ description: > / pitfalls) → synthesize into //findings.md → one AFK spike (verdicts) → file a spike ticket → optionally preserve the spike code on a branch. findings.md is the /specs baton: its Integrated Brief carries three named lenses (Domain Model / Established Decisions / Prior Research) - that /specs' load-context reads verbatim. Never required; a precursor to /specs, off the main line - (PLAN decision #3/#8). Ported to the key-discriminated node schema (ADR-0011); self-contained prompt - nodes, no plugin-lib import (ADR-0023 §5); artefacts at // (ADR-0015). Interactive + that /specs' load-context reads verbatim. Never required; a precursor to /specs, off the main line. + Ported to the key-discriminated node schema; self-contained prompt + nodes, no plugin-lib import; artefacts at //. Interactive prototyping stays Matt's /prototype skill (referenced, never invoked — nodes have no live - conversation). See ADR-0029. + conversation). -# Required at the workflow level so the spike-branch approval message reaches the user (ADR-0011). +# Required at the workflow level so the spike-branch approval message reaches the user. interactive: true nodes: # --------------------------------------------------------------------------------------------- # bootstrap — parse the slug from $ARGUMENTS, read config. A prompt node (NOT a script) so it needs - # no plugin-lib import and no $CLAUDE_PLUGIN_ROOT (ADR-0023 §5): the agent reads + # no plugin-lib import and no $CLAUDE_PLUGIN_ROOT: the agent reads # .archon/unic-dlc.config.yaml with its own tools. No PRD precondition — /explore WRITES findings.md, # it is not a consumer of a prior baton. # --------------------------------------------------------------------------------------------- @@ -63,7 +63,7 @@ nodes: required: [status, slug, artifacts_dir, gate, branching, message] # --------------------------------------------------------------------------------------------- - # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). One guard, + # guard — expected precondition failures cancel cleanly. One guard, # because there is no repository to resolve: `docs/agents/issue-tracker.md` names it. # --------------------------------------------------------------------------------------------- - id: guard-not-ready @@ -89,8 +89,8 @@ nodes: Goal: understand the technology stack of this project. Read `.archon/methods/research/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If it is missing that is FATAL for this node: + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If it is missing that is FATAL for this node: print "research Method not found at .archon/methods/research/SKILL.md — run /unic-archon-dlc:setup" and stop. The Method opens by spinning up a background agent to do the reading; this node's own fresh-context execution IS that agent, so do not spawn another. Apply its @@ -126,8 +126,8 @@ nodes: Goal: identify the key features and capabilities of this project. Read `.archon/methods/research/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If it is missing that is FATAL for this node: + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If it is missing that is FATAL for this node: print "research Method not found at .archon/methods/research/SKILL.md — run /unic-archon-dlc:setup" and stop. The Method opens by spinning up a background agent to do the reading; this node's own fresh-context execution IS that agent, so do not spawn another. Apply its @@ -166,8 +166,8 @@ nodes: Goal: understand the structural and architectural decisions of this project. Read `.archon/methods/research/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If it is missing that is FATAL for this node: + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If it is missing that is FATAL for this node: print "research Method not found at .archon/methods/research/SKILL.md — run /unic-archon-dlc:setup" and stop. The Method opens by spinning up a background agent to do the reading; this node's own fresh-context execution IS that agent, so do not spawn another. Apply its @@ -192,7 +192,7 @@ nodes: required: [findings] # --------------------------------------------------------------------------------------------- - # research-pitfalls — parallel research dimension 4 of 4. (HANDOFF.md is retired — ADR-0013 — so it + # research-pitfalls — parallel research dimension 4 of 4. (HANDOFF.md is retired, so it # is no longer a source here.) # --------------------------------------------------------------------------------------------- - id: research-pitfalls @@ -206,8 +206,8 @@ nodes: Goal: surface known pitfalls, edge cases, and areas of technical debt. Read `.archon/methods/research/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If it is missing that is FATAL for this node: + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If it is missing that is FATAL for this node: print "research Method not found at .archon/methods/research/SKILL.md — run /unic-archon-dlc:setup" and stop. The Method opens by spinning up a background agent to do the reading; this node's own fresh-context execution IS that agent, so do not spawn another. Apply its @@ -231,9 +231,9 @@ nodes: # --------------------------------------------------------------------------------------------- # synthesize — depends on all four research nodes; writes //findings.md directly - # with the Write tool (ADR-0023 §5 — no findings-writer lib). The Integrated Brief is written as THREE + # with the Write tool (no findings-writer lib). The Integrated Brief is written as THREE # explicitly-named lenses (Domain Model / Established Decisions / Prior Research) that /specs' - # load-context reads verbatim (ADR-0029) — this is the /explore → /specs contract. + # load-context reads verbatim — this is the /explore → /specs contract. # --------------------------------------------------------------------------------------------- - id: synthesize depends_on: [research-stack, research-features, research-architecture, research-pitfalls] @@ -296,8 +296,8 @@ nodes: # spike — one AFK experiment pass. Build/measure a throwaway experiment where AFK-feasible, else # reason it through; assign exactly one verdict per unknown and APPEND a "## Spike verdicts" section # to findings.md (Edit/Write — no spike-verdicts lib). Interactive prototyping is NOT done here — it - # is Matt's /prototype skill, which needs a live conversation and so cannot run in an Archon node - # (ADR-0017/0029). This node only references it. + # is Matt's /prototype skill, which needs a live conversation and so cannot run in an Archon node. + # This node only references it. # --------------------------------------------------------------------------------------------- - id: spike depends_on: [synthesize] @@ -351,7 +351,7 @@ nodes: # durable output survives a "discard" (the approval schema only supports on_reject, which # cancels/reworks — it never proceeds past a reject). Reads the tracker contract at # docs/agents/issue-tracker.md and docs/agents/triage-labels.md, which is where every host word - # lives (ADR-0024, amended). + # lives. # --------------------------------------------------------------------------------------------- - id: spike-ticket depends_on: [spike] @@ -371,7 +371,7 @@ nodes: - **Access** — `issue-tracker.md` § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Write no command, subcommand or flag here and name no provider: the DLC owns the what and none of the - how (ADR-0016), and a flag table frozen in this file is stale the moment the tool changes. + how, and a flag table frozen in this file is stale the moment the tool changes. - **Addressing** — its § Addressing names the repository this run acts on. Name that repository explicitly in every call, and derive nothing from a remote URL. - **Work-item scope** — its § Work-item scope names the one filter every search applies. Apply diff --git a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-pr-review.yaml b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-pr-review.yaml index b3868cca..e5ca24d0 100644 --- a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-pr-review.yaml +++ b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-pr-review.yaml @@ -5,21 +5,21 @@ description: > is where refactoring lives now that /build's loop has no REFACTOR phase) and Spec (does the diff match what was asked for) — as parallel sub-agents, grounded in the Intent Brief composed once in prep from the linked work items, the referenced docs pages, the PR description, and PRD.md. Synthesises this run's - findings, reconciles them against the prior iteration (new / still-present / fixed / regressed), then — + findings, reconciles them against the prior iteration (new / still-present / fixed / unverified / regressed), then — after a config-gated human confirm — posts/updates one structured summary discussion plus per-line discussions on the current PR, through the tracker its contract at `docs/agents/issue-tracker.md` names. Harvests unic-pr-review's review - learnings without its host-specific code or any dependency (ADR-0016/0017). Ported to the key-discriminated node - schema (ADR-0011); inherits /build's self-contained-node conventions (ADR-0023 §5). This Box holds the - only posting authority in the lifecycle. See ADR-0026, ADR-0030, ADR-0031. + learnings without its host-specific code or any dependency. Ported to the key-discriminated node + schema; inherits /build's self-contained-node conventions. This Box holds the + only posting authority in the lifecycle. -# Required at the workflow level so the review-gate approval message reaches the user (ADR-0011). +# Required at the workflow level so the review-gate approval message reaches the user. interactive: true nodes: # --------------------------------------------------------------------------------------------- # bootstrap — parse the slug from $ARGUMENTS, read config. A prompt node (NOT a script) so it needs - # no plugin-lib import and no $CLAUDE_PLUGIN_ROOT (ADR-0023 §5): it reads .archon/unic-dlc.config.yaml + # no plugin-lib import and no $CLAUDE_PLUGIN_ROOT. It reads .archon/unic-dlc.config.yaml # with its own tools. Posting is AFK-safe; the human checkpoint here is the review-gate, and the real # merge gate lives in /qa. # --------------------------------------------------------------------------------------------- @@ -40,12 +40,14 @@ nodes: `pr-review.confidence_threshold` (default 60) `pr-review.inline_comments` (default true) `docs.type` / `docs.access` (for intent composition from docs) - `project.branching` (default "gitflow") the whole `sdlc_needs` block 3. If slug and config are both present → status "ready". (There is no PRD precondition: intent is composed from whatever sources resolve — see prep.) - Compute `expected_base` from branching: "develop" when branching == "gitflow", else "main". + This run's base branch is `$BASE_BRANCH`. Archon resolves it before this node starts, from + `--base`, then `worktree.baseBranch` in `.archon/config.yaml`, then the codebase default. Read no + base branch from the config. prep diffs against the PR's own target branch when there is a PR, + and against `$BASE_BRANCH` only when there is none. ## The `sdlc_needs` block — a key names a need, never a tool Every key in it names a NEED of this project's development process. `test` names the need; @@ -60,11 +62,11 @@ nodes: arrives null stays null. What happens next depends on the node: one that wants that need reports an **unresolved** check, one that does not want it reports nothing at all, and neither fabricates a command — a guessed command reports a result that means nothing. - **This Box runs no build, lint, type-check or test command by instruction** (ADR-0026): it - carries the block so that `reconcile` can tell the reader which checks the project has and this - review did not run. `install` is the exception and is not a check — you run it below, once, - because a sub-agent that decides on its own that it needs to run something should fail on that - decision rather than on which worktree the run drew (ADR-0037 §6). + **This Box runs no build, lint, type-check or test command by instruction**. It + carries the block so that `reconcile` can tell the reader which checks the project declares and + that this Box instructed none of them. `install` is the exception and is not a check. You run it + below, once, because a sub-agent that decides on its own that it needs to run something should + fail on that decision rather than on which worktree the run drew. ## Install once, and say what you did One Archon worktree serves this whole run and may or may not already carry this project's @@ -85,9 +87,9 @@ nodes: ALWAYS emit EVERY required field, including in the no-slug and no-config branches, so the object validates before a guard can cancel. When a value cannot be read, fall back to: slug "" (if no-slug), artifacts_dir "workflows", gate "hitl", confidence_threshold 60, inline_comments - "true", branching "gitflow", expected_base "develop". `inline_comments` is emitted as the STRING - "true"/"false" (enum) so downstream `when` comparisons are reliable. Set `message` to a - human-readable reason for any non-ready status. + "true". `inline_comments` is emitted as the STRING "true"/"false" (enum) so downstream `when` + comparisons are reliable. In EVERY branch, start `message` with "Base branch: $BASE_BRANCH." + and follow it with a human-readable reason for any non-ready status. output_format: type: object properties: @@ -103,7 +105,6 @@ nodes: inline_comments: type: string enum: ['true', 'false'] - branching: { type: string } sdlc_needs: type: object properties: @@ -118,12 +119,11 @@ nodes: coverage: { type: [string, 'null'] } required: [install, build, test, e2e, lint, format, typecheck, dev, coverage] install_report: { type: string } - expected_base: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, confidence_threshold, inline_comments, branching, sdlc_needs, install_report, expected_base, message] + required: [status, slug, artifacts_dir, gate, confidence_threshold, inline_comments, sdlc_needs, install_report, message] # --------------------------------------------------------------------------------------------- - # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). One guard, + # guard — expected precondition failures cancel cleanly. One guard, # because there is no repository to resolve: `docs/agents/issue-tracker.md` names it. # --------------------------------------------------------------------------------------------- - id: guard-not-ready @@ -138,12 +138,12 @@ nodes: # prep — the shared context builder. Identifies the open PR, computes the diff, categorises the # changed files, composes ONE Intent Brief from every available source, detects the prior review # iteration, and writes everything to //pr-review/ so the review node reads it - # off disk (fresh nodes share no memory — ADR-0012/0015). Composing intent once and injecting it into - # both review axes is the primary quality lever (ADR-0026): each axis judges the diff against what the + # off disk (fresh nodes share no memory). Composing intent once and injecting it into + # both review axes is the primary quality lever, because each axis judges the diff against what the # PR is SUPPOSED to do. # # The five changed-file category booleans no longer gate node spawning — the seven conditionally - # spawned aspect nodes collapsed into one always-running `review` node (#281). They stay because they + # spawned aspect nodes collapsed into one always-running `review` node. They stay because they # are durable diff metadata in prep.json that a reader (and a future spawn decision) can use; nothing # reads them today. Retire them or find them a consumer in a follow-up, not in a rewiring slice. # --------------------------------------------------------------------------------------------- @@ -166,7 +166,7 @@ nodes: tracker contract. This node needs the first. Read it before the first tracker call. - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Write no command, subcommand or - flag here and name no provider: the DLC owns the what and none of the how (ADR-0016), and a + flag here and name no provider: the DLC owns the what and none of the how, and a flag table frozen in this file is stale the moment the tool changes. - **Addressing** — its § Addressing names the repository this run acts on. Name that repository explicitly in every call, and derive nothing from a remote URL. @@ -176,13 +176,37 @@ nodes: ## 1. Identify the open PR + read its description Ask the tracker for the open PR whose source branch is the current branch on that repository, and - read its number, title, description body, source branch and target branch. If the tracker has no - queryable PR at all, note that and continue — the review still runs off the diff. - Store PR_ID, PR_TITLE, and the PR DESCRIPTION BODY (an intent source). + read its number, title, description body, source branch and target branch. Four outcomes: + - The query succeeds and returns a PR. Store PR_ID, PR_TITLE, and the PR DESCRIPTION BODY (an + intent source). + - The query succeeds and confirms that no open PR exists for this branch. Note that and + continue. The review still runs off the diff. + - The tracker contract's § Access names no server or skill that can list pull requests, so + this tracker has no PRs to query. Print "prep: this tracker exposes no pull requests" and + continue as for a confirmed no-PR. This is a fact the contract states, not an error. + - The query fails or errors, for example on auth, a rate limit or a timeout. Print + "prep: BLOCKED: the tracker query for this branch's PR failed: ". Set `base_ref` to "", + write no diff, and skip to the emit rule at the end of this node. A failed query does not + prove that no PR exists, and the fallback to `$BASE_BRANCH` below is only for that proof. ## 2. Compute the diff + categorise changed files - BASE_REF = the upstream base (merge-base of the PR base branch and HEAD). Try - `git merge-base origin/$bootstrap.output.expected_base HEAD`; fall back to `HEAD~1`. + DIFF_BASE = the branch this PR will merge into. When step 1 found a PR, it is the PR's target + branch, because that is the diff that will merge. When step 1 confirmed that no PR exists, or that + the tracker exposes none, it is `$BASE_BRANCH`. When both exist and differ, set `base_warning` to "the PR targets + but this run's base branch is $BASE_BRANCH. Reviewing the diff against .", print it + as "prep: WARNING: ", and carry on. Otherwise set `base_warning` to "". + DIFF_BASE may be a branch this clone has never fetched, so run + `git fetch origin +refs/heads/:refs/remotes/origin/` first. The explicit + destination creates `origin/` even in a single-branch clone, whose refspec omits it. If the fetch fails, print "prep: fetch of origin/ failed: " and go on. + A failed fetch is not fatal on its own. Then set BASE_REF to + `git merge-base origin/ HEAD`, falling back to `git merge-base HEAD` when + `origin/` does not resolve. Print "prep: diff base is (, via + )", where is "PR target" or "run base, no PR" and is "origin/" or + "local ", whichever resolved. + If NEITHER resolves, do NOT fall back to `HEAD~1` or to any other commit. A one-commit diff holds + only the last commit, so the review would judge that commit instead of the whole change. Print + "prep: BLOCKED: cannot resolve diff base '' via origin or local", set `base_ref` to + "", write no diff, and skip to the emit rule at the end of this node. Write the unified diff to `SESSION/diff.patch` (`git diff ...HEAD`) and the changed-file list to `SESSION/changed-files.txt` (`git diff --name-only ...HEAD`). Categorise the changed files into these STRING booleans ("true"/"false"): @@ -222,20 +246,62 @@ nodes: (``) into `prior_findings` (each: hash, aspect, file, title, anchored, prior_verdict) so reconcile can classify against them. EVERY prior finding carries that marker: an anchored discussion carries it, and the prior summary carries one beside - each finding it renders. Set `anchored` to "true" when you found the hash on a discussion + each finding it renders. Read only `finding=` markers as findings. A reply this Box posts + carries a `` marker instead, and a reply is + never a finding of its own. Set `anchored` to "true" when you found the hash on a discussion anchored to a file and a line, and "false" when you found it only in the summary body — reconcile needs to know which - prior findings own a live thread. Set `prior_verdict` to "fixed" when you found the marker in - the summary's fixed list, and "open" otherwise: without it a finding the prior iteration RESOLVED + prior findings own a live thread. Set `prior_verdict` by the first rule that holds: + 1. If the marker is in the summary's fixed list, set "fixed". + 2. If the marker is in the summary's list headed "Not reported this iteration, not verified", + set "unverified". + 3. If the finding is anchored, its hash appears nowhere in the prior summary, and its thread + is resolved or its latest reply marker is ``, + set "fixed". A finding retired in an earlier iteration is in no part of the summary, so its + thread is the only record that it was resolved. That covers a thread an older Box resolved, + which carries no `reply=` marker. Without this rule, the next iteration reads such a + finding as "open" and rules it `unverified` on a resolved thread. The rule never applies to + a hash the prior summary shows anywhere, for example in a severity section. Such a finding + was open at the prior iteration, even when a killed run left its thread's latest reply + marker at `fixed`. + 4. Otherwise, set "open". + Without `prior_verdict`, a finding the prior iteration RESOLVED reads exactly like one it reported, so it would be re-reported as fixed on every later iteration - and `regressed` could never fire. Read `file` from the locator the summary renders, and + and `regressed` could never fire. A scoped `unverified` finding has no thread, so the summary's + "Not reported this iteration, not verified" list is the only place its hash survives. + Read `file` from the locator the summary renders beside the marker, in a severity section or in + either list, and **a locator that is one of the three literal scope tokens — `pull request`, `work item`, `repository` — is a scope, not a path: record the token verbatim in `file`.** A host may nest a reply under its parent rather than listing every comment flat, so walk each thread's replies too. If the tracker exposes no discussion at all, set iteration 1 and prior_findings []. + + ## 5. Record this head and read the prior one + Run `git rev-parse HEAD > SESSION/head.txt` in the reviewed tree. reconcile writes the summary's + head marker from that file, so the next iteration reads back the commit this run reviewed, never + a commit id a model typed. Check that SESSION/head.txt holds exactly 40 or 64 hexadecimal + characters and nothing else but a line end. A SHA-256 repository has 64. If it does not, print "prep: BLOCKED: SESSION/head.txt does not + hold a commit id", set `base_ref` to "", and skip to the emit rule at the end of this node, so + `guard-no-diff-base` cancels the run. + Then read the prior head. The prior summary, the discussion with the highest iteration marker, + carries `` beside its iteration marker. Set + `prior_head` and `prior_head_status` as follows: + - If there is no such marker, or iteration is 1, set `prior_head` "" and `prior_head_status` + "absent". A summary that an older Box posted carries no head marker. + - If the marker's commit resolves, set `prior_head` to the commit id and `prior_head_status` + "recorded". It resolves when `git cat-file -e ^{commit}` succeeds, first as it is + and again after `git fetch origin `. + - If the marker's commit still does not resolve after that fetch, set `prior_head` "" and + `prior_head_status` "unreachable". When the fetch failed, print "prep: fetch of prior head + failed: ". The cause may be a rebase or a force push, or a host that + refuses a fetch by commit id. This node cannot tell which. + Print "prep: prior head ". Write `SESSION/prep.json` mirroring the emitted object for durability. ALWAYS emit EVERY required field so the object validates (downstream nodes and `prep.json` assume - they exist). When no PR is queryable, use `pr_id` "" and `pr_title` "". `contradictions` and + they exist). When no PR is queryable, use `pr_id` "" and `pr_title` "". When step 1, 2 or 5 + stopped as BLOCKED, use `base_ref` "", the STRING "false" for every category, `intent_available` + "false", iteration 1, `prior_head` "", `prior_head_status` "absent", and `[]` for both arrays, and still write `SESSION/prep.json` mirroring that + object, so the cancelled run leaves a record of why. `contradictions` and `prior_findings` are ALWAYS arrays — `[]` when there is nothing to report. Emit the structured object. output_format: type: object @@ -243,6 +309,7 @@ nodes: pr_id: { type: string } pr_title: { type: string } base_ref: { type: string } + base_warning: { type: string } has_source: { type: string, enum: ['true', 'false'] } has_tests: { type: string, enum: ['true', 'false'] } has_types: { type: string, enum: ['true', 'false'] } @@ -251,6 +318,8 @@ nodes: intent_available: { type: string, enum: ['true', 'false'] } contradictions: { type: array, items: { type: string } } iteration: { type: number } + prior_head: { type: string } + prior_head_status: { type: string, enum: ['recorded', 'absent', 'unreachable'] } prior_findings: type: array items: @@ -261,9 +330,9 @@ nodes: file: { type: string } title: { type: string } anchored: { type: string, enum: ['true', 'false'] } - prior_verdict: { type: string, enum: ['open', 'fixed'] } + prior_verdict: { type: string, enum: ['open', 'fixed', 'unverified'] } required: [hash, aspect, file, title, anchored, prior_verdict] - required: [pr_id, pr_title, base_ref, has_source, has_tests, has_types, has_docs, touches_error_handling, intent_available, contradictions, iteration, prior_findings] + required: [pr_id, pr_title, base_ref, base_warning, has_source, has_tests, has_types, has_docs, touches_error_handling, intent_available, contradictions, iteration, prior_head, prior_head_status, prior_findings] # ============================================================================================= # review — ONE node hosting the `code-review` Method's OWN two-axis fan-out (Standards · Spec). @@ -273,17 +342,32 @@ nodes: # and upstream now maintains. The Method implements its own parallelism — its step 4 spawns two # sub-agents — so the Harness delegating to it is the whole point: nothing about running two # sub-agents needs the Harness, and re-implementing step 4 as two Archon nodes is the exact defect - # ADR-0030's structural bar forbids. The Harness keeps what no Method supplies: the Intent Brief, - # the finding contract below, hash-keyed re-review, the config gate and the two posting surfaces - # (#281, amending ADR-0026). + # to avoid, because upstream maintains the Method's procedure and a copy here drifts. The Harness + # keeps only what no Method supplies: the Intent Brief, the finding contract below, hash-keyed re-review, the config gate and the two posting surfaces. # # The finding contract lives in the node's own prompt below, under "## The finding contract". A YAML # comment is not part of a prompt, so a rule stated only here is invisible at run time. Every rule # this Box must honour is written in the prompt that honours it, for that reason. # ============================================================================================= + # --------------------------------------------------------------------------------------------- + # guard-no-diff-base. prep emits an empty base_ref when the tracker query failed, when prep cannot + # resolve the branch the PR merges into, or when SESSION/head.txt holds no commit id. A guessed base gives review a diff of other changes than + # the PR's, so the guard cancels the run. + # --------------------------------------------------------------------------------------------- + - id: guard-no-diff-base + depends_on: [prep] + when: "$prep.output.base_ref == ''" + cancel: > + /pr-review cannot resolve the branch this PR merges into, or cannot record the commit it + reviews, so it reviews nothing. prep's "prep: BLOCKED" line says which. When SESSION/head.txt + holds no commit id, check that the worktree is a git checkout with a HEAD, and re-run. When the tracker query failed, fix the tracker access and re-run. + When a PR exists, its target branch must exist on the remote, so retarget the PR or push that + branch, then re-run. With no PR, dispatch with --base . + - id: review depends_on: [prep] + when: "$prep.output.base_ref != ''" context: fresh allowed_tools: [Read, Write, Bash, Glob, Grep, Agent] prompt: | @@ -291,20 +375,32 @@ nodes: SESSION = //pr-review/ (artifacts_dir=$bootstrap.output.artifacts_dir). threshold = $bootstrap.output.confidence_threshold. + First delete SESSION/findings/review.json if it exists. SESSION persists between runs, so a file + an earlier iteration left there would let `synthesize` publish that iteration's findings, and + `reconcile` its reported checks, as this one's. + **Every stop after this point writes SESSION/findings/review.json first.** Write the sentence you + print as both `blocked` and `standards_report`, empty `findings`, `positives` and + `reported_checks` arrays, and `subagents_ran` as the rule below sets it. This holds for the + missing-Method stop, the `Agent`-unavailable stop, a fixed point that does not resolve, an empty + diff, and any other stop. A stopped review is not a clean review, and `blocked` is how `synthesize` + and `reconcile` tell the two apart. + Read `.archon/methods/code-review/SKILL.md` in full, by that literal repo-relative path. That is the - one path a Method lives at: an Archon node cannot import anything from the Plugin (ADR-0023 §5), - and the config and `.local` override tiers are retired (ADR-0031, amended). If the file is missing that + one path a Method lives at: an Archon node cannot import anything from the Plugin, + and the config and `.local` override tiers are retired. If the file is missing that is FATAL for this node: print "code-review Method not found at - .archon/methods/code-review/SKILL.md — run /unic-archon-dlc:setup" and stop. Do not review from + .archon/methods/code-review/SKILL.md — run /unic-archon-dlc:setup", write + SESSION/findings/review.json as the stop rule above says, and stop. Do not review from memory of what the Method says. Follow its Process steps 1–5. It asks a live human for two inputs; there is no human in this node, so both are supplied here and you must NOT ask: - **Step 1, the fixed point:** `$prep.output.base_ref` — prep already computed it as the - merge-base of the PR base branch and HEAD. Confirm it resolves and the diff is non-empty before - spawning anything, as the Method requires. prep also wrote the diff to SESSION/diff.patch and - the changed-file list to SESSION/changed-files.txt; use them rather than recomputing. + merge-base of HEAD and the PR's target branch, or `$BASE_BRANCH` when there is no PR. Confirm it + resolves and the diff is non-empty before spawning anything, as the Method requires. prep also + wrote the diff to SESSION/diff.patch and the changed-file list to SESSION/changed-files.txt; use + them rather than recomputing. - **Step 2, the spec source:** `SESSION/intent-brief.md` — prep already distilled it from the linked work items, the referenced docs, the PR description and PRD.md, with a numbered Acceptance Criteria list. Never ask where the spec is. If @@ -313,18 +409,21 @@ nodes: - **The "/setup-matt-pocock-skills if `docs/agents/issue-tracker.md` is missing" fallback never applies.** `/unic-archon-dlc:setup` owns that file and `docs/agents/triage-labels.md`, and `setup-matt-pocock-skills` is not a Plugin dependency — a run writes another host's template over - the first and reverts the second to a five-role vocabulary (ADR-0024). Never run it. + the first and reverts the second to a five-role vocabulary. Never run it. Then steps 3–5 verbatim: - Step 3: gather this repo's standards sources (CLAUDE.md / AGENTS.md / CONTRIBUTING.md / CODING_STANDARDS.md — whatever exists) and take the Method's twelve-item Fowler smell baseline as written. This baseline is where refactoring now lives: /build's loop has no REFACTOR phase, - because `tdd` puts refactoring in the review stage (#281). + because `tdd` puts refactoring in the review stage. - Step 4: send ONE message with two `Agent` tool calls, `general-purpose` subagent for both. **Paste the twelve-item smell baseline IN FULL into the Standards sub-agent's prompt** — step 4 is explicit that the sub-agent has no other access to it, and an abridged paste silently narrows the review. Add SESSION/intent-brief.md to BOTH sub-agent prompts: intent-grounding is this Box's quality lever, so neither axis judges the diff without knowing what it was for. + **End BOTH sub-agent prompts with this request, word for word:** "End your report with a + section headed `Commands I ran`. List every command you ran other than reading files and git, + one per line, each with the outcome you observed. If you ran none, write `none`." - Step 5: aggregate under `## Standards` and `## Spec`. Never merge or rerank the two axes. ## The finding contract @@ -359,18 +458,56 @@ nodes: documented requirement the diff misses. - Also collect `positives` (things done well) across both axes, for the "What's good" section. + ## Reported checks + This Box instructs no check, but a sub-agent may run one on its own. Read the `Commands I ran` + section of each sub-agent's report. A **check** is a command that runs the project's toolchain to + verify the code, as a build, test, lint, format, type-check, coverage or e2e run does, or a + command that starts a dev server. Record only checks. Drop every other command, such as one that + lists files, reads JSON, evaluates a snippet or queries the package manager. Record each check + in `reported_checks` as one entry per need it serves: + { "axis": "standards" | "spec", "need": "", "outcome": "" } + - `need` is exactly one of `build`, `test`, `e2e`, `lint`, `format`, `typecheck`, `dev`, + `coverage` or `undeclared`. A check that serves two needs, such as one command that lints and + type-checks, takes one entry per need. + - Judge which need a check serves from what it does. A check that type-checks the code serves + `typecheck`, whatever its text. Never decide it by comparing the command's text with a value + in `$bootstrap.output.sdlc_needs`. A Consumer's value can take a shape the reported command + does not share, and a text match then reports a declared need as undeclared. + - Set `need` to "undeclared" when the need the check serves has no value set in + `$bootstrap.output.sdlc_needs`, or when the check serves none of the eight needs above. + - A command that installs dependencies serves `install`. Never record it, because `bootstrap`'s + install report covers it. + - Never record the command text. The summary names the need only. + - `outcome` is "passed", "failed" or a short outcome such as "failed with 2 type errors", as the + sub-agent reported it. It names no command and no tool. It is a self-report, and nothing in + this Box verifies it. + The array is empty when no sub-agent reported a check. A finding a sub-agent raised from a + failed check stays a finding like any other. Record the check here as well, and leave the + finding where the contract above puts it. + **After you have recorded `reported_checks`, remove the `Commands I ran` section from both + reports** before you store them below. That section names commands and states their results as + reported, and `synthesize` renders `spec_report` in the summary. + Write SESSION/findings/review.json as: - {"findings": [...], "positives": [...], - "standards_report": "", "spec_report": ""} - Keep `standards_report` and `spec_report` as the Method's own prose — synthesize renders - `spec_report` as the Intent Check section, which is how AC-coverage survives the collapse of the - old intent-check node. + {"findings": [...], "positives": [...], "reported_checks": [...], "subagents_ran": , + "blocked": "", "standards_report": "", + "spec_report": ""} + Keep `standards_report` and `spec_report` as the Method's own prose, less the `Commands I ran` + section. synthesize renders `spec_report` as the Intent Check section, which is how AC-coverage + survives the collapse of the old intent-check node. + Set `subagents_ran` to true when at least one review sub-agent started, and to false otherwise, + on every path, a stop included. `reconcile` reads it to decide whether the checks block says + that a review sub-agent may still have run a check on its own. + `blocked` is "" for a review that ran to the end, and the printed sentence for a stop. A reader + of this file treats the review as **blocked** when `blocked` is not empty or `subagents_ran` is + not true. A review that ran to the end always started a sub-agent. If the `Agent` tool is unavailable in this node and you cannot spawn the two sub-agents, do NOT silently restructure the review into something else (not one axis, not seven aspects): print "REVIEW BLOCKED: Agent tool unavailable in an Archon prompt node — code-review step 4 cannot run - as written", write SESSION/findings/review.json with that sentence as `standards_report` and - empty arrays, and stop. That is a real finding about the container, not a detail to route around. + as written", write SESSION/findings/review.json as the stop rule above says, with + `subagents_ran: false`, and stop. That is a real finding about the container, not a detail to + route around. # --------------------------------------------------------------------------------------------- # synthesize — THIS RUN ONLY. Reads the review node's single SESSION/findings/review.json, dedupes @@ -387,7 +524,17 @@ nodes: You are the synthesize node of /pr-review for slug "$bootstrap.output.slug". SESSION = //pr-review/ (artifacts_dir=$bootstrap.output.artifacts_dir). - 1. Read SESSION/findings/review.json — the review node's Standards + Spec output. + 1. First delete SESSION/findings.json and SESSION/summary-sections.md if they exist, because + SESSION persists between runs. Then read SESSION/findings/review.json, the review node's + Standards and Spec output. If it is missing, print "synthesize: BLOCKED: no review.json", write + no SESSION/findings.json and no SESSION/summary-sections.md, emit 0 for every count, and stop. + An empty findings file would let `reconcile` rule fixed every prior finding whose file + changed between the two heads. + If the review is **blocked**, as the review node defines it, write `[]` to + SESSION/findings.json, and write to SESSION/summary-sections.md only a callout, + `> ⚠ This review stopped before it finished: `, with the `blocked` sentence, or "no reason + recorded" when the field is missing. Write no Intent Check, no severity section and no + "What's good". Emit 0 for every count and skip steps 2 to 5. 2. Merge its `findings`, DEDUPE overlaps (same file + near-identical title across the two axes → keep the highest-confidence one, note the other axis). Ensure each finding has its stable `hash`. Do NOT rerank one axis against the other; deduping identical findings is not reranking. @@ -398,8 +545,15 @@ nodes: 4. Collect any contradictions from `$prep.output.contradictions`. 5. Assemble the body SECTIONS (no iteration header / delta yet — reconcile adds those): - `> ⚠ Intent sources disagree: …` callout (only if contradictions) - - `### Intent Check` — render the review node's `spec_report` verbatim. The Spec axis IS the - AC-coverage judgement (requirements missing or partial, scope creep, requirements that look + - `### Intent Check`. Render the review node's `spec_report` verbatim, except that the + render states no check result as a fact. Two rules apply to each line: + - Drop a line that only reports a command the sub-agent ran, or that a build, lint, + format, type-check, test or other check passed or failed. + - When a line gives an acceptance-criterion verdict and also a check result, as in + "AC 2 met: `` passes", keep the verdict and remove the check result from the + line. A check result appears in the summary only in the checks block. + A line that cites a command only to judge an acceptance criterion stays. The Spec axis IS + the AC-coverage judgement (requirements missing or partial, scope creep, requirements that look implemented but wrong), so it replaces the old per-AC verdict list. Omit the section only when `$prep.output.intent_available == "false"`. - `### 🔴 Critical (N)` / `### 🟠 Important (N)` / `### 🟡 Minor / Suggestions` — each finding a @@ -414,6 +568,12 @@ nodes: iteration reads its hash back; a finding rendered without one is invisible to the run after this and starts again as new. Markdown hides the comment from the reader. - `### ✅ What's good` — always present, from `positives`. + **State no check result as a fact, in "What's good" or in any other section.** A positive + that says a build, lint, type-check or test passes is a sub-agent's report of a check this + Box did not run, so drop it from the sections. `reconcile` renders `reported_checks` in the + checks block, attributed to the sub-agent. The one exception is a finding raised from a failed + check. It stays in its severity section like any other finding, and its own body may cite the + failure it was raised from. Write the deduped this-run set to SESSION/findings.json (each finding incl. its hash) and the body sections to SESSION/summary-sections.md. **Every finding in findings.json is rendered in the sections — count them and check.** The @@ -443,7 +603,31 @@ nodes: You are the reconcile node of /pr-review for slug "$bootstrap.output.slug" — the re-review coordinator. SESSION = //pr-review/. ITERATION = $prep.output.iteration. - 1. Read SESSION/findings.json (this run) and SESSION/prep.json → `prior_findings` (prior iteration). + 1. First delete SESSION/comment.md, SESSION/threads.json and SESSION/changed-between-heads.txt + if they exist. SESSION persists between runs, so a file an earlier run left there would let + `post` publish that run's verdicts when this node stops early. + If SESSION/findings.json is missing, print "reconcile: BLOCKED: no findings.json", write no + SESSION/comment.md, and stop, because `synthesize` stopped and no prior finding can be ruled. + Read SESSION/findings.json (this run) and SESSION/prep.json → `prior_findings` (prior iteration), + `prior_head` and `prior_head_status`. Read SESSION/findings/review.json too, and note whether + the review is **blocked**, as the review node defines it. Treat a file you cannot read or + parse as blocked as well. + Before you classify anything, write the change between the two heads to + SESSION/changed-between-heads.txt. When `prior_head_status` is "recorded", run + `git -c core.quotePath=false diff --name-only "$(cat SESSION/head.txt)" > SESSION/changed-between-heads.txt`. + `core.quotePath=false` makes git print a non-ASCII path as it is, not quoted, so it can match a + finding's `file`. A rename lists only the new path, so a finding on the old path is + `unverified`. + Otherwise create the file empty, because there is no prior head to compare against. + Check the exit status of that `git diff`. A failed diff leaves the file empty, and an empty + file would make every reply say that the file did not change. If it exits non-zero, print + "reconcile: diff between heads failed: ", and from here on treat this run as one whose + `prior_head_status` is "unreachable": no finding is `fixed`, and step 5c adds its section. + This file is the only evidence a `fixed` verdict may rest on. SESSION/changed-files.txt is the + PR's diff against its base, and it plays no part in a `fixed` verdict. + Before you compare a finding's `file` with a line of this file, remove one leading `/` from + the `file`. Some hosts store a thread's path with a leading `/`, and `git diff --name-only` + prints none. 2. Classify each finding against prior_findings. Match on the `hash` first. Where no hash matches, match on **aspect + semantic title + `file`**, with one exception that is the whole point of this step: **`file` differing between a path and a scope token is not a mismatch.** @@ -455,12 +639,12 @@ nodes: prior findings that share a title in different files. Step 3 needs each prior hash used once, so break the tie in this order and record which prior you picked: the closest semantic title, then the one whose `anchored` is "true", then the lexically smallest hash. A prior finding you - did not pick stays unmatched, and step 3 rules it `fixed`. + did not pick stays unmatched, and the rule for an unmatched prior below gives its verdict. Two one-off notes cover the prior iterations already live on real pull requests: - - A prior iteration posted before #281 carries the retired seven aspect names (code-quality, + - A prior iteration posted by an earlier version of this Box carries the retired seven aspect names (code-quality, tests, silent-failure, type-design, comment-rot, simplifier, intent-check) where this run carries "standards" / "spec". Match those on semantic title alone, ignoring the aspect, so - the first re-review after the rewiring does not report every finding as new. + the first re-review after the switch to two axes does not report every finding as new. - A prior iteration posted by a Box older than the anchorable/scoped rule gave a scoped finding an invented `file` — a path that merely sat near the problem. Its prior `file` is a path and this run's is `pull request`, `work item` or `repository`, so they differ by @@ -468,21 +652,35 @@ nodes: aspect + semantic title alone. Reading the differing `file` as "no match" reports a live finding as `fixed` and replies "✅ resolved" to it, which is the defect this whole step exists to prevent. - Classifications: - - new — no prior match - - still-present — matched a prior finding still valid this run - - fixed — a prior finding whose `prior_verdict` is "open" with NO match this run + Classifications, by `prior_verdict`: + - new — a finding this run with no prior match + - still-present — a prior finding whose `prior_verdict` is "open" or "unverified" and which + matches a finding this run + - fixed — a prior finding whose `prior_verdict` is "open" or "unverified", with NO + match this run, whose `file` is a path, and whose path appears as a line + of SESSION/changed-between-heads.txt. The file changed between the two + recorded heads. That change is necessary for a fix and does not prove it. + - unverified — a prior finding whose `prior_verdict` is "open" or "unverified", with NO + match this run, and which is not `fixed` by the rule above. Its file did + not change between the two heads, its `file` is a scope token, or there is + no prior head. When `prior_head_status` is not "recorded", or the review + is blocked, every unmatched prior of this kind is `unverified` and none + is `fixed`. The review omitting + a finding is not evidence that the finding is gone, because the reviewer + varies between runs on the same commit. - regressed — a prior finding whose `prior_verdict` is "fixed" and which matches a finding this run: the previous iteration resolved it and it is back - retired — a prior finding whose `prior_verdict` is "fixed" and which STILL has no - match. It was reported fixed and stayed fixed, so it leaves the ledger: - step 5 does not re-list it and it carries no marker forward. Without - this, an already-fixed finding is ruled `fixed` again every run, the - fixed list grows without bound, and the "since iteration N−1" delta - counts findings fixed several iterations ago as though they were fixed - this one. The ceiling: a retired finding that reappears two iterations - later reads as `new`, because nothing carries its hash any more. The - ledger has to end somewhere and this is where it ends. + match. It was reported fixed and stayed fixed, so step 5 does not + re-list it. Without this, an already-fixed finding is ruled `fixed` + again every run, the fixed list grows without bound, and the "since + iteration N−1" delta counts findings fixed several iterations ago as + though they were fixed this one. An anchored retired finding keeps its + thread, and prep reads it back as "fixed", so it stays `retired` and + returns as `regressed`. Only a retired finding with no thread leaves the + ledger. If it comes back, it reads as `new`. Only a prior + `fixed` can retire. A prior `unverified` with no match is `fixed` or + `unverified` by the rules above, never `retired`. **The prior hash is the identity. Carry it forward.** The moment a finding matches a prior one, overwrite this run's hash with the prior finding's hash and use that hash everywhere downstream: in the summary, in the plan, in every count. Only a `new` finding keeps the hash @@ -491,42 +689,51 @@ nodes: loses the thread and the history that go with it. A finding that was anchorable last iteration and is scoped this one is still-present, not new: the scope changed, the finding did not. 3. **Emit a verdict for EVERY prior finding.** Walk `prior_findings` and confirm each hash appears - exactly once in the classified set, as still-present, fixed, regressed or retired. A prior - finding you cannot match to anything this run gets `fixed` or `retired` per its - `prior_verdict` — say so explicitly rather than dropping it, because a dropped prior finding - leaves its thread live with no reply and no resolution. + exactly once in the classified set, as still-present, fixed, unverified, regressed or + retired. A prior finding you cannot match to anything this run gets `fixed`, `unverified` or + `retired` per the rules above. Say so explicitly rather than dropping it, because a dropped + prior finding leaves its thread live with no reply and no resolution. + Then check every `fixed` verdict against SESSION/changed-between-heads.txt. Its `file`, without + a leading `/`, must be a line of that file. Rule a `fixed` that fails the check `unverified`. Then state both numbers: the count of prior findings and the count of verdicts. They are equal. If they are not, you have dropped one — go back and classify it. **Because you loop until they match, that equality alone evidences nothing — so publish the two numbers that CAN come out wrong.** - `unverified_fixed` is how many of your `fixed` verdicts rest on absence alone: the prior - finding named a changed file and a line, and THIS run's diff does not touch that file at all. - Nothing in the diff could have fixed it, so the match probably failed instead. Count a scoped - prior here too — no diff localises a finding about the pull request or the repository, so a - scoped `fixed` is never more than "it did not come up again". This is the match-health signal: - it is not the same as `fixed`, because a `fixed` verdict on a finding whose file the diff DID - change is evidence, and one on a file nothing touched is a guess. + `unverified` is how many prior findings you ruled `unverified`. A reworded re-report that + fails to match shows up as an `unverified` prior beside a `new` finding with a similar title + in the same file. `unmatched_priors` is how many prior findings you could not account for at all, and it is the number that must be zero. - 4. Compute the "since iteration N−1" delta: X fixed · Y still present · Z regressed · W new + 4. Compute the "since iteration N−1" delta: X fixed · U unverified · Y still present · Z regressed · W new (skip the delta line entirely when ITERATION == 1). `retired` findings are NOT in the delta: they were already reported fixed, so counting them again is what made the delta describe the whole history instead of this iteration. 5. FINALISE the summary body → SESSION/comment.md, in this exact frame: + ## unic-dlc /pr-review — $prep.output.pr_title · Iteration + - ### Checks this review did not run + ### Checks this review was not asked to run ### Since iteration (only when ITERATION > 1) + --- 🤖 Generated by unic-archon-dlc · Iteration · re-run `/unic-dlc-pr-review $bootstrap.output.slug` to update. + **Write the head marker from SESSION/head.txt with a command, never by typing the commit id**, + for example `printf '\n' "$(cat SESSION/head.txt)"`, and + check afterwards that the commit id in SESSION/comment.md is 40 or 64 hexadecimal characters + and equals the content of SESSION/head.txt. If it does not, print "reconcile: BLOCKED: the head + marker does not match SESSION/head.txt", write no SESSION/comment.md, and stop. The next + iteration's prep reads this marker back as its prior head. `post` + rewrites the summary in place on every run, so the marker always names the head of the latest + iteration. Before anything else, check what you were handed: every hash in SESSION/findings.json appears exactly once as a marker in the pasted sections. If one is absent, synthesize dropped a finding — render it into its severity section yourself rather than publishing a summary that @@ -537,37 +744,93 @@ nodes: just replaced. Pasting the sections unchanged would publish the very hash the carry-forward exists to retire, and the next iteration would read it back and report the finding as new. Tag each still-present / regressed finding inline in its section (e.g. "(still present since - iteration N−1)" / "(⚠ regressed)"). Under the delta line, name every `fixed` finding by title - in one short list, **each entry preceded by its own + iteration N−1)" / "(⚠ regressed)"). Under the delta line, name every `fixed` finding by its + locator and title in one short list, **each entry preceded by its own `` marker**: a fixed finding leaves no other trace in this iteration's sections, and a scoped one leaves no thread either, so this list is both the only place the reader sees it resolved and the only place the next iteration can read its hash back. A fixed finding whose marker you omit cannot return as `regressed` — it returns as new, which is the classification this whole node exists to keep stable. - 5b. Fill the "Checks this review did not run" block. It is a block of its own, in the frame - above every finding, and it is present in EVERY iteration — a reader skimming for severity - must not be able to pass over it. Three sentences, in this order: - - **This Box runs no build, lint, type-check or test command by instruction**, so nothing - below checks a claim the author made about one. That is deliberate: running a check - against a claim is a Method's procedure, not this Harness's (ADR-0030). - - Which **checks** the project declares: name the keys of `$bootstrap.output.sdlc_needs` - whose value is set, `install` excluded, and say that this review executed none of them. - Leave `install` out of that sentence — this Box does run it, and the line below reports - what happened, so naming it here would deny something the next line proves. Name the - keys, never their values: the command is the project's business and this Box names no - tool. + Under the fixed list, name every `unverified` finding by its locator and title in a second + short list, headed "Not reported this iteration, not verified", each entry preceded by its own + `` marker. The next iteration's prep reads + `prior_verdict` "unverified" from this list, and for a scoped finding it is the only place the + hash survives. + In both lists the locator is the one the severity sections use, `` `file:line` `` or the + scope token alone. The next iteration's prep reads `file` from it, so a scoped finding keeps + its scope token and can never be ruled `fixed` against a guessed path. + 5b. Fill the "Checks this review was not asked to run" block. It is a block of its own, in the + frame above every finding, and it is present in EVERY iteration, so a reader skimming for + severity cannot pass over it. Read `reported_checks` and `subagents_ran` from + SESSION/findings/review.json. When the file cannot be parsed, or lacks `subagents_ran` or + `reported_checks`, the report is **unreadable**. A missing file counts as unreadable too, as a + defence only, because `synthesize` and this node stop first when it is missing. Then treat + `reported_checks` as empty and keep the sentence on sub-agents below. An unreadable report is + never a sub-agent's claim. Then write these parts, in this order: + - **This Box instructed none of the project's declared checks**, so nothing in this review + verifies a claim that a build, lint, type-check or test passes. That is deliberate, + because running a check against a claim is a Method's procedure, not this Harness's. + - Which **checks** the project declares. Name the keys of `$bootstrap.output.sdlc_needs` + whose value is set, `install` excluded. Leave `install` out of that sentence. This Box + does run it, and the last line reports what happened. Name the keys, never their values, + because the command is the project's business and this Box names no tool. When no key + other than `install` is set, write that the project declares no check. + - Say that a review sub-agent may still have run one of them on its own, or, when the + project declares no check, that a review sub-agent may still have run a check on its + own. Leave this sentence out only when the file says `subagents_ran: false`, because + then no sub-agent ran. + - When the report is unreadable, say in one line that `review` left no readable report of + what its sub-agents ran. + - One line per entry of `reported_checks`, as a claim of the sub-agent that names the need + and the outcome, never the command. For example, "The Standards review sub-agent + reported that it ran `typecheck` and that it passed. This Box did not run or verify it." + For an entry whose `need` is "undeclared", write "a check that matches none of the + project's declared needs" in place of the need. Write no line when the array is empty. - `$bootstrap.output.install_report`, verbatim. - Without this block that admission lives only in the plugin's own issue tracker, where the + Never write that no check ran in this review, or that the review ran none of the declared + checks. The Box's instruction does not bind a sub-agent, so neither sentence is known to be + true. Without this block the warning lives only in the plugin's own issue tracker, where the developer reading a review never sees it. + 5c. When ITERATION > 1 and there is no usable prior head, add a section above the delta line. + There is no usable prior head when `$prep.output.prior_head_status` is not "recorded", or + when the diff in step 1 failed. The section tells the developer, in plain words: + - Why no finding could be ruled fixed this iteration. Either the prior iteration recorded + no head commit, as a summary from an older Box does ("absent"), or the recorded head + could not be reached ("unreachable", or a failed diff). + - What is asked of them. They check each finding in the "Not reported this iteration, not + verified" list by hand, and resolve the ones that no longer apply. + - That the check is automatic again from the next iteration, unless the head again cannot + be reached. 6. Build the thread plan → SESSION/threads.json: an array of - { kind, file, line, severity, hash, classification, body }. Every entry's `body` starts with - the hidden marker `` and then the rendered finding. + { kind, file, line, severity, hash, classification, body, reply }. `body` is the thread's + first comment: the hidden marker `` and then the + rendered finding. `reply` is a reply on the thread: the hidden marker + `` and then the reply text. The verdict is + the entry's `classification`, except that a `retired` entry writes `fixed`. `hash` in + both markers is the hash of the thread's own finding, the one the thread was opened with, + which step 2 carried forward. Set a field to null where the entry needs none. `kind` is "reply" or "anchor", and each kind has its own admission rule: - **reply** — one entry for EVERY prior finding whose `anchored` is "true", carrying its - verdict as `classification` (still-present / fixed / regressed) and the hash its thread's - marker already holds. `post` finds the thread by that marker alone, so a reply entry sets - `file: null` and `line: null` — the thread already sits where it sits. A `fixed` entry's - body is a "✅ resolved in iteration " reply. + verdict as `classification` (still-present / fixed / unverified / regressed / retired) + and the hash its thread's marker already holds. `post` finds the thread by that marker + alone, so a reply entry sets `file: null` and `line: null` — the thread already sits where + it sits. Fill `body` and `reply` per classification: + - still-present. `body` is the updated finding. `reply` says the finding was reported + again in iteration , and `post` uses it only when the thread is resolved. + - fixed, and retired. `reply` is "✅ resolved in iteration ". Its marker + carries the verdict `fixed` for both, ``, + because prep reads that verdict back. `post` posts a retired entry's reply only when + the thread is not resolved and carries no fixed reply yet. So a rerun posts the fixed reply that a killed + run never posted. It does not resolve a thread whose fixed reply is already there, + because a killed run that replied but changed no status looks the same as a person who + reopened the thread on purpose. + - unverified. `reply` says that the review did not report the finding this iteration, + why it is not ruled fixed (its file did not change since the prior iteration, or the + finding is scoped, or there is no prior head, or the review is blocked), and that the + author should check it by hand and resolve the thread if the finding is gone. For a + blocked review, the reason reads "the review stopped before it finished, so this + finding could not be checked", followed by the `blocked` sentence. + - regressed. `reply` says the finding is back in iteration . - **anchor** — one entry for every finding this run that is anchorable AND owns no thread yet: classification `new`, or a match against a prior finding whose `anchored` is "false". It carries the real changed-file path in `file` and the real diff line in `line`. @@ -580,7 +843,12 @@ nodes: it owns no prior thread the summary carries it alone, tagged by step 5. A prior finding whose `anchored` is "false" is likewise carried by the summary: its verdict is the tag, and there is no thread to reply to. - Emit iteration, the delta counts, and the two counts from step 3. + Emit iteration, the delta counts, and the two counts from step 3. Emit `stopped` as "" when you + wrote SESSION/comment.md and the review is not blocked. When the review is blocked and you wrote + SESSION/comment.md, emit the `blocked` sentence as `stopped`, or "review stopped, no reason + recorded" when the field is missing. When you stop without SESSION/comment.md, emit the BLOCKED + sentence you printed as `stopped`, ITERATION as `iteration`, and 0 for every count, so the gate + shows the stop. output_format: type: object properties: @@ -592,10 +860,23 @@ nodes: prior_count: { type: number } verdicts: { type: number } retired: { type: number } - unverified_fixed: { type: number } + unverified: { type: number } unmatched_priors: { type: number } + stopped: { type: string } required: - [iteration, fixed, still_present, regressed, new, retired, prior_count, verdicts, unverified_fixed, unmatched_priors] + [ + iteration, + fixed, + unverified, + still_present, + regressed, + new, + retired, + prior_count, + verdicts, + unmatched_priors, + stopped, + ] # --------------------------------------------------------------------------------------------- # review-gate — HITL by default; skipped when gates.pr-review == afk. Shows the finalised summary + @@ -606,16 +887,23 @@ nodes: when: "$bootstrap.output.gate == 'hitl'" approval: message: > - /pr-review for "$bootstrap.output.slug" — iteration $reconcile.output.iteration. Findings: + /pr-review for "$bootstrap.output.slug" — iteration $reconcile.output.iteration. Stopped: + "$reconcile.output.stopped". When that starts with "reconcile: BLOCKED", reconcile wrote no + SESSION/comment.md, its counts below are zeros, and APPROVE posts nothing. Any other non-empty + value is the review node's stop. Then the summary carries a stop callout in place of this + iteration's findings, and no prior finding is ruled fixed. Findings: $synthesize.output.critical critical · $synthesize.output.important important · $synthesize.output.minor minor. Since last: $reconcile.output.fixed fixed · + $reconcile.output.unverified unverified · $reconcile.output.still_present still present · $reconcile.output.regressed regressed · $reconcile.output.new new. Prior findings $reconcile.output.prior_count · verdicts - $reconcile.output.verdicts, of which $reconcile.output.unverified_fixed were ruled fixed on - absence alone — this run's diff touches nothing that could have fixed them — and - $reconcile.output.unmatched_priors could not be accounted for at all. The last number should be - zero; a high one before it means the match is failing rather than that the author fixed - everything. Those two are what a broken match shows up in, not the equal pair. + $reconcile.output.verdicts, of which $reconcile.output.unverified are unverified and + $reconcile.output.unmatched_priors could not be accounted for at all. An unverified finding is + one the review omitted whose file did not change since the prior iteration's head, or which is + scoped, or which has no prior head to compare against. The last number should be zero. A + reworded re-report that fails to match shows up as an unverified prior beside a new finding + with a similar title in the same file. Those two are what a broken match shows up in, not the + equal pair. Review SESSION/comment.md (the composed summary) and SESSION/threads.json (the thread plan). APPROVE to post/update the summary discussion and the per-line ones on the PR. REJECT to halt without posting. @@ -641,7 +929,7 @@ nodes: writes a review — so read it before the first call. - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Write no command, subcommand or - flag here and name no provider: the DLC owns the what and none of the how (ADR-0016), a flag + flag here and name no provider: the DLC owns the what and none of the how, a flag table frozen in this file is stale the moment the tool changes, and inventing a subcommand is the one defect no test without a live tenant can catch. Ask what the tracker supports; do not assume. @@ -657,6 +945,14 @@ nodes: If the file is absent, STOP and print "BLOCKED: this repository has no tracker contract at docs/agents/issue-tracker.md", and post nothing. + ## Before anything is posted + Check SESSION/comment.md first. If it is missing, or its `head=` marker differs from the content + of SESSION/head.txt, or its `iteration=` marker differs from ITERATION, print "post: BLOCKED: + SESSION/comment.md is missing or belongs to another run", post nothing, and stop. Print that + BLOCKED line only, and no closing line. This node runs + even when reconcile stopped early, and SESSION persists between runs, so a stale file would + publish an earlier run's verdicts. + ## The summary discussion (always) Read SESSION/comment.md. **A discussion on the pull request itself** is what carries it — the review addresses the whole PR, not one line of it. Hosts carry that differently: one opens a @@ -687,15 +983,46 @@ nodes: finding. Within the anchored discussions the marker is the thread's identity and is the only key you need — the entry carries no file and no line on purpose, because the file the thread sits on is where an earlier iteration put it, not necessarily where the finding lives now. + **Read the thread's status and its replies before you act.** Two facts decide what you write: + - Whether the thread is resolved. If the tracker cannot report a thread's status, print + "inline: this tracker reports no thread status; reopen and resolve skipped where they + depend on it" once, and treat every thread as not resolved. + - The thread's latest reply marker, ``, + found in its replies. The entry's hash is the hash in the thread's own `finding=` marker, + never one this run computed fresh. + **The repeat rule.** Post the entry's `reply` only when the thread's latest reply marker + does not hold this hash and this verdict. A `retired` entry's verdict is `fixed`. This rule + keeps a rerun after a killed run from repeating a reply. It also keeps a third iteration on + the same commit from repeating one. A finding that went fixed, then regressed, then fixed + again still gets each reply once. Read what is posted from the PR's threads and their + markers only, never from an earlier run's session directory. + **A reopen always posts its reply**, whatever the repeat rule says. Reopen first, then post + the reply. A rerun finds the thread already open and does not reopen it, so this repeats + nothing on a rerun. A run killed after the reopen and before the reply leaves the thread open + with no new reply. Then act on `classification`: - - still-present → update that discussion's body in place. - - fixed → reply "✅ resolved in iteration " and mark it resolved where the - tracker carries a resolved status (best-effort; otherwise just reply). + - still-present → update that discussion's body in place with `body`. When the thread is + resolved, reopen it, whoever resolved it, then post `reply`. Where the + tracker offers no reopen, post `reply` under the repeat rule. + - fixed → post `reply` under the repeat rule. Mark the thread resolved where the + tracker carries a resolved status and the thread is not resolved yet. + Where the tracker has no resolved status, the reply is all you post. Replying and changing a status may be two separate operations on the same thread; ask, rather than assuming one call does both. - - regressed → reopen it noting the regression. Where the tracker offers no reopen, - reply on that same thread saying the finding is back. A reply entry + - retired → do nothing when `$prep.output.prior_head_status` is "absent". An older + Box posted the prior iteration, and its resolved reply carries no + `reply=` marker, so a reply now would be a second "resolved" and could + resolve a thread a person reopened on purpose. Do nothing when the thread + is already resolved. Otherwise, when the thread carries no + `reply=:fixed` marker, act as for fixed. When it carries one, do + nothing, even when the thread is open. + - unverified → post `reply` under the repeat rule, and leave the thread's status as it + is. A person resolves it once they have checked it by hand. + - regressed → reopen it when it is resolved, then post `reply`. Where the tracker + offers no reopen, or the thread is open, post `reply` under the repeat + rule on that same thread. A reply entry carries no file and no line, so it has nothing to anchor a new thread to. + Reopening, like resolving, may be a status change separate from the reply; ask. Answer every reply entry before you finish, and report a marker that matches no thread by its hash in your final line: a prior thread left with no reply and no resolution is the defect this plan exists to prevent. @@ -733,7 +1060,12 @@ nodes: AFK the gate is skipped, so this line is the only place the run states that every prior finding was answered: "pr-review: iteration posted for \"$bootstrap.output.slug\" — - , inline ( new · updated · resolved) · + , inline ( new · updated · resolved · reopened · + unverified replied · replies already posted) · $reconcile.output.prior_count prior findings, $reconcile.output.verdicts verdicts, - $reconcile.output.retired retired, $reconcile.output.unverified_fixed unverified-fixed, + $reconcile.output.retired retired, $reconcile.output.unverified unverified, $reconcile.output.unmatched_priors unaccounted. ✓" + The inline counts are what this node did. `` counts anchor entries opened. `` counts + still-present bodies updated. `` counts threads this node marked resolved, including + retired entries it handled as fixed. `` counts threads it reopened. `` counts unverified + replies it posted. `` counts replies the repeat rule held back. diff --git a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-qa.yaml b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-qa.yaml index d6909507..6332a0e5 100644 --- a/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-qa.yaml +++ b/apps/claude-code/unic-archon-dlc/.archon/workflows/unic-dlc-qa.yaml @@ -1,23 +1,24 @@ name: unic-dlc-qa description: > - QA box + issue-producing on-ramp. Runs after /build's PR is approved: test → e2e → coverage-gate → - UAT sign-off → verify-pr-base → merge, config-driven and generic. `test` is the floor the merge gate - cannot advance without. Two human gates (UAT + merge) + QA box + issue-producing on-ramp. Runs after /build's PR is approved: pr-check → test → e2e → + coverage-gate → UAT sign-off → verify-pr-base → merge, config-driven and generic. `test` is the floor + the merge gate cannot advance without. Every path that does not merge on a result ends cancelled, + never completed. A node that fails ends the run failed. Two human gates (UAT + merge) honour gates.qa — HITL by default, AFK opt-in. When UAT surfaces a defect, it is filed directly as an agent-ready tracker item, through the contract at `docs/agents/issue-tracker.md` and - `docs/agents/triage-labels.md` (ADR-0016/0024), so the finding feeds /tickets rather than only + `docs/agents/triage-labels.md`, so the finding feeds /tickets rather than only halting. Ported to the key-discriminated node - schema (ADR-0011); inherits /build's self-contained-node conventions (ADR-0023 §5). See ADR-0025. + schema; inherits /build's self-contained-node conventions. -# Required at the workflow level so the UAT + merge approval messages reach the user (ADR-0011). +# Required at the workflow level so the UAT + merge approval messages reach the user. interactive: true nodes: # --------------------------------------------------------------------------------------------- # bootstrap — parse the slug from $ARGUMENTS, read config, confirm the QA baton exists. - # A prompt node (NOT a script) so it needs no plugin-lib import and no $CLAUDE_PLUGIN_ROOT - # (ADR-0023 §5): the agent reads .archon/unic-dlc.config.yaml with its own tools. Every command - # comes from the one `sdlc_needs` block (#430); only the coverage threshold still falls back + # A prompt node (NOT a script) so it needs no plugin-lib import and no $CLAUDE_PLUGIN_ROOT. + # The agent reads .archon/unic-dlc.config.yaml with its own tools. Every command + # comes from the one `sdlc_needs` block; only the coverage threshold still falls back # qa.* → build.*, so a team can hold a heavier QA bar than the build's. # --------------------------------------------------------------------------------------------- - id: bootstrap @@ -35,14 +36,18 @@ nodes: `artifacts_dir` (default "workflows") `gates.qa` (default "hitl") `qa.coverage_threshold` (may be null) — else fall back to `build.coverage_threshold` - `project.branching` (default "gitflow") + `project.branching` (default "gitflow"), which decides only whether `merge` + deletes the merged `feature/*` branch the whole `sdlc_needs` block 3. Confirm the QA baton exists: `//PRD.md`. This is what /specs wrote and /build implemented — the acceptance criteria the UAT checklist is derived from. - If it is absent → status "no-prd". 4. If slug, config, and PRD.md are all present → status "ready". - Compute `expected_base` from branching: "develop" when branching == "gitflow", else "main". + This run's base branch is `$BASE_BRANCH`. Archon resolves it before this node starts, from + `--base`, then `worktree.baseBranch` in `.archon/config.yaml`, then the codebase default. It is + the base the PR must target. Read no base branch from the config, and never derive one from + `project.branching`. `coverage_threshold` is the resolved (qa.* ?? build.*) value — null when neither is set. ## The `sdlc_needs` block — a key names a need, never a tool @@ -79,8 +84,8 @@ nodes: ALWAYS emit EVERY required field, including in the no-slug / no-config / no-prd branches, so the object validates before a guard can cancel. When a value cannot be read (e.g. config is missing), fall back to: slug "" (if no-slug), artifacts_dir "workflows", gate "hitl", branching - "gitflow", expected_base "develop". Set `message` to a human-readable reason for any non-ready - status. + "gitflow". In EVERY branch, start `message` with "Base branch: $BASE_BRANCH." and follow it with + a human-readable reason for any non-ready status. output_format: type: object properties: @@ -108,12 +113,11 @@ nodes: required: [install, build, test, e2e, lint, format, typecheck, dev, coverage] install_report: { type: string } branching: { type: string } - expected_base: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, sdlc_needs, install_report, expected_base, message] + required: [status, slug, artifacts_dir, gate, sdlc_needs, install_report, branching, message] # --------------------------------------------------------------------------------------------- - # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). One guard, + # guard — expected precondition failures cancel cleanly. One guard, # because there is no repository to resolve: `docs/agents/issue-tracker.md` names it, which matters # most here — this is the Box that MERGES. # --------------------------------------------------------------------------------------------- @@ -126,16 +130,81 @@ nodes: needs /specs + /tickets), so the acceptance criteria the UAT checklist reads from exist. # --------------------------------------------------------------------------------------------- - # test — the floor. This Box ran no test suite at all before #430 and merged on an e2e result that - # was almost always a skip. A prompt node (ADR-0023 §5): it reads the declared command from + # pr-check — read the PR state before any test runs. A /qa on a branch with no open PR can + # merge nothing, so it must not spend the test, e2e and coverage runs, and it must not end completed. + # `test` runs only on `pr_state == 'open'`, and the cancel below ends the run on every other state. + # verify-pr-base reads the state again, because a PR can be merged or closed during UAT. + # --------------------------------------------------------------------------------------------- + - id: pr-check + depends_on: [bootstrap] + when: "$bootstrap.output.status == 'ready'" + # always_run: true. The PR state is a fact about the remote, not about this node's exit code. A + # resume must query it again. + always_run: true + context: fresh + allowed_tools: [Read, Bash] + prompt: | + You are the pr-check node of /qa for slug "$bootstrap.output.slug". + + ## The tracker contract + `docs/agents/issue-tracker.md` in this repository is the tracker contract. Read it before the + query. Its § Access names the MCP server or skill that serves this tracker. Read that server's own + current tool list and build the call from it. Write no command, subcommand or flag here, and name + no provider. Its § Addressing names the repository to query. Name that repository + explicitly, and derive nothing from a remote URL. + + Query the PRs whose head is the current feature branch on that repository, in every state. Set + `pr_state`: + · a PR is open → "open" + · no PR is open, and one is merged → "merged" + · no PR is open or merged, and one is closed → "closed" + · the query succeeds and finds no PR → "none" + · the query fails, or the contract is missing or cannot target a repository explicitly → + "unknown" + Never guess a state. A query that did not answer is "unknown". + + Set `message` to the text for the state, word for word, with the placeholders filled: + · open → "The PR for this branch is open." + · none → "No PR was ever opened for this branch. Fix the build and re-run /build." + · merged → "The PR for this branch is already merged. Nothing to do." + · closed → "The PR for this branch was closed without merging. Reopen it, or abandon the work." + · unknown, failed query → "The PR state could not be read: . Check + access to the tracker and re-run /qa." + · unknown, missing or unusable contract → "The PR state could not be read: + docs/agents/issue-tracker.md . Add it + with /unic-archon-dlc:setup." + Print `message` as one line. + output_format: + type: object + properties: + pr_state: + type: string + enum: [open, merged, closed, none, unknown] + message: { type: string } + required: [pr_state, message] + + # --------------------------------------------------------------------------------------------- + # guard-no-open-pr — its `when` is the exact negation of the `when` on `test`. When pr-check + # succeeds, one of the two always holds and the run never goes on with nothing to merge. + # --------------------------------------------------------------------------------------------- + - id: guard-no-open-pr + depends_on: [pr-check] + when: "$pr-check.output.pr_state != 'open'" + cancel: > + /qa did not run the checks, because the PR for this branch is not open or could not be read. + PR state: "$pr-check.output.pr_state". $pr-check.output.message + + # --------------------------------------------------------------------------------------------- + # test — the floor. An earlier version of this Box ran no test suite and merged on an e2e + # result that was almost always a skip. It is a prompt node that reads the declared command from # bootstrap and runs it with its own tools. It REPORTS a verdict rather than hard-failing, and the # merge `when` below requires that verdict to be `pass` — the one clause there that is not `!= fail`. # --------------------------------------------------------------------------------------------- - id: test - depends_on: [bootstrap] - when: "$bootstrap.output.status == 'ready'" + depends_on: [pr-check] + when: "$pr-check.output.pr_state == 'open'" # always_run: true — the result is a fact about the CURRENT branch; a resume must re-run it - # rather than replay a cached pass from before the code changed underneath it (ADR-0033). + # rather than replay a cached pass from before the code changed underneath it. always_run: true context: fresh allowed_tools: [Read, Bash] @@ -175,7 +244,7 @@ nodes: depends_on: [test] when: "$bootstrap.output.status == 'ready'" # always_run: true — the result is a fact about the CURRENT branch; a resume must re-run it - # rather than replay a cached pass from before the code changed underneath it (ADR-0033). + # rather than replay a cached pass from before the code changed underneath it. always_run: true context: fresh allowed_tools: [Read, Bash] @@ -209,13 +278,13 @@ nodes: # --------------------------------------------------------------------------------------------- # coverage-gate — enforce the coverage threshold against a declared coverage command. It wants a - # figure always, so a null on either is `unresolved`, never a skip — `skip` left this enum with #430. + # figure always, so a null on either is `unresolved`, never a skip. This enum has no `skip`. # Reports a verdict like e2e (fail-closes the merge `when`, never hard-fails the run). # --------------------------------------------------------------------------------------------- - id: coverage-gate depends_on: [e2e] # always_run: true — same reason as e2e: coverage is a fact about the CURRENT tree, not a - # promise carried by exit code 0 (ADR-0033). + # promise carried by exit code 0. always_run: true context: fresh allowed_tools: [Read, Bash] @@ -317,7 +386,7 @@ nodes: # --------------------------------------------------------------------------------------------- # uat-gate — HITL by default; skipped when gates.qa == afk. On REJECT, on_reject captures the - # failing criteria as agent-ready tracker issues (finding-capture, ADR-0025) BEFORE the run halts, + # failing criteria as agent-ready tracker issues (finding-capture) BEFORE the run halts, # so the defect is durably tracked and feeds /tickets — not lost when the workflow cancels. # max_attempts: 1 → capture once, then a second reject cancels the run. # @@ -325,9 +394,7 @@ nodes: # blocked by / additional context) and the thin-issues heuristic above it were originally Matt # Pocock's `qa` Method. Upstream v1.1.0 moved `qa` to `skills/deprecated/` with no replacement, so # there is nothing left to resolve by name: the shape is inlined verbatim here and this Harness owns - # it from now on. It is deliberately absent from the Method table in the `unic-archon-dlc` Plugin's - # own README.md § Dependencies — not this repository's README, which is a Consumer's — because an - # entry there would point at a dead upstream branch (#281, #381, #401). + # it from now on. It is deliberately not a listed Method, because its upstream branch is gone. # --------------------------------------------------------------------------------------------- - id: uat-gate depends_on: [uat-prep] @@ -347,14 +414,14 @@ nodes: The reviewer rejected UAT for "$bootstrap.output.slug" with this feedback: $REJECTION_REASON - FINDING-CAPTURE (ADR-0025) — turn each distinct defect into an agent-ready tracker issue so + FINDING-CAPTURE — turn each distinct defect into an agent-ready tracker issue so it feeds /tickets. This is an on-ramp: file directly (a human is present — no PR gate). 1. Read the tracker contract first: `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` in this repository. - **Access** — `issue-tracker.md` § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Write - no command, subcommand or flag here and name no provider (ADR-0016): a flag table + no command, subcommand or flag here and name no provider, because a flag table frozen in this file is stale the moment the tool changes. - **Addressing** — its § Addressing names the repository these findings belong to. Name that repository explicitly in every call, and derive nothing from a remote URL. @@ -378,7 +445,7 @@ nodes: 3. For each finding, create a tracker issue: - Title: "QA: — " - Roles: type `bug`, state `ready-for-agent`. The reviewer at this gate already vetted - the defect, so it is agent-ready — it needs no second /triage pass (ADR-0025). + the defect, so it is agent-ready — it needs no second /triage pass. - Body (the finding-capture brief shape): ## What happened / ## What I expected / ## Steps to reproduce / ## Blocked by (issue #s or "None — can start immediately") / ## Additional context (reference the failing AC-N and the test/e2e/coverage evidence, naming any that came @@ -393,75 +460,140 @@ nodes: max_attempts: 1 # --------------------------------------------------------------------------------------------- - # verify-pr-base — confirm the open PR targets the expected base before merge. trigger_rule: - # all_done so it still runs when uat-gate is SKIPPED in AFK mode (skipped counts as terminal); a - # rejected uat-gate cancels the whole run, so this never runs on a rejected build. Reports base_ok; - # the merge `when` fail-closes on a wrong base rather than this node failing. + # verify-pr-base — read the PR state again, confirm the open PR targets the expected base, and name + # every reason the merge cannot run. trigger_rule: all_done so it still runs when uat-gate is + # SKIPPED in AFK mode (skipped counts as terminal); a rejected uat-gate cancels the whole run, so + # this never runs on a rejected build. It never fails on a red result. guard-not-mergeable below + # cancels the run instead. # --------------------------------------------------------------------------------------------- - id: verify-pr-base depends_on: [uat-gate] trigger_rule: all_done - # always_run: true — base_ok is a fact about the remote PR, not about this node's own exit - # code; a resume must re-query it, never reuse a cached verdict for a PR retargeted since - # (the exact hole ADR-0033 closes). Independent of trigger_rule above; both apply. + # always_run: true — pr_state and base_ok are facts about the remote PR, not about this node's + # own exit code; a resume must re-query them, never reuse a cached verdict for a PR retargeted, + # merged or closed since. Independent of trigger_rule above; both + # apply. always_run: true context: fresh allowed_tools: [Read, Bash] prompt: | You are the verify-pr-base node of /qa for slug "$bootstrap.output.slug". - Expected base branch: $bootstrap.output.expected_base + Expected base branch: $BASE_BRANCH + + UAT checklist from uat-prep, for reference only: $uat-prep.output + ## The tracker contract - `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` in this repository are the - tracker contract. This node needs the first. Read it before the query. Its § Access names the MCP server or skill that serves this tracker — read that server's - own current tool list and build the call from it, writing no command, subcommand or flag here - and naming no provider (ADR-0016). Its § Addressing names the repository to query; name that - repository explicitly, and derive nothing from a remote URL. If the file is absent, or the - tracker cannot target a repository explicitly, treat the base as unverifiable (below) and say - which was the reason. - - Query the open PR for the current feature branch on that repository and read its target base - branch. - - Set `base_ok` to the STRING "true" when the actual base equals expected_base; the STRING "false" - on a genuine mismatch. (It is a string, not a boolean, so the downstream `when` gates compare it - reliably as `== 'true'`.) When the tracker has no queryable PR at all, set `base_ok` "true" and - note that the base is unverifiable here — a warning, non-blocking. Print - "verify-pr-base: base is ✓" or "verify-pr-base: PR targets but expected - ✗ — retarget first." + `docs/agents/issue-tracker.md` in this repository is the tracker contract. Read it before the + query. Its § Access names the MCP server or skill that serves this tracker. Read that server's own + current tool list and build the call from it. Write no command, subcommand or flag here, and name + no provider. Its § Addressing names the repository to query. Name that repository + explicitly, and derive nothing from a remote URL. + + ## 1. The PR state + A PR can be merged or closed during UAT, so read its state again. Query the PRs whose head is the + current feature branch on that repository, in every state. Set `pr_state`: + · a PR is open → "open" + · no PR is open, and one is merged → "merged" + · no PR is open or merged, and one is closed → "closed" + · the query succeeds and finds no PR → "none" + · the query fails, or the contract is missing or cannot target a repository explicitly → + "unknown" + Never guess a state. A query that did not answer is "unknown". + + ## 2. The base + `base_ok` is "true" only when the open PR targets $BASE_BRANCH. Set it to the STRING "true" only + when `pr_state` is "open" and the PR's target base equals $BASE_BRANCH. Set it to the STRING "false" + in every other case, including when no PR is open and when the query failed. (It is a string, not + a boolean, so the downstream `when` gates compare it reliably as `== 'true'`.) Set `actual_base` + to the open PR's target branch, or to "" when no PR is open. + + ## 3. Every reason the merge cannot run + The merge runs only when all of these hold: the test result "$test.output.result" is "pass", the + e2e result "$e2e.output.result" is not "fail", the coverage-gate result + "$coverage-gate.output.result" is not "fail", `pr_state` is "open", and `base_ok` is "true". An + "unresolved" e2e or coverage-gate result does not block the merge. + + Set `blockers` to one sentence for EVERY condition that does not hold, in this order, joined with + a space. Write all of them. One cause never replaces another. + · test is not "pass" → "The test result is , and a merge needs pass." + · e2e is "fail" → "The e2e suite failed." + · coverage-gate is "fail" → "The coverage gate failed." + · `pr_state` is not "open" → the text for that state, word for word, with the placeholders + filled: + · none → "No PR was ever opened for this branch. Fix the build and re-run /build." + · merged → "The PR for this branch is already merged. Nothing to do." + · closed → "The PR for this branch was closed without merging. Reopen it, or abandon the + work." + · unknown, failed query → "The PR state could not be read: . + Check access to the tracker and re-run /qa." + · unknown, missing or unusable contract → "The PR state could not be read: + docs/agents/issue-tracker.md . Add + it with /unic-archon-dlc:setup." + · `pr_state` is "open" and the base does not match → "The PR targets but + was expected. Retarget it, then re-run /qa." Never say the PR is missing in this case. + When every condition holds, set `blockers` to "". + + Print exactly one of these lines: + - nothing blocks → "verify-pr-base: base is ✓" + - otherwise → "verify-pr-base: the merge cannot run. ✗" output_format: type: object properties: + pr_state: + type: string + enum: [open, merged, closed, none, unknown] base_ok: type: string enum: ['true', 'false'] actual_base: { type: string } - required: [base_ok] + blockers: { type: string } + required: [pr_state, base_ok, actual_base, blockers] + + # --------------------------------------------------------------------------------------------- + # guard-not-mergeable — its `when` is the exact negation of the `when` on `merge`, written against + # the same outputs, so one of the two always holds. A run that cannot merge ends cancelled here + # instead of skipping `merge` and ending completed. The message carries the results as well + # as verify-pr-base's sentences, so the causes stay readable even if `blockers` came back short. + # --------------------------------------------------------------------------------------------- + - id: guard-not-mergeable + depends_on: [verify-pr-base] + when: "$test.output.result != 'pass' || $e2e.output.result == 'fail' || $coverage-gate.output.result == 'fail' || $verify-pr-base.output.pr_state != 'open' || $verify-pr-base.output.base_ok != 'true'" + cancel: > + /qa did not merge "$bootstrap.output.slug". $verify-pr-base.output.blockers Results: test + "$test.output.result", e2e "$e2e.output.result", coverage-gate "$coverage-gate.output.result", + PR state "$verify-pr-base.output.pr_state", base_ok "$verify-pr-base.output.base_ok". # --------------------------------------------------------------------------------------------- - # merge-gate — HITL by default; skipped in AFK. Only pauses when the base is verified good, so an - # approval is never solicited for a merge that would be blocked anyway. + # merge-gate — HITL by default; skipped in AFK. Its `when` is the gate check plus the conditions of + # the `when` on `merge`, so it asks for approval only when `merge` would run. # --------------------------------------------------------------------------------------------- - id: merge-gate depends_on: [verify-pr-base] - when: "$bootstrap.output.gate == 'hitl' && $verify-pr-base.output.base_ok == 'true'" + when: "$bootstrap.output.gate == 'hitl' && $test.output.result == 'pass' && $e2e.output.result != 'fail' && $coverage-gate.output.result != 'fail' && $verify-pr-base.output.pr_state == 'open' && $verify-pr-base.output.base_ok == 'true'" + # Archon 0.12.1 substitutes node outputs in an approval message but not $BASE_BRANCH, so + # this message names the PR target through verify-pr-base's actual_base and the run + # base through bootstrap's message, which starts with it. approval: message: > - UAT approved and PR base verified as "$bootstrap.output.expected_base" for - "$bootstrap.output.slug". The test result is "$test.output.result". A merge needs "pass": this - gate is not what enforces that — the merge node downstream is — so if the result above is not - "pass", approving here will not produce a merge. Which checks could not run is written in + UAT approved for "$bootstrap.output.slug", and verify-pr-base passed. PR target: + "$verify-pr-base.output.actual_base". $bootstrap.output.message + The test result is "$test.output.result". Which checks could not run is written in //qa-checks.md. Approve to merge the PR through the tracker (and delete the feature branch on Gitflow). Reject to halt without merging. # --------------------------------------------------------------------------------------------- # merge — merge the PR through the tracker. trigger_rule: all_done so it runs when merge-gate - # is SKIPPED in AFK; the `when` fail-closes so it NEVER runs on a red test/e2e/coverage or a wrong - # base, in either mode. A rejected merge-gate cancels the run before this node (max_attempts). + # is SKIPPED in AFK; the `when` fail-closes so it NEVER runs on a red test/e2e/coverage, a PR that + # is not open or a wrong base, in either mode. A rejected merge-gate cancels the run before this + # node (max_attempts). # # The `test` clause reads `== 'pass'` while e2e and coverage read `!= 'fail'`, and that asymmetry is - # the floor (#430): `test` is the one need this gate cannot advance without, so an `unresolved` test + # the floor: `test` is the one need this gate cannot advance without, so an `unresolved` test # holds the merge, while an unresolved e2e or coverage reports and lets it through. Every node here # reports three states and `skip` is gone, so `!= 'fail'` no longer means "ran or was skipped" — it # means "did not fail", unresolved included, deliberately. @@ -469,7 +601,7 @@ nodes: - id: merge depends_on: [merge-gate] trigger_rule: all_done - when: "$test.output.result == 'pass' && $e2e.output.result != 'fail' && $coverage-gate.output.result != 'fail' && $verify-pr-base.output.base_ok == 'true'" + when: "$test.output.result == 'pass' && $e2e.output.result != 'fail' && $coverage-gate.output.result != 'fail' && $verify-pr-base.output.pr_state == 'open' && $verify-pr-base.output.base_ok == 'true'" context: fresh allowed_tools: [Read, Bash] prompt: | @@ -480,17 +612,38 @@ nodes: tracker contract. This node is the one irreversible act in the lifecycle, and step 2 below writes a role, so read both before the merge call. Its § Access names the MCP server or skill that serves this tracker — read that server's own current tool list and build the call from it, - writing no command, subcommand or flag here and naming no provider (ADR-0016). Its § Addressing + writing no command, subcommand or flag here and naming no provider. Its § Addressing names the repository to merge in; name that repository explicitly, and derive nothing from a - remote URL. If the file is absent, STOP and print "BLOCKED: this repository has no tracker - contract at docs/agents/issue-tracker.md" and merge nothing. + remote URL. If the file is absent, merge nothing, set `merged` to "false", and set `reason` to + "BLOCKED: this repository has no tracker contract at docs/agents/issue-tracker.md. Add it with + /unic-archon-dlc:setup, then re-run /qa." + + ## What this node reports + Set `merged` to the STRING "true" only when the merge call returned success and the PR now reads + as merged. Set it to the STRING "false" in every other case, and set `reason` to why: + · the tracker contract is missing (the text above) + · the tracker has no merge capability → say so, then give the manual steps for a human + (merge the PR, then write the role `resolved` and the value its row gives on every linked + item) + · the branch policy refuses the merge → the refusal as received + · the merge call errors → the error as received + · the merge call returned success, but the PR state could not be read again → say so, give + the error as received, and tell the operator to check the PR. If it merged, the operator + writes the role `resolved` and the value its row gives on every linked item by hand, + because a re-run of /qa reads the PR as merged and does nothing + · the merge call returned success, but the PR is not merged yet, as under a merge queue or + auto-merge → say so, and tell the operator to watch the PR. When it merges, the operator + writes the role `resolved` and the value its row gives on every linked item by hand + A "false" ends the run cancelled with `reason` as its message. Whenever `merged` is "false", + the unconfirmed cases included, skip steps 2 and 3, so no item takes `resolved`. Say that in + `reason`. When `merged` is "true", set `reason` to "". 1. Merge the open PR for the current feature branch on that repository. **The merge style is the host's to enforce, not this Box's to choose.** Use whatever the repository's own branch policy allows; where it allows more than one, the team's `CLAUDE.md` states the rule — read it. Pick nothing yourself, and do not override a policy that refuses a style. - If the tracker has no merge capability at all, merge nothing, skip step 2, and print the manual - steps for a human instead — including the role `resolved` and the value its row gives. + If the tracker has no merge capability at all, merge nothing. + Whenever `merged` is "false", skip steps 2 and 3. 2. **After the merge lands, write the `resolved` role on every item the PR links.** Read the PR's links collection for them; nothing else is authoritative, and this is why `open-pr` created @@ -513,8 +666,32 @@ nodes: The PR is already merged when this runs, so a failure here is reported and never retried into a second merge: print which items took the role, which were retracted, and which failed. - 3. On Gitflow ($bootstrap.output.branching == "gitflow"), after a successful merge: check out the base - branch and delete the merged `feature/*` branch. Under any other branching model, leave branch + 3. On Gitflow ($bootstrap.output.branching == "gitflow"), after a successful merge: check out + `$BASE_BRANCH` and delete the merged `feature/*` branch. Under any other branching model, leave branch cleanup to whatever auto-delete the host provides. - Finally print: "merge: QA for \"$bootstrap.output.slug\" complete — item(s) resolved. ✓" + Only when `merged` is "true", finally print: + "merge: QA for \"$bootstrap.output.slug\" complete — item(s) resolved. ✓" + `` counts only the items that took `resolved`. If writing `resolved` failed on some items, + add " Not resolved: ." before the ✓. `/qa` never reverts a merge that landed, so the + run still ends completed. When `merged` is "false", never print this line. + output_format: + type: object + properties: + merged: + type: string + enum: ['true', 'false'] + reason: { type: string } + required: [merged, reason] + + # --------------------------------------------------------------------------------------------- + # guard-not-merged — `merge` can run and still not confirm a merge (no tracker contract, no merge + # capability, a branch policy that refuses, an error, a PR state it could not read again, or a + # call that succeeded while the PR is not merged yet, as under a merge queue or auto-merge). Its + # `when` is the exact negation of "merged", so such a run ends cancelled instead of completed. + # --------------------------------------------------------------------------------------------- + - id: guard-not-merged + depends_on: [merge] + when: "$merge.output.merged != 'true'" + cancel: > + /qa could not confirm a merge for "$bootstrap.output.slug". $merge.output.reason diff --git a/apps/claude-code/unic-archon-dlc/.claude-plugin/marketplace.json b/apps/claude-code/unic-archon-dlc/.claude-plugin/marketplace.json index 1d4c1e35..83ea86c0 100644 --- a/apps/claude-code/unic-archon-dlc/.claude-plugin/marketplace.json +++ b/apps/claude-code/unic-archon-dlc/.claude-plugin/marketplace.json @@ -21,7 +21,7 @@ "name": "unic-archon-dlc", "source": "./", "tags": ["productivity", "workflow", "ai-development"], - "version": "0.29.0" + "version": "0.36.1" } ] } diff --git a/apps/claude-code/unic-archon-dlc/.claude-plugin/plugin.json b/apps/claude-code/unic-archon-dlc/.claude-plugin/plugin.json index 2e88f014..49c536da 100644 --- a/apps/claude-code/unic-archon-dlc/.claude-plugin/plugin.json +++ b/apps/claude-code/unic-archon-dlc/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "unic-archon-dlc", - "version": "0.29.0", + "version": "0.36.1", "description": "[DEPRECATED — being replaced, see #677] A config-driven Harness for the SDLC — the AI development lifecycle — as an installable Claude Code plugin — main line /specs → /tickets → /build → /pr-review → /qa, plus /triage, /explore, /improve-architecture, and /cleanup. Four Archon workflows (build, qa, pr-review, explore) with human approval gates; the rest are commands/skills composing Matt Pocock's methods.", "author": { "name": "Unic AG", diff --git a/apps/claude-code/unic-archon-dlc/AGENTS.md b/apps/claude-code/unic-archon-dlc/AGENTS.md index 267020a2..f8fd7dee 100644 --- a/apps/claude-code/unic-archon-dlc/AGENTS.md +++ b/apps/claude-code/unic-archon-dlc/AGENTS.md @@ -57,15 +57,15 @@ Load-bearing invariants. These either originate in a Plugin ADR or are policy de - **A Consumer's formatter is what forks an installed file, so `/setup` excludes what it installs — by reading the project, never from a list of tools.** One `pnpm format` on `DXP-DesignSystem` rewrote **thirteen** Method files, because a Markdown glob reached `.archon/methods/` and nothing excluded it (2026-08-18). The failure is silent: a reflowed file raises no error and no test failure, it just stops matching the release it was copied from — and the generated header is then the only provenance a Consumer has. So `/setup` reads whatever this project runs to format or lint, whatever the language, works out each tool's exclusion mechanism from that tool's own documentation, and excludes both installed trees plus every file whose name carries `.generated.`. **Name no tool and no config filename in this Plugin's prose**: knowing that an installed file must not be reformatted is ours, and knowing which tool would do it is the project's. The Boxes are `.yaml` and that day's glob missed them by luck, so they are excluded too — the exclusion is written for the glob that grows. Where a tool's exclusion mechanism cannot carry a comment marker, `/setup` prints the entry the operator must add and reports it as an open item rather than editing a value it could not later tell from a hand-written one. - **The DLC owns the _what_; it composes team system-skills for the _how_.** No box hardcodes a tracker/docs/design system. For docs and design it reads `.archon/unic-dlc.config.yaml` and composes the configured skill/CLI/MCP (MCP-first, CLI-fallback); for the **tracker** it reads the contract at `docs/agents/issue-tracker.md`, which names the server and the repository. See [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md). - **The Plugin is a Harness; the Methods it hosts own procedure.** The Harness owns isolation, gates, config, red/green integrity, system-skill composition, artefact durability and posting. A **Method** — Matt Pocock's skill text — owns _how_ the work is done, and no Box paraphrases, summarises or improves it: any paragraph in a `commands/*.md` file explaining how to do the work is a defect, because the Method already says it and the two will diverge. A Box therefore survives **only for what no Method can supply** — the structural bar that replaces ADR-0021's "adds value" judgement. Config carries _parameters_ (which, where, whether); a Method carries _procedure_, so a team wanting different method text forks the Method rather than gaining a config key. Vocabulary in [`CONTEXT.md`](CONTEXT.md); rationale in [ADR-0030](docs/adr/0030-harness-hosts-methods.md) and [ADR-0032](docs/adr/0032-box-method-vocabulary.md). -- **One dependency list, and it is prose.** The table under [README.md § Dependencies](README.md#dependencies) is the only place a Method name sits beside the Box that reads it. Nothing generates it and nothing checks it, so edit it in the same commit as the Box or command whose Methods changed. **Never restate the list** — not here, not in a command file. A Box's own prose may name the Methods _it_ reads, but only names the table agrees with; the roll-up of what the Plugin depends on exists once. Three surfaces once said 7, 6 and 11, which is how the upstream v1.1.0 rename wave broke `/specs` and `/tickets` with CI green — the fix then was a manifest and a drift test, and the fix now is fewer surfaces: one table, no mirrors. **A rename wave is found by running a Box against a live Consumer**, which is where both 0.22.0 command defects were found; nothing in this repository can see upstream. Every Box and every command reads a Method at the literal path `.archon/methods//SKILL.md` — one path, no resolution order, no tier line, and no config or `.local` override (#381 retired both). `/qa` reads no Method: upstream deprecated `qa` at v1.1.0, so its brief shape is inlined and this Harness owns it. Methods ship in the Bundle at `vendor/mattpocock-skills/`, are installed by `/setup` into `.archon/methods/`, and are **read by path, never registered as skills** — registering them would give a Consumer running Matt's own plugin every skill twice. The plugin version is the pin; there is no `skills.pin` key. See [ADR-0031](docs/adr/0031-methods-bundled-three-tier-resolution.md), amended. -- **A Box states WHICH repository and never HOW; it stages named paths and never sweeps.** Two rules, both written **inline in every prompt that needs them** — a Box node is self-contained and imports nothing from the Plugin ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5), so a rule that lives only here is invisible at run time. (1) **The repository comes from the tracker contract.** `docs/agents/issue-tracker.md` § Addressing names it, every PR and tracker call names that repository explicitly, and **nothing derives one from a remote URL** — one remote has several spellings and a fork clone names two repositories, which is why the old `origin` derivation needed a `project.repo_ref` override and an ambiguity guard to correct itself. All three are deleted (#389). A node that finds the contract file missing STOPS and says so; it never falls back to a guess. No prompt or command doc may carry a host CLI token, subcommand or flag, or a provider name: it names a **role** and a **file**, and reads the current tool list of whatever server § Access names ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). A flag table frozen in a YAML file is stale the moment the tool changes, cannot be verified without a live tenant, and is copied as precedent by the next agent that reads it — a closed attempt shipped a subcommand that does not exist. **One carve-out, and it is narrow: a dated evidence citation may name a host.** A rule earned by a rejected call cannot be checked later without the run that earned it, so the date, the pull request and what the host did are the part worth keeping — the citation records what happened, and the instruction beside it stays generic, naming a role and a file. What stays forbidden is a payload, a subcommand or a flag written as something to send, and a date next to it changes nothing. Two citations of this shape are live in `unic-dlc-pr-review.yaml`: the anchor a host accepted on a file no revision touched, and the four coordinates it required of an inline thread (#407). Both sit under a generic rule that tells a node to ask the tracker. (2) **Explicit staging.** Every committing node stages a named list, one `git add ` per path, then confirms with `git status --porcelain`; never `git add -A`/`.`/`-u`, and never `pr-body.md`, `*.tmp.md`, `*.scratch.md`, or anything under `$ARTIFACTS_DIR` (Archon's per-run dir, outside the repo tree). `/build`'s `build-state.json` is committed **once**, at `open-pr` — never in a loop iteration — because it is the durable proof of [ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md)'s anti-cheat claim. Both rules are enforced by reading, across every Box YAML this Plugin ships, each Box's operator doc under `docs/boxes/` (they moved out of `.archon/commands/` in #294), and every command doc. Read them when you touch a Box, and again in review — a repository derived from a remote URL and a `git add -A` are both visible on the page. +- **One dependency list, and it is prose.** The table under [README.md § Dependencies](README.md#dependencies) is the only place a Method name sits beside the Box that reads it. Nothing generates it and nothing checks it, so edit it in the same commit as the Box or command whose Methods changed. **Never restate the list** — not here, not in a command file. A Box's own prose may name the Methods _it_ reads, but only names the table agrees with; the roll-up of what the Plugin depends on exists once. Three surfaces once said 7, 6 and 11, which is how the upstream v1.1.0 rename wave broke `/specs` and `/tickets` with CI green — the fix then was a manifest and a drift test, and the fix now is fewer surfaces: one table, no mirrors. **A rename wave is found by running a Box against a live Consumer**, which is where both 0.22.0 command defects were found; nothing in this repository can see upstream. Every Box and every command reads a Method at the literal path `.archon/methods//SKILL.md` — one path, no resolution order, no tier line, and no config or `.local` override (#381 retired both). `/qa` reads no Method: upstream deprecated `qa` at v1.1.0, so its brief shape is inlined and this Harness owns it. Methods ship in the Bundle at `vendor/mattpocock-skills/`, are installed by `/setup` into `.archon/methods/`, and are **read by path, never registered as skills** — registering them would give a Consumer running Matt's own plugin every skill twice. The plugin version is the pin; there is no `skills.pin` key. See [ADR-0031](docs/adr/0031-methods-bundled-three-tier-resolution.md). +- **A Box states WHICH repository and never HOW; it stages named paths and never sweeps.** Two rules, both written **inline in every prompt that needs them** — a Box node is self-contained and imports nothing from the Plugin ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5), so a rule that lives only here is invisible at run time. (1) **The repository comes from the tracker contract.** `docs/agents/issue-tracker.md` § Addressing names it, every PR and tracker call names that repository explicitly, and **nothing derives one from a remote URL** — one remote has several spellings and a fork clone names two repositories, which is why the old `origin` derivation needed a `project.repo_ref` override and an ambiguity guard to correct itself. All three are deleted (#389). A node that finds the contract file missing STOPS and says so; it never falls back to a guess. No prompt or command doc may carry a host CLI token, subcommand or flag, or a provider name: it names a **role** and a **file**, and reads the current tool list of whatever server § Access names ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). A flag table frozen in a YAML file is stale the moment the tool changes, cannot be verified without a live tenant, and is copied as precedent by the next agent that reads it — a closed attempt shipped a subcommand that does not exist. **One carve-out, and it is narrow: a dated evidence citation may name a host.** A rule earned by a rejected call cannot be checked later without the run that earned it, so the date, the pull request and what the host did are the part worth keeping — the citation records what happened, and the instruction beside it stays generic, naming a role and a file. What stays forbidden is a payload, a subcommand or a flag written as something to send, and a date next to it changes nothing. Two citations of this shape are live in `unic-dlc-pr-review.yaml`: the anchor a host accepted on a file no revision touched, and the four coordinates it required of an inline thread (#407). Both sit under a generic rule that tells a node to ask the tracker. (2) **Explicit staging.** Every committing node stages a named list, one `git add ` per path, then confirms with `git status --porcelain`; never `git add -A`/`.`/`-u`, and never `pr-body.md`, `*.tmp.md`, `*.scratch.md`, or anything under `$ARTIFACTS_DIR` (Archon's per-run dir, outside the repo tree). `/build`'s `build-state.json` is committed **once**, in the node that commits the Session artefacts after `report` — never in a loop iteration — because it is the durable proof of [ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md)'s anti-cheat claim. Both rules are enforced by reading, across every Box YAML this Plugin ships, each Box's operator doc under `docs/boxes/` (they moved out of `.archon/commands/` in #294), and every command doc. Read them when you touch a Box, and again in review — a repository derived from a remote URL and a `git add -A` are both visible on the page. - **A Box is read where it runs, so every reference in one resolves in the Consumer.** `/setup` copies the four Box YAMLs into a Consumer's `.archon/workflows/`, so a repo-relative path inside a Box is read against the **Consumer's** root and never this Plugin's. Most references are therefore correct as bare paths, and that is the majority on purpose — the tracker contract, the config, the installed Methods, and anything a node reads about the repository under review all mean the Consumer's own file. **Do not enumerate them here.** A list of what the Boxes reference is a mirror of the Boxes, nothing checks it, and the first draft of this bullet had already dropped `.archon/unic-dlc.config.yaml` despite eleven uses across the four — which is the failure mode the one-dependency-list rule above exists to prevent. The test is the question, not a list: **does this path name something the Consumer has?** A reference that means **this Plugin's** tree has to name the Plugin — "the `unic-archon-dlc` Plugin's own `README.md` § Dependencies" — because a bare `README.md` resolves in both trees and to the wrong document in one of them, which is harder to notice than a path resolving nowhere: the pointer it replaced said `lib/methods-manifest.mjs`, a file no Consumer has, and the rewrite traded a dangling pointer for a plausible one (#401). A bare `ADR-NNNN` in a Box always means a Plugin ADR, and this sentence is the whole of that convention. Be clear about what it buys and what it does not. A Consumer keeps a `docs/adr/` of its own, so the two numberings share a namespace — and `/build` is what grows the Consumer's: `unic-dlc-build.yaml`'s report node drafts `docs/adr/NNNN-.md` there. So a collision is scheduled rather than hypothetical, even though none exists today (`DXP-DesignSystem` holds `0001` and `0002`; the Boxes cite `0011` upward). This convention is what a reader lands on when one arrives, and it was chosen over rewriting every citation in the four Boxes because a citation carrying its own repository is a sentence nobody keeps current. **If the maintainer would rather have the citations qualified, that is a decision this bullet does not get to make for him** (#401). - **Container follows structural need.** Archon workflows only for AFK-isolated, no-live-conversation work (`/build`, `/qa`, `/pr-review`, `/explore`); Claude Code commands/skills for interactive or repo-global work, composing Matt's originals. See [ADR-0017](docs/adr/0017-container-follows-structural-need.md). - **No code at all.** This Plugin ships prose, four Box YAMLs and the Method Bundle. [ADR-0018](docs/adr/0018-generic-core-config-compose.md) drew a line between tested code and config-plus-composition and expected `lib/` to shrink to a deterministic core; it shrank to nothing (#381, amending that ADR), because an installed Plugin cannot resolve a module or an external package — `$CLAUDE_PLUGIN_ROOT` is unset inside the Bash tool and there is no `node_modules` ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5, amended). A command reads config, the tracker contract and its Methods with its own tools; an Archon `script:` node inlines its logic. **Do not reintroduce a module** — not for schema validation, not for a topological sort, and above all not so that prose becomes testable, which the root `AGENTS.md` names as a defect rather than rigour. - **A Plugin ADR takes its form from the `domain-modeling` Method, with two local rules.** Every Plugin ADR carries a `Status` line, and a decision that still stands while a detail of it changes is revised **inline** — the body edited to read as current, the status line gaining the revision date, no amendment block added — while a superseding ADR is reserved for a decision that is reversed or replaced; the shape, the Status set and the reasoning are in [`docs/adr/README.md`](docs/adr/README.md) (#452). - **Session is scoped by Slug.** Every Session artefact (Findings, PRD, Issues JSON, `build-state.json`, report) is keyed by a single Slug under `//`. No cross-Session bleed. See [CONTEXT.md](CONTEXT.md). - **The issue tracker is the single source of truth for "where are we."** `HANDOFF.md` and `ROADMAP.md` are dropped, and the old state-snapshot `triage` workflow is retired — no workflow writes either file. Per-thread continuity is handled by `/handoff` — one of Matt's skills, referenced in prose here and never implemented in this Plugin (it compacts the live conversation, so it cannot be an Archon workflow — see [ADR-0017](docs/adr/0017-container-follows-structural-need.md)), which writes a throwaway file, not a durable repo snapshot. See [ADR-0013](docs/adr/0013-tracker-single-source-of-truth.md). -- **The tracker contract is two repo-local prose files, not a config schema.** `docs/agents/issue-tracker.md` carries **Access** (which MCP server or skill serves this tracker), **Addressing** (the repository) and **Work-item scope** (the one filter every search applies); `docs/agents/triage-labels.md` carries the seventeen canonical roles, each row naming its value **and** the **axis** that carries it. `/unic-archon-dlc:setup` owns both, and `setup-matt-pocock-skills` must never run over them — it writes another host's template over the first and reverts the second to a five-role `wontfix` vocabulary. **The axis belongs to the role, not to the tier**: measured on a real tenant, **five of the eight** state roles cannot be states at all — writing a state while work is open moves an already-active item backwards on the board, so only the three terminal roles are states there and the five open-work roles are tags. So `.archon/unic-dlc.config.yaml` holds **no tracker facts** — the `tracker` block, `classification.labels` and `project.pr_strategy` are gone and `MANDATORY_PATHS` is `project.branching` alone (#389). A section earns its place in those two files only when it states a fact about this tenant: a server discovers its own API, so writing operations down freezes a flag table in Markdown, the same defect ADR-0016 forbids in a prompt and harder to notice. See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended 2026-08-18. +- **The tracker contract is two repo-local prose files, not a config schema.** `docs/agents/issue-tracker.md` carries **Access** (which MCP server or skill serves this tracker), **Addressing** (the repository) and **Work-item scope** (the one filter every search applies); `docs/agents/triage-labels.md` carries the seventeen canonical roles, each row naming its value **and** the **axis** that carries it. `/unic-archon-dlc:setup` owns both, and `setup-matt-pocock-skills` must never run over them — it writes another host's template over the first and reverts the second to a five-role `wontfix` vocabulary. **The axis belongs to the role, not to the tier**: measured on a real tenant, **five of the eight** state roles cannot be states at all — writing a state while work is open moves an already-active item backwards on the board, so only the three terminal roles are states there and the five open-work roles are tags. So `.archon/unic-dlc.config.yaml` holds **no tracker facts** — the `tracker` block, `classification.labels` and `project.pr_strategy` are gone and `MANDATORY_PATHS` is `project.branching` alone (#389). A section earns its place in those two files only when it states a fact about this tenant: a server discovers its own API, so writing operations down freezes a flag table in Markdown, the same defect ADR-0016 forbids in a prompt and harder to notice. See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md). - **`/triage` is the intake on-ramp — a thin wrapper over Matt's method, bound to the tracker contract.** It turns raw work (bugs, requests, QA findings, external PRs) into agent-ready tracker items feeding `/tickets`. It **composes Matt's `triage` method and reads the same two `docs/agents/` files the method reads** — the rule that once forbade that read is dead, because the two-writer problem it solved is now solved by removing one writer. `setup-matt-pocock-skills` is still **not** a Plugin dependency — only Matt's skill _methods_ are. It writes only to the tracker + `` (human present → no PR gate); it produces no `issues.json`/PRD. See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md). - **The transition table: every point the Harness writes a Canonical role, and which one.** Stated here because it is Harness policy — the lifecycle is the _what_, so which role each Box writes is ours, not a tenant fact and not a Method's procedure. It is written down as a whole because the two holes below were invisible while it was scattered, and because "before you write a role" is uncheckable without a list of the places that do. @@ -79,24 +79,26 @@ Load-bearing invariants. These either originate in a Plugin ADR or are policy de | `/qa` | UAT is rejected | `bug` + `ready-for-agent` on each finding | | `/qa` | the PR merges | `resolved` on every item the PR links | - **`/build` writes no role, and needs none.** "An agent has started this" is already on the item: `open-pr` links each work item to the pull request, and every host surfaces that link on the item itself. So no `in-progress` Canonical role is needed — adding one would change the protocol every Box shares to duplicate a signal the host already gives. Recorded so nobody re-derives it. + **`/build` writes no role, and needs none.** "An agent has started this" is already on the item: on a `pass` verdict `open-pr` links each work item to the pull request, and every host surfaces that link on the item itself. A build whose verdict is not `pass` opens no pull request and links nothing; how that build reaches a human is [#658](https://github.com/unic/unic-agents-plugins/issues/658). So no `in-progress` Canonical role is needed — adding one would change the protocol every Box shares to duplicate a signal the host already gives. Recorded so nobody re-derives it. **A `state`, `type` or `priority` role is single-valued.** Only one role of a tier is true of an item at a time. Before a Box writes such a role it reads the other rows of that tier in `docs/agents/triage-labels.md` and retracts every one whose axis **holds many** values; an axis that holds one value retracts the old value itself. Every surface in the table above states this rule inline, because a Box node imports nothing ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The rule reads the `holds` column and **never an axis name**: `Tag` and `Label` are host words, and a rule phrased on one is wrong on the next host — the same defect [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md) forbids in a prompt, one level up. Before #389 every state role resolved to a single-value field, so nothing could accumulate and this rule had nothing to do; the moment a tenant puts a state role on a multi-value axis it becomes load-bearing. **An extra row in `triage-labels.md` changes no Box.** A Box names its roles literally — `/tickets` writes `ready-for-agent` — so a team-added role is inert, and that is by design: the Canonical roles are the protocol the Boxes share ([`CONTEXT.md`](CONTEXT.md)). The team owns each role's value, its axis and its cardinality; it never owns the role set. A team that wants a Box to behave differently forks the Method, because a transition is procedure, not a parameter. -- **The `## unic-archon-dlc` block in a Consumer's `CLAUDE.md` enumerates no Box and no pipeline stage.** Setup replaces the whole block delimited by `` and `` in place, markers included, and preserves everything outside it verbatim across re-runs. A Box list and a pipeline order are what a Plugin release renames, reorders and drops with no Consumer-side change — `/archon-upgrade` fell out of the old block and nobody noticed — so the block carries only paths on the Consumer's own disk (`.archon/unic-dlc.config.yaml`, the tracker contract at `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md`, `.archon/workflows/` and its `unic-dlc-*.yaml` naming, and `.archon/methods/` — `.archon/methods.local/` left the block with the override tier it advertised, #381), `archon workflow list` and `archon workflow run ""` to discover and run what is actually installed there, and a link to this Plugin's README for what each Box does. The contract pair earns its line because the block is a Consumer's only pointer to the two files a Box refuses to run without, and because those two are tenant-owned: the block says nothing regenerates them once they exist (#383). It names `/unic-archon-dlc:setup` alone, because it cannot say what regenerates it without naming it. Nothing detects a hand-edit between the markers and nothing will, so the block never presents itself as a managed document and carries no `AUTO-GENERATED — DO NOT EDIT` banner (#296). +- **The `## unic-archon-dlc` block in a Consumer's `CLAUDE.md` carries one hand-kept line per Box and no pipeline order.** Setup replaces the whole block delimited by `` and `` in place, markers included, and preserves everything outside it verbatim across re-runs. When it finds a block under the pre-0.30.0 `unic-archon-dlc` markers, it rewrites that block in place under the new ones, so a re-run leaves one block, never two ([ADR-0038](docs/adr/0038-practice-packs-stack-by-prefix.md)). A Box list is what a Plugin release renames and drops with no Consumer-side change, and `/archon-upgrade` once fell out of the block unnoticed. So a pull request that adds, renames or drops a Box changes the Box lines in `commands/setup.md` too, and `/setup` rewrites the block on every run, so the change reaches a Consumer on its next run. Beside the Box lines, the block carries paths on the Consumer's own disk (`.archon/unic-dlc.config.yaml`, the tracker contract at `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md`, `.archon/workflows/` and its `unic-dlc-*.yaml` naming, and `.archon/methods/` — `.archon/methods.local/` left the block with the override tier it advertised, #381), `archon workflow list` and `archon workflow run ""` to discover and run what is actually installed there. Each Box line says what the Box does and which `gates.*` key decides its human pauses. The contract pair earns its line because the block is a Consumer's only pointer to the two files a Box refuses to run without, and because those two are tenant-owned: the block says nothing regenerates them once they exist (#383). It names `/unic-archon-dlc:setup` alone, because it cannot say what regenerates it without naming it. Nothing detects a hand-edit between the markers and nothing will, so the block never presents itself as a managed document and carries no `AUTO-GENERATED — DO NOT EDIT` banner (#296). - **Slopcheck before build.** Every new package referenced in `package.json` is verified against the npm registry before any RED/GREEN phase runs. Packages that fail are flagged `[ASSUMED]` and require explicit human approval. - **Nyquist map gates the build.** Every issue in Issues JSON must carry a `test_command` (or `test_command_planned`) before `/build` consumes it — the gate runs in `/tickets` ([ADR-0022](docs/adr/0022-tickets-slice-to-build.md)); `/build` reads the build-ready `issues.json` directly ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md)). -- **The evidence gate is a deterministic writer's presence check, never a self-judging prompt.** `/build` declares `evidence_policy: { required: true }`; `verification` and `goals-check` report a boolean `passed` alongside their existing prose verdicts, and `verification` adds a three-state `test_outcome` — `pass`, `fail` or `unresolved` — because a check that could not run is not a green one, and `test` is the one need this gate cannot advance without (#430); a dedicated `evidence` script node deletes any stale file left by a prior resumed attempt, prints its own three-state verdict as JSON (a `script:` node has no `output_format`, so only stdout resolves downstream), then writes `$ARTIFACTS_DIR/evidence.json` only when both booleans are true and the test outcome is `pass`, mirroring it to `//evidence.json` so `open-pr` can stage it. `always_run: true` on `slopcheck`, `verification`, `goals-check`, `evidence` (and on `/qa`'s `verify-pr-base`, `e2e`, `coverage-gate`) opts each out of Archon's resume cache, because their exit code reports a fact about external or mutable state, not about itself — the same hole that let a resumed `/qa` run merge against a since-retargeted PR base. See [ADR-0033](docs/adr/0033-archon-070-schema-target.md) and [ADR-0034](docs/adr/0034-evidence-gate-deterministic-writer.md). -- **`/qa` gates on `gates.qa` and fail-closes the merge.** The QA Archon pipeline (test → e2e → coverage → UAT → verify-pr-base → merge) has two `approval:` gates (UAT + merge), both HITL by default and skipped in AFK; downstream nodes use `trigger_rule: all_done` so AFK auto-merges a clean build, while the merge node's fail-closed `when` blocks auto-merging a red test, e2e or coverage, a wrong PR base, **or a `test` need that could not run at all** — `test` is the floor, so its clause reads `== 'pass'` while the other two read `!= 'fail'` (#430). `always_run: true` covers `verify-pr-base`, `test`, `e2e` and `coverage-gate`. `/qa` is also an **issue-producing on-ramp**: a UAT rejection files each defect directly as a `ready-for-agent` tracker item (through the tracker contract in `docs/agents/`, the inlined finding-capture brief shape — upstream's own `qa` Method was deprecated at v1.1.0 with no replacement (#281) — and the AI disclaimer) that feeds `/tickets` — it does not just halt. Never `lib/tracker-adapter.mjs` (dissolved). See [ADR-0025](docs/adr/0025-qa-pipeline-onramp.md). -- **`/pr-review` is a self-contained fan-out Archon review that harvests, never depends.** The PR-review Archon pipeline (bootstrap → guard → prep → review → synthesize → reconcile → review-gate → post) reviews the open PR and posts one summary discussion on the pull request itself plus per-line discussions, through the tracker its contract names — a host carries the first as a thread with no file anchored to it or as a comment on the PR, and the Box asks which rather than promising either. It **harvests `unic-pr-review`'s review learnings** (confidence→severity rubric, structured summary, hidden-marker idempotency, conditional spawn table, two-surface posting) with **no ADO code and no runtime dependency** on that plugin. `prep` composes **one Intent Brief** (linked work items + Confluence/MD docs + PR description + `PRD.md`) that is **injected into both review axes** (the quality lever) and surfaces cross-source contradictions. The single `review` node reads the `code-review` Method and runs **its own** two-axis sub-agent fan-out — Standards (repo standards + the twelve-item Fowler smell baseline, pasted in full because the sub-agent has no other access to it) and Spec (AC coverage, scope creep, wrong-looking implementations) — replacing seven hand-written aspect prompts (#281, ADR-0026 §8). Refactoring lives in that Standards axis, having left `/build`'s loop. This Box holds the **only review-posting authority**: `/build`'s `implement-review-precheck` runs the same Method as a local pre-check that posts nothing. Re-review is first-class — a dedicated `reconcile` node keyed on the `` marker (never author identity) classifies findings new / still-present / fixed / regressed. The `review-gate` confirms before posting `when gates.pr-review == hitl` (AFK posts directly); posting is advisory — the real merge gate is `/qa`. Never `lib/tracker-adapter.mjs` (dissolved). See [ADR-0026](docs/adr/0026-pr-review-generic-archon-harvest.md). +- **The evidence gate is a deterministic script writer, never a self-judging prompt.** `/build` declares `evidence_policy: { required: true }`; `verification` and `goals-check` report a boolean `passed` alongside their existing prose verdicts, and `verification` adds a three-state `test_outcome` — `pass`, `fail` or `unresolved` — because a check that could not run is not a green one, and `test` is the one need this gate cannot advance without (#430); a dedicated `evidence` script node deletes any stale file left by a prior resumed attempt, prints its own three-state verdict as JSON (a `script:` node has no `output_format`, so only stdout resolves downstream), then writes `$ARTIFACTS_DIR/evidence.json` on every verdict, with the verdict and the failures behind it, mirroring it to `//evidence.json` so the Session commit carries it. The verdict is `pass` only when both booleans are true and the test outcome is `pass`. Only a `pass` verdict opens a pull request; any other verdict cancels the run with the work pushed, and two guards cover `evidence` ending without writing. `push-branch` refuses a missing Session mirror, and `evidence_policy` catches a committed mirror with no `$ARTIFACTS_DIR` copy ([ADR-0040](docs/adr/0040-build-verdict-decides-the-pull-request.md)). `always_run: true` on `slopcheck`, `verification`, `goals-check`, `evidence` (and on `/qa`'s `pr-check`, `verify-pr-base`, `e2e`, `coverage-gate`) opts each out of Archon's resume cache, because their exit code reports a fact about external or mutable state, not about itself — the same hole that let a resumed `/qa` run merge against a since-retargeted PR base. See [ADR-0033](docs/adr/0033-archon-070-schema-target.md) and [ADR-0034](docs/adr/0034-evidence-gate-deterministic-writer.md). +- **`/qa` gates on `gates.qa` and fail-closes the merge.** The QA Archon pipeline (pr-check → test → e2e → coverage → UAT → verify-pr-base → merge) has two `approval:` gates (UAT + merge), both HITL by default and skipped in AFK; downstream nodes use `trigger_rule: all_done` so AFK auto-merges a clean build, while the merge node's fail-closed `when` blocks auto-merging a red test, e2e or coverage, a wrong PR base, **or a `test` need that could not run at all** — `test` is the floor, so its clause reads `== 'pass'` while the other two read `!= 'fail'` (#430). `always_run: true` covers `pr-check`, `verify-pr-base`, `test`, `e2e` and `coverage-gate`. Every path that does not merge on a result ends `cancelled` through a `cancel:` leaf whose `when` is the exact negation of the node it guards: `guard-no-open-pr` before `test` when the PR is not open, `guard-not-mergeable` after `verify-pr-base` with every cause in its message, and `guard-not-merged` when `merge` cannot confirm a merge. A node that fails ends the run `failed` (#652). `/qa` is also an **issue-producing on-ramp**: a UAT rejection files each defect directly as a `ready-for-agent` tracker item (through the tracker contract in `docs/agents/`, the inlined finding-capture brief shape — upstream's own `qa` Method was deprecated at v1.1.0 with no replacement (#281) — and the AI disclaimer) that feeds `/tickets` — it does not just halt. Never `lib/tracker-adapter.mjs` (dissolved). See [ADR-0025](docs/adr/0025-qa-pipeline-onramp.md). +- **`/pr-review` is a self-contained fan-out Archon review that harvests, never depends.** The PR-review Archon pipeline (bootstrap → guard → prep → guard-no-diff-base → review → synthesize → reconcile → review-gate → post) reviews the open PR and posts one summary discussion on the pull request itself plus per-line discussions, through the tracker its contract names — a host carries the first as a thread with no file anchored to it or as a comment on the PR, and the Box asks which rather than promising either. It **harvests `unic-pr-review`'s review learnings** (confidence→severity rubric, structured summary, hidden-marker idempotency, conditional spawn table, two-surface posting) with **no ADO code and no runtime dependency** on that plugin. `prep` composes **one Intent Brief** (linked work items + Confluence/MD docs + PR description + `PRD.md`) that is **injected into both review axes** (the quality lever) and surfaces cross-source contradictions. The single `review` node reads the `code-review` Method and runs **its own** two-axis sub-agent fan-out — Standards (repo standards + the twelve-item Fowler smell baseline, pasted in full because the sub-agent has no other access to it) and Spec (AC coverage, scope creep, wrong-looking implementations) — replacing seven hand-written aspect prompts (#281, ADR-0026 §8). Refactoring lives in that Standards axis, having left `/build`'s loop. This Box holds the **only review-posting authority**: `/build`'s `implement-review-precheck` runs the same Method as a local pre-check that posts nothing. Re-review is first-class — a dedicated `reconcile` node keyed on the `` marker (never author identity) classifies findings new / still-present / fixed / unverified / regressed. The `review-gate` confirms before posting `when gates.pr-review == hitl` (AFK posts directly); posting is advisory — the real merge gate is `/qa`. Never `lib/tracker-adapter.mjs` (dissolved). See [ADR-0026](docs/adr/0026-pr-review-generic-archon-harvest.md). - **`/improve-architecture` is a skill that composes Matt's method and owns ADR superseding.** The off-line, on-demand arch-health box is a **command/skill** (its core is a design grilling loop that needs the live conversation — [ADR-0017](docs/adr/0017-container-follows-structural-need.md)), not an Archon workflow. It **composes `improve-codebase-architecture` verbatim** (technical drift + deepening HTML report + `/grilling` loop, with `/codebase-design` vocabulary + `/domain-modeling`) and **earns its place** ([ADR-0021](docs/adr/0021-earns-its-place-compose-verbatim.md)) by adding three DLC layers the raw skill lacks: an **intent-drift** pass (PRD stories/ACs vs shipped), a **durable `arch-review.md`** artifact under `/` ([ADR-0015](docs/adr/0015-workflows-slug-artifact-home.md)), and an **ADR-consolidation gate with superseding**. Two modes: `` → intent-grounded against that build session; no-arg → repo-wide sweep (dated artifact, intent-drift skipped). Superseding works across **both ADR homes** (plugin-local + repo-root), never deletes an ADR (old status → `Superseded by ADR-NNNN`, index updated in the matching `README.md`). Config load is **lenient** (off-line, touches no tracker); **no new config key** and no auto-hook (PLAN #8). It does **not** touch the legacy `unic-dlc-cleanup.yaml` — that is `/cleanup`'s (step 11) scope. See [ADR-0027](docs/adr/0027-improve-architecture-skill-superseding.md). - **`/cleanup` is the repo-global operational janitor — a report-first command, not an Archon workflow.** It prunes the debris a lifecycle accumulates: merged/stale worktrees, stale branches/PRs, and stale `//` dirs. It **composes** Archon's own `archon isolation list` / `archon isolation cleanup [days] [--merged] [--include-closed]` / `archon complete ` for worktree/branch lifecycle and the tracker `docs/agents/issue-tracker.md` § Access names, for PR/branch state — never a `tracker-adapter` lib (dissolved). It is a **command** because it mutates sibling worktrees/branches/PRs and so cannot run inside an isolated worktree ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)). It **defaults to a report-only dry-run and never auto-deletes**: pruning needs `--apply` plus an explicit **per-category** confirmation, and `cleanup.dry_run: true` (the shipped default) keeps even `--apply` in report mode until overridden. A slug dir is prunable **only if** its PR/branch is merged or closed (`cleanup.prune_slug_dirs` defaults `false`); slug-dir pruning skips any dir containing a `LICENSE` (repo policy). It reads the new `cleanup` config block (`stale_days`/`dry_run`/`prune_slug_dirs`); config load is lenient (off-line). This step **retired** the legacy `unic-dlc-cleanup.yaml` + command stub, whose arch-review + ADR-consolidation content moved to `/improve-architecture` ([ADR-0027](docs/adr/0027-improve-architecture-skill-superseding.md)). See [ADR-0028](docs/adr/0028-cleanup-operational-janitor.md). - **`/archon-upgrade` reports what a new Archon release means for this Plugin, and writes nothing in the repository it assesses.** A **command**, not a workflow: it needs the live conversation to ask which repository to read release notes from when discovery fails, and to put a decision table in front of a human ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)). It compares the installed `archon` against the `0.7.0` floor stated in the command and stops when they match; discovers Archon's own upstream repository at run time from `brew`, and **asks rather than guesses** when that fails, so no Archon URL is hardcoded anywhere; reads the release notes for the range through `gh` and classifies each notable change **ADOPT / DEFER / VERIFY-ONLY / BREAKS-US**, naming the affected file and node and one next step per row. Two classifications are locked and **cited, never re-derived** — `workflow:` sub-runs are DEFER per [ADR-0033](docs/adr/0033-archon-070-schema-target.md) § Sub-runs, and Archon's own remote-resolution algorithm is VERIFY-ONLY, never BREAKS-US, per that ADR's § "Repository derivation": a recorded, deliberate divergence re-raised as a defect would relitigate a settled decision on every run. A **separate mandatory sub-pass** reads the notes for changed, removed and deprecated **defaults** against the Boxes' own node bodies, because both real 0.7.0 defects were an upstream default a Box still assumed, invisible to a new-field scan. The [ADR-0011](docs/adr/0011-archon-schema-target.md) trap re-assertion runs unconditionally: the command reads each installed `.archon/workflows/unic-dlc-*.yaml` and checks the four conventions itself, and a file it cannot read is a FAIL, never a silent PASS. Nothing else guards those conventions, so this read is the only place they are re-asserted. A **second unconditional step probes the Archon config keys this Plugin depends on**: it gives each key a distinctive value in a throwaway git repository outside every clone, runs a one-node Archon workflow there, and reads the verdict out of Archon's own output — **READ — value ``**, **NOT READ**, or **INCONCLUSIVE**. NOT READ is the silent failure it exists for; a committed top-level `baseBranch:` in this monorepo was inert for weeks against an Archon that reads a nested path (#396). The keys live in **one table**, inside that step, and the prose says the path is Archon's to change — the method is behavioural because grepping the binary proves a string is present, not that anything reads it. That throwaway repository is the one thing the command writes, and it deletes it afterwards. `allowed-tools` carries `Read` and `Glob` beside `Bash` — the command reads files — and no `Write`: adoption stays a human decision recorded by hand. See [ADR-0035](docs/adr/0035-archon-upgrade-report.md). - **`/explore` is the off-line, optional research + AFK-spike on-ramp — an Archon workflow whose `findings.md` is the `/specs` baton.** Never required; a precursor to `/specs` (PLAN #3/#8). The pipeline (bootstrap → guard → four parallel research nodes, each reading the `research` Method for its primary-source citation discipline (#281) → synthesize → spike → spike-ticket → spike-branch-gate → preserve-spike) writes `//findings.md`, where the **Integrated Brief** carries three explicitly-named lenses — **Domain Model / Established Decisions / Prior Research** — that `/specs`' load-context reads verbatim (the tightened handoff contract). The `spike` node is **AFK** (build/measure where feasible, else reason → VALIDATED/INVALIDATED/PARTIAL verdicts); **interactive prototyping stays Matt's `/prototype` skill**, referenced but never invoked (nodes have no live conversation — [ADR-0017](docs/adr/0017-container-follows-structural-need.md)). The `spike-branch-gate` is a config-gated `approval:` node (`gates.explore`, HITL default): APPROVE preserves the spike on `spike/`, AFK skips it (worktree left for `/cleanup`); the **spike ticket is filed before the gate** (through the tracker contract in `docs/agents/`, never a `tracker-adapter`/`labels-config` lib) so the durable output survives a discard. Every node is a self-contained prompt node — no plugin-`lib/` import ([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5); this step dissolved `findings-writer` + `spike-verdicts` and added no config key. See [ADR-0029](docs/adr/0029-explore-research-spike-onramp.md). - **This monorepo is not a Consumer.** `/unic-archon-dlc:setup` must never run against it. An earlier dogfooding run left generated artefacts under `docs/agents/` and a marker-delimited block in the root `AGENTS.md`, which collided with `setup-matt-pocock-skills` over the same paths — the collision this Plugin's own `/triage` doctrine forbids. Both were removed and `docs/agents/*.md` is now hand-owned by the repo; Matt's skills are its only agent-skill driver. Test the Plugin against a scratch Consumer clone instead. See [root ADR-0033](../../../docs/adr/0033-de-dogfood-unic-archon-dlc.md). +- **What a Consumer session reads carries its content inline, never a reference back to this repository.** That is every file in `commands/`, every Box in `.archon/workflows/`, `README.md`, `vendor/mattpocock-skills/README.md`, and any text `/setup` writes into a Consumer file. None of them may hold a link to this repository, a path in it, or a citation by number such as `ADR-0023` or `#281`. A command runs from the Plugin cache and resolves a relative link against the Consumer's working directory, and a copied Box reads `ADR-0023` as the Consumer's own ADR of that number. When a command or a Box needs a rule from a Plugin ADR, state the rule in one or two sentences. Two exceptions hold. A relative link in either README that resolves inside the Plugin stays, because `/setup` and the agent read both READMEs from ``, where the link resolves. A path `/setup` resolves through `installPath` stays too. A step that tells the agent to read the Consumer's own `docs/adr/` is not a reference back, so it stays. This rule is the whole guard. Nothing checks it, so a diff that adds a reference merges green unless a reviewer reads it. + ## External dependencies - **A project-scoped MCP server in the Consumer's `.mcp.json` reaches a Box.** Every Box that composes "the server § Access names" depends on this, and nothing had tested it until 2026-08-18: an Archon run under `~/.archon/workspaces///worktrees/…` loaded the Consumer's registered tracker server and read work items, pull requests and threads. Project scope travels with the worktree, so a node gets the access an interactive session gets — at a path no human approved. A **personal**-scope server does not travel, which is why `/setup` tells a team to register theirs at project scope. diff --git a/apps/claude-code/unic-archon-dlc/CHANGELOG.md b/apps/claude-code/unic-archon-dlc/CHANGELOG.md index d527e461..fdc2b32d 100644 --- a/apps/claude-code/unic-archon-dlc/CHANGELOG.md +++ b/apps/claude-code/unic-archon-dlc/CHANGELOG.md @@ -11,6 +11,113 @@ ### Fixed - (none) +## [0.36.1] — 2026-10-28 + +### Breaking +- (none) + +### Added +- (none) + +### Fixed +- The commands and the Boxes no longer refer back to this repository. A Consumer session could follow none of those references, and a copied Box read a Plugin ADR number as the Consumer's own ADR. Every link to a Plugin ADR, every citation of a Plugin ADR or issue by number, and every path in this repository is gone from `commands/` and `.archon/workflows/`. Where a command or a Box needed the rule behind a citation, it now states the rule. The steps that read the Consumer's own `docs/adr/` stay (#556). +- `/setup` reads § Dependencies, § Configuration reference and § The tracker contract from `/README.md`, and checks that the file is present. The block it writes into the Consumer's `CLAUDE.md` replaces the URL to this repository with one line per Box that says what the Box does (#556). +- `/improve-architecture` picks the ADR home from the `docs/adr/` directories the Consumer already has and confirms it with the user. With none, it proposes the root `docs/adr/`. It no longer names this repository's layout (#556). +- `/setup` stops when `/README.md` lacks § Dependencies, § Configuration reference or § The tracker contract, and names the missing section. Without § Dependencies, the check for a missing Method passed with nothing to compare (#556). +- The `CLAUDE.md` block says that `unic-dlc-pr-review` asks for a confirmation only when `gates.pr-review` is `hitl`, and that `unic-dlc-qa` asks for UAT sign-off and a merge confirmation only when `gates.qa` is `hitl`. It also says that `unic-dlc-build` pauses for PR review only when `gates.build` is `hitl`, that `unic-dlc-explore` offers to keep the spike code only when `gates.explore` is `hitl`, and that `unic-dlc-qa` merges only when the checks pass and the PR is open against the right base. `/setup` and `AGENTS.md` now say that the block lists the Boxes and that a pull request that adds, renames or drops a Box updates that list (#556). +- `/archon-upgrade` ties DEFER to the locked precedents it states, and offers **file an issue** or **nothing** as the next step. It no longer asks the reader to cite or amend a Plugin ADR (#556). +- The Plugin `README.md` turns every bare ADR citation into a link into its own `docs/adr/`, which resolves inside the installed Plugin (#556). + +## [0.36.0] — 2026-10-28 + +### Breaking +- (none) + +### Added +- `/pr-review`'s `review` node writes a `reported_checks` array into `review.json`. Each entry is a check a review sub-agent reports having run, with its axis, the need it serves and the reported outcome. `review` records only commands that run the project's toolchain. It judges the need from what the command does, never from the command's text. It never records `install`. The array is empty when no sub-agent reported a check. `review.json` also carries `subagents_ran` and `blocked`. `reconcile` emits a `stopped` string, and the `review-gate` message shows it, so a stop no longer reads as a review with zero findings. `review` deletes the file an earlier iteration left before it starts, and every stop writes a new one (#660). + +### Fixed +- `/pr-review`'s summary block said the review executed none of the declared checks, while a review sub-agent could run one on its own. `reconcile` now heads the block "Checks this review was not asked to run". The block states that the Box instructed none of the declared checks, says that a sub-agent may still have run one, and renders each reported check as that sub-agent's claim, by need and never by command. When `review.json` is missing or unreadable, the block keeps that warning and says that `review` left no readable report. `synthesize` states no check result as a fact, in "What's good" or elsewhere. A stopped review no longer reads as a clean one. `synthesize` renders a callout with the reason in place of the severity sections, and `reconcile` rules no prior finding `fixed`, so `post` resolves no thread from it. This release revises ADR-0037 §8 to match (#660). + +## [0.35.0] — 2026-10-28 + +### Breaking +- A `/build` run whose code-review pre-check reports a hard finding, or cannot run, used to end `pass` with a pull request open. It now ends `cancelled` with none. `evidence` reads the pre-check. A hard finding makes the verdict `fail`. A pre-check that is `blocked`, reports another status, or cannot be read makes it `unresolved` when nothing else failed (#483). +- `implement-review-precheck` emits `status`, `blocked_reason`, `hard_findings` and `notes` beside `findings`, and all five fields are required. `evidence.json` carries them as `precheck_status`, `precheck_blocked_reason`, `hard_findings` and `precheck_notes` on every verdict, with `precheck_read_error` naming any field `evidence` could not read. Section 2 of `report.md` leads with the number of hard findings, and the pull request description lists the notes under `## Pre-check notes` (#483). + +### Added +- (none) + +### Fixed +- (none) + +## [0.34.0] — 2026-10-28 + +### Breaking +- A `/build` run whose verdict is not `pass` used to end `failed` with a pull request open. It now ends `cancelled` and opens no pull request. An operator or a script that waited for a red pull request sees a different outcome (#439). +- `evidence` writes `evidence.json` on every verdict. The file names the verdict and lists the failures that produced it (#439). +- A new `commit-session` node stages and commits the work, and a new `push-branch` node pushes the branch, on every verdict. `push-branch` pushes to `origin`, and refuses an empty base branch, a detached HEAD, the run's base branch, and Session artefacts that are not committed, which includes a resumed run whose rewritten `evidence.json` differs from the committed one. A refusal or a failed push ends the run `failed` (#439). +- `open-pr` runs only on a `pass` verdict and no longer stages or commits. On any other verdict, `guard-not-pass` cancels the run, and its message names the verdict, the pushed branch and the path of `report.md` on that branch (#439). + +### Added +- (none) + +### Fixed +- (none) + +## [0.33.0] — 2026-10-27 + +### Breaking +- `/qa` runs that merged nothing used to end `completed`. They now end `cancelled`, with a message that names why. A `/qa` run now ends `completed` only after a merge it confirmed. A `cancelled` run either merged nothing or could not confirm the merge, and its message says which (#652). +- A new `pr-check` node reads the PR state before any test runs. A branch whose PR is not open (`none`, `merged`, `closed` or `unknown`) cancels before `test` (#652). +- `verify-pr-base` now reports `pr_state`. `base_ok` is no longer `"true"` when no PR exists (#652). +- A test result other than `pass`, a red e2e or coverage gate, a PR that is not open, a wrong base or a failed base query cancels the run after `verify-pr-base`. The message joins every cause that held (#652). +- `merge-gate` asks for approval only when `merge` would run (#652). +- `merge` reports whether it merged. When it cannot confirm a merge, the run cancels with the reason (#652). +- The `complete` line appears only after a merge that landed, and it names any item that did not take `resolved` (#652). + +### Added +- (none) + +### Fixed +- (none) + +## [0.32.0] — 2026-10-27 + +### Breaking +- `/pr-review`'s `reconcile` no longer emits `unverified_fixed`. It emits `unverified` instead, and the review gate and `post`'s closing line read the new count. A tool that reads `unverified_fixed` from a run must read `unverified` (#482). + +### Added +- (none) + +### Fixed +- `/pr-review` no longer marks a prior finding `fixed`, and resolves its thread, only because the next review did not report it. Every iteration now records the commit it reviewed in a hidden marker in its summary. A prior finding that the review omits is `fixed` only when its file changed between the prior iteration's head and this one. Otherwise it is `unverified`. `post` then replies once on its thread, asks the author to check it by hand, and leaves the thread's status as it is. When there is no usable prior head, every omitted finding is `unverified` and the summary says why. That happens when an older Box posted the prior iteration, or when the recorded head cannot be reached. `post` now reads each thread's status first and reopens a resolved thread whose finding is reported again, with a reply. Every reply carries a hidden marker with the finding's hash and its verdict, so a rerun after a `post` that failed halfway repeats no reply. A thread that this Box or an older Box resolved stays retired and gets no second "resolved" reply. `post` posts nothing when the session holds no summary for this run (#482). + +## [0.31.0] — 2026-10-21 + +### Breaking +- `/build`, `/pr-review` and `/qa` now read the run's base branch from Archon's `$BASE_BRANCH`, not from `project.branching`. Archon resolves it from `archon workflow run --base `, then `worktree.baseBranch` in `.archon/config.yaml`, then the codebase default branch. If your Archon base differs from the branch that `branching` implied, the Boxes now target the Archon base. `/build` opens its PR there, and `/qa` refuses to merge a PR that targets another branch. Run `/unic-archon-dlc:setup` and read its base line before your next run. `project.branching` now decides only whether `/qa` deletes the merged `feature/*` branch (ADR-0039, #505). +- `/pr-review` takes its review diff from the merge-base with the PR's own target branch when the tracker returns a PR, and from `$BASE_BRANCH` when it does not. When the two differ, its summary carries a warning that names both. When the tracker query for the PR fails, or the diff base does not resolve after a fetch, the run cancels instead of reviewing a guessed diff (#505). + +### Added +- (none) + +### Fixed +- `/qa`'s `verify-pr-base` now sets `base_ok` to `false` when the tracker query fails, or when the tracker contract is missing or cannot address the repository, so `merge` refuses. Before, it treated those cases as an unverifiable base and let the merge through. It still passes when a query succeeds and confirms that no PR exists (#505). +- `/build`'s stub scan diffs against `origin/$BASE_BRANCH` first and falls back to the local branch, so a base this clone has never checked out no longer fails the scan. Its code-review pre-check, like `/pr-review`'s `prep`, fetches the base into `origin/` with an explicit refspec before it takes the merge-base, so the ref resolves in a single-branch clone too (#505). + +## [0.30.0] — 2026-10-21 + +### Breaking +- `/setup` now writes its managed blocks, in the Consumer's `CLAUDE.md` and in each formatter ignore file, between `unic-dlc:begin` and `unic-dlc:end`. The old markers were `unic-archon-dlc:begin` and `unic-archon-dlc:end`. On its next run, `/setup` finds a block under the old markers, rewrites it in place under the new ones and reports the file as migrated, so a Consumer holds one block, never two. A tool or script outside this Plugin that searches for the old markers must change to the new ones (#520). + +### Added +- ADR-0038 records how Practice Packs stack in one Consumer beside the DLC. Each installer owns only the names that carry its prefix, and `unic-dlc` is the DLC's prefix. A pack ships context artefacts and config values, never a Box or a Method. The DLC alone writes `.archon/methods/`. Config is layered, with the Consumer's file last. An `include:` slot is the decided, unbuilt route for a pack that must replace a Box step. ADR-0031, ADR-0024 and ADR-0013 point at it, and `CONTEXT.md` states the Practice Pack model as decided (#520). +- `/setup` states that it touches no name carrying another prefix, in `.claude/skills/`, `.archon/packs/`, `CLAUDE.md` or a formatter ignore file. When it reports a retired Method override path, it also names ADR-0038 as where the route to replace a Box step is recorded (#520). + +### Fixed +- (none) + ## [0.29.0] — 2026-09-27 ### Breaking diff --git a/apps/claude-code/unic-archon-dlc/CONTEXT.md b/apps/claude-code/unic-archon-dlc/CONTEXT.md index 2c654e9e..1211109d 100644 --- a/apps/claude-code/unic-archon-dlc/CONTEXT.md +++ b/apps/claude-code/unic-archon-dlc/CONTEXT.md @@ -11,7 +11,9 @@ Claude Code commands/skills for the interactive ones. Configured via the `/unic- slash command. See `docs/adr/0016`–`0018` for the two-axis architecture, `docs/adr/0014` for the box set, and `docs/adr/0030`–`0032` for the Harness/Method division. -Requires the Archon workflow engine (version ≥ 0.7.0) in the target project. +Requires the Archon workflow engine (version ≥ 0.7.0) in the target project. `/build` needs version +≥ 0.10.0, because `evidence` reads the pre-check through a script node's `with:` bindings (ADR-0034). +On an older engine the binding does not arrive, so no build ends `pass`. Whether such an engine loads the Box at all is not measured. ## Language @@ -37,8 +39,10 @@ One step of the lifecycle. See `docs/adr/0030-harness-hosts-methods.md`. _Avoid_: step, stage, phase **Method**: -The skill text a Box reads for procedure. See -`docs/adr/0031-methods-bundled-three-tier-resolution.md`. +The skill text a Box reads for procedure, by path from `.archon/methods/`. Only this plugin writes that +tree. A skill under `.claude/skills/` is not a Method, even when a node names it in `skills:`. It is a +context artefact. See `docs/adr/0031-methods-bundled-three-tier-resolution.md` and +`docs/adr/0038-practice-packs-stack-by-prefix.md`. _Avoid_: skill (a Method is text the repository holds, not an installed skill), prompt, playbook **Bundle**: @@ -51,13 +55,15 @@ A unit meant to be installed in a Consumer. It carries one family's way of worki reference architecture and procedure. A Consumer can run the DLC without one. Acceptance criteria and the shape of criteria in the PRD come from the Consumer, as [Is an untestable acceptance criterion refused by context, or by a change to /specs and /tickets?](https://github.com/unic/unic-agents-plugins/issues/551) -decides. The maintainer's working model is that a Practice Pack carries context artefacts and config -values only, never a Box or a Method. A Consumer can override a value that a Practice Pack sets, as +decides. A Practice Pack carries context artefacts and config values only, never a Box or a Method. A +skill it installs under `.claude/skills/` is a context artefact. It reaches a Box run through its own +block in the Consumer's `CLAUDE.md`. Each Practice Pack has a unique prefix and brings its own installer, +which touches only the names that carry that prefix, so several coexist in one Consumer. Once +[Build the layered Practice Pack config](https://github.com/unic/unic-agents-plugins/issues/647) lands, a +Practice Pack writes its config defaults to its own file. A Consumer can override a value that a +Practice Pack sets, as [Does an e2e test become an outer loop around /build's unit TDD loop for a new feature?](https://github.com/unic/unic-agents-plugins/issues/541) -decides. That model is open on -[Practice Packs must stack](https://github.com/unic/unic-agents-plugins/issues/520), together with -whether a skill under `.claude/skills/` counts as a context artefact, how a Practice Pack is installed, -how it writes a config value, and how several coexist in one Consumer. +decides. See `docs/adr/0038-practice-packs-stack-by-prefix.md`. _Avoid_: Pack, Method Bundle, plugin **System-skill**: @@ -102,7 +108,7 @@ row naming the axis that carries it). A Box names a role and a file; it never na field or a provider. `/unic-archon-dlc:setup` owns both, and `setup-matt-pocock-skills` must never run over them. A section earns its place there only when it states a fact about this tenant — an MCP server discovers its own API, so writing operations down freezes a flag table in Markdown. See -`docs/adr/0024-triage-intake-on-ramp.md` (amended 2026-08-18). +`docs/adr/0024-triage-intake-on-ramp.md`. _Avoid_: repository derivation (deleted — nothing is derived from a remote), repo pinning, label mapping (`classification.labels` is gone), tracker config @@ -266,14 +272,18 @@ _Avoid_: progress file, checkpoint **Evidence gate**: The workflow-level `evidence_policy: { required: true }` on `/build` that refuses terminal `completed` unless `$ARTIFACTS_DIR/evidence.json` exists. The engine checks file presence only; -producing valid evidence is the `evidence` node's contract, never a prompt's. See -`docs/adr/0034-evidence-gate-deterministic-writer.md`. +producing valid evidence is the `evidence` node's contract, never a prompt's. `evidence` writes the +file on every verdict. For `evidence` ending without writing, `push-branch` refuses a missing Session +mirror, and this gate catches a committed mirror with no `$ARTIFACTS_DIR` copy. The verdict, not the +file, decides whether a pull request opens. See +`docs/adr/0034-evidence-gate-deterministic-writer.md` and +`docs/adr/0040-build-verdict-decides-the-pull-request.md`. _Avoid_: verification gate, quality gate **Evidence set**: The Session-artifact mirror of the evidence gate's content, at `//evidence.json` — written by the same `evidence` node that satisfies the engine's presence check at -`$ARTIFACTS_DIR/evidence.json`, so a reviewer (and `open-pr`, which stages it) can see what the gate +`$ARTIFACTS_DIR/evidence.json`, so a reviewer (and `commit-session`, which stages it) can see what the gate saw after `/cleanup` prunes the worktree. _Avoid_: evidence.json (ambiguous — two files share the name at two different paths), evidence file @@ -400,13 +410,13 @@ _Avoid_: review aspect, reviewer, agent, check ## Relationships - The **Harness** hosts **Methods**: a **Box** reads a Method for procedure, and a Box exists only for what no Method can supply (ADR-0030) -- A **Method** is read at `.archon/methods//SKILL.md`, the one path **Setup** installs the **Bundle** into (ADR-0031, amended) +- A **Method** is read at `.archon/methods//SKILL.md`, the one path **Setup** installs the **Bundle** into (ADR-0031) - **Configuration** carries parameters and a **Method** carries procedure, so wanting different method text means forking the Method rather than adding a config key (ADR-0032) - A **Session** is scoped by a **Slug** and produces **Findings**, a **PRD**, **Issues JSON**, `build-state.json`, and a build **report**, all under `//` (default `workflows//`) - The **Nyquist map** gate — every issue carrying a `test_command` — runs in `/tickets` before `/build` consumes the build-ready `issues.json` (ADR-0022) - `/build` runs a generic **red → green** loop over each issue in `issues.json`; RED and GREEN run in **fresh-context** isolation so GREEN never sees RED's reasoning (ADR-0012 / ADR-0023 — no per-slice DAG codegen; `dag-builder` / `yaml-gen` are dissolved) - Refactoring is **not** in that loop: the `tdd` Method puts it in the review stage, so it reaches the code as `/pr-review`'s Standards **Review axis** and its Fowler smell baseline (ADR-0023 §7 / ADR-0026 §8) -- A **Method** is read at one path, `.archon/methods//SKILL.md`, by every Box and every command alike — the team-source and Local-Method tiers are retired (#381), so there is no resolution order and no tier to report (ADR-0023 §5 / ADR-0031, amended) +- A **Method** is read at one path, `.archon/methods//SKILL.md`, by every Box and every command alike — the team-source and Local-Method tiers are retired (#381), so there is no resolution order and no tier to report (ADR-0023 §5 / ADR-0031) - Within an issue, **green** depends on **red**; the loop processes issues in order on the current linear path - **adr-consolidation** (in `/improve-architecture`) sources candidates from the "Decisions Made" section of `report.md` and "Accept as ADR" items from **arch-review** - The **issue tracker** is the single source of truth for project state; there is no `HANDOFF.md`/`ROADMAP.md` diff --git a/apps/claude-code/unic-archon-dlc/README.md b/apps/claude-code/unic-archon-dlc/README.md index 9712e2a1..d66cbcd0 100644 --- a/apps/claude-code/unic-archon-dlc/README.md +++ b/apps/claude-code/unic-archon-dlc/README.md @@ -65,19 +65,19 @@ OFF-LINE /setup · /explore · /improve-architecture · /cleanup · /archon-u -| Box | Container | Gate | Role | -| ---------------------------------------------------------------------------------------------------------- | --------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `/setup` ([diagram](docs/architecture/20260925-unic-dlc-setup-command.html)) | skill | HITL | Conversational config: detects the stack, writes `.archon/unic-dlc.config.yaml` (ADR-0019) | -| `/explore` | Archon | `gates.explore` | Off-line, optional research + AFK spike → `findings.md` (ADR-0029) | -| `/specs` ([diagram](docs/architecture/20260925-unic-dlc-specs-command.html)) | skill | HITL | Branch-on-input → `PRD.md`, plus one design contract per component when `design.type` is set (ADR-0020) | -| `/tickets` ([diagram](docs/architecture/20260925-unic-dlc-tickets-command.html)) | skill | HITL | Slice the PRD into build-ready `issues.json` with a `test_command` each (ADR-0022) | -| `/triage` ([diagram](docs/architecture/20260925-unic-dlc-triage-command.html)) | skill | HITL | Intake on-ramp: raw work → agent-ready tracker issues, DLC-config labels (ADR-0024) | -| `/build` | Archon | `gates.build` | Anti-cheat red/green loop over `issues.json` (ADR-0012 / ADR-0023) | -| `/pr-review` | Archon | `gates.pr-review` | Fan-out review of the open PR, intent-grounded; posts summary + inline (ADR-0026) | -| `/qa` | Archon | `gates.qa` | test → e2e → coverage → UAT → merge; a UAT reject files agent-ready issues (ADR-0025) | -| `/improve-architecture` ([diagram](docs/architecture/20260925-unic-dlc-improve-architecture-command.html)) | skill | HITL | Arch-health + intent-drift + ADR superseding → `arch-review.md` (ADR-0027) | -| `/cleanup` ([diagram](docs/architecture/20260925-unic-dlc-cleanup-command.html)) | command | HITL | Repo-global janitor: prune stale worktrees / branches / PRs / slug dirs, report-first (ADR-0028) | -| `/archon-upgrade` ([diagram](docs/architecture/20260925-unic-dlc-archon-upgrade-command.html)) | command | — | Report what a new Archon release means for this Plugin; writes nothing here, probes config keys in a throwaway repo (ADR-0035) | +| Box | Container | Gate | Role | +| ---------------------------------------------------------------------------------------------------------- | --------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/setup` ([diagram](docs/architecture/20260925-unic-dlc-setup-command.html)) | skill | HITL | Conversational config: detects the stack, writes `.archon/unic-dlc.config.yaml` ([ADR-0019](docs/adr/0019-conversational-setup.md)) | +| `/explore` | Archon | `gates.explore` | Off-line, optional research + AFK spike → `findings.md` ([ADR-0029](docs/adr/0029-explore-research-spike-onramp.md)) | +| `/specs` ([diagram](docs/architecture/20260925-unic-dlc-specs-command.html)) | skill | HITL | Branch-on-input → `PRD.md`, plus one design contract per component when `design.type` is set ([ADR-0020](docs/adr/0020-specs-branch-on-input.md)) | +| `/tickets` ([diagram](docs/architecture/20260925-unic-dlc-tickets-command.html)) | skill | HITL | Slice the PRD into build-ready `issues.json` with a `test_command` each ([ADR-0022](docs/adr/0022-tickets-slice-to-build.md)) | +| `/triage` ([diagram](docs/architecture/20260925-unic-dlc-triage-command.html)) | skill | HITL | Intake on-ramp: raw work → agent-ready tracker issues, DLC-config labels ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md)) | +| `/build` | Archon | `gates.build` | Anti-cheat red/green loop over `issues.json` ([ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md) / [ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md)) | +| `/pr-review` | Archon | `gates.pr-review` | Fan-out review of the open PR, intent-grounded; posts summary + inline ([ADR-0026](docs/adr/0026-pr-review-generic-archon-harvest.md)) | +| `/qa` | Archon | `gates.qa` | test → e2e → coverage → UAT → merge; a UAT reject files agent-ready issues ([ADR-0025](docs/adr/0025-qa-pipeline-onramp.md)) | +| `/improve-architecture` ([diagram](docs/architecture/20260925-unic-dlc-improve-architecture-command.html)) | skill | HITL | Arch-health + intent-drift + ADR superseding → `arch-review.md` ([ADR-0027](docs/adr/0027-improve-architecture-skill-superseding.md)) | +| `/cleanup` ([diagram](docs/architecture/20260925-unic-dlc-cleanup-command.html)) | command | HITL | Repo-global janitor: prune stale worktrees / branches / PRs / slug dirs, report-first ([ADR-0028](docs/adr/0028-cleanup-operational-janitor.md)) | +| `/archon-upgrade` ([diagram](docs/architecture/20260925-unic-dlc-archon-upgrade-command.html)) | command | — | Report what a new Archon release means for this Plugin; writes nothing here, probes config keys in a throwaway repo ([ADR-0035](docs/adr/0035-archon-upgrade-report.md)) | Archon boxes gate via config (`gates.: hitl | afk`, HITL default); interactive skill boxes are inherently HITL. `/handoff` and `/prototype` are **referenced** Matt skills, named in prose for a @@ -162,23 +162,26 @@ slices. A new conversation starts `/tickets` with only the PRD. The Archon boxes ship as key-discriminated workflow YAMLs in `.archon/workflows/` ([ADR-0011](docs/adr/0011-archon-schema-target.md)): -| Workflow | Node pipeline | -| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `unic-dlc-build` ([diagram](docs/architecture/20260925-unic-dlc-build-pipeline.html)) | `bootstrap → guard-not-ready → slopcheck → run-build → implement-review-precheck → verification → goals-check → evidence → report → open-pr → build-pr-gate ✓` | -| `unic-dlc-pr-review` ([diagram](docs/architecture/20260925-unic-dlc-pr-review-pipeline.html)) | `bootstrap → guard-not-ready → prep → review → synthesize → reconcile → review-gate ✓ → post` | -| `unic-dlc-qa` ([diagram](docs/architecture/20260925-unic-dlc-qa-pipeline.html)) | `bootstrap → guard-not-ready → test → e2e → coverage-gate → uat-prep → uat-gate ✓ → verify-pr-base → merge-gate ✓ → merge` | -| `unic-dlc-explore` ([diagram](docs/architecture/20260925-unic-dlc-explore-pipeline.html)) | `bootstrap → guard-not-ready → {research-stack · research-features · research-architecture · research-pitfalls} → synthesize → spike → spike-ticket → spike-branch-gate ✓ → preserve-spike` | +| Workflow | Node pipeline | +| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unic-dlc-build` ([diagram](docs/architecture/20260925-unic-dlc-build-pipeline.html)) | `bootstrap → guard-not-ready → slopcheck → run-build → implement-review-precheck → verification → goals-check → evidence → report → commit-session → push-branch → {guard-not-pass · open-pr → build-pr-gate ✓}` | +| `unic-dlc-pr-review` ([diagram](docs/architecture/20260925-unic-dlc-pr-review-pipeline.html)) | `bootstrap → guard-not-ready → prep → guard-no-diff-base → review → synthesize → reconcile → review-gate ✓ → post` | +| `unic-dlc-qa` ([diagram](docs/architecture/20260925-unic-dlc-qa-pipeline.html)) | `bootstrap → guard-not-ready → pr-check → guard-no-open-pr → test → e2e → coverage-gate → uat-prep → uat-gate ✓ → verify-pr-base → guard-not-mergeable → merge-gate ✓ → merge → guard-not-merged` | +| `unic-dlc-explore` ([diagram](docs/architecture/20260925-unic-dlc-explore-pipeline.html)) | `bootstrap → guard-not-ready → {research-stack · research-features · research-architecture · research-pitfalls} → synthesize → spike → spike-ticket → spike-branch-gate ✓ → preserve-spike` | > **✓** = config-gated `approval:` node — it pauses for a human when the box's gate is `hitl` and -> auto-proceeds when `afk` (ADR-0017). Parallel nodes are shown in `{…}`. +> auto-proceeds when `afk` ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)). Parallel nodes are shown in `{…}`. > > `/pr-review`'s `review` and `/build`'s `implement-review-precheck` each run the `code-review` Method's > own two parallel sub-agents (Standards · Spec) **inside one node**, so their parallelism does not appear -> in the DAG (ADR-0026 §8). +> in the DAG ([ADR-0026](docs/adr/0026-pr-review-generic-archon-harvest.md) §8). > -> `evidence` writes `$ARTIFACTS_DIR/evidence.json` only when `verification` and `goals-check` both report -> `passed: true` — the workflow-level `evidence_policy: { required: true }` fails the run closed otherwise -> (ADR-0034). +> `evidence` writes `$ARTIFACTS_DIR/evidence.json` on every verdict. The verdict is `pass` only when +> `verification` and `goals-check` both report `passed: true` with an empty `failures` list, the test +> outcome is `pass`, and the code-review pre-check reports `ran` with no hard finding. Only then +> does `open-pr` run; any other verdict reaches `guard-not-pass`, which cancels the run with the work on +> the pushed branch ([ADR-0034](docs/adr/0034-evidence-gate-deterministic-writer.md), [ADR-0040](docs/adr/0040-build-verdict-decides-the-pull-request.md)). `{guard-not-pass · open-pr}` are siblings, and the verdict picks +> one. Each workflow name links its pipeline diagram, made with Archify from the workflow YAML. Its source JSON sits alongside. Each diagram shows the nodes in run order, the nodes that run in parallel, each @@ -191,7 +194,7 @@ scheme; export both again whenever the JSON changes. - unic-dlc-build pipeline: bootstrap and slopcheck, the run-build loop, the review precheck, verification, goals-check and evidence, then report, open-pr and build-pr-gate + unic-dlc-build pipeline: bootstrap and slopcheck, the run-build loop, the review precheck, verification, goals-check and evidence, then report, commit-session and push-branch on every verdict, then guard-not-pass on a verdict other than pass, or open-pr and build-pr-gate on pass @@ -209,7 +212,7 @@ scheme; export both again whenever the JSON changes. - unic-dlc-qa pipeline: bootstrap, test, e2e, coverage-gate, uat-prep and uat-gate, then verify-pr-base, merge-gate and merge + unic-dlc-qa pipeline: bootstrap, pr-check, test, e2e, coverage-gate, uat-prep and uat-gate, then verify-pr-base, merge-gate and merge, with the cancel leaves guard-not-ready, guard-no-open-pr, guard-not-mergeable and guard-not-merged @@ -255,7 +258,7 @@ A rename wave is now found by running a Box against a live Consumer, not by a te `/unic-archon-dlc:setup` **verifies the bundle by reading it** — every Method directory carries its `SKILL.md` and the companion files that Method reads, and `LICENSE` is present — and stops if either check fails, because that means the shipped plugin is incomplete. The licence hash and the manifest -closure it used to compare against are gone with the plugin's code (#381); nothing replaces them. +closure it used to compare against are gone with the plugin's code; nothing replaces them. > **Do _not_ run Matt's `setup-matt-pocock-skills`.** Both it and `/unic-archon-dlc:setup` write > `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` — this repository's **tracker @@ -310,40 +313,40 @@ flows into `/tickets` next. The `/unic-archon-dlc:setup` command writes the rich `.archon/unic-dlc.config.yaml` ([ADR-0018](docs/adr/0018-generic-core-config-compose.md), [ADR-0019](docs/adr/0019-conversational-setup.md)). It is the config substrate every box reads, and `/setup` is its sole writer. The file is **tenant-owned**: `/setup` writes it on the first run, and on a later run reports what differs from what it would write and changes nothing — pass `reconfigure` to be offered the change file by file. A present-but-malformed config stops the run rather than being overwritten, and so does a legacy flat `.archon/unic-dlc.config.json`: no migration ships any more, and reading such a file as "no config" would write a second config beside it. Top-level sections: -| Path | Default | Valid values | Description | -| -------------------------------------------------------- | ---------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `project.name` | asked | any string | Project name | -| `project.repo_layout` | auto-detected | `single-context` · `multi-context` | `multi-context` when the repository holds more than one independently-releasable project | -| `project.branching` | asked | `gitflow` · `github-flow` | Branching model (mandatory) | -| `docs.type` | `markdown` | `markdown` · docs system name · `none` | Where the team's product specs live (drives `/specs` publishing) | -| `docs.access` · `design.access` | auto-detected | `{mcp, cli}` | Which surface serves the docs and design systems, filled from setup's verify-only discovery | -| `docs.publish` | `false` | `true` · `false` | Opt-in publishing of the PRD to the docs system | -| `design.type` | `none` | design system name · `none` | Design system source; boxes test set-versus-`none` and never compare the value to a literal — `design.access.mcp` resolves the tool | -| `templates.prd` | 7-section scaffold | template string | Config-driven PRD template `/specs` fills (ADR-0018); override to change PRD shape. The `Confirmations` section sits outside it and no override removes it | -| `templates.{issue,bug}` | `null` | template string | Config-driven artifact templates (ADR-0018) | -| `specs.discuss_mode` | `discuss` | `discuss` · `assumptions` | `/specs` grilling style: `discuss` composes `grilling` + `domain-modeling`; `assumptions` enumerates upfront (ADR-0020) | -| `specs.gate` | `open-pr` | `open-pr` · `stage-only` | `/specs` PRD gate: `open-pr` commits + opens a PR to `develop` (never merged); `stage-only` stages and stops. Both refuse when a halt's `Confirmations` entry is absent or unanswered | -| `tickets.gate` | `open-pr` | `open-pr` · `stage-only` | `/tickets` gate: `open-pr` commits `issues.json` + opens a PR to `develop` (never merged); `stage-only` stages and stops (ADR-0022) | -| `triage.out_of_scope_dir` | `.out-of-scope` | dir name | Where `/triage` records rejected enhancements (the out-of-scope KB) (ADR-0024) | -| `triage.external_prs` | `auto` | `auto` · `always` · `never` | Whether `/triage` treats external PRs as a request surface; `auto` = ask the tracker whether it carries them at all (ADR-0024) | -| `gates.{build,qa,pr-review,explore}` | `hitl` | `hitl` · `afk` | Per-Archon-box gate mode (ADR-0017); interactive boxes are HITL | -| `build.fresh_context_red_green` | `true` | `true` · `false` | Anti-cheat fresh-context red/green separation (ADR-0012) | -| `build.{tdd_mode,nyquist_validation,slopsquatting_gate}` | `true` | `true` · `false` | Build discipline toggles | -| `build.coverage_threshold` | `null` | number (0–100) or `null` | Minimum % coverage for `/build`, and the fallback `/qa` uses when `qa.coverage_threshold` is unset. `null` means `/build`'s `verification` wants no coverage figure and reports nothing about it — which is silence, not a passing check (ADR-0037 §5) | -| `qa.coverage_threshold` | `null` | number (0–100) or `null` | `/qa`'s own minimum % coverage, falling back to `build.coverage_threshold` when unset, so the QA gate can hold a different bar from the build's. `/qa`'s `coverage-gate` wants a figure always, so a null on both keys is `unresolved` there rather than a skip (ADR-0037 §5) | -| `sdlc_needs.install` | `null` | shell command string | Installs this project's dependencies. `/build`, `/qa` and `/pr-review` each run it once, at `bootstrap`; `/explore` does not, since it runs no checks and so has no dependencies to prepare | -| `sdlc_needs.build` | `null` | shell command string | Builds the project. No node reads it yet — see the note below the table | -| `sdlc_needs.test` | `null` | shell command string | Runs the test suite. The one need that is mandatory: `/qa`'s merge gate and `/build`'s evidence verdict require an outcome for it | -| `sdlc_needs.e2e` | `null` | shell command string | Runs the end-to-end suite. Read by `/qa`'s `e2e` node and by `/build`'s `verification` | -| `sdlc_needs.lint` | `null` | shell command string | Lints the project. No node reads it yet — see the note below the table | -| `sdlc_needs.format` | `null` | shell command string | Formats the project. No node reads it yet — see the note below the table | -| `sdlc_needs.typecheck` | `null` | shell command string | Type-checks the project. Read by `/build`'s per-slice GREEN phase | -| `sdlc_needs.dev` | `null` | shell command string | Starts the development server. No node reads it yet — see the note below the table | -| `sdlc_needs.coverage` | `null` | shell command string | Produces a coverage figure, compared against the resolved threshold — `qa.coverage_threshold ?? build.coverage_threshold` in `/qa`, `build.coverage_threshold` in `/build` | -| `estimations` | `off` | `off` · `provisional` · `definitive` · `both` | Estimation waves (ADR-0020) | -| `cleanup.{stale_days,dry_run,prune_slug_dirs}` | `7` · `true` · `false` | number · bool · bool | `/cleanup` thresholds; report-first, never auto-deletes (ADR-0028) | -| `artifacts_dir` | `workflows` | dir name | Session artifact home base (`//`) | -| `model_profile` | `balanced` | `fast` · `balanced` · `max` | Model tier for workflow nodes | +| Path | Default | Valid values | Description | +| -------------------------------------------------------- | ---------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `project.name` | asked | any string | Project name | +| `project.repo_layout` | auto-detected | `single-context` · `multi-context` | `multi-context` when the repository holds more than one independently-releasable project | +| `project.branching` | asked | `gitflow` · `github-flow` | Branching model (mandatory). `/qa` reads it only to decide whether to delete the merged `feature/*` branch. A run's base branch is Archon's `$BASE_BRANCH`, which `/unic-archon-dlc:setup` reports | +| `docs.type` | `markdown` | `markdown` · docs system name · `none` | Where the team's product specs live (drives `/specs` publishing) | +| `docs.access` · `design.access` | auto-detected | `{mcp, cli}` | Which surface serves the docs and design systems, filled from setup's verify-only discovery | +| `docs.publish` | `false` | `true` · `false` | Opt-in publishing of the PRD to the docs system | +| `design.type` | `none` | design system name · `none` | Design system source; boxes test set-versus-`none` and never compare the value to a literal — `design.access.mcp` resolves the tool | +| `templates.prd` | 7-section scaffold | template string | Config-driven PRD template `/specs` fills ([ADR-0018](docs/adr/0018-generic-core-config-compose.md)); override to change PRD shape. The `Confirmations` section sits outside it and no override removes it | +| `templates.{issue,bug}` | `null` | template string | Config-driven artifact templates ([ADR-0018](docs/adr/0018-generic-core-config-compose.md)) | +| `specs.discuss_mode` | `discuss` | `discuss` · `assumptions` | `/specs` grilling style: `discuss` composes `grilling` + `domain-modeling`; `assumptions` enumerates upfront ([ADR-0020](docs/adr/0020-specs-branch-on-input.md)) | +| `specs.gate` | `open-pr` | `open-pr` · `stage-only` | `/specs` PRD gate: `open-pr` commits + opens a PR to `develop` (never merged); `stage-only` stages and stops. Both refuse when a halt's `Confirmations` entry is absent or unanswered | +| `tickets.gate` | `open-pr` | `open-pr` · `stage-only` | `/tickets` gate: `open-pr` commits `issues.json` + opens a PR to `develop` (never merged); `stage-only` stages and stops ([ADR-0022](docs/adr/0022-tickets-slice-to-build.md)) | +| `triage.out_of_scope_dir` | `.out-of-scope` | dir name | Where `/triage` records rejected enhancements (the out-of-scope KB) ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md)) | +| `triage.external_prs` | `auto` | `auto` · `always` · `never` | Whether `/triage` treats external PRs as a request surface; `auto` = ask the tracker whether it carries them at all ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md)) | +| `gates.{build,qa,pr-review,explore}` | `hitl` | `hitl` · `afk` | Per-Archon-box gate mode ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)); interactive boxes are HITL | +| `build.fresh_context_red_green` | `true` | `true` · `false` | Anti-cheat fresh-context red/green separation ([ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md)) | +| `build.{tdd_mode,nyquist_validation,slopsquatting_gate}` | `true` | `true` · `false` | Build discipline toggles | +| `build.coverage_threshold` | `null` | number (0–100) or `null` | Minimum % coverage for `/build`, and the fallback `/qa` uses when `qa.coverage_threshold` is unset. `null` means `/build`'s `verification` wants no coverage figure and reports nothing about it — which is silence, not a passing check ([ADR-0037](docs/adr/0037-config-declares-sdlc-needs.md) §5) | +| `qa.coverage_threshold` | `null` | number (0–100) or `null` | `/qa`'s own minimum % coverage, falling back to `build.coverage_threshold` when unset, so the QA gate can hold a different bar from the build's. `/qa`'s `coverage-gate` wants a figure always, so a null on both keys is `unresolved` there rather than a skip ([ADR-0037](docs/adr/0037-config-declares-sdlc-needs.md) §5) | +| `sdlc_needs.install` | `null` | shell command string | Installs this project's dependencies. `/build`, `/qa` and `/pr-review` each run it once, at `bootstrap`; `/explore` does not, since it runs no checks and so has no dependencies to prepare | +| `sdlc_needs.build` | `null` | shell command string | Builds the project. No node reads it yet — see the note below the table | +| `sdlc_needs.test` | `null` | shell command string | Runs the test suite. The one need that is mandatory: `/qa`'s merge gate and `/build`'s evidence verdict require an outcome for it | +| `sdlc_needs.e2e` | `null` | shell command string | Runs the end-to-end suite. Read by `/qa`'s `e2e` node and by `/build`'s `verification` | +| `sdlc_needs.lint` | `null` | shell command string | Lints the project. No node reads it yet — see the note below the table | +| `sdlc_needs.format` | `null` | shell command string | Formats the project. No node reads it yet — see the note below the table | +| `sdlc_needs.typecheck` | `null` | shell command string | Type-checks the project. Read by `/build`'s per-slice GREEN phase | +| `sdlc_needs.dev` | `null` | shell command string | Starts the development server. No node reads it yet — see the note below the table | +| `sdlc_needs.coverage` | `null` | shell command string | Produces a coverage figure, compared against the resolved threshold — `qa.coverage_threshold ?? build.coverage_threshold` in `/qa`, `build.coverage_threshold` in `/build` | +| `estimations` | `off` | `off` · `provisional` · `definitive` · `both` | Estimation waves ([ADR-0020](docs/adr/0020-specs-branch-on-input.md)) | +| `cleanup.{stale_days,dry_run,prune_slug_dirs}` | `7` · `true` · `false` | number · bool · bool | `/cleanup` thresholds; report-first, never auto-deletes ([ADR-0028](docs/adr/0028-cleanup-operational-janitor.md)) | +| `artifacts_dir` | `workflows` | dir name | Session artifact home base (`//`) | +| `model_profile` | `balanced` | `fast` · `balanced` · `max` | Model tier for workflow nodes | **Every `sdlc_needs` key names a need of the development process, never a tool.** `test` is the need; whichever runner a project uses is the tool that serves it, and no Box names one. A `null` value means the @@ -402,7 +405,7 @@ A shape that satisfies the contract: | `needs-triage` | — | — | Not written. | ``` -See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended 2026-08-18. +See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md). ### The Method bundle @@ -421,7 +424,7 @@ reads it there and nowhere else, so there is no resolution order to report and n The two override tiers this plugin used to offer — `methods..source` in config, and `.archon/methods.local//SKILL.md` — are retired: to change a Method, edit the installed file and expect the next `/setup` run to overwrite it. See -[ADR-0031](docs/adr/0031-methods-bundled-three-tier-resolution.md), amended. +[ADR-0031](docs/adr/0031-methods-bundled-three-tier-resolution.md). **Methods are read by path and never registered as skills.** `/setup` writes them to `.archon/methods/`, not to `.claude/skills/` or `.agents/skills/`, and no box invokes one as a skill. @@ -470,7 +473,7 @@ workflows/ ├── PRD.md # /specs — product requirements ├── issues.json # /tickets — build-ready vertical slices + test commands ├── build-state.json # /build — per-slice red/green progress - ├── evidence.json # /build — evidence set; present only when the build gate passed + ├── evidence.json # /build — evidence set; the verdict and its failures, on every verdict ├── report.md # /build — build outcomes └── arch-review.md # /improve-architecture — drift analysis ``` @@ -485,7 +488,7 @@ stale `/` dir only once its PR/branch is merged or closed (report-first, n ## Dependency map -- **Archon**: version ≥ 0.7.0 required — the key-discriminated node schema plus `evidence_policy`/`always_run` is the stable contract, not the release number ([ADR-0011](docs/adr/0011-archon-schema-target.md), [ADR-0033](docs/adr/0033-archon-070-schema-target.md)) +- **Archon**: version ≥ 0.7.0 required. The key-discriminated node schema plus `evidence_policy`/`always_run` is the stable contract, not the release number ([ADR-0011](docs/adr/0011-archon-schema-target.md), [ADR-0033](docs/adr/0033-archon-070-schema-target.md)). `/build` needs Archon ≥ 0.10.0, because `evidence` reads the pre-check through a script node's `with:` bindings ([ADR-0034](docs/adr/0034-evidence-gate-deterministic-writer.md)). On an older engine the binding does not arrive, so no build ends `pass`. Whether such an engine loads the Box at all is not measured. - **Required peer plugins**: none - **Optional tool**: Python `slopcheck` CLI (GSD's slopsquatting gate) — if on `PATH`, the slopcheck node defers to it; otherwise falls back to npm registry HEAD checks diff --git a/apps/claude-code/unic-archon-dlc/commands/archon-upgrade.md b/apps/claude-code/unic-archon-dlc/commands/archon-upgrade.md index 07540c6f..0259b95a 100644 --- a/apps/claude-code/unic-archon-dlc/commands/archon-upgrade.md +++ b/apps/claude-code/unic-archon-dlc/commands/archon-upgrade.md @@ -1,16 +1,14 @@ --- allowed-tools: ['Bash', 'Read', 'Glob'] -description: 'Report what a new Archon release means for this Plugin: compare the installed version against the 0.7.0 floor, classify each upstream change as ADOPT / DEFER / VERIFY-ONLY / BREAKS-US against the four installed Box YAMLs, check for changed upstream defaults, re-assert ADR-0011 trap conformance, and probe that the Archon config keys this Plugin depends on are still read. Read-only in this repository — the key probe writes a throwaway repository outside every clone.' +description: 'Report what a new Archon release means for this Plugin: compare the installed version against the 0.7.0 floor, classify each upstream change as ADOPT / DEFER / VERIFY-ONLY / BREAKS-US against the four installed Box YAMLs, check for changed upstream defaults, re-assert the four node-schema traps, and probe that the Archon config keys this Plugin depends on are still read. Read-only in this repository — the key probe writes a throwaway repository outside every clone.' --- # unic-archon-dlc:archon-upgrade -> Design rationale: [ADR-0035 — `/archon-upgrade` reports what a new Archon release means for this Plugin](docs/adr/0035-archon-upgrade-report.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); floor, node-schema conventions and the four silent-failure traps per [ADR-0011](docs/adr/0011-archon-schema-target.md); the two locked classification precedents per [ADR-0033](docs/adr/0033-archon-070-schema-target.md); the evidence-gate shape a future release may touch per [ADR-0034](docs/adr/0034-evidence-gate-deterministic-writer.md)). - `/archon-upgrade` answers one question: **Archon shipped a new release — what does it mean for this Plugin?** It compares the installed `archon` against this Plugin's floor, reads the release notes for everything in between, classifies each notable change against the four installed Box YAMLs, and -re-asserts [ADR-0011](docs/adr/0011-archon-schema-target.md)'s silent-failure traps. The output is one +re-asserts the four node-schema silent-failure traps of Step 5. The output is one decision table. **It is read-only here, absolutely.** There is no apply mode — this is a stronger claim than @@ -27,8 +25,7 @@ Follow these steps in order. Do not skip any step. ## Step 1 — Compare installed against the floor This Plugin's floor is **Archon `0.7.0`** — the version the key-discriminated schema needs: gates, -loops, `context: fresh`, `evidence_policy` and `always_run` -([ADR-0033](docs/adr/0033-archon-070-schema-target.md)). Run: +loops, `context: fresh`, `evidence_policy` and `always_run`. Run: ```bash archon --version @@ -63,7 +60,7 @@ GitHub URL. If this fails for any reason — no `brew` on `PATH` (expected on Windows), Archon installed another way, a non-zero exit, or a changed JSON shape — **do not guess**. Print one line explaining what failed and **ask the user, in this conversation, for the `owner/repo` to read release notes from.** This command -runs with a human present ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)); asking costs +runs with a human present; asking costs one turn, guessing costs a wrong report. If the user declines or does not know, skip to Steps 5 and 6 and say in the final report that the classification table could not be produced. @@ -98,29 +95,27 @@ Walk every kept release's notes. Emit one row per notable change: The four classifications: - **ADOPT** — a new capability this Plugin should take on. Name the file and node that would change. -- **DEFER** — recognised as valuable, blocked on a condition already recorded in an ADR. Cite the - recorded condition; never invent one. +- **DEFER** — recognised as valuable, blocked on a condition one of the locked precedents below + records. Cite that precedent; never invent one. - **VERIFY-ONLY** — a deliberate, already-documented divergence or an area we track but do not follow. Confirm it still holds. Do not re-litigate it. - **BREAKS-US** — something this Plugin currently relies on has changed behaviour or been removed. Every row names an **affected file and node** (for example `.archon/workflows/unic-dlc-build.yaml:evidence`) -and one suggested next step: **file an issue**, **amend an ADR**, or **nothing**. +and one suggested next step: **file an issue** or **nothing**. -### Two locked precedents — cite them, never re-derive them +### Two locked precedents — apply them, never re-derive them - **`workflow:` sub-runs (dynamic fan-out, `with:` parameter mapping over a dynamic list) → - classify DEFER.** The rationale in that row is a citation and nothing else: _see ADR-0033 § Sub-runs - (`workflow:` nodes): deferred, with the trigger to revisit_. Do not restate the trigger, do not - paraphrase it, and do not invent a second one. Reclassify the row as ADOPT **only** when the release - notes show upstream has shipped what that section already records as the trigger — nothing else - moves it. + classify DEFER.** The rationale in that row is this sentence and nothing else: _sub-runs stay + deferred until upstream ships fan-out and `with:` parameter mapping over a dynamic list_. Do not + invent a second trigger. Reclassify the row as ADOPT **only** when the release notes show upstream + has shipped both. - **Archon's own repository / remote-resolution algorithm (the remote key in Step 6's table, an `origin`-then-sole-remote fallback, or similar) → classify VERIFY-ONLY, never BREAKS-US.** This Harness resolves no repository from a remote at all: `docs/agents/issue-tracker.md` § Addressing names it, so Archon's algorithm and - this Harness answer different questions and cannot disagree - ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended 2026-08-18, which retired the derivation - ADR-0033 § "Repository derivation" recorded as a deliberate divergence). Cite that. Classifying it as + this Harness answer different questions and cannot disagree. The Harness once derived the + repository from a remote and has retired that derivation. Say so in the row. Classifying it as BREAKS-US makes every run re-raise a decision that has already been superseded once. ### Mandatory sub-pass — changed upstream defaults, not just new fields @@ -136,12 +131,12 @@ only asks "is there a new field?" is blind to a removed default a Box still assu Report each hit as its own row, marked `default-change`, in the same table. -## Step 5 — Re-assert ADR-0011's traps against the installed Boxes +## Step 5 — Re-assert the node-schema traps against the installed Boxes This step runs **unconditionally**, even when Steps 2–4 failed. Read every `.archon/workflows/unic-dlc-*.yaml` in this repository — the Boxes actually installed here, which is what a run would use — and check each one against -[ADR-0011](docs/adr/0011-archon-schema-target.md)'s node-schema conventions 1–4: +these four node-schema conventions: 1. No node carries a `type:` discriminator. 2. Every node with an `approval:` key sits in a workflow whose top level declares `interactive: true`. @@ -153,7 +148,7 @@ wrong`. If you cannot read a file, that file is a FAIL naming the read error — run is never a silent PASS. A FAIL here is not caused by the new Archon release: it means an installed Box has drifted from -ADR-0011. Nothing else guards these four conventions, so this read is the only place they are +these conventions. Nothing else guards these four conventions, so this read is the only place they are re-asserted — say which file drifted and stop short of fixing it, because this command changes nothing in this repository. @@ -249,7 +244,7 @@ One block, in this order: | ------- | ------ | -------------- | ------------------ | ------------------- | ...one row per notable change, default-change rows included... - ADR-0011 traps (installed Boxes) + Node-schema traps (installed Boxes) unic-dlc-build.yaml PASS unic-dlc-explore.yaml PASS unic-dlc-pr-review.yaml PASS @@ -267,4 +262,4 @@ One block, in this order: Close with this line, verbatim: > This command wrote nothing in this repository — only the throwaway repository Step 6 built, now -> deleted. Adoption is a human decision — file an issue or amend an ADR by hand. +> deleted. Adoption is a human decision. File an issue. diff --git a/apps/claude-code/unic-archon-dlc/commands/cleanup.md b/apps/claude-code/unic-archon-dlc/commands/cleanup.md index 69f4c6ec..30835b69 100644 --- a/apps/claude-code/unic-archon-dlc/commands/cleanup.md +++ b/apps/claude-code/unic-archon-dlc/commands/cleanup.md @@ -5,25 +5,22 @@ description: 'Repo-global operational janitor: report (and, on explicit opt-in, # unic-archon-dlc:cleanup -> Design rationale: [ADR-0028 — `/cleanup` is the repo-global operational janitor](docs/adr/0028-cleanup-operational-janitor.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); compose-don't-reimplement per [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md); tracker is the single source of truth per [ADR-0013](docs/adr/0013-tracker-single-source-of-truth.md); artifact home per [ADR-0015](docs/adr/0015-workflows-slug-artifact-home.md)). - **Arguments:** "$ARGUMENTS" `/cleanup` is the **off-line operational janitor**: it prunes the debris an Archon-driven lifecycle accumulates — **merged/stale worktrees**, **stale branches/PRs**, and **stale `workflows//` artifact dirs**. It is a **repo-global** box: it inspects and mutates sibling worktrees, branches, and PRs, so it **cannot** run inside an isolated worktree — that is exactly why it is a **Claude -Code command, not an Archon workflow** ([ADR-0017](docs/adr/0017-container-follows-structural-need.md)). +Code command, not an Archon workflow**. It is a **thin composing wrapper**: it **owns the _what_** — deciding what is prunable and enforcing the report-first / per-category-confirm posture — and **composes the _how_**: Archon's own `archon isolation` / `archon complete` commands for worktree/branch lifecycle, and the tracker that `docs/agents/issue-tracker.md` § Access names, for PR and branch state. -Compose those tools — never reimplement them, and never introduce a `tracker-adapter` lib -([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md) / [ADR-0018](docs/adr/0018-generic-core-config-compose.md)). +Compose those tools — never reimplement them, and never introduce a `tracker-adapter` lib. > **This is not `/improve-architecture`.** The name `cleanup` was repointed: the old arch-review + -> ADR-consolidation content moved to `/improve-architecture` ([ADR-0027](docs/adr/0027-improve-architecture-skill-superseding.md)). +> ADR-consolidation content moved to `/improve-architecture`. > `/cleanup` is git/Archon hygiene only — it touches no code, no ADRs, and no `arch-review.md`. **Destructive-action posture (load-bearing).** `/cleanup` **defaults to a report-only dry-run** and @@ -37,8 +34,7 @@ Follow these steps in order. Do not skip any step. `/cleanup` reads (never writes) `.archon/unic-dlc.config.yaml`. Read it with your own tools. Do not shell out to Node, do not import a Plugin module, and do not read `$CLAUDE_PLUGIN_ROOT`: an installed -Plugin ships no `node_modules`, and that variable is not set inside the Bash tool -([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The four Archon Boxes read +Plugin ships no `node_modules`, and that variable is not set inside the Bash tool. The four Archon Boxes read their config this way already; this is the same shape. Like `/improve-architecture` this is an **off-line** box, so an absent or unreadable config is @@ -63,8 +59,7 @@ this is non-blocking. Read `docs/agents/issue-tracker.md`. Its § Access names the MCP server or skill that serves this tracker and its § Addressing names the repository — read that server's own current tool list and build -each call from it, naming no provider and writing no command, subcommand or flag -([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). Keep `TRACKER_READY` = whether the file +each call from it, naming no provider and writing no command, subcommand or flag. Keep `TRACKER_READY` = whether the file resolved. If it did not, warn that PR/branch-state detection and slug-dir pruning will be skipped (they need the tracker), then continue — this box never halts on it. @@ -115,7 +110,7 @@ the merge itself — surface only genuine leftovers. Only if `TRACKER_READY` (a slug dir's disposition depends on its PR/branch state). Using `node:fs`, list the immediate child directories of `/` (default `workflows/`). Each -child dir name is a Slug ([ADR-0015](docs/adr/0015-workflows-slug-artifact-home.md)). For each: +child dir name is a Slug. For each: - Resolve its PR/branch via the tracker + the isolation list from Step 3. - A slug dir is **prunable only if** its PR/branch is **merged or closed**. A slug with an **open diff --git a/apps/claude-code/unic-archon-dlc/commands/improve-architecture.md b/apps/claude-code/unic-archon-dlc/commands/improve-architecture.md index b61f64f4..4985d7cc 100644 --- a/apps/claude-code/unic-archon-dlc/commands/improve-architecture.md +++ b/apps/claude-code/unic-archon-dlc/commands/improve-architecture.md @@ -5,15 +5,12 @@ description: 'Off-line architecture health: surface technical + intent drift and # unic-archon-dlc:improve-architecture -> Design rationale: [ADR-0027 — `/improve-architecture` is a skill that composes Matt's method and owns ADR superseding](docs/adr/0027-improve-architecture-skill-superseding.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); compose-don't-reimplement per [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md); earns-its-place per [ADR-0021](docs/adr/0021-earns-its-place-compose-verbatim.md); artifact home per [ADR-0015](docs/adr/0015-workflows-slug-artifact-home.md); tracker/ADRs are the source of truth per [ADR-0013](docs/adr/0013-tracker-single-source-of-truth.md)). - **Arguments:** "$ARGUMENTS" `/improve-architecture` is an **off-line, on-demand** box: it surfaces architectural drift + deepening opportunities and **consolidates ADRs (including superseding older ones)**. It runs off the main line — periodically or when you sense drift, **not** per-feature and **not** as an end-of-cycle -auto-hook. It is an **in-session command/skill** because design grilling needs the live conversation -(inherently HITL — [ADR-0017](docs/adr/0017-container-follows-structural-need.md)). +auto-hook. It is an **in-session command/skill** because design grilling needs the live conversation. It is a **thin composing wrapper**: it **owns the _what_** — the DLC config binding, the durable `arch-review.md` artifact, the intent-drift pass against the PRD, and the ADR-superseding gate — and @@ -22,11 +19,11 @@ Method **verbatim**, with `codebase-design` for the architecture vocabulary, `gr walk, and `domain-modeling` to keep `CONTEXT.md` current. All four are read by path, per Step 1. -**What the DLC adds over the raw Method (why this box earns its place — [ADR-0021](docs/adr/0021-earns-its-place-compose-verbatim.md)):** +**What the DLC adds over the raw Method:** The Method produces an ephemeral HTML deepening report + a design grill for **technical** drift. On top of that this box adds (a) an **intent-drift** pass comparing the PRD's stories/acceptance criteria against what shipped, (b) a **durable `arch-review.md`** committed under the artifacts dir, -and (c) an **ADR-consolidation gate with superseding** across both ADR homes. +and (c) an **ADR-consolidation gate with superseding** across every ADR home the repository has. Follow these steps in order. Do not skip any step. @@ -34,8 +31,7 @@ Follow these steps in order. Do not skip any step. `/improve-architecture` reads (never writes) `.archon/unic-dlc.config.yaml`. Read it with your own tools. Do not shell out to Node, do not import a Plugin module, and do not read `$CLAUDE_PLUGIN_ROOT`: -an installed Plugin ships no `node_modules`, and that variable is not set inside the Bash tool -([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The four Archon Boxes read +an installed Plugin ships no `node_modules`, and that variable is not set inside the Bash tool. The four Archon Boxes read their config this way already; this is the same shape. Unlike the tracker-bound boxes this one is **off-line and touches no tracker**, so an absent or @@ -64,7 +60,7 @@ _parameters_ (which, where, whether) and this Box can sensibly default those; a _procedure_, and there is no default for that. When all four are present, print nothing and continue. That text **is** the procedure — the steps below add only the DLC layers named above, and never -restate, summarise or improve a Method ([ADR-0030](docs/adr/0030-harness-hosts-methods.md)). A Method's +restate, summarise or improve a Method. A Method's sub-files sit beside its `SKILL.md`, in the same directory: `HTML-REPORT.md` for `improve-codebase-architecture`; `DEEPENING.md` and `DESIGN-IT-TWICE.md` for `codebase-design`. @@ -102,8 +98,9 @@ Bind that procedure to the DLC: **capture-only**. Do **not** accept, number, or write any ADR during Step 3, and do not let the delegated procedure write one — instead record each proposed decision (the "accept as ADR" candidates and load-bearing rejections) as a candidate and hand it to Step 6. Step 6 is the **single place** - ADRs are written, so the A/R/E gate, MADR-lite format, both-homes numbering, and superseding/index - mechanics always apply and ADRs can never drift into an inconsistent format or bypass the gate. + ADRs are written, so the A/R/E gate, the chosen home's format and numbering, and the + superseding/index mechanics always apply. No ADR bypasses the gate or departs from its home's + format. ## Step 4 — Intent drift (DLC value-add — per-slug mode only) @@ -126,7 +123,7 @@ available for ` and continue. ## Step 5 — Write the durable arch-review report Complement the Method's ephemeral HTML (which lands in the OS temp dir) with a durable markdown artifact -committed to the repo ([ADR-0015](docs/adr/0015-workflows-slug-artifact-home.md)). All file IO via +committed to the repo. All file IO via Node `node:fs`/`node:path`. - **Per-slug mode** → `//arch-review.md` @@ -177,7 +174,7 @@ Present each candidate and wait for **A / R / E**: ``` --- ADR Candidate N of M -Proposed file: /docs/adr/NNNN-.md (home = plugin-local | repo-root) +Proposed file: /NNNN-.md (home = a docs/adr/ directory of this repository) Status: Proposed Supersedes: ADR-NNNN (omit if it supersedes nothing) Context: @@ -187,14 +184,16 @@ Consequences: Accept (A) / Reject (R) / Edit (E)? ``` -- **Choose the ADR home.** Infer from what the decision concerns and **confirm with the user**: a - decision about one plugin → that plugin's `docs/adr/` (e.g. - `apps/claude-code//docs/adr/`); a cross-cutting / monorepo-wide decision → the repo-root - `docs/adr/`. Both homes use MADR-lite (`# NNNN. Title` · `**Status:** Accepted (YYYY-MM)` · - `## Context` / `## Decision` / `## Consequences`) and per-directory zero-padded numbering — see the - root `docs/adr/README.md`. +- **Choose the ADR home** from the `docs/adr/` directories this repository already has. Find them + with your own tools, skipping `node_modules/` and other dependency trees. If there are several, + offer each one, propose the one closest to what the decision concerns, and **confirm with the + user**. If there is one, propose it and confirm. If there is none, propose the root `docs/adr/`, + which `/specs` already reads, and confirm. Follow the format and numbering the chosen home's + existing ADRs use. In an empty home, use MADR-lite + (`# NNNN. Title` · `**Status:** Accepted (YYYY-MM)` · `## Context` / `## Decision` / + `## Consequences`) and zero-padded numbering from `0001`. - **A (Accept):** compute the next `NNNN` = highest existing number in the **chosen home** + 1 (scan - that dir's files). Write the ADR. Print `ADR written: /docs/adr/NNNN-.md`. + that dir's files). Write the ADR. Print `ADR written: /NNNN-.md`. - **R (Reject):** skip. Print `ADR skipped: `. - **E (Edit):** re-show the draft, let the user revise Context / Decision / Consequences / home / Supersedes, then re-present for A/R. @@ -205,11 +204,10 @@ When an accepted ADR supersedes an existing one: 1. In the **old** ADR file, change its status line to `**Status:** Superseded by ADR-NNNN`. 2. In the **new** ADR, add a `Supersedes ADR-NNNN` reference (in the status area or Context). -3. Update the **matching home's** `README.md` index: set the **old** row's Status cell to - `Superseded by ADR-NNNN`, and add a **new** row for `NNNN`. (Each home has its own index — - plugin-local `docs/adr/README.md` and repo-root `docs/adr/README.md`.) +3. If the **matching home** has a `README.md` index, update it: set the **old** row's Status cell to + `Superseded by ADR-NNNN`, and add a **new** row for `NNNN`. Each home keeps its own index. -An ADR in one home may supersede an ADR in the other; amend whichever home(s) the two files live in. +An ADR in one home may supersede an ADR in another home. Amend each home the two files live in. ## Step 7 — Summary diff --git a/apps/claude-code/unic-archon-dlc/commands/setup.md b/apps/claude-code/unic-archon-dlc/commands/setup.md index 25c6f5e4..f1cc003e 100644 --- a/apps/claude-code/unic-archon-dlc/commands/setup.md +++ b/apps/claude-code/unic-archon-dlc/commands/setup.md @@ -6,17 +6,12 @@ description: 'Install unic-archon-dlc into this project: copy the Boxes and the # unic-archon-dlc:setup -> Design rationale: [ADR-0019 — Conversational `/setup`](../docs/adr/0019-conversational-setup.md); -> [ADR-0036 — `/setup` owns a named install set](../docs/adr/0036-setup-owns-a-named-install-set.md) for what -> it owns and how it replaces it; [ADR-0016 — a thin process layer](../docs/adr/0016-dlc-thin-process-layer.md) -> for why this command reads the project instead of holding a list of tools. - **Arguments:** "$ARGUMENTS" `/setup` is this Plugin's installer and its sole configuration entry point. It is prose: do every action below with your own tools — Read, Write, Edit, Glob, Grep and Bash. Import nothing, shell out to no Node script, and do not read `$CLAUDE_PLUGIN_ROOT`: an installed Plugin ships no `node_modules`, and that -variable is not set inside the Bash tool ([ADR-0023](../docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). +variable is not set inside the Bash tool. ## What `/setup` owns, and what it only visits @@ -32,14 +27,33 @@ every action below names which one it takes: **report** has one override: the `reconfigure` argument (Step 3). Outside a marked block, nothing else rewrites a tenant file. +**This Plugin's prefix is `unic-dlc`, and `/setup` touches no name that carries another prefix.** A +Consumer may hold Practice Packs beside this Plugin, each with its own prefix and its own installer. In `.claude/skills/`, in `.archon/packs/`, +in `CLAUDE.md` and in every formatter ignore file, `/setup` writes, overwrites or removes only these names: + +- a `.claude/skills/unic-dlc-*/` directory. Today `/setup` writes, overwrites or removes nothing in + `.claude/skills/`. +- `.archon/packs/unic-dlc.config.yaml`. `/setup` writes nothing in `.archon/packs/` until a + later Plugin version adds the layered Practice Pack config. The + Consumer's config stays at `.archon/unic-dlc.config.yaml`, as Step 6 says. +- the block between the `unic-dlc:begin` and `unic-dlc:end` markers in `CLAUDE.md` and in each ignore + file. + +Leave every other skill directory, config file and marked block as you found it, whatever it contains. The +one exception is the legacy `unic-archon-dlc:begin` and `unic-archon-dlc:end` markers. They are reserved to +this Plugin, and Step 7 migrates a block under them. + +Whether one prefix may begin another is still open, and this rule does not settle it. A pack prefixed +`unic-dlc-e2e` would own `unic-dlc-e2e-*` skill directories, which also match `unic-dlc-*`. That overlap is +an open risk until a later Plugin version decides the boundary. + ## Step 1 — Preflight Two checks, in this order. Each one stops the run when it fails, before any question is asked and before anything is written. **Archon.** Run `archon --version`. This Plugin's floor is **`0.7.0`** — the version whose schema carries -gates, loops, `context: fresh`, `evidence_policy` and `always_run` -([ADR-0033](../docs/adr/0033-archon-070-schema-target.md)). Compare the three numbers, never the raw strings: +gates, loops, `context: fresh`, `evidence_policy` and `always_run`. Compare the three numbers, never the raw strings: the output may carry a program name or a `v` prefix. Three ways this stops: the command is not found; the version parses and sits below the floor, so print both versions; or the output yields no three numbers at all, so print it raw and say the floor could not be checked against it. @@ -64,6 +78,9 @@ Then verify it before trusting it, and keep `PLUGIN_VERSION` from the manifest y 1. `.claude-plugin/plugin.json` there names `unic-archon-dlc`, at the same version as the registry entry. 2. `vendor/mattpocock-skills/` is present. 3. `.archon/workflows/` holds at least one `unic-dlc-*.yaml`. +4. `README.md` is present and holds all three sections later steps read from `<installPath>/README.md`: + § Dependencies, § Configuration reference and § The tracker contract. A missing section fails the + check. Name it in the stop. Print what you found and stop on any of: no entry for this Plugin, a registry whose top-level `version` is not `2`, or a failed check above. The registry entry and the manifest agreeing is the whole of the version @@ -107,13 +124,26 @@ unreadable → say so, keep `ARCHON_REMOTE_RESOLVED` empty and carry on. One sum file, so it never stops the run; the rule two reads above stops the run because overwriting the config would destroy it. +**The base branch Archon will use, verify-only.** Every Box reads a run's base branch from Archon's +`$BASE_BRANCH` and from nothing else. Archon resolves it from `archon workflow run --base +<branch>`, then `worktree.baseBranch` in `.archon/config.yaml`, then the codebase's stored default branch. +Each dispatch passes its own `--base`, so this command cannot see it. From the same file, keep +`worktree.baseBranch` as `ARCHON_BASE_CONFIGURED`. It is empty when the key is unset or the file is +unreadable. This command cannot read the codebase default itself, and what Archon stores depends on its +version. Archon 0.7 to 0.11 store the branch that was checked out when the repository was first +registered. From 0.12.0, new projects follow the remote's default branch unless a base is chosen +([v0.12.0 release notes](https://github.com/coleam00/Archon/releases/tag/v0.12.0)). So the remote's default is an approximate stand-in, and `worktree.baseBranch` is the value to rely on. Run +`git symbolic-ref --short refs/remotes/<ARCHON_REMOTE_RESOLVED>/HEAD`, strip the remote prefix, and keep +the result as `REMOTE_DEFAULT_BRANCH`. It is empty when the ref does not resolve. Set `ARCHON_BASE_RESOLVED` +to `ARCHON_BASE_CONFIGURED` when it is set, else to `REMOTE_DEFAULT_BRANCH`. Write nothing to the file. + **The repo layout.** Keep `REPO_LAYOUT`: `multi-context` when this repository holds more than one independently-releasable project (a `packages/` or `apps/` tree with its own manifests), otherwise `single-context`. Never ask for it. Then set `STATE`: `fresh` when `CURRENT` is null, `partial` when `CURRENT` exists with `project.branching` unset, `full` otherwise. `project.branching` is the one key a config needs before the Boxes can read it; -every other key has a default, named in the § Configuration reference in [README.md](../README.md). +every other key has a default, named in § Configuration reference in `<installPath>/README.md`. ## Step 3 — Read the arguments @@ -143,7 +173,7 @@ and fail with their own message. Never abort the run for a missing capability. Do not probe for the Methods here. They ship inside this Plugin and Step 5 installs them; their presence is bundle integrity, not discovery. Which Method each Box reads is recorded once, in -[README.md § Dependencies](../README.md#dependencies). +§ Dependencies in `<installPath>/README.md`. **What this project runs.** One read, two harvests — read the manifests, task files and scripts this project actually runs, whatever the language, and keep both of these from that one read: @@ -173,13 +203,15 @@ board already carries. Through whichever tracker surface Step 4 found, read what Both trees come out of the directory Step 1 verified. This is the **replace** treatment: overwrite silently, every run. Every installed **Box** carries a header naming this Plugin and its version and saying that `/setup` replaces it, which is what makes an overwrite legible — an operator's edit shows up as a -tracked `git diff` after a run, not as a warning dialog -([ADR-0036](../docs/adr/0036-setup-owns-a-named-install-set.md) D3). **No Method file carries one**, and the +tracked `git diff` after a run, not as a warning dialog. **No Method file carries one**, and the Methods bullet below says why. **The Methods.** Verify the bundle at `vendor/mattpocock-skills/`, then copy it into `.archon/methods/` — the one path every Box and command reads a Method from. +This Plugin alone owns `.archon/methods/`. A Practice Pack never ships a Method, and a skill a pack installs +under `.claude/skills/` is not a Method. So the wholesale replace below removes nothing another installer wrote. + **The copy flattens.** The bundle groups Methods by category, so a Method sits at `vendor/mattpocock-skills/skills/<category>/<name>/`; a Box reads `.archon/methods/<name>/SKILL.md`, one level deep. Copy each Method directory to `.archon/methods/<name>/`, dropping the category, and copy the @@ -199,7 +231,7 @@ installed. - `vendor/mattpocock-skills/README.md` records the upstream repository, tag and commit this bundle was copied from. Read it and keep the tag as `BUNDLE_TAG`. - Check what the bundle **lacks**, not only what it holds: compare the Method names in it against the table - under [README.md § Dependencies](../README.md#dependencies). A Method that table names and the bundle does + under § Dependencies in `<installPath>/README.md`. A Method that table names and the bundle does not hold is a fault — report it by name and stop here, before anything is written. Keep the names you do install as `METHODS_INSTALLED` for the summary. - Copy clean: replace `.archon/methods/` wholesale, so a Method dropped from a later Plugin version cannot @@ -212,15 +244,18 @@ installed. version on the Boxes carry the provenance for this tree. - Two override paths an earlier version offered now resolve nothing, because `.archon/methods/` is the only path anything reads. Report each one you find, once, as retired, and change neither — both are the team's - own work: a `.archon/methods.local/` directory, and any `methods.<name>.source` key in the config. + own work: a `.archon/methods.local/` directory, and any `methods.<name>.source` key in the config. When + you report one, say that the route to replace a Box step is now an `include:` slot, decided and not yet + built. **The Boxes.** Install every `unic-dlc-*.yaml` this Plugin ships into `.archon/workflows/`, discovered by -reading the Plugin's own `.archon/workflows/` — no Box name is a literal in this file. +reading the Plugin's own `.archon/workflows/`. The install takes no Box name from this file. Only the +`CLAUDE.md` block below lists the Boxes by name. Install is **name-scoped, never a whole-directory replace**: `.archon/workflows/` is shared with the Consumer's own workflows, so only names matching `unic-dlc-*.yaml` are read, written, overwritten or swept. A file named outside that pattern stays untouched whatever it contains — that is what makes the variant -escape hatch in [README.md](../README.md) true. The sweep retires a `unic-dlc-*.yaml` this Plugin version +escape hatch in `<installPath>/README.md` true. The sweep retires a `unic-dlc-*.yaml` this Plugin version no longer ships, whether or not it carries the header: ownership is decided by name alone. Before overwriting, read the first line of each `unic-dlc-*.yaml` already on disk and keep the version it @@ -238,8 +273,7 @@ a fresh project and on a Box carrying no such line. Boxes naming **different** v install: report every version you found rather than picking one, because picking one claims an install state that never existed. -`/setup` writes nothing into `.archon/commands/`. The Box command docs live at `docs/boxes/` in this -Plugin's own repository, as operator documentation. +`/setup` writes nothing into `.archon/commands/`. Keep `WORKFLOWS_WRITTEN`, `WORKFLOWS_ADDED` and `WORKFLOWS_REMOVED` for Step 8. @@ -266,7 +300,7 @@ Which gaps: `STATE = 'fresh'` → every field; `STATE = 'partial'` → the missi decide which already-set fields to revisit. Surface `GIT_REMOTE` as a hint while asking. Pass `REPO_LAYOUT` through without asking. -The fields, mapped onto the schema paths in the § Configuration reference in [README.md](../README.md), +The fields, mapped onto the schema paths in § Configuration reference in `<installPath>/README.md`, which is the single source of truth for every default: - **project** — `project.name`; `project.branching` (`gitflow | github-flow`, mandatory); @@ -319,7 +353,7 @@ human reads this file. Keep the path as `CONFIG_PATH`. ### The tracker contract Two files: `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md`. Every Box reads a tracker -fact from these and from nothing else. § **The tracker contract** in [README.md](../README.md) states what +fact from these and from nothing else. § **The tracker contract** in `<installPath>/README.md` states what each file carries, the `Axis`/`Holds` columns, the rule that a row with no axis writes nothing, and a table shape that satisfies it. Read it and follow it: those columns and their rules are stated there, and this step does not repeat them. @@ -351,23 +385,50 @@ rules hold whatever they answer: ## Step 7 — Patch the managed blocks (**patch**) One block in the Consumer's `CLAUDE.md`, and one more in each line-based ignore file Step 4 found. Each is -delimited by markers written in that file's own comment syntax, `unic-archon-dlc:begin` and -`unic-archon-dlc:end`. Rewrite the whole block between the markers, markers included, and leave everything -outside them verbatim. Absent block in a file that exists → append it. Absent file → create it, but only -where this step has a block to put in it: a tool whose exclusions live in a structured config gets no file -of its own invented for it. A -re-run replaces the block in place and never appends a second one. The block carries no -`AUTO-GENERATED — DO NOT EDIT` banner, and it does say plainly that the next run rewrites it — two different -things, and the second is what makes an overwrite legible. What it never claims is to be maintained: nothing -here detects a hand edit -between the markers. +delimited by markers written in that file's own comment syntax, `unic-dlc:begin` and `unic-dlc:end`. +Rewrite the whole block between the markers, markers included, and leave everything outside them verbatim. +A marker is a line whose whole comment text is `unic-dlc:begin` or `unic-dlc:end`. Never match it as part +of a longer word, because a marker that carries another prefix belongs to another installer. + +**A block under the old markers is migrated in place.** Before version 0.30.0 this Plugin wrote its blocks +between `unic-archon-dlc:begin` and `unic-archon-dlc:end`. Before you write to a file, look in it for both +marker pairs, matched as whole comment lines in the same way. + +Check the malformed cases first. In each of them, write nothing to that file and report it in Step 8 as +`ACTION REQUIRED`, because a rewrite could remove the Consumer's text or leave a stray marker: + +- A begin marker with no end marker of the same name after it. A begin of one name followed by an end of + the other name counts as this case. +- An end marker with no begin marker of the same name before it, under either name. +- Two blocks under the same marker name. +- More than one begin marker, or more than one end marker, under the same name. + +Then, for a well-formed file: + +- A block under the new markers only → rewrite it in place. +- A block under the old markers only → replace it, from the old begin line to the old end line, with the + block under the new markers, at the same position in the file. Report the file as migrated in Step 8. +- Both → rewrite the new block in place and remove the old block, markers included. + Report the file in Step 8 as `patched` and `old block removed`. + +Then search the repository, skipping `.git/` and `node_modules/`, for any other file that holds +`unic-archon-dlc:begin` or `unic-archon-dlc:end` as a whole comment line. Write nothing to such a file. +Report each one in Step 8 as `ACTION REQUIRED`, as a file that holds an old marker outside the files this +step patches. The operator decides what to do with it. + +Absent block in a file that exists → append it. Absent file → create it, but only where this step has a +block to put in it: a tool whose exclusions live in a structured config gets no file of its own invented for +it. A re-run replaces the block in place and leaves one block per file, never two, under the new markers. +The block carries no `AUTO-GENERATED — DO NOT EDIT` banner, and it does say plainly that the next run +rewrites it — two different things, and the second is what makes an overwrite legible. What it never claims +is to be maintained: nothing here detects a hand edit between the markers. ### `CLAUDE.md` Write exactly this, markers included, as the whole block. Every line is static: ```markdown -<!-- unic-archon-dlc:begin --> +<!-- unic-dlc:begin --> ## unic-archon-dlc @@ -384,15 +445,27 @@ anything between the markers is replaced on the next run. `/unic-archon-dlc:setup` replaces them on every run. - **Methods** — `.archon/methods/` is replaced wholesale on every `/unic-archon-dlc:setup` run. To change a Method, edit the file there and expect the next run to overwrite it. -- **What each box does** — - <https://github.com/unic/unic-agents-plugins/blob/main/apps/claude-code/unic-archon-dlc/README.md> - -<!-- unic-archon-dlc:end --> +- **What each box does**: + - `unic-dlc-explore` researches a topic in parallel, writes `findings.md` for `/specs` and runs one + spike. It is optional. It offers to keep the spike code on a branch only when `gates.explore` is + `hitl`. + - `unic-dlc-build` implements each slice in `issues.json` test-first, then verifies, pushes, and + opens a PR when the verdict is `pass`. It pauses for a human to review that PR only when + `gates.build` is `hitl`. + - `unic-dlc-pr-review` reviews a PR against the repository's standards and the linked work items, + and posts the findings. It asks a human to confirm first only when `gates.pr-review` is `hitl`. + - `unic-dlc-qa` runs the tests, e2e and coverage checks, and merges the PR only when the tests pass, + no other check fails, and the PR is open against the right base. It asks for UAT sign-off and a + merge confirmation only when `gates.qa` is `hitl`. It files a UAT defect as an agent-ready + tracker item. + +<!-- unic-dlc:end --> ``` -The block names one command, `/unic-archon-dlc:setup`, and no pipeline stage: a Plugin release renames and -reorders both without touching this repository. Everything it carries is a path the reader can open, a -command they can run, or a link. +The block names one command, `/unic-archon-dlc:setup`, and one hand-kept line per Box. `/setup` rewrites +the block on every run, so a Box renamed or dropped in a Plugin release reaches this repository on its +next run. Everything it carries is a path the reader can open, a command they can run, or a line that +says what a Box does. ### The formatter exclusions @@ -425,6 +498,7 @@ unic-archon-dlc installed. config: {CONFIG_PATH} contract: {issue-tracker.md and triage-labels.md — written, or reported unchanged} archon remote: {ARCHON_REMOTE_RESOLVED} (Archon's own worktree.remote — verified, never written here) + archon base: {ARCHON_BASE_RESOLVED} ({source}), the $BASE_BRANCH of a dispatch without --base. {verified}never written here docs: {docs.type} (publish: {docs.publish}) design: {design.type} gates: {box}={mode} … @@ -433,12 +507,43 @@ unic-archon-dlc installed. workflows removed: {path} · … | none workflows added: {path} · … | none formatters: {tool} → {ignore file patched} | {tool} → ACTION REQUIRED: {entry to add} + blocks: {file} → patched | migrated from unic-archon-dlc markers | old block removed | ACTION REQUIRED: {malformed markers, or an old marker outside the patched files} ``` Name **paths**, never a count alone: the point is a reviewable diff. `ARCHON_REMOTE_RESOLVED` null → print `none resolved — Archon Boxes may need worktree.remote set manually`. +Print `{source}` as `worktree.baseBranch` when `ARCHON_BASE_CONFIGURED` is set, else as +`remote default; Archon's stored default may differ`. Print `{verified}` as `Verified, ` when the source is +`worktree.baseBranch`, and as nothing otherwise, because the remote default is only a stand-in. Wherever +this line or a warning below prints `{ARCHON_BASE_RESOLVED}`, print `none resolved` when it is empty. + +This is the one place in the Plugin that maps a branching model to a run's base. `/cleanup` keeps its own +main-line hint. Set +`BRANCHING_BASE` to `develop` when `project.branching` is `gitflow`, else to `main`. Only the warnings +below read it. No Box reads it. Print each warning that applies, after the summary block. The first +warning is the main signal, because without the key the base depends on the Archon version. + +- `ARCHON_BASE_CONFIGURED` is empty → print `WARNING: worktree.baseBranch is unset in .archon/config.yaml, +so every run without --base uses Archon's codebase default. The remote default is +{ARCHON_BASE_RESOLVED}, and Archon's stored default may differ. Pin the base that project.branching +implies in .archon/config.yaml.` +- `ARCHON_BASE_RESOLVED` is set and differs from `BRANCHING_BASE` → print `WARNING: Archon will use +{ARCHON_BASE_RESOLVED} as the base branch, but project.branching {branching} implies {BRANCHING_BASE}. +The Boxes follow Archon. If {BRANCHING_BASE} is the right base, set it in .archon/config.yaml.` + +When at least one warning printed, print this block once after them, with the value of `BRANCHING_BASE` +in place of `<BRANCHING_BASE>`: + +```text +worktree: + baseBranch: <BRANCHING_BASE> +``` + +The key must sit under `worktree:`. A top-level `baseBranch:` has no reader in Archon. A run that must +target another branch for one dispatch passes `--base <branch>` and needs no change to this file. + Build the `methods:` line from `METHODS_INSTALLED`, the names Step 5 kept. Step 5 already stopped the run if the bundle lacked a Method the dependency table names, so this line reports what landed and judges nothing. @@ -460,9 +565,8 @@ Build the version line in one of three forms, and compute it nowhere else: The line is informational and gates nothing. Step 5 runs unattended on the upgrade path, so it never becomes a prompt. -Close with three things. Every `ACTION REQUIRED` formatter entry, which is the one open item that outlives -a successful run. Then: **commit what this run wrote.** The installed trees are committed generated files -([ADR-0036](../docs/adr/0036-setup-owns-a-named-install-set.md) D4) — that is what gives a Plugin upgrade a +Close with three things. Every `ACTION REQUIRED` entry, from the formatters or the blocks. These are the +open items that outlive a successful run. Then: **commit what this run wrote.** The installed trees are committed generated files — that is what gives a Plugin upgrade a diff and a reviewer, and what makes an operator's edit to a Box legible as a `git diff` after the next run. This command stages nothing itself. And: **re-run `/unic-archon-dlc:setup` after updating the plugin.** It refreshes the Boxes and the Methods and leaves every tenant-owned file alone. diff --git a/apps/claude-code/unic-archon-dlc/commands/specs.md b/apps/claude-code/unic-archon-dlc/commands/specs.md index 5990e7e6..70f51bbb 100644 --- a/apps/claude-code/unic-archon-dlc/commands/specs.md +++ b/apps/claude-code/unic-archon-dlc/commands/specs.md @@ -5,13 +5,11 @@ description: 'Turn an idea (or an existing spec / design / UX) into one human-ap # unic-archon-dlc:specs -> Design rationale: [ADR-0020 — `/specs` reaches an aligned PRD by branch-on-input](docs/adr/0020-specs-branch-on-input.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); compose-don't-reimplement per [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md); template-in-config per [ADR-0018](docs/adr/0018-generic-core-config-compose.md)). - **Arguments:** "$ARGUMENTS" `/specs` is the **first main-line box**: it turns an idea into **one human-approved PRD** by the cheapest path given what already exists, then hands off to `/tickets`. It is an **in-session -command/skill** (grilling needs the live conversation — ADR-0017), and it **owns the _what_** (the +command/skill** (grilling needs the live conversation), and it **owns the _what_** (the branch-on-input flow, the seam-approval halt, the PRD shape) while **composing the _how_**: the `to-spec`, `grilling` and `domain-modeling` Methods for the conversation — read by path, per Step 1 — and the configured docs / design / tracker system-skill (MCP-first, CLI-fallback) to read an @@ -26,8 +24,7 @@ its screenshot beside it. Everything else is conversation, until the gate in Ste `/specs` reads (never writes) `.archon/unic-dlc.config.yaml`. Read it with your own tools. Do not shell out to Node, do not import a Plugin module, and do not read `$CLAUDE_PLUGIN_ROOT`: an installed Plugin -ships no `node_modules`, and that variable is not set inside the Bash tool -([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The four Archon Boxes read +ships no `node_modules`, and that variable is not set inside the Bash tool. The four Archon Boxes read their config this way already; this is the same shape. If the file is absent or unreadable, print @@ -53,7 +50,7 @@ tracker item. Read it then: - **Access** — its § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build the call from it. Name no provider and write no command, subcommand - or flag ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). + or flag. - **Addressing** — its § Addressing names the repository this run acts on. Name it explicitly, and derive nothing from a remote URL. - **Work-item scope** — its § Work-item scope names the filter every search applies. @@ -108,7 +105,7 @@ three is absent, print that exact path followed by `Run /unic-archon-dlc:setup.` cannot run a procedure it cannot read. When all three are present, print nothing and continue. That text **is** the procedure — the steps below add only what the Harness owns, and never restate, -summarise or improve a Method ([ADR-0030](docs/adr/0030-harness-hosts-methods.md)). A Method's +summarise or improve a Method. A Method's sub-files sit beside its `SKILL.md`, in the same directory. ## Step 2 — Slug + re-entry @@ -128,7 +125,7 @@ Ground yourself before grilling. Read, if present: root `CONTEXT.md` / `CONTEXT- prior `/explore` run). Summarise the **Domain Model**, **Established Decisions**, and **Prior Research** you found — this is the backdrop every question and the PRD must respect. -## Step 4 — Branch on input (ADR-0020) +## Step 4 — Branch on input Classify `SOURCE` into one of **two** values, and keep the one you chose as `INPUT`: @@ -246,7 +243,7 @@ Two things in `to-spec` are **overridden** here, because the Harness owns them: `/setup-matt-pocock-skills` if not" fallback never applies. `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` are that vocabulary, and `setup-matt-pocock-skills` must not be run over them: it writes another host's template over the first and reverts the second to a five-role - `wontfix` vocabulary ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended). + `wontfix` vocabulary. Check the rendered PRD against the active template before you write anything: every `##` heading in `PRD_TEMPLATE` must appear in the content. When `PRD_TEMPLATE` is unset, these seven are the required @@ -286,7 +283,7 @@ _Generated by unic-archon-dlc /specs._ <!-- Anything else worth recording. --> ``` -A team overrides the shape in `templates.prd` ([ADR-0018](docs/adr/0018-generic-core-config-compose.md)); +A team overrides the shape in `templates.prd`; the default text lives here, in the one Box that writes a PRD. If a heading is missing, fill that section and check again. Never write a partial PRD. @@ -414,7 +411,7 @@ tool: them reads as though someone looked and found nothing. - **A component is keyed on the stable identity `DESIGN_DOC` names, never on its name.** One name repeats inside a single design file — measured 2026-08-25 on the Consumer's file: one name carried by - four distinct components ([#404](https://github.com/unic/unic-agents-plugins/issues/404)) — so a name + four distinct components — so a name key is ambiguous today, not in theory. Record the name as a label. #### What a contract holds @@ -548,8 +545,7 @@ Both gates stage the **same named paths**, and nothing else: - each design contract Step 7 wrote, by its own path, and the screenshot beside it — one `git add` per file. A contract is staged with the PRD it belongs to and reaches the same review, which is the only place a human sees it before it is used. **An asset a contract needs is committed as bytes, never as - a link**: an exported asset URL expires seven days after it is issued (measured 2026-08-25, - [#405](https://github.com/unic/unic-agents-plugins/issues/405)), so a + a link**: an exported asset URL expires seven days after it is issued (measured 2026-08-25), so a committed link is dead within the week and dead in a way that looks like an asset nobody drew. **Staging rule — named paths only.** Run one `git add <path>` per path above. Never `git add -A`, @@ -576,8 +572,7 @@ the named paths. Unstage anything else with `git restore --staged <path>` and sa `develop`, title `plan(<SLUG>): PRD and ADRs`, body `<why + summary>`. Name that repository explicitly and derive nothing from a remote URL. Reach the host through the server its § Access names, reading that server's own current tool list to build the call: write no command, subcommand - or flag here and name no provider — this Box owns the _what_ and none of the _how_ - ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). If the tracker contract is absent, or the + or flag here and name no provider — this Box owns the _what_ and none of the _how_. If the tracker contract is absent, or the server cannot target a repository explicitly, stop and say which of the two it was rather than opening the PR. diff --git a/apps/claude-code/unic-archon-dlc/commands/tickets.md b/apps/claude-code/unic-archon-dlc/commands/tickets.md index 4708a663..652413f0 100644 --- a/apps/claude-code/unic-archon-dlc/commands/tickets.md +++ b/apps/claude-code/unic-archon-dlc/commands/tickets.md @@ -5,22 +5,19 @@ description: 'Decompose an approved PRD into independently-grabbable vertical-sl # unic-archon-dlc:tickets -> Design rationale: [ADR-0022 — `/tickets` slices a PRD into build-ready issues](docs/adr/0022-tickets-slice-to-build.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); compose-don't-reimplement per [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md); generic-core + config per [ADR-0018](docs/adr/0018-generic-core-config-compose.md); red/green contract per [ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md)). - **Arguments:** "$ARGUMENTS" `/tickets` is the **second main-line box**: it turns one approved PRD into a set of **independently-grabbable vertical tracer-bullet slices**, makes each slice build-ready (a test seam per slice), validates the whole set, publishes the issues to the team's tracker, and stops at the -tickets gate. It is an **in-session command/skill** (slicing is a live conversation — ADR-0017), and +tickets gate. It is an **in-session command/skill** (slicing is a live conversation), and it **owns the _what_** (the slicing flow, the build-ready checks, the DAG of `blocked_by` edges) while **composing the _how_**: the `to-tickets` Method for slicing — read by path, per Step 1 — and the configured **tracker system-skill** (MCP-first, CLI-fallback) for publishing. `/tickets` **stops at a build-ready `issues.json`** (dependency-ordered, each slice carrying its acceptance criteria + test command) plus the published tracker issues. It does **not** generate a -build workflow: `/build` (step 06) consumes `issues.json` directly via a generic loop -([ADR-0022](docs/adr/0022-tickets-slice-to-build.md)). Intent lives on the tracker issue (contract C) +build workflow: `/build` consumes `issues.json` directly via a generic loop. Intent lives on the tracker issue **and** in `issues.json` (the durable baton `/build` reads). Follow these steps in order. Do not skip any step. The only files you write are `issues.json` @@ -31,8 +28,7 @@ the gate in Step 10. `/tickets` reads (never writes) `.archon/unic-dlc.config.yaml`. Read it with your own tools. Do not shell out to Node, do not import a Plugin module, and do not read `$CLAUDE_PLUGIN_ROOT`: an installed -Plugin ships no `node_modules`, and that variable is not set inside the Bash tool -([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The four Archon Boxes read +Plugin ships no `node_modules`, and that variable is not set inside the Bash tool. The four Archon Boxes read their config this way already; this is the same shape. If the file is absent or unreadable, print @@ -57,7 +53,7 @@ print `This repository has no tracker contract at docs/agents/. Run /unic-archon - **Access** — `issue-tracker.md` § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Name no provider and write no - command, subcommand or flag ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). + command, subcommand or flag. - **Addressing** — its § Addressing names the repository this run publishes to and opens its PR against. Name it explicitly in every call, and derive nothing from a remote URL. - **Work-item scope** — its § Work-item scope names the one filter every search applies, and the scope @@ -84,7 +80,7 @@ absent, print that exact path followed by `Run /unic-archon-dlc:setup.` and **st a procedure it cannot read. When it is present, print nothing and continue. That text **is** the slicing procedure — the steps below add only what the Harness owns, and never -restate, summarise or improve a Method ([ADR-0030](docs/adr/0030-harness-hosts-methods.md)). +restate, summarise or improve a Method. ## Step 2 — Slug + re-entry @@ -116,8 +112,9 @@ Three things the Harness adds or overrides on top of it: - **Granularity litmus (the DLC's "thin enough" test):** one slice = **one demoable behaviour** — thin enough that a single failing test can capture it and a minimal implementation can satisfy it, so - strict red/green (contract B) is safe. If a slice needs more than one failing test to express its - behaviour, or bundles two independently-demoable behaviours, split it. If a slice can't be + strict red/green is safe. In strict red/green, separate fresh sessions write the failing test and + the implementation. If a slice needs more than one failing test to express its behaviour, or + bundles two independently-demoable behaviours, split it. If a slice can't be demonstrated without another, wire the dependency instead of merging. The litmus does **not** apply to the Method's **wide-refactor** exception: an expand–contract sequence is not a tracer bullet by design, and green is promised where the Method says it is. @@ -129,8 +126,7 @@ Three things the Harness adds or overrides on top of it: tracker items (Step 9). Its closing "work the frontier one ticket at a time with `/implement`" maps to `/unic-archon-dlc:build`. And its "run `/setup-matt-pocock-skills` if not" fallback never applies — Step 1 read the vocabulary from `docs/agents/triage-labels.md`, which is the file that - skill would overwrite, with a five-role `wontfix` vocabulary that drops every mapping - ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended). + skill would overwrite, with a five-role `wontfix` vocabulary that drops every mapping. Draft each slice with these fields (Step 8 checks them): @@ -189,7 +185,7 @@ fails, propose fixes and re-present; iterate until the user approves (or explici ## Step 7 — Definitive estimation (config-gated) -If `ESTIMATIONS` is `definitive` or `both`, **compose** an estimator (never build one — ADR-0021) to +If `ESTIMATIONS` is `definitive` or `both`, **compose** an estimator (never build one) to attach a **definitive** per-slice estimate, refining any provisional estimate from `/specs`. If `ESTIMATIONS` is `off` or `provisional`, skip this step. @@ -231,7 +227,7 @@ each item can reference the real tracker ids of its blockers. For each slice, build the body from `ISSUE_TEMPLATE` (use `BUG_TEMPLATE` for `type: bug`; fall back to the `to-tickets` Method's issue template if the config template is null). The body MUST carry -the slice's **acceptance criteria** (contract C — intent lives on the tracker item) and its +the slice's **acceptance criteria** (intent lives on the tracker item) and its **Blocked by** references (real tracker ids, or "None — can start immediately"). Write the `ready-for-agent` state role unless the user says otherwise. Do NOT close or modify any parent item. diff --git a/apps/claude-code/unic-archon-dlc/commands/triage.md b/apps/claude-code/unic-archon-dlc/commands/triage.md index 339f0336..720bdf7c 100644 --- a/apps/claude-code/unic-archon-dlc/commands/triage.md +++ b/apps/claude-code/unic-archon-dlc/commands/triage.md @@ -5,14 +5,12 @@ description: "Intake on-ramp: turn a raw bug/request/QA-finding/external-PR into # unic-archon-dlc:triage -> Design rationale: [ADR-0024 — `/triage` is the intake on-ramp](docs/adr/0024-triage-intake-on-ramp.md) (container per [ADR-0017](docs/adr/0017-container-follows-structural-need.md); compose-don't-reimplement per [ADR-0016](docs/adr/0016-dlc-thin-process-layer.md); generic-core + config per [ADR-0018](docs/adr/0018-generic-core-config-compose.md); tracker is the single source of truth per [ADR-0013](docs/adr/0013-tracker-single-source-of-truth.md); earns-its-place per [ADR-0021](docs/adr/0021-earns-its-place-compose-verbatim.md)). - **Arguments:** "$ARGUMENTS" `/triage` is an **intake on-ramp**: it turns RAW incoming work (bug reports, feature requests, QA findings, external PRs) into **agent-ready issues on the team's tracker**, feeding the `/tickets` convergence point. It is an **in-session command/skill** (triage is a live conversation, inherently -HITL — [ADR-0017](docs/adr/0017-container-follows-structural-need.md)). +HITL). It is a **thin binding wrapper**: it **owns the _what_** — the DLC config binding and the on-ramp contract — and **delegates the procedure** to the `triage` Method verbatim, with `grilling` and @@ -26,8 +24,7 @@ and a file, and never an organisation, a field or a provider. `/unic-archon-dlc:setup` owns both files. `setup-matt-pocock-skills` is **not** a dependency of this plugin and must not be run over them: it writes another host's template over the first and reverts the -second to a five-role `wontfix` vocabulary. See -[ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended. +second to a five-role `wontfix` vocabulary. `/triage` writes **only** to the tracker (labels + comments) and, on a rejected enhancement, to the out-of-scope knowledge base. It produces **no** `issues.json` / PRD — a triaged `ready-for-agent` @@ -40,8 +37,7 @@ Follow these steps in order. Do not skip any step. `/triage` reads (never writes) `.archon/unic-dlc.config.yaml`. Read it with your own tools. Do not shell out to Node, do not import a Plugin module, and do not read `$CLAUDE_PLUGIN_ROOT`: an installed Plugin -ships no `node_modules`, and that variable is not set inside the Bash tool -([ADR-0023](docs/adr/0023-build-generic-red-green-refactor-loop.md) §5). The four Archon Boxes read +ships no `node_modules`, and that variable is not set inside the Bash tool. The four Archon Boxes read their config this way already; this is the same shape. If the file is absent or unreadable, print @@ -69,7 +65,7 @@ three is absent, print that exact path followed by `Run /unic-archon-dlc:setup.` cannot run a procedure it cannot read. When all three are present, print nothing and continue. That text **is** the procedure — this wrapper adds only the config binding, and never restates, -summarises or improves a Method ([ADR-0030](docs/adr/0030-harness-hosts-methods.md)). The `triage` +summarises or improves a Method. The `triage` Method's `AGENT-BRIEF.md` and `OUT-OF-SCOPE.md` sit beside its `SKILL.md`, in the same directory. ## Step 2 — Read the tracker contract and build the injected context @@ -81,8 +77,7 @@ do not guess a role value and do not run `setup-matt-pocock-skills`. - **Access** — `issue-tracker.md` § Access names the MCP server or skill that serves this tracker. Read that server's own current tool list and build every call from it. Name no provider and write no - command, subcommand or flag: a flag table written down here is stale the moment the tool changes - ([ADR-0016](docs/adr/0016-dlc-thin-process-layer.md)). + command, subcommand or flag: a flag table written down here is stale the moment the tool changes. - **Addressing** — its § Addressing names the repository to act on. Name it explicitly in every call, and derive nothing from a remote URL. - **Work-item scope** — its § Work-item scope names the one filter every search applies. A search that @@ -121,8 +116,7 @@ calls for grilling, follow the `grilling` and `domain-modeling` Methods. Bind that procedure to the DLC with the Step 2 context, and enforce these overrides: -- **Roles** resolve through `docs/agents/triage-labels.md`, each row naming the axis that carries it - ([ADR-0024](docs/adr/0024-triage-intake-on-ramp.md), amended). Write the value and the axis that file +- **Roles** resolve through `docs/agents/triage-labels.md`, each row naming the axis that carries it. Write the value and the axis that file gives; invent neither. - **`AGENT-BRIEF.md` is written for GitHub** and names `ready-for-agent` and `wontfix` as literal labels. Read those as canonical roles too, resolve them through `triage-labels.md`, and apply the @@ -147,7 +141,7 @@ Bind that procedure to the DLC with the Step 2 context, and enforce these overri Apply the agreed outcome (a human is present — write directly, no PR gate): - **`ready-for-agent`** — post the agent brief comment and write the `ready-for-agent` state role. - The brief is the durable intent baton (contract C — intent lives on the tracker item). + The brief is the durable intent baton (intent lives on the tracker item). - **`ready-for-human`** — agent-brief structure, plus a note on why it can't be delegated. - **`needs-info`** — post the Triage Notes (established / still-needed), write `needs-info`. - **`needs-specs`** — write `needs-specs`; the item needs a PRD before slicing. Point the user to diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0013-tracker-single-source-of-truth.md b/apps/claude-code/unic-archon-dlc/docs/adr/0013-tracker-single-source-of-truth.md index 788543c2..3a8fbd5c 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0013-tracker-single-source-of-truth.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0013-tracker-single-source-of-truth.md @@ -1,6 +1,6 @@ # 0013. Issue tracker is the single source of truth; HANDOFF.md/ROADMAP.md dropped -**Status:** Accepted (2026-06-30) +**Status:** Accepted (2026-06-30, revised 2026-10-21) ## Context @@ -25,3 +25,4 @@ This duplicates state that the **issue tracker already holds authoritatively**. - The `HANDOFF.md` and `ROADMAP.md` vocabulary terms are removed from [`CONTEXT.md`](../../CONTEXT.md); the plugin's `AGENTS.md` doctrine and "Do not add" entries that referenced them are removed/replaced. - Per-thread continuity that `HANDOFF.md` partly served is now covered by the dedicated `/handoff` workflow (Matt's session-bridge; redesign step 02), which writes a **throwaway** per-thread file, not a durable repo snapshot. - "Where are we" queries go to the tracker, not a checked-in file — consistent with the fresh-context principle that all state lives external (tracker + disk), never in conversation memory. +- The DLC's managed-block markers are `unic-dlc:begin` and `unic-dlc:end` since 2026-10-21. The `unic-archon-dlc:begin/end` markers in the Context above are the name they had when this ADR was written. [ADR-0038](0038-practice-packs-stack-by-prefix.md) §5 records why they moved. The block this ADR quotes lived in `ROADMAP.md`, which this ADR retired, so nothing migrates it. `/setup` migrates an old block only in `CLAUDE.md` and in formatter ignore files. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0019-conversational-setup.md b/apps/claude-code/unic-archon-dlc/docs/adr/0019-conversational-setup.md index 693dbe1f..5e874b34 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0019-conversational-setup.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0019-conversational-setup.md @@ -8,12 +8,12 @@ idempotency is three treatments rather than one merge, and it asks no label ques > **Amended (2026-08-25) — three things this ADR says about `/setup` are now false (#383).** > -> - **It asks no label question.** The Consequence below points at [ADR-0024](0024-triage-intake-on-ramp.md)'s -> 2026-08-11 amendment for what `/setup` asks about labels. #389 moved that mapping out of the config +> - **It asks no label question.** The Consequence below points at [ADR-0024](0024-triage-intake-on-ramp.md) +> for what `/setup` asks about labels. #389 moved that mapping out of the config > and into `docs/agents/triage-labels.md`, and #383 made `/setup` write that file — by reading what the > tracker already uses and proposing a value per role, never by offering a default. So the tier-grouped > question, the seeded-default rule and the closed key set describe a config key that no longer exists. -> ADR-0024's own 2026-08-18 amendment already says the key is gone; this one says which Consequence +> ADR-0024 already says the key is gone; this one says which Consequence > here goes with it. > - **Idempotency is three treatments, not one merge.** "A re-run merges, never clobbers" held while the > config was the only thing `/setup` wrote. It now lands four kinds of artefact: a Plugin-owned tree is @@ -61,4 +61,4 @@ The runtime Archon version check moves to a **behavioural min-floor check (`≥ - New config keys (`gates.<box>: hitl|afk`, `build.fresh_context_red_green`, `templates`, `tracker.access`, `docs.type`) are written by the conversational flow, validated by the thin lib. - Idempotency and mandatory-field invariants are preserved by the thin lib, not lost to prose. - Implementation (dissolving the libs, JSON→YAML migration, the version-check fix) is owned by the `/setup` redesign step, not this foundations PR. -- **What `/setup` asks about labels** — the one tier-grouped question, the refusal to inspect a tracker or create a label, the absence of a seeded default, and the closed key set — is recorded in [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-11 amendment, which owns the `classification.labels` contract. +- **What `/setup` asks about labels** — the one tier-grouped question, the refusal to inspect a tracker or create a label, the absence of a seeded default, and the closed key set — was recorded in [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-11 amendment. The 2026-10-21 inline revision of ADR-0024 removed that amendment, because #389 retired `classification.labels`. Git keeps the text. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0023-build-generic-red-green-refactor-loop.md b/apps/claude-code/unic-archon-dlc/docs/adr/0023-build-generic-red-green-refactor-loop.md index e36a94fe..178d1011 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0023-build-generic-red-green-refactor-loop.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0023-build-generic-red-green-refactor-loop.md @@ -1,28 +1,9 @@ # 0023. `/build` is one generic red/green loop over `issues.json` -**Status:** Accepted (2026-07-02); amended 2026-08-20 — §5's rule covers the commands too, not only -the Archon nodes, and the Plugin's `lib/` is deleted (#381). Amended 2026-08-04 — refactor left the loop and moved to -`/pr-review`'s `code-review` Fowler smells, and the loop's procedure is now the `tdd` and `implement` -Methods rather than prose written here (#281). See §7. The filename keeps its original slug; five -sibling documents link to it by name, and renaming it would buy nothing behavioural. - -> **Amended (2026-08-20) — §5's rule covers the commands as well.** -> -> This section exempted the Archon nodes and left the seven `commands/*.md` files importing plugin `lib/` -> through `$CLAUDE_PLUGIN_ROOT`. That exemption was the defect, not the fix. -> -> The same two facts apply to a command: `$CLAUDE_PLUGIN_ROOT` is not set inside the Bash tool either, and -> an installed plugin ships no `node_modules`, so `import 'yaml'` cannot resolve outside this monorepo — -> `"yaml": "catalog:"` is a workspace protocol. Measured on 0.22.0 in a Consumer that installed the -> plugin through the marketplace: all seven commands load and **none runs past Step 1**. -> -> So the rule reads without an exemption. **Nothing this plugin ships imports a plugin module or reads -> `$CLAUDE_PLUGIN_ROOT`**: an Archon `script:` node inlines its logic, and a command reads config, the -> tracker contract and its Methods with its own tools, in prose. `lib/` and `test/` are deleted. -> -> One command keeps a bounded need for the plugin's own directory: `/setup`'s install step copies the Method -> Bundle and the Box YAMLs out of it, so it locates that directory and confirms the path with the operator -> rather than reading a variable. How `/setup` learns its own location is settled by #383. +**Status:** Accepted (2026-07-02, revised 2026-10-28) + +The filename keeps its original slug, although refactoring has left the loop (§7). Five sibling documents +link to it by name, and renaming it would change no behaviour. ## Context @@ -61,30 +42,22 @@ slices in one feature often touch overlapping files, so serial is also collision ### 2. Two fresh phases per slice, state on disk -> **Amended by §7 (#281).** This section is preserved as written, with `refactor-done` and the REFACTOR -> row now historical: the loop advances each slice through TWO phases, and the phase set is -> `pending → red-done → green-done`. The anti-cheat argument below is unchanged and still load-bearing. - A loop iteration is **one fresh session**, so RED and GREEN cannot both live in one iteration without sharing context — the exact cheat [ADR-0012](0012-fresh-context-red-green-separation.md) forbids. -Therefore each slice is advanced by **three separate fresh iterations**: +Therefore each slice is advanced by **two separate fresh iterations**: ``` RED (fresh) ← slice INTENT (acceptance_criteria) → write failing test → prove RED → commit GREEN (fresh) ← slice INTENT + committed test (NOT red's session) → min impl → assert GREEN → commit -REFACTOR (fresh) ← committed impl + green tests as the safety net → clean up → assert still GREEN → commit ``` The baton between phases is **artefacts on disk, never session memory**: the committed failing test is -what GREEN must satisfy; the committed implementation + green suite is what REFACTOR guards. A -`<artifacts_dir>/<slug>/build-state.json` file records each slice's phase (`pending → red-done → -green-done → refactor-done`), so any fresh iteration can compute the next `(slice, phase)` by reading -`issues.json` + `build-state.json`. The loop signals `<promise>COMPLETE</promise>` when every slice is -`refactor-done`. +what GREEN must satisfy. A `<artifacts_dir>/<slug>/build-state.json` file records each slice's phase +(`pending → red-done → green-done`), so any fresh iteration can compute the next `(slice, phase)` by +reading `issues.json` + `build-state.json`. The loop signals `<promise>COMPLETE</promise>` when every +slice is `green-done`. -Refactor is a **separate third fresh phase**, not a tail of GREEN: it keeps every commit single-purpose -and keeps the loop's state machine uniform (one phase per iteration). Refactor writes no tests, so its -fresh context costs no anti-cheat guarantee; it reads the committed impl from disk. +Refactoring is not a phase. §7 records where it went. ### 3. RED proves failure by exit code, inside the RED iteration @@ -119,16 +92,28 @@ own tools). Concretely for `/build`: object ([ADR-0037](0037-config-declares-sdlc-needs.md)), and `bootstrap` also runs the declared install once for the whole run. - **`slopcheck`** is a self-contained `script:` node (`runtime: bun`) that inlines the new-package - registry gate. `lib/slopcheck.mjs` remains the **tested reference** the inline mirrors, but the - shipped node does not import it. + registry gate and imports nothing. + +This convention is binding for the remaining Archon boxes, and for the commands too. **Nothing this +plugin ships imports a plugin module or reads `$CLAUDE_PLUGIN_ROOT`.** The same two facts hold for a +command: `$CLAUDE_PLUGIN_ROOT` is not set inside the Bash tool either, and an installed plugin ships no +`node_modules`, so `import 'yaml'` cannot resolve outside this monorepo, where `"yaml": "catalog:"` is a +workspace protocol. Measured on 0.22.0 in a Consumer that installed the plugin through the marketplace, +all seven commands loaded and none ran past Step 1 while they still imported `lib/`. So an Archon +`script:` node inlines its logic, a command reads config, the tracker contract and its Methods with its +own tools, in prose. #381 deleted the plugin's `lib/` and `test/`. -This convention is binding for the remaining Archon boxes. +One command keeps a bounded need for the plugin's own directory. `/setup`'s install step copies the Method +Bundle and the Box YAMLs out of it, so it locates that directory and confirms the path with the operator +rather than reading a variable. #383 settles how `/setup` learns its own location. ### 6. Gate honoring; reject returns to verification, not a full rebuild The `build-pr-gate` is an `approval:` node with `interactive: true` at the workflow level ([ADR-0011](0011-archon-schema-target.md)). It honours `gates.build` from config: it runs `when` the -gate is `hitl` and is skipped when `afk` (the PR is still opened by the preceding `open-pr` node). On +gate is `hitl` and is skipped when `afk`. The gate depends on `open-pr`, which runs only on a `pass` +verdict, so in `afk` a green build's PR stays open and a build with any other verdict opens none +([ADR-0040](0040-build-verdict-decides-the-pull-request.md)). On **reject**, `approval.on_reject.prompt` re-runs verification-and-fix from the reviewer's feedback and re-pauses at the same gate (`max_attempts: 3`) — it does **not** restart the whole build. @@ -178,7 +163,7 @@ seams, the exact outcome the rule exists to prevent. Injection holds in both mod | Method asks | Injected answer | | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tdd`: confirm the seams with the user before writing any test | Already approved twice — at `/specs` Step 5 (the seam-approval halt, recorded in `PRD.md` § Testing Decisions) and again at `/tickets`' Nyquist-map gate, which is why the slice carries a `test_command`. The node is told the seams **are** confirmed and where to read them. | -| `code-review`: the fixed point, "if they didn't specify one, ask for it" | The branch point of the build branch — `git merge-base origin/<expected_base> HEAD`, with `expected_base` computed by `bootstrap` from `project.branching`. | +| `code-review`: the fixed point, "if they didn't specify one, ask for it" | The branch point of the build branch, `git merge-base origin/$BASE_BRANCH HEAD`. Archon's `$BASE_BRANCH` is the only source of a run's base branch ([ADR-0039](0039-base-branch-is-archons-base-branch.md)). | | `code-review`: "if nothing is found, ask the user where the spec is" | `PRD.md` + `issues.json` for the slug, plus the tracker issue per slice. | | `code-review`, `implement`, `tdd`: run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing | Never. `.archon/unic-dlc.config.yaml` is this Harness's single source of truth, and that skill writes a competing label file ([ADR-0024](0024-triage-intake-on-ramp.md)). | @@ -196,19 +181,18 @@ this tranche exists to grow; an unrecorded choice is the defect, not the choice - The keystone contract B ([ADR-0012](0012-fresh-context-red-green-separation.md)) is **preserved**; only its delivery mechanism moved from per-slug codegen to a generic runtime loop with on-disk state. -- `max_iterations` is a static YAML integer (it cannot be derived from the slice count at author time); - it is set generously (`60` ≈ 20 slices × 3 phases). A pathological over-large `issues.json` would need - the ceiling raised — a known, logged limit, not silent truncation. +- `max_iterations` is a static YAML integer, because it cannot be derived from the slice count at author + time. It is `60`, which covers 30 slices × 2 phases. It was sized for 20 slices × 3 phases while + refactoring was a phase, and was left at 60 when that phase went, since lowering it would only narrow + the margin. An over-large `issues.json` needs the ceiling raised. That is a known, logged limit, not + silent truncation. - The self-contained-script convention (§5) is the pattern the next Archon boxes follow; it is recorded here because `/build` is the first port. - `docs/workflow/<slug>/` artefact paths become `<artifacts_dir>/<slug>/` ([ADR-0015](0015-workflows-slug-artifact-home.md)). -- **Amended 2026-08-04 (#281):** `max_iterations: 60` was sized for 20 slices × 3 phases. With refactor - gone the same ceiling covers 30 slices × 2 phases, so the limit loosened rather than tightened; it is - left at 60 deliberately, since lowering it would only narrow the margin. -- **Amended 2026-08-04 (#281):** the loop's procedure now lives upstream, so an upstream `tdd` or - `implement` edit changes how this Box behaves without a change in this repo. That is the intended - trade of [ADR-0030](0030-harness-hosts-methods.md); the Bundle tag in `lib/methods-manifest.mjs` is the - pin that makes the change deliberate rather than continuous. +- The loop's procedure lives upstream, so an upstream `tdd` or `implement` edit changes how this Box + behaves without a change in this repo. That is the intended trade of + [ADR-0030](0030-harness-hosts-methods.md). The plugin version is the pin that makes the change + deliberate rather than continuous ([ADR-0031](0031-methods-bundled-three-tier-resolution.md)). - Full behavioural validation (gates pause, loop iterates, fresh context isolates) is required beyond `archon validate` ([ADR-0011](0011-archon-schema-target.md) §6), since `validate` passes the inert forms too. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0024-triage-intake-on-ramp.md b/apps/claude-code/unic-archon-dlc/docs/adr/0024-triage-intake-on-ramp.md index 4667cfbb..52493025 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0024-triage-intake-on-ramp.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0024-triage-intake-on-ramp.md @@ -1,129 +1,109 @@ -# 0024. `/triage` is the intake on-ramp; a thin wrapper that binds Matt's method to DLC config as the single source of truth - -**Status:** Accepted (2026-07-02); amended 2026-08-11, 2026-08-13, and 2026-08-18. The 2026-08-18 -amendment is the one to read first: it kills two rules the rest of this ADR still states. - -> **Amended (2026-08-18) — two rules in this ADR are dead.** The tracker contract moved out of -> `.archon/unic-dlc.config.yaml` and into two repo-local prose files, `docs/agents/issue-tracker.md` -> and `docs/agents/triage-labels.md`. A Box now names a role and a file, and never a host word. The -> whole `tracker` block, `classification.labels` and `project.pr_strategy` left the config with it, and -> `MANDATORY_PATHS` is `project.branching` alone. Two things stated below no longer hold: -> -> - **The compose rule is reversed.** This ADR says _"DLC commands feed Matt's methods DLC config; -> Matt's own setup artifacts are never consulted in DLC flows"_, and `/triage` enforced it by -> instructing the Method **not** to read `docs/agents/triage-labels.md` or -> `docs/agents/issue-tracker.md`. Those two files are now the contract, and every Box and command -> reads them. The rule solved a real two-writer problem by forbidding the read; this decision solves -> the same problem by removing one writer — `/unic-archon-dlc:setup` owns both files, and -> `setup-matt-pocock-skills` must never be run over them. -> - **"The tier carries the axis" is disproved.** The 2026-08-11 amendment claimed a Box hands the -> composed skill the tier alongside the string, so the skill knows whether to write a state, a status -> or a label. Measured against a live tenant, five of the eight state roles cannot be states at all: -> writing a state while the work is still open moves an already-active item backwards on the board, so -> only the three terminal roles are states there and the rest are tags. **The axis belongs to the -> role, not to the tier**, which is why `triage-labels.md` names an axis per row. -> -> What survives unchanged: `/triage` is a thin binding wrapper, the container decision, the -> label-taxonomy mapping between Matt's five roles and the DLC's seventeen, best-effort verification, -> and the on-ramp contract. What the wrapper _binds_ changed; that it binds did not. -> -> The amendments below are kept as the record of how the config-shaped answer was reached. Read them -> as history, not as instructions. - -> **Amended (2026-08-11, extended 2026-08-13):** this ADR ended its label section with "Teams override `classification.labels` in YAML" and never said who fills the mapping. So `defaultConfig()` seeded one and `/setup` never raised the subject, shipping a guess that is correct only for a tracker already using the Plugin's own vocabulary (#329). Seven decisions complete the sentence — the first five recorded on 2026-08-11, the last two on 2026-08-13 when #329 was implemented and an audit found them recorded on the issue alone. -> -> - **The team names the Label strings, and `/setup` asks.** One question, the seventeen roles grouped by tier — `state`, `type`, `priority` — with a one-line gloss per tier, offering the names the Plugin ships. The gloss is conversation copy in `commands/setup.md`, never data in `lib/`. This is the shape `setup-matt-pocock-skills` uses for its own five roles: one question, ships no code. -> - **`/setup` never inspects the tracker and never creates a label.** It does not probe for existing labels, does not report which are absent, and does not offer to add one. A tracker with a different vocabulary is answered by mapping the role onto a string that tracker already carries, not by adding a seventeenth label to someone else's board. -> - **The tier carries the axis.** A Box hands the composed tracker skill the tier alongside the string, so the skill knows whether to write an Azure DevOps `System.State`, a Jira status or a GitHub label. No Box ever learns how the tracker stores a role. This is what keeps the mapping tracker-agnostic while it stays a plain `Record<role, string>`, and it is why the label-shaped model needs no generalisation. -> - **No default is seeded.** `defaultConfig()` emits no `classification.labels`, and `classification.labels` joins `MANDATORY_PATHS`. A mapping is therefore present because a human accepted it, or absent and collected on the next run. The names the Plugin ships are what `/setup` offers, never what it writes on the team's behalf. -> - **The key set is closed downward.** A `classification.labels` missing a shipped Canonical role is a fault: `validateConfig` reports it, so the config reads `partial` and `/setup` collects it, and `toYaml` refuses to write it. An extra key is allowed and ignored — nothing reads it, and `migrateLegacy` preserves a hand-added type such as `release` on purpose. The team owns the right-hand column and never the left. -> - **No drift mechanism, and no migration for an already-seeded config** _(2026-08-13)_. `classification.labels` gets no `sha256` entry in `.archon/unic-dlc.install.json`: #295 draws the line at generated files, and the config is hand-editable by design, so hashing a file the operator is invited to edit inverts that. A config an older `/setup` wrote therefore keeps the seeded map on disk, present and indistinguishable from an accepted answer. That boundary is accepted rather than migrated — treating an identity map as unanswered would misfire on every team that genuinely accepted identity, and stripping it would discard a mapping a team may have hand-edited. New installs are clean, existing ones keep their map, and the CHANGELOG names `/unic-archon-dlc:setup reconfigure` as the way to review it. -> - **The written YAML carries no comments** _(2026-08-13)_. `toYaml` stays a single `stringifyYaml(config)` call. Per-tier comments would need a `yaml` `Document`, because `mergeConfig` works on plain objects and drops comments on a re-run — so they would have to be re-stamped on every write, which fights an operator who adds their own and churns the diff. The config is meant to be hand-edited; a generator that rewrites its comments is friction. The explanation lives in the `CLAUDE.md` marker block instead, which is the one surface `/setup` installs into a Consumer. -> -> Recorded because it surprised the session that produced this amendment: **no Box reads a state Label string today.** `/triage`, `/tickets` and `/qa` write states; nothing selects work by label, and the handoff between Boxes is the Slug a human passes. Key-closure still earns its place, because a missing key breaks the **writer** — `/triage` resolving `needs-specs` through `LABELS` either fails mid-run or invents a string, which is #329's failure mode. Whether the reads should exist is #332. - -> **Amended (2026-08-13):** the known item under § Consequences — "the block-ownership overlap remains for a later `/setup`/finalize step" — is settled. #296 is that step: `/setup` renames the block it writes in a Consumer's `CLAUDE.md` from `## Agent skills` to `## unic-archon-dlc`, so the two installers no longer manage a heading of the same name and nothing in the block is a registered skill (the Methods this Plugin ships are read by path — [ADR-0031](0031-methods-bundled-three-tier-resolution.md)). The `<!-- unic-archon-dlc:begin -->` / `<!-- unic-archon-dlc:end -->` markers are unchanged, so the rename costs an existing Consumer nothing. The same issue drops the "auto-managed" framing (nothing detects a hand-edit between the markers, and nothing will) and rewrites the block to name no Box and no pipeline stage: it carries paths on the Consumer's own disk, `archon workflow list` to enumerate what is installed there, and a link to this Plugin's README. The § Consequences bullet below stands as written on 2026-07-02. +# 0024. `/triage` is the intake on-ramp; a thin wrapper that binds Matt's method to the tracker contract in `docs/agents/` + +**Status:** Accepted (2026-07-02, revised 2026-10-21) ## Context The shipped `triage` workflow produced a `HANDOFF.md` state snapshot and updated a `ROADMAP.md`. -[ADR-0013](0013-tracker-single-source-of-truth.md) retired that role — the tracker is the single -source of truth for "where are we", and no workflow writes durable repo-state snapshots. The box set +[ADR-0013](0013-tracker-single-source-of-truth.md) retired that role. The tracker is the single source of +truth for "where are we", and no workflow writes durable repo-state snapshots. The box set ([ADR-0014](0014-workflow-per-box-decomposition.md)) then gave `/triage` a **new** meaning: an **intake on-ramp** that turns raw incoming work (bug reports, feature requests, QA findings, external PRs) into agent-ready issues feeding the `/tickets` convergence point. Two questions surfaced when implementing the box: -1. **Container + earns-its-place.** Triage is a live conversation (categorise, verify, grill), so per +1. **Container and earns-its-place.** Triage is a live conversation (categorise, verify, grill), so per [ADR-0017](0017-container-follows-structural-need.md) it is a command/skill, not an Archon workflow, and it is inherently HITL. But Matt Pocock's `triage` skill already carries the full - method (gather → recommend → verify → grill → apply, with agent briefs and the `.out-of-scope/` - KB). Per [ADR-0021](0021-earns-its-place-compose-verbatim.md) a box ships only if it adds value - over the raw composed skill — so what does a DLC `/triage` add? + method (gather, recommend, verify, grill, apply, with agent briefs and the `.out-of-scope/` KB). Per + [ADR-0021](0021-earns-its-place-compose-verbatim.md) a box ships only if it adds value over the raw + composed skill. So what does a DLC `/triage` add? 2. **Label drift between two setups.** Matt's skills assume `setup-matt-pocock-skills` ran and wrote - `docs/agents/triage-labels.md` (five canonical roles) and `docs/agents/issue-tracker.md`. The - DLC's `/setup` writes `classification.labels` (the plugin's eight-state taxonomy) in - `.archon/unic-dlc.config.yaml`, which `/tickets` and `/build` already read. If both setups exist, - the two label sources diverge and a triaged issue can carry a label the downstream boxes don't - recognise. + `docs/agents/triage-labels.md` (five canonical roles) and `docs/agents/issue-tracker.md`. If a second + writer keeps its own label map, the two label sources diverge and a triaged issue can carry a label + the downstream boxes don't recognise. + +The first answer, on 2026-07-02, put the label map in `.archon/unic-dlc.config.yaml` as +`classification.labels` and told Matt's method **not** to read the two `docs/agents/` files. That solved +the two-writer problem by forbidding the read. A measurement against a live tenant showed that it +failed. The tier alone could not describe a role's value, because five of the eight state roles there cannot +be states. +Writing a state while the work is still open moves an already-active item backwards on the board, so only +the three terminal roles are states there and the rest are tags. On 2026-08-18 (#389) the tracker contract +moved out of the config and into the two `docs/agents/` files. The whole `tracker` block, +`classification.labels` and `project.pr_strategy` left the config with it, and `project.branching` is the +only key a config needs. ## Decision -`/triage` ships as a **thin binding wrapper** ([ADR-0016](0016-dlc-thin-process-layer.md)): it **owns -the _what_** (the DLC config binding + the on-ramp contract) and **delegates the _method_** to Matt's -`triage` skill verbatim (composing `/grilling` + `/domain-modeling` for the grill step). It does not -re-narrate Matt's steps. - -**Single source of truth, injected at compose time.** Matt's `triage` states the label mapping -_"should have been provided to you"_ — it need not come from Matt's file. So the wrapper loads -`classification.labels` (+ `tracker`, `triage.out_of_scope_dir`, `triage.external_prs`) from -`.archon/unic-dlc.config.yaml` and **hands the mapping to Matt's method inline, instructing it NOT to -read `docs/agents/triage-labels.md` / `docs/agents/issue-tracker.md`**. This is the general -**compose rule**: DLC commands feed Matt's methods DLC config; Matt's own setup artifacts are never -consulted in DLC flows. Consequences: - -- `.archon/unic-dlc.config.yaml` is the **single source of truth** for labels; there is no second - file to keep in sync, and `/triage` speaks the same vocabulary `/tickets` + `/build` read. -- **`setup-matt-pocock-skills` is not a dependency of this plugin** — only Matt's skill _methods_ - are. `/setup` already only _verifies the suite is present_ ([ADR-0019](0019-conversational-setup.md), - verify-only); it never needs Matt's config. The binding is exactly what `/triage` adds over the raw - skill — the same category of value that justifies shipping `/specs` and `/tickets`. - -**Label taxonomy binding.** The DLC eight-state taxonomy is a superset of Matt's five roles. The -wrapper maps: category `bug`→type `bug`, `enhancement`→type `feature`; states `needs-triage`, -`needs-info`, `ready-for-agent`, `ready-for-human` map through unchanged; Matt's `wontfix` splits -into `rejected` (a rejected enhancement → also writes the `.out-of-scope/` record) and `closed` (an -already-implemented request → point to the code, no KB write); and the DLC adds `needs-specs` — a -raw-but-valid idea that routes to `/specs`. Teams override `classification.labels` in YAML. +`/triage` ships as a **thin binding wrapper** ([ADR-0016](0016-dlc-thin-process-layer.md)). It **owns +the _what_**, the tracker contract binding and the on-ramp contract, and **delegates the _method_** to +Matt's `triage` skill verbatim, composing `/grilling` and `/domain-modeling` for the grill step. It does not +re-narrate Matt's steps. What the wrapper binds changed on 2026-08-18; that it binds did not. + +**The tracker contract is two repo-local files, and one writer owns them.** `docs/agents/issue-tracker.md` +and `docs/agents/triage-labels.md` carry every tracker fact. Every Box and command, `/triage` included, +reads them, and a Box names a role and a file, never a host word. The two-writer problem is solved by +removing one writer: `/unic-archon-dlc:setup` writes both files, and `setup-matt-pocock-skills` must never +be run over them. **`setup-matt-pocock-skills` is not a dependency of this plugin**, only Matt's skill +_methods_ are. + +**The axis belongs to the role, not to the tier.** `triage-labels.md` names, per row, the role's value and +the axis that carries it. A tier does not decide whether a role is a state, a tag or a label. + +**The team names the values, and `/setup` creates nothing on the tracker.** `/setup` proposes a value for +each canonical role from what the board already uses, and writes what the operator confirms. A role the +board has no name for is mapped onto a name it already carries, or left unmapped. It is never answered by +adding a label to someone else's board. The role names in the left column are this plugin's; the team owns +every other column and never the role set. On 2026-08-13 no Box read a state label string: `/triage`, +`/tickets` and `/qa` wrote states, and nothing selected work by label. + +**Label taxonomy binding.** The DLC taxonomy is a superset of Matt's five roles. The wrapper maps: category +`bug` to type `bug`, `enhancement` to type `feature`; states `needs-triage`, `needs-info`, +`ready-for-agent` and `ready-for-human` map through unchanged; Matt's `wontfix` splits into `rejected` (a +rejected enhancement, which also writes the `.out-of-scope/` record) and `closed` (an already-implemented +request, which points to the code and writes no KB entry); and the DLC adds `needs-specs`, a raw but valid +idea that routes to `/specs`. Teams set each role's value in `triage-labels.md`. **Verification is best-effort, always; no config knob** ([ADR-0021](0021-earns-its-place-compose-verbatim.md)). -Triage is inherently HITL, so there is no true AFK case — a human is present at every transition. -The method reproduces a bug / checks out a PR when it can; when it cannot reproduce or no runner -exists, it downgrades to `needs-info` with specific questions rather than guessing. - -**On-ramp contract.** `/triage` writes only to the tracker (labels + comments) and, on a rejected -enhancement, to `triage.out_of_scope_dir`. Writes go **directly** (human present → no PR gate). It -produces **no** `issues.json` / PRD. A `ready-for-agent` item enters the `/tickets` backlog (the -convergence point — sliced there) or `/build` directly if already atomic. Where the pre-two-axis step -body said triage output is "consumed by `/build` exactly like `/tickets` output", **PLAN.md wins**: -on-ramps feed `/tickets` (decision #8). +Triage is inherently HITL, so there is no true AFK case: a human is present at every transition. The method +reproduces a bug or checks out a PR when it can. When it cannot reproduce, or no runner exists, it +downgrades to `needs-info` with specific questions rather than guessing. + +**On-ramp contract.** `/triage` writes only to the tracker (labels and comments) and, on a rejected +enhancement, to `triage.out_of_scope_dir`. Writes go **directly** (human present, so no PR gate). It +produces **no** `issues.json` or PRD. A `ready-for-agent` item enters the `/tickets` backlog (the +convergence point, sliced there) or `/build` directly if already atomic. Where the pre-two-axis step body +said triage output is "consumed by `/build` exactly like `/tickets` output", **PLAN.md wins**: on-ramps +feed `/tickets` (decision #8). + +**The `CLAUDE.md` block is this plugin's alone.** `/setup` writes its block in a Consumer's `CLAUDE.md` +under the heading `## unic-archon-dlc`, not `## Agent skills`, so `/setup` and `setup-matt-pocock-skills` +no longer manage a heading of the same name (#296). Nothing in the block is a registered skill: the Methods +this plugin ships are read by path ([ADR-0031](0031-methods-bundled-three-tier-resolution.md)). The block +lies between `<!-- unic-dlc:begin -->` and `<!-- unic-dlc:end -->`, because every installer that writes into +a Consumer owns only the names that carry its own prefix, and `unic-dlc` is the DLC's +([ADR-0038](0038-practice-packs-stack-by-prefix.md) §5). Before 0.30.0 the markers were +`<!-- unic-archon-dlc:begin -->` and `<!-- unic-archon-dlc:end -->`. `/setup` finds a block under the old +markers and rewrites it in place under the new ones, so a re-run leaves one block, never two. The block +names no Box and no pipeline stage. It carries paths on the Consumer's own disk, `archon workflow list` to +enumerate what is installed there, and a link to this plugin's README. It never presents itself as +auto-managed, because nothing detects a hand edit between the markers. ## Consequences -- **New `commands/triage.md`** — the thin wrapper (config load → injected context → delegate to - Matt's method with the single-source override → enforce the on-ramp contract). -- **New `triage` config block** — `{ out_of_scope_dir: '.out-of-scope', external_prs: 'auto' }` in - `defaultConfig()`. This is the DLC-config home for the two knobs Matt's setup would otherwise write - to `docs/agents/*` (the out-of-scope location; whether external PRs are a request surface). - `mergeConfig` auto-fills them for existing configs — no `/setup` change this step. -- **The old `unic-dlc-triage` Archon workflow + command stub are deleted** — the state-snapshot role - is retired ([ADR-0013](0013-tracker-single-source-of-truth.md)) and it used the inert `type:`-style +- **New `commands/triage.md`**: the thin wrapper. It loads the config and the tracker contract, delegates + to Matt's method, and enforces the on-ramp contract. +- **New `triage` config block**: `out_of_scope_dir` (default `.out-of-scope`) and `external_prs` (default + `auto`). The § Configuration reference in the README holds the defaults. This is the DLC-config home for + the two knobs Matt's setup would otherwise write to `docs/agents/*`. +- **The old `unic-dlc-triage` Archon workflow and command stub are deleted.** The state-snapshot role + is retired ([ADR-0013](0013-tracker-single-source-of-truth.md)), and it used the inert `type:`-style schema ([ADR-0011](0011-archon-schema-target.md)). -- **`unic-dlc-cleanup` is de-referenced** — its dangling terminal `run-triage` node (which invoked - `archon workflow run unic-dlc-triage` to refresh HANDOFF/ROADMAP) is removed. This is a surgical +- **`unic-dlc-cleanup` is de-referenced.** Its dangling terminal `run-triage` node, which invoked + `archon workflow run unic-dlc-triage` to refresh HANDOFF and ROADMAP, is removed. This is a surgical reference cleanup only; the full `/cleanup` redesign is step 11. -- **Known item, not fixed here:** both `/setup` and `setup-matt-pocock-skills` manage an - `## Agent skills` block in the consumer's `CLAUDE.md`/`AGENTS.md`. The single-source rule removes - the _label_ collision, but the block-ownership overlap remains for a later `/setup`/finalize step. +- **The block-ownership overlap is settled** (#296): the two installers no longer share the + `## Agent skills` heading. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0028-cleanup-operational-janitor.md b/apps/claude-code/unic-archon-dlc/docs/adr/0028-cleanup-operational-janitor.md index c63d1f76..854519af 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0028-cleanup-operational-janitor.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0028-cleanup-operational-janitor.md @@ -6,7 +6,7 @@ > or names the tracker through `tracker.access` / `tracker.coords`, read > `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` instead. Those two repo-local files > are the tracker contract now, and each role carries its own axis. See -> [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-18 amendment (#389). +> [ADR-0024](0024-triage-intake-on-ramp.md) (#389). ## Context diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0029-explore-research-spike-onramp.md b/apps/claude-code/unic-archon-dlc/docs/adr/0029-explore-research-spike-onramp.md index 4137b4da..704c92fd 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0029-explore-research-spike-onramp.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0029-explore-research-spike-onramp.md @@ -8,7 +8,7 @@ the note below. > or names the tracker through `tracker.access` / `tracker.coords`, read > `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` instead. Those two repo-local files > are the tracker contract now, and each role carries its own axis. See -> [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-18 amendment (#389). +> [ADR-0024](0024-triage-intake-on-ramp.md) (#389). ## Context diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0031-methods-bundled-three-tier-resolution.md b/apps/claude-code/unic-archon-dlc/docs/adr/0031-methods-bundled-three-tier-resolution.md index d39ac2bd..718c8e1a 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0031-methods-bundled-three-tier-resolution.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0031-methods-bundled-three-tier-resolution.md @@ -1,41 +1,9 @@ -# 0031. Methods are bundled in the plugin; the plugin version is the pin; resolution is three-tier - -**Status:** Accepted (2026-08-03); amended 2026-08-20 — two of the three tiers are retired and §5's -integrity mechanism is gone, so §2, §3 and §5 record what was rather than what is (#381). The title -keeps the word three-tier because an ADR filename is a stable address, not a description. - -> **Amended (2026-08-20) — one tier, and integrity is checked by reading.** -> -> The plugin went to zero code, so the two mechanisms this ADR named as modules went with it. -> -> **Resolution is one tier.** Every Box and every command reads a Method at the literal path -> `.archon/methods/<name>/SKILL.md`. The `config` tier (`methods.<name>.source`) and the `local` tier -> (`.archon/methods.local/<name>/SKILL.md`) are retired, with them the resolution order, the tier line a -> command printed, and the `forked_from` frontmatter convention. §2 and §3 record what was, not what is. -> -> The reason is the one §2 gave for having tiers at all, read back: an Archon node cannot import plugin -> `lib/`, so `resolveMethod` never reached inside a Box, and the Archon Boxes had been reading the single -> literal path since they shipped. The tiers therefore existed only in the command half — a resolution -> order two of the seven surfaces could not honour. Measured against a live Consumer on 0.22.0, the -> command half could not run at all: `$CLAUDE_PLUGIN_ROOT` is unset inside the Bash tool and an installed -> plugin ships no `node_modules`, so every Step 1 halted before resolving anything. Deleting the tiers -> made the two halves agree, which is what §2's own constraint had been asking for. -> -> A team that wants different Method text edits `.archon/methods/<name>/SKILL.md` in its own repository -> and expects the next `/setup` run to overwrite it. That is a real loss of an override that survived -> upgrades; it is accepted because nothing ever declared one. -> -> **Integrity is checked by reading, not by hashing.** §5's `verifyLicence` and `verifyBundle` are gone -> with `lib/methods-bundle.mjs`, and so is the manifest they compared against. `/setup`'s install step now reads -> the bundle: every Method directory must carry its `SKILL.md` and the companion files that Method reads, -> and `LICENSE` must be present. Either failure still stops setup. -> -> What this trades away is worth naming plainly. The pinned licence hash and the `upstreamPath` closure -> check were the only automated tripwire for an upstream rename wave — the failure this ADR's §1 exists -> to prevent. Nothing replaces it inside this repository, because nothing here can see upstream. A rename -> wave is now found by running a Box against a live Consumer, which is where every command defect of the -> 0.22.0 line was found. The upstream repository, tag and commit stay recorded in -> `vendor/mattpocock-skills/README.md`. +# 0031. Methods are bundled in the plugin; the plugin version is the pin; resolution is one path + +**Status:** Accepted (2026-08-03, revised 2026-10-21) + +The filename keeps the word three-tier because an ADR filename is a stable address. Resolution had three +tiers until #381 retired two of them on 2026-08-20. ## Context @@ -56,51 +24,55 @@ version-pinning scheme that routes through that CLI is unpinned in practice. Grilled with the maintainer on 2026-07-31 (see #279); the mechanism was pinned before implementation in #284. +The first version of this ADR resolved a Method through three tiers, in a module, `resolveMethod`: a +`config` tier (`methods.<name>.source` in the config), a `local` tier (`.archon/methods.local/<name>/SKILL.md`) +and the bundle. A Local Method declared the bundle version it forked from in `forked_from` frontmatter. Two +modules, `verifyLicence` and `verifyBundle`, checked the bundle against a pinned licence hash and a manifest +of every file. The plugin went to zero code in #381, and those modules went with it. The tiers had only ever +worked in the command half: an Archon node cannot import plugin `lib/`, so `resolveMethod` never reached +inside a Box, and the Archon Boxes read the single literal path from the day they shipped. Measured against +a live Consumer on 0.22.0, the command half could not run at all. `$CLAUDE_PLUGIN_ROOT` is unset inside the +Bash tool and an installed plugin ships no `node_modules`, so every Step 1 halted before resolving anything. + +Practice Packs now stack in one Consumer beside the DLC +([ADR-0038](0038-practice-packs-stack-by-prefix.md)). That raised again whether anything other than the DLC +may write a Method. + ## Decision ### 1. The Methods are vendored in the plugin, and the plugin version is the pin -`vendor/mattpocock-skills/` holds the upstream files at a pinned tag, mirroring upstream's -`skills/<category>/<name>/` layout so each manifest entry's `upstreamPath` is load-bearing — an -upstream relocation fails a test instead of silently resolving nothing. Only the manifest's transitive -closure is vendored, sub-files included. - -Provenance is the frozen `METHODS_BUNDLE` constant in `lib/methods-manifest.mjs` (repo, tag, commit, -licence, licence sha256), not a file inside the bundle: `/setup` needs the tag and hash -programmatically, a constant typechecks and has no parse-failure branch, and keeping it beside the -manifest forces a re-vendor to touch the file that defines the closure. `vendor/mattpocock-skills/README.md` -is its human mirror and a test asserts the two agree. +`vendor/mattpocock-skills/` holds the upstream files at a pinned tag, in upstream's +`skills/<category>/<name>/` layout. `vendor/mattpocock-skills/README.md` records the upstream repository, tag +and commit. **There is no `skills.pin` config key.** The plugin version _is_ the pin: upgrading Methods means upgrading the plugin and re-running `/setup`. A separate pin would let a Consumer's config and the shipped bundle disagree, with no way to detect it. -Fetch with `git clone --depth 1 --branch <tag>`, never `npx skills` — see the Context above. - -### 2. Three-tier resolution, first hit wins +Fetch with `git clone --depth 1 --branch <tag>`, never `npx skills`. See the Context above. -`resolveMethod(name, …)` in `lib/methods-resolver.mjs` answers with the `SKILL.md` to read and the -tier that answered: +### 2. A Method resolves at one path -1. **`config`** — `methods.<name>.source` in `.archon/unic-dlc.config.yaml`: the team's own fork. -2. **`local`** — `.archon/methods.local/<name>/SKILL.md`: an uncommitted working override. -3. **`bundle`** — `.archon/methods/<name>/SKILL.md`: the vendored default, written by `/setup`. +Every Box and every command reads a Method at the literal path `.archon/methods/<name>/SKILL.md`. There is +no resolution order and no tier line to print. The `config` tier, the `local` tier and the `forked_from` +convention are retired. `/setup` reports a `.archon/methods.local/` directory or a `methods.<name>.source` +key it finds as retired, and changes neither. -The config tier is authoritative **on declaration alone**. If an operator names a path and the file is -missing, resolution fails rather than falling through to the bundle — silently ignoring a declared -override is the failure class this whole design exists to remove. +A team that wants different Method text edits `.archon/methods/<name>/SKILL.md` in its own repository and +expects the next `/setup` run to overwrite it. That loses an override that used to survive upgrades. It is +accepted because no Consumer ever declared one. -A Local Method declares which Bundle version it forked from in its own frontmatter -(`forked_from: v1.1.0`), not in config: committed metadata describing an uncommitted file drifts the -moment someone edits the override. A missing `forked_from` is flagged, not skipped. +### 3. The DLC alone writes `.archon/methods/`, and a Practice Pack never ships a Method -### 3. Repo-relative paths only +A Practice Pack ships context artefacts and config values only +([ADR-0038](0038-practice-packs-stack-by-prefix.md) §1 and §2). So `/setup`'s wholesale replace of the tree +removes nothing a pack wrote. A skill a pack installs under `.claude/skills/` is a context artefact, not a +Method, even when a node names it in `skills:` (ADR-0038 §3). The retired tiers do not come back as a way +for a pack to change a Method. -Every resolved path must sit inside the Consumer repository. Absolute paths, Windows drive letters, -UNC prefixes, `~` home references, and `../` or `..\` escapes are all rejected — the last two after -normalising backslashes, so a Windows-style escape is caught on POSIX too. A Method is content the -repository vouches for; reading procedure from outside it would make a Box's behaviour depend on the -operator's home directory. +A pack that must replace a Box step uses an `include:` slot, decided and not built. ADR-0038 §7 records it, +with the rule that one slot holds one occupant and that a collision never resolves silently. ### 4. Methods are read by path and never registered as skills @@ -109,45 +81,47 @@ A Box reads a Method as a file. The plugin never installs Methods into `.claude/ Two reasons. A Consumer running Matt's own Claude Code plugin would otherwise end up with **every skill twice**, with no way to tell which copy answered. And 7 of the 10 Methods carried -`disable-model-invocation: true` at v1.0 while `prototype` flipped back at v1.1 — invocation is a -churning coupling surface, path-reading is not. +`disable-model-invocation: true` at v1.0 while `prototype` flipped back at v1.1, so invocation is a +coupling that keeps changing, and reading a path is not. -### 5. Integrity is checked, not assumed +### 5. Integrity is checked by reading -`/setup` runs `verifyLicence` (the vendored `LICENSE` hashed against the pinned tag's) and -`verifyBundle` (every file each manifest entry declares — its `SKILL.md` **and** its `subFiles`) -before installing anything, and stops on either failure: both mean the shipped plugin is incomplete or -altered, which no Consumer action fixes. A missing `LICENSE` asks the maintainer to restore it and is -never auto-created, per the repository's LICENSE policy. +`/setup`'s install step reads the bundle before it installs anything. Every Method directory must carry its +`SKILL.md` and the companion files that Method reads, and `LICENSE` must be present. Either failure stops +setup: both mean the shipped plugin is incomplete, which no Consumer action fixes. A missing `LICENSE` asks +the maintainer to restore it and is never created automatically, per the repository's LICENSE policy. -`installMethods` clean-replaces `.archon/methods/` rather than merging, so a Method dropped from a -later manifest cannot linger on disk where the bundle tier would keep resolving it. It never reads or -writes `.archon/methods.local/`. +`/setup` replaces `.archon/methods/` wholesale rather than merging, so a Method dropped from a later bundle +cannot linger on disk. + +The pinned licence hash and the manifest closure check were the only automated check for an upstream rename +wave, the failure §1 exists to prevent. Nothing replaces them inside this repository, because nothing here +can see upstream. A rename wave is now found by running a Box against a live Consumer, which is where every +command defect of the 0.22.0 line was found. ## Considered options -**Per-box mapping — rejected.** Each Box names its own Method path in its own prose or its own config +**Per-box mapping, rejected.** Each Box names its own Method path in its own prose or its own config block. This is the pre-#279 status quo: the same name in three files with nothing tying them together, -which is precisely what let the v1.1.0 rename wave break `/specs` and `/tickets` with CI green -([ADR-0030](0030-harness-hosts-methods.md) Context). It also makes the dependency list unanswerable — +which is what let the v1.1.0 rename wave break `/specs` and `/tickets` with CI green +([ADR-0030](0030-harness-hosts-methods.md) Context). It also makes the dependency list unanswerable: you would have to read every Box to learn what the plugin depends on. Recorded here because it is the obvious-looking design and will otherwise be proposed again. -**Fetching Methods at run time — rejected.** A Box could clone or download the skill at the pinned tag +**Fetching Methods at run time, rejected.** A Box could clone or download the skill at the pinned tag on demand. That adds a network dependency to every AFK run, breaks reproducibility for a Consumer behind a proxy, and reintroduces the CLI's silent-`main` trap if it routes through `npx skills`. -**A `skills.pin` config key — rejected.** See §1: it lets config and the shipped bundle disagree +**A `skills.pin` config key, rejected.** See §1: it lets config and the shipped bundle disagree undetectably. +**Three-tier resolution, retired on 2026-08-20 (#381).** See the Context. It existed only in the command +half, so deleting it made the commands and the Boxes agree. + ## Consequences -- Upgrading Methods is a plugin upgrade plus a `/setup` re-run. `/setup`'s install step is idempotent and runs - even for an already-configured project, so an upgrade lands the new bundle without a reconfigure. -- A team fork costs one config line (`methods.<name>.source`) or one uncommitted directory. Neither - requires a plugin change. -- `.archon/methods/` is generated. It is committed because Archon nodes need it (see Context), but it - must never be hand-edited — `/setup` clean-replaces it. -- The closure test reads the real bundle rather than a pinned test copy. Two copies of the same - upstream text would let a bumped bundle keep passing against stale fixtures, which is the drift the - test exists to catch. +- Upgrading Methods is a plugin upgrade plus a `/setup` re-run. `/setup`'s install step runs even for an + already-configured project, so an upgrade lands the new bundle without a reconfigure. +- `.archon/methods/` is generated. It is committed because Archon nodes need it (see Context), and `/setup` + replaces it wholesale, so a hand edit lasts until the next run. +- A team fork of a Method costs an edit that the next `/setup` overwrites. No override survives an upgrade. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0032-box-method-vocabulary.md b/apps/claude-code/unic-archon-dlc/docs/adr/0032-box-method-vocabulary.md index f3ae9539..72bdbcfb 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0032-box-method-vocabulary.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0032-box-method-vocabulary.md @@ -7,13 +7,12 @@ vocabulary (#381); amended 2026-08-18 — see the notes below. > or names the tracker through `tracker.access` / `tracker.coords`, read > `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` instead. Those two repo-local files > are the tracker contract now, and each role carries its own axis. See -> [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-18 amendment (#389). +> [ADR-0024](0024-triage-intake-on-ramp.md) (#389). > **Amended (2026-08-20) — `Local Method` is retired.** The term named the middle tier of Method > resolution, `.archon/methods.local/<name>/SKILL.md`. #381 deleted the Plugin's code, and with it both > override tiers: every Box and every command now reads a Method at the one literal path -> `.archon/methods/<name>/SKILL.md` ([ADR-0031](0031-methods-bundled-three-tier-resolution.md), -> amended). So the definition below describes a path nothing reads, and the term is gone from +> `.archon/methods/<name>/SKILL.md` ([ADR-0031](0031-methods-bundled-three-tier-resolution.md)). So the definition below describes a path nothing reads, and the term is gone from > [`CONTEXT.md`](../../CONTEXT.md) rather than kept as a word with no referent. The title keeps it: an > ADR filename is a stable address, and four documents link to this one by name. To change a Method a > team edits the installed file and expects the next `/setup` run to overwrite it — the fork that diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0033-archon-070-schema-target.md b/apps/claude-code/unic-archon-dlc/docs/adr/0033-archon-070-schema-target.md index 84d3660e..0d801d02 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0033-archon-070-schema-target.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0033-archon-070-schema-target.md @@ -1,27 +1,6 @@ # 0033. Archon 0.7.0 schema target — floor bump, `always_run`, sub-runs deferred -**Status:** Accepted (2026-08-10); amended 2026-08-18 — see the note below; amended 2026-08-24 — `worktree.remote` measured on 0.7.0: read, and base-branch resolution only (#396). Amends [ADR-0011](0011-archon-schema-target.md) on the version floor only — its key-discriminated schema conventions stand unchanged. - -> **Amended (2026-08-18):** § "Repository derivation: settled by #289, not reopened here" describes a -> mechanism that no longer exists. This Harness derives no repository from a remote: each Box reads the -> one `docs/agents/issue-tracker.md` § Addressing names, so there is nothing left to diverge from -> Archon's own `worktree.remote` resolution. `/archon-upgrade` still classifies that algorithm -> VERIFY-ONLY, on the new reason. See [ADR-0024](0024-triage-intake-on-ramp.md)'s 2026-08-18 -> amendment (#389). - -> **Amended (2026-08-24):** § "Where Archon's own remote resolution differs from this Harness's -> derivation" described `worktree.remote` as present in 0.7.0 without anyone having measured it. It is -> measured now, by the behavioural probe `/archon-upgrade` Step 6 carries -> ([#396](https://github.com/unic/unic-agents-plugins/issues/396)): on Archon 0.7.0 the key is **read** -> — set to a second remote's name, the binary's own startup error names that remote as the one it -> resolved the base branch against, where the control run naming no key says `origin`. Two limits on -> that result. It belongs to 0.7.0 and to no other release, which is why the key is a claim Step 6 -> re-probes rather than a fact this ADR asserts. And it governs base-branch resolution only: in the -> same run the workspace path still came out of `origin`, so `worktree.remote` does not steer the -> `owner/repo` derivation — the shared `~/.archon/workspaces/_git/` directory every `dxp` repository -> lands in is Archon reading the second-to-last URL segment as the organisation, an Archon defect -> recorded on [#373](https://github.com/unic/unic-agents-plugins/issues/373), and this key is not its -> fix. +**Status:** Accepted (2026-08-10, revised 2026-10-28). Amends [ADR-0011](0011-archon-schema-target.md) on the version floor only. Its key-discriminated schema conventions stand unchanged. ## Context @@ -43,17 +22,17 @@ Raise the floor to Archon ≥ 0.7.0 (`MIN_ARCHON_VERSION` in `lib/archon-check.m `always_run: true` is set on every node in `/build` and `/qa` whose exit code reports a verdict about EXTERNAL or MUTABLE state rather than about itself: `slopcheck`, `verification`, `goals-check`, `evidence` (build); `verify-pr-base`, `e2e`, `coverage-gate` (qa). Each carries an inline one-line comment naming the specific fact its exit code fails to validate — the same discipline [ADR-0023](0023-build-generic-red-green-refactor-loop.md) §5 already applies to provider knowledge: a Box node is self-contained, so a rule that lives only in this ADR is invisible at run time. -Not every node gets it. `bootstrap`, `open-pr`, `report`, the `run-build` loop, and the various `guard-*`/`approval:` nodes are excluded: their exit code either IS the fact being asserted (a guard's `cancel`), or their output is durable committed state that a resume should not redundantly regenerate (`report.md`, the PR itself). +Not every node gets it. `bootstrap`, `report`, `commit-session`, `push-branch`, `open-pr`, the `run-build` loop, and the various `guard-*`/`approval:` nodes are excluded: their exit code either IS the fact being asserted (a guard's `cancel`), or their output is durable committed state that a resume should not redundantly regenerate (`report.md`, the Session commit, the pushed branch, the PR itself). Since [ADR-0040](0040-build-verdict-decides-the-pull-request.md), `commit-session` and `push-branch` hold the commit `open-pr` used to make. `evidence` declares `always_run` and `commit-session` does not, so a resumed run re-runs `evidence` after the commit. `push-branch` refuses that run when the rewritten `evidence.json` differs from the committed one. Start a new run instead. ### Evidence gate: a script writes it, a prompt never does -[ADR-0034](0034-evidence-gate-deterministic-writer.md) (companion, authored alongside this one) records the evidence gate's own shape: `unic-dlc-build.yaml` declares `evidence_policy: { required: true }`; `verification` and `goals-check` gain a boolean `output_format`; a new `evidence` script node deletes any stale `evidence.json` from a prior resumed attempt, writes `$ARTIFACTS_DIR/evidence.json` only when both verdicts are green, and mirrors it to `<artifacts_dir>/<slug>/evidence.json` (the Session-artifact home, [ADR-0015](0015-workflows-slug-artifact-home.md)) so it survives the worktree `/cleanup` prunes. +[ADR-0034](0034-evidence-gate-deterministic-writer.md) (companion, authored alongside this one) records the evidence gate's own shape: `unic-dlc-build.yaml` declares `evidence_policy: { required: true }`; `verification` and `goals-check` gain a boolean `output_format`; a new `evidence` script node deletes any stale `evidence.json` from a prior resumed attempt, writes `$ARTIFACTS_DIR/evidence.json` (on every verdict since [ADR-0040](0040-build-verdict-decides-the-pull-request.md)), and mirrors it to `<artifacts_dir>/<slug>/evidence.json` (the Session-artifact home, [ADR-0015](0015-workflows-slug-artifact-home.md)) so it survives the worktree `/cleanup` prunes. -### Repository derivation: settled by #289, not reopened here +### Repository derivation: not this ADR's subject -This bump does not touch repository derivation. Each Box derives its target repository from the worktree's `origin` remote in its `bootstrap` node ([ADR-0011](0011-archon-schema-target.md)'s cancel-vs-fail distinction governs the ambiguity guard); `project.repo_ref` is an optional override, absent from `MANDATORY_PATHS` and from `defaultConfig()`, and `/setup` does not ask for it or write it. A regression test asserts that absence so a later bump cannot quietly promote it. This ADR also does not change [ADR-0016](0016-dlc-thin-process-layer.md)'s explicit-staging rule. +This bump does not touch repository derivation, and the Harness now derives no repository from a remote. Each Box reads the one `docs/agents/issue-tracker.md` § Addressing names, so nothing is left to diverge from Archon's own `worktree.remote` resolution ([ADR-0024](0024-triage-intake-on-ramp.md), #389). `/archon-upgrade` still classifies that algorithm VERIFY-ONLY. This ADR also does not change [ADR-0016](0016-dlc-thin-process-layer.md)'s explicit-staging rule. -**Where Archon's own remote resolution differs from this Harness's derivation** (recorded deliberately, NOT re-aligned): Archon 0.7.0 carries a `worktree.remote` key in **its own** `.archon/config.yaml` — a different file from this plugin's `.archon/unic-dlc.config.yaml`, the exact naming trap [ADR-0015](0015-workflows-slug-artifact-home.md) already documents for `workflows/<slug>/` vs `.archon/workflows/` — plus auto-detection: `origin` if present → **the sole remote if only one is configured** → an actionable error only when neither resolves. This Harness's own `bootstrap` derivation has no "sole remote" fallback tier: it reads `origin` only, and treats a checkout with **no `origin` at all** as `ambiguous-repo` — cancelling — even when exactly one other remote exists and would have resolved unambiguously under Archon's own algorithm. This is deliberate, not a bug to fix here: widening the derivation to match Archon's fallback is a separate decision with its own blast radius, not a byproduct of the floor bump. `/setup` reports what Archon's config resolves and **never writes that file**. +**What `worktree.remote` does on 0.7.0.** Archon 0.7.0 carries a `worktree.remote` key in **its own** `.archon/config.yaml`. That is a different file from this plugin's `.archon/unic-dlc.config.yaml`, the naming trap [ADR-0015](0015-workflows-slug-artifact-home.md) documents for `workflows/<slug>/` and `.archon/workflows/`. The behavioural probe that `/archon-upgrade` Step 6 carries measured the key ([#396](https://github.com/unic/unic-agents-plugins/issues/396)). On 0.7.0 the key is **read**, and it governs base-branch resolution only. Set to a second remote's name, the binary's startup error named that remote as the one it resolved the base branch against, where the control run said `origin`. In the same run the workspace path still came from `origin`, so the key does not steer the `owner/repo` derivation. The shared `~/.archon/workspaces/_git/` directory is an Archon defect recorded on [#373](https://github.com/unic/unic-agents-plugins/issues/373), and this key is not its fix. The result belongs to 0.7.0 only, so Step 6 re-probes it on each release. `/setup` reports what Archon's config resolves and **never writes that file**. ### Sub-runs (`workflow:` nodes): deferred, with the trigger to revisit diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0034-evidence-gate-deterministic-writer.md b/apps/claude-code/unic-archon-dlc/docs/adr/0034-evidence-gate-deterministic-writer.md index 58306eba..fd0ccbe2 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0034-evidence-gate-deterministic-writer.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0034-evidence-gate-deterministic-writer.md @@ -1,6 +1,6 @@ # 0034. The evidence gate is a deterministic script writer, never a self-judging prompt -**Status:** Accepted (2026-08-10, revised 2026-09-04) +**Status:** Accepted (2026-08-10, revised 2026-10-28) ## Context @@ -13,15 +13,39 @@ An AI prompt node that both judges the build (reads `verification`/`goals-check` A dedicated **script node**, `evidence`, is the only writer of `evidence.json`. It is deterministic code, not an AI judgment: 1. **Refuse a missing `$ARTIFACTS_DIR` loudly, before anything else.** The engine exports it to script nodes and this is the plugin's only read of it, so an absent value is a breach of the engine contract, not a red verdict — the node exits **non-zero** with a message naming the contract and the floor. The check tests falsiness rather than `undefined` because the empty-string case is the dangerous one: unset throws inside `join()`, but `join('', 'evidence.json')` returns a **relative** path and does not throw, so the node would log a successful write while the engine's presence gate read the real `$ARTIFACTS_DIR` and found nothing — a run failing closed with a success line in its log. Falling back to the Session dir is explicitly rejected: the gate reads only `$ARTIFACTS_DIR`, so a fallback would make that silent case permanent. -2. **Delete-first, both copies.** Remove any `$ARTIFACTS_DIR/evidence.json` **and** any mirrored `<artifacts_dir>/<slug>/evidence.json` left by a prior attempt before doing anything else — `$ARTIFACTS_DIR` is per-run but a resumed run reuses it ([ADR-0033](0033-archon-070-schema-target.md)), and the Session dir is reused too, so a stale file from a previous, since-superseded attempt would certify a tree that is no longer green. The mirror needs this more than the original: the engine's presence gate reads only the `$ARTIFACTS_DIR` copy, so deleting that copy alone still leaves a stale mirror for `open-pr` to stage and a reviewer to read on a run the engine refused to complete. -3. **Read, don't judge.** It reads what `verification` and `goals-check` already computed (via their `output_format`, not by re-running or re-interpreting anything): the `passed` boolean **and** the `failures` list, cross-checked, **and `verification`'s three-state `test_outcome`** ([ADR-0037](0037-config-declares-sdlc-needs.md)). The third state is why the two booleans are not enough on their own: a suite that could not run reports no failures, so on the booleans alone an absent result and a green result are the same fact. A `test_outcome` that is not `pass` is red here whatever the booleans say, and the node's own verdict — printed to stdout as JSON, because a `script:` node has no `output_format` — carries the same three states. `passed` alone is a self-report from the same prompt that wrote `failures`, so a `passed: true` beside a non-empty `failures` array would otherwise certify a tree whose own certificate listed the failures. Both prompts define `failures` as blockers only, so a non-empty list is a red verdict whatever the boolean says. This is a cross-check, not independence — only re-running the suite would give that, and [ADR-0023](0023-build-generic-red-green-refactor-loop.md) §5 rules it out. -4. **Write iff both are green and the test resolved.** If either verdict is not green, or `test_outcome` is not `pass`, it exits 0 having written nothing. The run does **not** stop there: `report`, `open-pr` and `build-pr-gate` still fire, deliberately, so a human gets the report and a PR-shaped surface showing what failed. What the engine refuses is the terminal `completed` status, once every node has succeeded and no `evidence.json` exists. Exit 0 rather than non-zero: a non-zero exit would fail the run at this node and destroy the report. The cost of that choice is that a red build still opens a PR — in `gates.build == afk` with no `build-pr-gate` pause at all — so `open-pr` must treat a missing `evidence.json` as expected and skip staging it rather than failing on `git add`. If that cost is judged too high, gate `open-pr` on the verdict; that is a separate decision with its own blast radius, not a byproduct of this one. If both are green, it writes `$ARTIFACTS_DIR/evidence.json`, then mirrors it to `<artifacts_dir>/<slug>/evidence.json` (the Session-artifact home, [ADR-0015](0015-workflows-slug-artifact-home.md)) — `$ARTIFACTS_DIR` resolves outside the repo tree and is gone once `/cleanup` prunes the worktree, so without the mirror the certification is invisible to a reviewer and to `open-pr`, which stages it. -5. **`always_run: true`.** Its own exit code is a write op, not a verdict about the tree; a resumed run must re-derive presence from the CURRENT `verification`/`goals-check` output on every attempt, never trust that a file existing means the CURRENT tree is green. +2. **Delete-first, both copies.** Remove any `$ARTIFACTS_DIR/evidence.json` **and** any mirrored `<artifacts_dir>/<slug>/evidence.json` left by a prior attempt before doing anything else — `$ARTIFACTS_DIR` is per-run but a resumed run reuses it ([ADR-0033](0033-archon-070-schema-target.md)), and the Session dir is reused too, so a stale file from a previous, since-superseded attempt would certify a tree that is no longer green. The mirror needs this more than the original: the engine's presence gate reads only the `$ARTIFACTS_DIR` copy, so deleting that copy alone would leave a stale mirror for the Session commit to stage and a reviewer to read after an attempt that failed before writing. +3. **Read, don't judge.** It reads what `verification`, `goals-check` and the pre-check already computed through their `output_format`, without re-running or re-interpreting anything. That is the `passed` boolean **and** the `failures` list, cross-checked, **and `verification`'s three-state `test_outcome`** ([ADR-0037](0037-config-declares-sdlc-needs.md)). The third state is why the two booleans are not enough on their own. A suite that could not run reports no failures, so on the booleans alone an absent result and a green result are the same fact. A `test_outcome` that is not `pass` is red here whatever the booleans say, and the node's own verdict, printed to stdout as JSON because a `script:` node has no `output_format`, carries the same three states. `passed` alone is a self-report from the same prompt that wrote `failures`, so a `passed: true` beside a non-empty `failures` array would otherwise certify a tree whose own certificate listed the failures. Both prompts define `failures` as blockers only, so a non-empty list is a red verdict whatever the boolean says. This is a cross-check, not independence. Only re-running the suite would give that, and [ADR-0023](0023-build-generic-red-green-refactor-loop.md) §5 rules it out. + - **Read the pre-check as a gate input.** `implement-review-precheck` runs the `code-review` Method over the whole build. Beside its prose `findings` it emits `status` (`ran` or `blocked`), `blocked_reason`, `hard_findings` and `notes`. A hard finding is a breached documented standard, or an acceptance criterion that is missing or implemented wrong, with a citation. A judgement call is a note. `goals-check` counts that a test and an implementation exist for each criterion, so it marks a criterion `COVERED` that the pre-check has read and found wrong. Before this revision `evidence` read only `verification` and `goals-check`, and a run whose pre-check named a broken criterion still certified `pass`. Now any `hard_findings` entry makes the verdict `fail`. A `status` other than `ran`, `blocked` included, makes it `unresolved` when nothing else is `fail`. `status` and `hard_findings` are self-reported by a prompt, exactly as `passed` is for `verification` and `goals-check`, so this is the same cross-check, not independence. `evidence` treats a malformed entry as hard rather than dropping it. An entry with an empty `citation` or a `category` outside the enum still makes the verdict `fail`. A field that `evidence` cannot read gives a null status or no arrays, so the verdict is `unresolved` or worse. `evidence` writes the cause to stderr and to `precheck_read_error` in `evidence.json`, which is an empty string when every read worked. A read counts as failed when the variable is unset, does not parse, or gives the wrong type, such as a `status` other than `ran` or `blocked`, or a `hard_findings` or `notes` that is not an array. A `notes` value that is not an array keeps its text in `precheck_read_error`. Notes change no verdict. They reach `evidence.json` and the pull request description. + - **The pre-check reaches the script through a `with:` binding, never through its source.** A node-output reference in a `script:` body substitutes raw into the source. A boolean or an array lands as a valid literal, but a string lands unquoted, and wrapping it in quotes breaks on the first quote inside it. The pre-check's strings are free text, and the blocked sentence for a missing merge-base holds single quotes. This node's `with:` binds only the four fields it reads, `status`, `blocked_reason`, `hard_findings` and `notes`, never `findings`. So the prose `findings` never counts against the environment size limit. The engine hands each one to the script as an `INPUTS_PRECHECK_<FIELD>` environment variable. A string field arrives as its raw text, and an array field as JSON text. The script parses the arrays inside a `try`, so no pre-check text is ever parsed as code. A script node's `with:` arrived in Archon 0.10.0 ([Archon CHANGELOG](https://github.com/coleam00/Archon/blob/main/CHANGELOG.md), 0.10.0), so `/build` needs Archon 0.10.0 or later. +4. **Write on every verdict.** The node writes `$ARTIFACTS_DIR/evidence.json` whatever the verdict, with the verdict, `test_outcome`, `unresolved_needs`, both failure arrays, and the pre-check's `precheck_status`, `precheck_blocked_reason`, `hard_findings`, `precheck_notes` and `precheck_read_error`, so the file the engine demands says what failed. It then mirrors the file to `<artifacts_dir>/<slug>/evidence.json` (the Session-artifact home, [ADR-0015](0015-workflows-slug-artifact-home.md)), because `$ARTIFACTS_DIR` resolves outside the repo tree and is gone once `/cleanup` prunes the worktree, so without the mirror the file is invisible to a reviewer and to the node that commits the Session artefacts. The verdict, not the file's presence, decides how the run ends. Only `pass` opens a pull request, and any other verdict cancels the run with the work pushed ([ADR-0040](0040-build-verdict-decides-the-pull-request.md)). The engine's presence check stays, as a backstop for `evidence` ending without writing. +5. **`always_run: true`.** Its own exit code is a write op, not a verdict about the tree; a resumed run must re-derive the verdict from the CURRENT `verification`, `goals-check` and pre-check output on every attempt, never trust that a file left by an earlier attempt describes the CURRENT tree. **Rejected alternative: a prompt writes `evidence.json` when it judges itself green.** This was the first design considered and is recorded here specifically so it is not proposed again: it collapses judgment and certification into the same actor, the exact failure [ADR-0012](0012-fresh-context-red-green-separation.md) and the slopcheck gate exist to prevent for red/green and for dependency trust respectively. A script has no incentive to agree with itself; a prompt asked to both decide and attest does. +### Measured on Archon 0.12.1 + +Measured on 2026-10-28 with the Archon v0.12.1 release binary `archon-darwin-arm64` (commit `5f2b4841`), downloaded with `gh release download v0.12.1 --repo coleam00/Archon`, with its sha256 checked against `checksums.txt`. A wrapper set `ARCHON_HOME` to a scratch folder and ran the binary by its path, against a scratch repository outside every clone, with `--no-worktree`. + +- A string field read in quotes, the shape `evidence` uses for `test_outcome`, fails on a pre-check string. With a `blocked_reason` that holds single quotes, the `script:` node failed with `Expected ";" but found "develop"`. +- A `with:` binding of one field arrives in `INPUTS_<NAME>`. A string field arrives as its raw text and an array field as JSON text. A `blocked_reason` and array entries holding single and double quotes, a backslash, a backtick, a newline, `${` and a literal `$BASE_BRANCH` arrived unchanged and unsubstituted, beside a 200 KB `findings` that was not bound. +- `evidence` itself, taken from the Box and fed by stub nodes, ended exit 0 and wrote `evidence.json` in each of these cases: + - one hard finding gave `fail`; + - a hard finding with an empty `citation` and the category `vibes` gave `fail`; + - a blocked pre-check with the no-merge-base sentence gave `unresolved`, with the sentence intact; + - the same sentence followed by a quote, a backslash, a backtick, `${` and a newline gave `unresolved`, with the text intact; + - a `status` of `maybe` gave `unresolved`; + - notes only gave `pass`, with the notes in the file; + - a failing `verification` beside a blocked pre-check gave `fail`; + - a `haiku` prompt node with the pre-check's `output_format`, told to report the no-merge-base block, gave `unresolved` with the sentence intact; + - a `haiku` prompt node whose schema did not require the four fields, and which emitted only `findings`, gave `unresolved`. The missing string arrived empty and each missing array as empty text, and `precheck_read_error` named all three. +- A producer that printed prose in place of JSON did not reach `evidence`. Its field references raised `OutputRefError` before the node started, and the run ended `failed`. Binding the producer's whole output instead would reach the script, but it would also put the prose `findings` into the environment. +- The machine's Archon v0.10.1 delivered a `with:` binding the same way, also measured on 2026-10-28. + +Two limits are not measured. A `with:` value is an environment variable, and Linux caps one at 128 KiB. Only the four bound fields count against that cap, never `findings`, so the cap is reached only by a `blocked_reason`, a `hard_findings` array or a `notes` array larger than 128 KiB. Then the node would not start and the run would end `failed`. A pre-check that ends without the structured output its schema requires fails as a node, so `evidence` does not run and the run ends `failed`, as before this revision. + ## Consequences - `verification` and `goals-check` in `unic-dlc-build.yaml` carry a boolean `output_format` (`passed`, `failures`) alongside their existing prose verdicts, and `verification` adds `test_outcome` and `unresolved_needs` — the prose stays for the human at `build-pr-gate`; those fields are what the `evidence` node reads, and the engine's gate then checks only that `evidence` wrote the file. -- The `evidence` node depends on both and runs after them, before `report`; `report.md` is unaffected — it already folds in `$verification.output` and `$goals-check.output` verbatim. +- The `evidence` node depends on both and on `implement-review-precheck`, and runs after them, before `report`. `report.md` already folds in `$verification.output` and `$goals-check.output` verbatim. It now also leads section 2 with the number of pre-check hard findings, read from `evidence.json`. +- `implement-review-precheck` carries `status`, `blocked_reason`, `hard_findings` and `notes` in its `output_format` beside `findings`. Its prompt states which finding is hard and which is a note. - No other Box gains an evidence gate in this ADR. `/qa`, `/pr-review`, and `/explore` have their own verdict-reporting nodes (`e2e`, `coverage-gate`, `uat-prep`, …) but none currently reaches a terminal `completed` state whose evidence needs this exact deterministic-writer shape; extending the pattern there is a separate decision. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0037-config-declares-sdlc-needs.md b/apps/claude-code/unic-archon-dlc/docs/adr/0037-config-declares-sdlc-needs.md index ba12b473..bccee5a8 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/0037-config-declares-sdlc-needs.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0037-config-declares-sdlc-needs.md @@ -1,6 +1,6 @@ # 0037. The config declares the SDLC's needs; the Plugin names no tool -**Status:** Accepted (2026-09-04) +**Status:** Accepted (2026-09-04, revised 2026-10-28) ## Context @@ -83,11 +83,17 @@ whatever their stack uses, and a node decides at run time which of those needs i tool and files `qa-checks.md`. Every field added to a node's output in this decision has a named reader in the same change. -8. **`/pr-review` says out loud that it runs nothing.** It gains no procedure that checks a claim - against an author's assertion — that belongs to a Method, not to this Harness - ([ADR-0030](0030-harness-hosts-methods.md)) — so its summary block names which needs the project - declares and states that this Box executed none of them. Without that sentence the admission lives - only in the Plugin's own issue tracker, where the developer reading a review never sees it. +8. **`/pr-review` states that it was not asked to run any check.** It gains no procedure that + checks a claim against an author's assertion. That belongs to a Method, not to this Harness + ([ADR-0030](0030-harness-hosts-methods.md)). So its summary block names which needs the project + declares and states that this Box instructed none of them. The block states what the Box + instructed, not what ran. A review sub-agent can run a check on its own, so a sentence saying that + nothing ran can be false. The block says that a review sub-agent may still have run one, unless + `review` records that no sub-agent ran. `review` records each check a sub-agent reports in + `reported_checks`, named by the need it serves and never by its command. The block renders each as + that sub-agent's claim, unverified by this Box. No other section states a check result as a fact. + Without the block the warning lives only in the Plugin's own issue tracker, where the developer + reading a review never sees it. 9. **The two `e2e_command` keys are retired, and the retirement is deliberate.** The `qa.e2e_command ?? build.e2e_command` pair becomes the single `sdlc_needs.e2e`. The per-Box override goes on purpose: all diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0038-practice-packs-stack-by-prefix.md b/apps/claude-code/unic-archon-dlc/docs/adr/0038-practice-packs-stack-by-prefix.md new file mode 100644 index 00000000..7f96d6d8 --- /dev/null +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0038-practice-packs-stack-by-prefix.md @@ -0,0 +1,161 @@ +# 0038. Practice Packs stack by prefix; the DLC alone owns `.archon/methods/` + +**Status:** Accepted (2026-10-21) + +## Context + +A Consumer may install several Practice Packs beside the DLC. Before this decision, nothing said how they +share the Consumer's files. `/setup` replaces `.archon/methods/` wholesale, and +[ADR-0031](0031-methods-bundled-three-tier-resolution.md) retired the two override tiers that a pack could +otherwise have used. So the question was how several installers write into one repository without one of them +removing, or silently overriding, what another one wrote. + +The maintainer settled these decisions in a grilling on 2026-10-20 and 2026-10-21, on +[Practice Packs must stack](https://github.com/unic/unic-agents-plugins/issues/520). The Archon facts below +come from the +[research note of 2026-10-21](../../../../../docs/research/2026-10-21-archon-workflow-extensibility.md), and +from one measurement made for this ADR. Each fact names the Archon version it was read or measured on. + +- **A Claude node loads the project's `CLAUDE.md` without declaring it.** The Archon docs at v0.12.1 + state it. The `settingSources` default is `project` and `user`, and it still applies after 0.12.0. +- **A node follows a pointer in `CLAUDE.md`.** A measurement for #520 ran on the v0.12.1 release + binary with Claude Code 2.1.306. A node whose prompt named no file read the file that `CLAUDE.md` pointed at, in three runs of + three. A control repository without the pointer gave zero of three. + [The measurement](https://github.com/unic/unic-agents-plugins/issues/520#issuecomment-6313878448) states + the probe, the control and what it does not show. +- **A node reaches a project skill only when it names it in `skills:`.** The docs and the source at v0.12.1 + both state it. Omitting `skills:` selects no skill, and no workflow-level or config-level default exists. +- **A node reaches a Claude Code plugin only when it names it in `plugins:`.** This holds since 0.12.0, and the + docs at v0.12.1 state it. The node fails before its first turn when a named plugin is missing, or when one it did not name + loads. +- **An `include:` target resolves by `name:`, is overridden by filename, and is never shared.** The research + measured this on 0.10.1. A Box names its include target by the target's `name:`. Across the bundled, + `~/.archon` and repository scopes, a file in a higher scope overrides one with the same filename. Two files in reach + that declare the same `name:` under different filenames are both dropped, and the workflow that includes + them then fails to load. Source at v0.12.1 shows the same logic. Nobody has measured it on 0.12. + +## Decision + +### 1. A Practice Pack ships context artefacts and config values, never a Box or a Method + +This holds for v1. The maintainer decided it on the e2e pack, which has nothing to override, and nobody has +tested it against a pack that must replace a step such as `implement`. §7 records the route for that pack. + +### 2. The DLC alone owns `.archon/methods/` + +`/setup`'s wholesale replace of that tree stays correct, because no pack writes there. + +### 3. A skill under `.claude/skills/` is a context artefact, not a Method + +This holds even when a node declares the skill in `skills:`. A Method is the text a Box reads by path from +`.archon/methods/` ([ADR-0031](0031-methods-bundled-three-tier-resolution.md)). A skill is a file a node may +load as context. v1 Boxes declare no pack skill in `skills:` and no pack plugin in `plugins:`, so on 0.12.1 +a pack skill reaches a Box run only through `CLAUDE.md`. + +### 4. A pack reaches a Box run through its own block in the Consumer's `CLAUDE.md` + +The block points at the pack's conventions. A node receives `CLAUDE.md` without declaring it, and it +follows the pointer, as measured on 0.12.1 above. That probe used a short prompt. It does not show that a node +follows the pointer reliably inside a long Box prompt, which +[#648](https://github.com/unic/unic-agents-plugins/issues/648) depends on. + +### 5. Each installer owns what carries its prefix, with no manifest + +Each pack has a unique prefix. The DLC's prefix is `unic-dlc`. An installer writes, overwrites or removes +only these names that carry its prefix: + +| Where | The name a pack with prefix `<prefix>` owns | +| ------------------------------------- | ----------------------------------------------------- | +| `.claude/skills/` | `<prefix>-*/` | +| The Consumer's `CLAUDE.md` | one block between `<prefix>:begin` and `<prefix>:end` | +| Each line-based formatter ignore file | one block between `<prefix>:begin` and `<prefix>:end` | +| `.archon/packs/` | `<prefix>.config.yaml` | + +In v1 these are the only files a pack writes. An installer assumes nothing about any other name. If its prefix is +already held by another pack, it stops and asks the operator. Each pack brings its own installer, and the +DLC's `/setup` installs no pack in v1. + +The DLC follows the same rule in these four places. It also writes `.archon/methods/`, `.archon/workflows/`, +`.archon/unic-dlc.config.yaml`, `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md`, which no +pack writes in v1. The Consumer owns the two `docs/agents/` files after the first run. The DLC's markers +move from `unic-archon-dlc:begin` and `unic-archon-dlc:end` to `unic-dlc:begin` and `unic-dlc:end`. `/setup` finds a block under the old markers and rewrites it in place +under the new ones, so a Consumer never holds both. No pack may take `unic-dlc` or `unic-archon-dlc` as its +prefix. + +### 6. Config is layered, and the Consumer's file holds only the Consumer's choices + +[Build the layered Practice Pack config](https://github.com/unic/unic-agents-plugins/issues/647) builds +this. Once it lands, a Box reads three layers, and a later layer wins key by key: + +1. `.archon/packs/unic-dlc.config.yaml`, the DLC's defaults. +2. Each `.archon/packs/<prefix>.config.yaml`, one pack's defaults. +3. `.archon/unic-dlc.config.yaml`, the Consumer's choices. + +The DLC's defaults come first and the Consumer's file comes last. The order between two packs' defaults +files is not decided. #647 decides it before a second pack sets a default. + +Each installer replaces its own defaults file on every run. No installer writes a default into the +Consumer's file. So the Consumer's value wins, and it survives a re-run of `/setup` and a pack upgrade. +The reader sees a Consumer override in the Consumer's own file, because that file holds nothing else. On a +re-run, `/setup` reports every key in the Consumer's file whose value equals the current default, as a +candidate to remove, and changes nothing. + +Until #647 lands, the Consumer's file is the only layer, and `/setup` writes no defaults file. + +### 7. Replacing a Box step is a future route, decided and not built + +A Box may declare an `include:` slot that ships with a DLC default block, and a pack may replace that block. +Archon resolves an include target by its `name:`. Across the bundled, `~/.archon` and repository scopes, a +file in a higher scope overrides one with the same filename. Two files that declare the same `name:` under +different filenames are both dropped. The research measured that across scopes on 0.10.1. So inside one scope +Archon gives no precedence either. That result follows from the cross-scope measurement, and the source at +v0.12.1 shows the same logic. The DLC and every pack +install into repository scope, so the installer has to decide who wins. + +One slot holds one occupant. When a second pack claims an occupied slot, its installer detects the +collision, names both packs and offers a resolution. The installer writes the Consumer's choice into +`.archon/unic-dlc.config.yaml` as a Consumer override, which is where the reader sees who won. That entry +is a choice the Consumer made, not a default, so §6 allows it in the Consumer's file. Nothing resolves +silently. This is the route for a pack that must replace `implement`. Its own ticket builds it when the first +pack needs it, so no step of `/setup` implements it today. + +This route is a future exception to the table in §5. The block a pack writes lives under +`.archon/workflows/` and carries no pack prefix. The ticket that builds this route must resolve how that +fits §5. + +### 8. This does not wait for #425 + +[#425](https://github.com/unic/unic-agents-plugins/issues/425) substitutes a value inside a file the DLC +owns, such as a node's `mcp:`. §7 replaces a whole file that another owner occupies. Both run at install time, +but they use different mechanisms, so neither one blocks the other. + +## Which file wins on a collision + +| Collision | Who wins | Where the reader sees it | +| ------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | +| One key set in several config layers | The later layer of §6; the Consumer's file last | `.archon/unic-dlc.config.yaml`, and the `/setup` report of default-valued keys | +| One key set by two packs, and not by the Consumer | Not decided (#647) | Not decided | +| Two packs claim one prefix | Nobody. The second installer stops and asks | That installer's run | +| Two packs claim one `include:` slot | The pack the Consumer chooses | `.archon/unic-dlc.config.yaml`, as a Consumer override | +| A Consumer edit inside a pack's own name | The pack. Its next run overwrites the edit | The `git diff` after that run | + +## Not decided here + +- **The prefix boundary.** Whether one prefix may begin another is open. A pack prefixed `unic` would claim + every `unic-dlc-*` name, so the prefix rule needs a boundary. The rule does not settle it today. A pack + prefixed `unic-dlc-e2e` would own `unic-dlc-e2e-*` skill directories, which also match the DLC's + `unic-dlc-*`. Today `/setup` writes, overwrites or removes nothing in `.claude/skills/`. The overlap is + an open risk until the maintainer decides the boundary. +- **How the include slot of §7 fits the table of §5.** A pack's block lives under `.archon/workflows/` and + carries no pack prefix. +- **How a slot records its occupant**, so that the DLC's next `/setup` leaves it alone. +- **Whether the DLC's `/setup` runs each installed pack's installer.** +- **The order between two packs' defaults files.** When two packs set the same key differently and the + Consumer's file omits it, no winner is defined. [#647](https://github.com/unic/unic-agents-plugins/issues/647) + decides the order before a second pack sets a default. +- **Whether Archon 0.12.x treats include slots as measured on 0.10.1.** Source at v0.12.1 shows the same + logic, and nobody has measured it. +- **Whether a `SessionStart` hook or `.claude/rules/*.md` reach a node.** Both are unmeasured on every + version, and neither is used here. +- **How a pack picks a prefix that no other pack holds.** §5 detects a collision, and no registry prevents + one. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0039-base-branch-is-archons-base-branch.md b/apps/claude-code/unic-archon-dlc/docs/adr/0039-base-branch-is-archons-base-branch.md new file mode 100644 index 00000000..71c5c51b --- /dev/null +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0039-base-branch-is-archons-base-branch.md @@ -0,0 +1,63 @@ +# 0039. A run's base branch is Archon's `$BASE_BRANCH` + +**Status:** Accepted (2026-10-21) + +## Context + +`/build`, `/pr-review` and `/qa` each computed `expected_base` in `bootstrap` from `project.branching`: +`develop` when `gitflow`, else `main`. No value could name a third branch, so no Box could target a +long-lived integration branch. `/build` also read a second answer. Its verify node diffed against +Archon's `$BASE_BRANCH`, while `open-pr` and `code-review` in the same Box read `expected_base`, and +nothing checked that the two agreed. `/pr-review` took its review diff from the merge-base with +`expected_base`. With a wrong base the review read the right diff until `develop` moved, and then it read +the wrong diff and reported nothing (#505). + +Archon already resolves a base branch for every run. It takes `archon workflow run --base <branch>`, then +`worktree.baseBranch` in `.archon/config.yaml`, then the codebase's stored default branch, then git, and +exposes the result to every node as `$BASE_BRANCH` +([variables reference at v0.12.1](https://github.com/coleam00/Archon/blob/v0.12.1/packages/docs-web/src/content/docs/reference/variables.md)). +`--base` exists since Archon 0.7.1 ([coleam00/Archon#2203](https://github.com/coleam00/Archon/pull/2203)). +A spike on Archon 0.12.1 +([#505 comment](https://github.com/unic/unic-agents-plugins/issues/505#issuecomment-6313975252)) measured +five facts. A prompt node sees the `--base` value, and Archon cuts the worktree from it. A resumed run +keeps that value after an approval gate. A `--base` branch missing on the remote fails the run before its +first node. With `--from <feature-branch>` and `--base` together, Archon cuts the worktree from the +feature branch, and `$BASE_BRANCH` keeps the `--base` value. With no PR to query, +`/pr-review` diffs against `$BASE_BRANCH`, re-measured on the Box with its fetch and guard in place +([#505 comment](https://github.com/unic/unic-agents-plugins/issues/505#issuecomment-6314490544)). + +## Decision + +1. **The one source of a run's base branch is Archon's `$BASE_BRANCH`.** The Plugin adds no config key + for it. A DLC key would be a third answer next to `branching` and Archon's own, with nothing to keep the + three in step. Archon already reads `--base` per dispatch, so a run can target another branch without a + committed change in the Consumer. Every line in the three Boxes that names the base reads + `$BASE_BRANCH`, except the review diff in decision 3. On Archon 0.12.1 an approval `message` substitutes node outputs but not `$BASE_BRANCH`. So `/build`'s gate embeds the `bootstrap` message, which starts with the base, and `/qa`'s `merge-gate` names the PR's verified target. +2. **`project.branching` stops naming the base.** Its one job left in the Boxes is to decide whether + `/qa` deletes the merged `feature/*` branch after the merge. `/cleanup` and `/explore` keep their own reads of it as + hints, and neither names a run's base. +3. **The review diff reads the PR's own target branch** when the tracker returns a PR, because that is + the diff that will merge. When a query confirms that no PR exists, or the tracker exposes none, it + reads `$BASE_BRANCH`. When both exist and differ, `/pr-review` puts a warning that names both in its + summary and carries on. `prep` fetches the diff base first. When the tracker query fails, or the diff + base still does not resolve, `guard-no-diff-base` cancels the run. A guessed base would give the review + a diff of other changes than the PR's. The spike did not reach this cancel path, because it needs a + PR whose target branch does not resolve, and a scratch repository has no tracker to return one. #502 + proves it in the Consumer. `/qa`'s `verify-pr-base` compares the + PR's target with `$BASE_BRANCH`, and `merge-gate` and `merge` still fail closed on + `base_ok != 'true'`. So a `/qa` whose `$BASE_BRANCH` differs from the PR's target refuses to merge. +4. **`/setup` stays verify-only on `.archon/config.yaml`.** It reports `worktree.baseBranch` when it is set. Otherwise it reports the remote's default branch, labelled as an approximate stand-in, because Archon 0.7 to 0.11 store the branch checked out at the first registration. From 0.12.0, new + projects follow the remote's default branch unless a base is chosen ([v0.12.0 release notes](https://github.com/coleam00/Archon/releases/tag/v0.12.0)). Its main signal is the warning that `worktree.baseBranch` is unset. It also warns when the resolved base disagrees with the base that `branching` implies, and prints the block to add once. It writes nothing to that file. + `commands/setup.md` is the one place in the Plugin that maps a branching model to a run's base, and + only to warn. `/cleanup` keeps its own main-line hint. + +## Consequences + +- A Consumer whose Archon base differs from the branch `branching` implied now targets the Archon base. + `/setup`'s base line shows which branch that is. +- The `code-review` fixed point in [ADR-0023](0023-build-generic-red-green-refactor-loop.md) reads + `$BASE_BRANCH`. +- `--base` refuses a branch missing on the remote, so no Box creates an integration branch. A person cuts + it before the dispatch. +- The PR-target path and the cancel path of decision 3 need a tracker that returns a PR, so the spike + could not measure them. #502 proves both in the Consumer before the first run that depends on them. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/0040-build-verdict-decides-the-pull-request.md b/apps/claude-code/unic-archon-dlc/docs/adr/0040-build-verdict-decides-the-pull-request.md new file mode 100644 index 00000000..e616c617 --- /dev/null +++ b/apps/claude-code/unic-archon-dlc/docs/adr/0040-build-verdict-decides-the-pull-request.md @@ -0,0 +1,53 @@ +# 0040. `/build`'s verdict decides whether a pull request opens + +**Status:** Accepted (2026-10-28, revised 2026-10-28) + +## Context + +Until this decision, `evidence` in `unic-dlc-build.yaml` wrote `evidence.json` only on a `pass` verdict, and the run went on to `report` and `open-pr` whatever the verdict was. The engine's `evidence_policy` then refused terminal `completed`, so a red build ended `failed` with a complete, mergeable pull request already open. In `gates.build == afk` no human paused before it. [ADR-0034](0034-evidence-gate-deterministic-writer.md) § Decision item 4 accepted that cost and named the alternative: gate `open-pr` on the verdict. This ADR takes that alternative (#439). + +A second defect came with it. The one file the engine demands carried no information. It existed only on `pass`, so both failure arrays in it were empty by construction, and it could never say what failed. + +## Decision + +1. **`evidence` writes `evidence.json` on every verdict.** The file names the verdict and lists the failures that produced it: the failure arrays from `verification` and `goals-check`, `test_outcome` and `unresolved_needs`, and the pre-check's `precheck_status`, `precheck_blocked_reason`, `hard_findings`, `precheck_notes` and `precheck_read_error` ([ADR-0034](0034-evidence-gate-deterministic-writer.md)). `evidence` writes the Session-dir mirror on every verdict too. +2. **The verdict on stdout is the branch signal.** `evidence` prints `{ verdict, test_outcome, unresolved_needs }` as JSON on stdout and every human line on stderr. Two `when` expressions read the `verdict` field, and both compare it with `'pass'` only, so `fail` and `unresolved` take the same branch. +3. **The work is committed and pushed on every verdict, before the verdict decides anything.** `commit-session` stages the named-path list `open-pr` used to stage, now with `evidence.json` always on it, and commits. `push-branch` pushes the branch and prints `{ branch, commit }` as JSON. So a red build leaves its commits, `report.md` and `evidence.json` on the remote branch. +4. **Two siblings follow `push-branch`, and the verdict picks one.** `guard-not-pass` is a `cancel:` leaf with `when` on a verdict other than `pass`. It ends the run `cancelled` and its message names the verdict, the pushed branch and commit, and the path of `report.md` on that branch. It writes nothing to the tracker. `open-pr` has `when` on a verdict equal to `pass`, opens the pull request against `$BASE_BRANCH` ([ADR-0039](0039-base-branch-is-archons-base-branch.md)) and links it to the slices' work items. `build-pr-gate` still depends on `open-pr`, so a green build in `hitl` still pauses there. No node in `/build` sets a trigger rule. +5. **A failed push ends the run `failed`, not `cancelled`.** `push-branch` is a `bash:` node, so a failed push exits non-zero, both siblings are skipped, and the run ends `failed`. No work reached the remote, which is a different fact from a red verdict. Before it pushes, `push-branch` also exits non-zero in four cases. The first is an empty `$BASE_BRANCH`, because the next check would then pass in silence. The second is HEAD detached or on the run's base branch, because a push there would put the work on the base branch directly. The third is a branch name that holds `"` or `\`, because the node prints the name into JSON. The fourth is a Session artefact (`PRD.md`, `issues.json`, `report.md`, `build-state.json` or `evidence.json`) missing at HEAD or different from the working tree. `commit-session` is a prompt node and cannot fail when its commit did not happen, so this check catches it. A resumed run is the other cause. `verification` and `evidence` declare `always_run` and `commit-session` does not, so a resume re-runs `evidence` after the commit. When the rewritten `evidence.json` differs from the committed one, `push-branch` refuses, and the refusal message names this cause. Start a new run instead. `push-branch` pushes to `origin`. A contract repository that is not `origin` is one cause of `open-pr` failing to open the pull request, and it then prints `BLOCKED:` with the tool's error. +6. **`evidence_policy: { required: true }` stays, as a backstop.** With `evidence` writing on every verdict it no longer refuses a red run. Two guards now cover `evidence` ending without writing. `push-branch` refuses a Session mirror that is missing at HEAD, so that run ends `failed` before any pull request. `evidence_policy` catches the one case left, where the mirror is committed but the `$ARTIFACTS_DIR` copy is absent, for example after an edit that writes the engine's copy to another path. Keep the key. Without it, that run would end `completed` on a `pass`. An `evidence` node that fails outright already ends the run `failed`. + +### Why siblings, and not a chain + +The first design ran a chain with `trigger_rule: all_done`, so that the cancel ran after `open-pr` was skipped. A `trigger_rule` is the setting an implementer most easily gets wrong, and on a chain the green path depends on it. The sibling shape needs none. `guard-not-ready` in `/build` and `guard-no-diff-base` in `/pr-review` already use it, and the `pass`-only comparison sends `unresolved` to the cancel side without a third branch. + +### Why two nodes for the commit and the push + +#439's criterion 6 asks for one node that commits and pushes, and fails when the push fails. Telling the loop's uncommitted source and test files from scratch files is a judgement, so staging needs a prompt node. A prompt node cannot exit non-zero, so it cannot fail on a failed push, and the branch and commit it reports would be self-reported. So `commit-session` is the prompt that stages and commits, and `push-branch` is the `bash:` node that pushes, fails on a failed push, and reads the branch and commit from git. + +## The probe + +Measured on 2026-10-28 with the Archon v0.12.1 release binary `archon-darwin-arm64` (commit `5f2b4841`), downloaded with `gh release download v0.12.1 --repo coleam00/Archon` and checked against `checksums.txt`. A wrapper set `ARCHON_HOME` to a scratch folder and ran the binary by its path, in a scratch repository with a bare remote, outside every clone, with `--no-worktree`. The machine's own `archon` stayed v0.10.1. + +The probe workflow mirrors the tail of the Box. It has a `script:` node on `runtime: bun` that writes `evidence.json` to `$ARTIFACTS_DIR` and prints `{ verdict, test_outcome, unresolved_needs }`; a second `script:` node that reads the verdict back; a `bash:` push node that prints `{ branch, commit }`; a `cancel:` leaf on `verdict != 'pass'`; a sibling on `verdict == 'pass'`; and a node that depends on that sibling. The leaf and the sibling depend on the push node only, as `guard-not-pass` and `open-pr` depend on `push-branch`, and read the verdict from an ancestor two nodes up. The workflow declares `evidence_policy: { required: true }`. + +1. **A `script:` node's JSON stdout is read field by field in a `when`: holds.** With `fail` and `unresolved`, the sibling logged `Skipped (condition: $verdict.output.verdict == 'pass')`. With `pass`, the cancel leaf logged `Skipped (condition: $verdict.output.verdict != 'pass')`. The stderr line did not reach the output. A node-output reference in a `script:` body is still substituted: the read-back node printed the verdict and the array. +2. **A `cancel:` leaf whose `when` holds sets `cancelled`, and neither the sibling nor its dependant runs: holds.** With `fail` and `unresolved`, the run's row in the scratch database reads `cancelled`, the sibling was skipped, and the dependant never started. +3. **The skipped `cancel:` leaf does not stop `completed`: holds.** With `pass`, the sibling and its dependant ran, the CLI printed `Workflow completed successfully.` and exited 0, and the row reads `completed`. +4. **A `cancel:` message substitutes node-output references: holds.** The message printed as `Probe cancelled. Verdict fail. Work on main at <sha>.`, with the verdict from the `script:` node, which is not a direct dependency of the leaf, and the branch and commit from the `bash:` node, which is its one dependency. + +A fifth run measured decision item 5's failed push. With a push that fails, the `bash:` node failed with exit 128, the cancel leaf, the sibling and the dependant were each skipped as `upstream failed`, and the row reads `failed`. + +The same items 2 to 4 were measured on the same day for #652 with `bash:` nodes, and [PR #663](https://github.com/unic/unic-agents-plugins/pull/663) records them. This probe ran them again with a `script:` node as the verdict's source. + +Three more runs measured `push-branch`'s own body in a `bash:` node. 0.12.1 substitutes `$BASE_BRANCH` in a `bash:` body and also exports it: with a worktree and `--base develop` the node read `develop`, and with `--no-worktree` it read `main`. On HEAD at `main` with `main` as the base, the node refused with exit 1 and the run ended `failed`. With the Session artefacts uncommitted, it refused naming `evidence.json` and pushed nothing. With them committed on a feature branch, it pushed. + +One more run measured what a `when` does with output that carries no `verdict` field. A `script:` node printed a line that is not JSON, and two siblings read `$verdict.output.verdict`, one with `!= 'pass'` and one with `== 'pass'`. Neither skipped: both failed with `'$verdict.output.verdict' references field 'verdict', but node 'verdict's output is not a JSON object`, and the run ended `failed`. A JSON object without the key gave the same result, with `JSON output has no such key`. So a broken `evidence` stdout ends the run `failed`, never `completed` and never `cancelled`. + +Found on the way, Archon 0.12.1 no longer substitutes `$ARGUMENTS` into a `script:` body, and warns `script_node_literal_user_var`. It reads `process.env.ARGUMENTS` instead. `evidence` reads no user variable, so this changes nothing here. + +## Consequences + +- A red `/build` run now ends `cancelled` with no pull request, where it used to end `failed` with one open. An operator or a script that waited for a red pull request sees a different outcome. +- How a human who is not watching the run learns about a red build is not decided here. #658 takes it. +- `/qa` on a branch with no pull request ends `cancelled` at `guard-no-open-pr` (#652), so a red branch cannot be merged by `/qa` either. diff --git a/apps/claude-code/unic-archon-dlc/docs/adr/README.md b/apps/claude-code/unic-archon-dlc/docs/adr/README.md index 2c6d612d..6183eb18 100644 --- a/apps/claude-code/unic-archon-dlc/docs/adr/README.md +++ b/apps/claude-code/unic-archon-dlc/docs/adr/README.md @@ -54,7 +54,7 @@ Legacy forms that existing files and rows still carry, and new work does not use | 0010 | Retire the `/implement-feature` skill; Feature Runner backed solely by `unic-dlc-build` | Accepted | | 0011 | Archon version target (≥ 0.5.0) and key-discriminated node-schema conventions | Accepted; floor amended by ADR-0033 | | 0012 | Fresh-context red/green separation for anti-cheating | Accepted | -| 0013 | Issue tracker is the single source of truth; HANDOFF.md/ROADMAP.md dropped | Accepted | +| 0013 | Issue tracker is the single source of truth; HANDOFF.md/ROADMAP.md dropped | Accepted (2026-06-30, revised 2026-10-21) | | 0014 | Workflow-per-box decomposition | Accepted; revised by ADR-0017 | | 0015 | `workflows/<slug>/` is the artifact home | Accepted | | 0016 | DLC is a thin process layer; compose team system-skills for the _how_ | Accepted; amended by ADR-0030, #381 | @@ -64,18 +64,21 @@ Legacy forms that existing files and rows still carry, and new work does not use | 0020 | `/specs` reaches an aligned PRD by branch-on-input | Accepted (2026-07-02, revised 2026-09-02) | | 0021 | A box ships only if it adds value; reference verbatim skills | Accepted; amended by ADR-0030 | | 0022 | `/tickets` slices a PRD into build-ready issues; `/build` consumes them via a generic loop | Accepted | -| 0023 | `/build` is one generic red/green loop; dag-builder dissolved | Accepted (amended #281, #381) | -| 0024 | `/triage` is the intake on-ramp; thin wrapper binds Matt's method to DLC config | Accepted (amended #296, #389) | +| 0023 | `/build` is one generic red/green loop; dag-builder dissolved | Accepted (2026-07-02, revised 2026-10-28) | +| 0024 | `/triage` is the intake on-ramp; thin wrapper binds Matt's method to the tracker contract | Accepted (2026-07-02, revised 2026-10-21) | | 0025 | `/qa` is an Archon pipeline with two config-gated approvals + an issue-producing on-ramp | Accepted (2026-07-02, revised 2026-09-04; #389 folded in) | | 0026 | `/pr-review` is a generic fan-out Archon workflow harvesting unic-pr-review's learnings | Accepted (amended #281, 2026-08-24) | | 0027 | `/improve-architecture` is a skill composing Matt's method + owns ADR superseding | Accepted | | 0028 | `/cleanup` is the repo-global operational janitor; retires the legacy cleanup workflow | Accepted (amended #389) | | 0029 | `/explore` is an off-line research + AFK-spike on-ramp; findings.md is the /specs baton | Accepted (amended #281, #389) | | 0030 | The DLC is a Harness hosting Methods; a Box survives only for what no Method can supply | Accepted (amended #381) | -| 0031 | Methods are bundled, the plugin version is the pin, resolution is one path | Accepted (amended #381) | +| 0031 | Methods are bundled, the plugin version is the pin, resolution is one path | Accepted (2026-08-03, revised 2026-10-21) | | 0032 | Vocabulary: Box, Method, Bundle; config is parameters, a Method is procedure | Accepted (amended #389, #381) | -| 0033 | Archon 0.7.0 schema target — floor bump, always_run, sub-runs deferred | Accepted (amended #389, #396) | -| 0034 | The evidence gate is a deterministic script writer, never a self-judging prompt | Accepted (2026-08-10, revised 2026-09-04) | +| 0033 | Archon 0.7.0 schema target — floor bump, always_run, sub-runs deferred | Accepted (2026-08-10, revised 2026-10-28) | +| 0034 | The evidence gate is a deterministic script writer, never a self-judging prompt | Accepted (2026-08-10, revised 2026-10-28) | | 0035 | `/archon-upgrade` reports Archon-release impact; read-only, cites 0011/0033 by reference | Accepted (amended #381, #396) | | 0036 | `/setup` owns a named install set; a Box workflow retires by name, never by header | Accepted (amended #295, #381) | -| 0037 | The config declares the SDLC's needs; the Plugin names no tool | Accepted | +| 0037 | The config declares the SDLC's needs; the Plugin names no tool | Accepted (2026-09-04, revised 2026-10-28) | +| 0038 | Practice Packs stack by prefix; the DLC alone owns `.archon/methods/` | Accepted | +| 0039 | A run's base branch is Archon's `$BASE_BRANCH` | Accepted | +| 0040 | `/build`'s verdict decides whether a pull request opens | Accepted (2026-10-28, revised 2026-10-28) | diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.dark.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.dark.png index 53d857f1..c8ea66aa 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.dark.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.dark.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.html b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.html index 6358e206..c7d9d754 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.html +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.html @@ -5313,8 +5313,8 @@ <h1>unic-archon-dlc architecture</h1> <text data-detail="context" x="855" y="556" class="t-muted" font-size="9" text-anchor="middle">CONTEXT.md · ADRs · own skills</text> </g> - <g id="node-pack" data-node-id="pack" data-node-label="Practice Pack" tabindex="0" role="button" aria-label="Focus Practice Pack, optional · design open, Practice Pack repo" aria-pressed="false" data-node-kind="external" data-node-sublabel="optional · design open" data-node-context="Practice Pack repo"> - <title>Practice Pack · optional · design open · Practice Pack repo + + Practice Pack · optional · own prefix · Practice Pack repo Practice Pack - optional · design open + optional · own prefix @@ -5671,7 +5671,7 @@

HOW: the team's own

  • • CONTEXT.md and ADRs tell the Boxes about this project
  • • Tracker, docs and design through the team's own system-skills
  • -
  • • A Practice Pack could package this context for reuse (design open)
  • +
  • • A Practice Pack packages this context for reuse, under its own prefix
diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.json b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.json index 99edb3d3..cbfe3853 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.json +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.json @@ -147,7 +147,7 @@ "id": "pack", "type": "external", "label": "Practice Pack", - "sublabel": "optional · design open", + "sublabel": "optional · own prefix", "pos": [1110, 540], "size": [150, 64] }, @@ -320,7 +320,7 @@ "items": [ "CONTEXT.md and ADRs tell the Boxes about this project", "Tracker, docs and design through the team's own system-skills", - "A Practice Pack could package this context for reuse (design open)" + "A Practice Pack packages this context for reuse, under its own prefix" ] } ] diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.light.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.light.png index 23ec4dfd..d748e15d 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.light.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-architecture.light.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.dark.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.dark.png index f5cf1ed8..6dd91729 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.dark.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.dark.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.html b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.html index 42e4b281..54d96ea6 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.html +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.html @@ -5085,7 +5085,7 @@

unic-dlc-build pipeline

- + diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.json b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.json index 075406e5..e14f0bbf 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.json +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.json @@ -41,6 +41,8 @@ "goals-check", "evidence", "report", + "commit-session", + "push-branch", "open-pr", "build-pr-gate" ], @@ -48,125 +50,143 @@ }, { "id": "evidence-and-approval", - "label": "Evidence and approval", - "focus": ["verification", "goals-check", "evidence", "report", "open-pr", "build-pr-gate"], - "note": "open-pr runs whatever evidence concluded. The evidence gate decides only whether the run ends completed." + "label": "Verdict and approval", + "focus": ["evidence", "report", "commit-session", "push-branch", "guard-not-pass", "open-pr", "build-pr-gate"], + "note": "Every verdict is reported, committed and pushed. Only a pass opens the PR. Any other verdict cancels the run." } ] }, "components": [ + { + "id": "guard-not-ready", + "type": "external", + "label": "guard-not-ready", + "sublabel": "cancels the run", + "pos": [40, 40], + "size": [150, 48] + }, { "id": "bootstrap", "type": "backend", "label": "bootstrap", "sublabel": "reads config", - "pos": [290, 40], - "size": [220, 48] + "pos": [260, 40], + "size": [190, 48] }, { "id": "slopcheck", "type": "messagebus", "label": "slopcheck", "sublabel": "new packages exist", - "pos": [910, 40], - "size": [220, 48] - }, - { - "id": "guard-not-ready", - "type": "external", - "label": "guard-not-ready", - "sublabel": "cancels the run", - "pos": [40, 40], - "size": [160, 48] + "pos": [530, 40], + "size": [190, 48] }, { "id": "run-build", "type": "backend", "label": "run-build", "sublabel": "writes build-state.json", - "pos": [290, 190], - "size": [220, 48] + "pos": [800, 40], + "size": [190, 48] }, { "id": "implement-review-precheck", "type": "backend", "label": "implement-review-precheck", "sublabel": "Standards + Spec, in parallel", - "pos": [600, 190], - "size": [220, 48] + "pos": [1070, 40], + "size": [190, 48] }, { "id": "verification", "type": "backend", "label": "verification", "sublabel": "tests, stubs, coverage", - "pos": [290, 340], - "size": [220, 48] + "pos": [260, 190], + "size": [190, 48] }, { "id": "goals-check", "type": "backend", "label": "goals-check", "sublabel": "criteria to evidence", - "pos": [600, 340], - "size": [220, 48] + "pos": [530, 190], + "size": [190, 48] }, { "id": "evidence", "type": "messagebus", "label": "evidence", "sublabel": "writes evidence.json", - "pos": [910, 340], - "size": [220, 48] + "pos": [800, 190], + "size": [190, 48] }, { "id": "report", "type": "backend", "label": "report", "sublabel": "writes report.md", - "pos": [290, 490], - "size": [220, 48] + "pos": [1070, 190], + "size": [190, 48] + }, + { + "id": "commit-session", + "type": "backend", + "label": "commit-session", + "sublabel": "stages named paths, commits", + "pos": [260, 340], + "size": [190, 48] + }, + { + "id": "push-branch", + "type": "messagebus", + "label": "push-branch", + "sublabel": "pushes the branch", + "pos": [530, 340], + "size": [190, 48] + }, + { + "id": "guard-not-pass", + "type": "external", + "label": "guard-not-pass", + "sublabel": "cancels the run", + "pos": [40, 426], + "size": [150, 48] }, { "id": "open-pr", "type": "backend", "label": "open-pr", "sublabel": "opens the PR", - "pos": [600, 490], - "size": [220, 48] + "pos": [800, 340], + "size": [190, 48] }, { "id": "build-pr-gate", "type": "security", "label": "build-pr-gate", "sublabel": "gates.build: hitl", - "pos": [910, 490], - "size": [220, 48] + "pos": [1070, 340], + "size": [190, 48] } ], "boundaries": [ { "kind": "region", - "label": "Prepare", - "wraps": ["bootstrap", "slopcheck"], - "pad": 16 - }, - { - "kind": "region", - "label": "Red / green", - "wraps": ["run-build", "implement-review-precheck"], + "label": "Prepare + red / green", + "wraps": ["bootstrap", "slopcheck", "run-build", "implement-review-precheck"], "pad": 16 }, { "kind": "region", - "label": "Verify", - "wraps": ["verification", "goals-check", "evidence"], + "label": "Verify + report", + "wraps": ["verification", "goals-check", "evidence", "report"], "pad": 16 }, { "kind": "region", - "label": "Deliver", - "wraps": ["report", "open-pr", "build-pr-gate"], + "label": "Commit + deliver", + "wraps": ["commit-session", "push-branch", "open-pr", "build-pr-gate"], "pad": 16 } ], @@ -187,10 +207,7 @@ { "id": "packages-ok", "from": "slopcheck", - "to": "run-build", - "fromSide": "bottom", - "toSide": "top", - "via": [[1020, 138], [400, 138]] + "to": "run-build" }, { "id": "loop-done", @@ -203,8 +220,8 @@ "from": "implement-review-precheck", "to": "verification", "fromSide": "bottom", - "toSide": "left", - "via": [[710, 288], [256, 288], [256, 364]] + "toSide": "top", + "via": [[1165, 138], [355, 138]] }, { "id": "to-goals", @@ -223,21 +240,53 @@ "variant": "dashed", "fromSide": "top", "toSide": "top", - "via": [[400, 328], [1020, 328]] + "via": [[355, 178], [895, 178]] + }, + { + "id": "precheck-evidence", + "from": "implement-review-precheck", + "to": "evidence", + "label": "status, hard findings", + "variant": "dashed", + "fromSide": "right", + "toSide": "top", + "via": [[1290, 64], [1290, 158], [895, 158]], + "labelAt": [1215, 158] }, { "id": "to-report", "from": "evidence", "to": "report", - "label": "any verdict", + "label": "any verdict" + }, + { + "id": "to-commit", + "from": "report", + "to": "commit-session", "fromSide": "bottom", "toSide": "top", - "via": [[1020, 438], [400, 438]] + "via": [[1165, 288], [355, 288]] + }, + { + "id": "to-push", + "from": "commit-session", + "to": "push-branch" + }, + { + "id": "not-pass", + "from": "push-branch", + "to": "guard-not-pass", + "label": "not pass", + "variant": "security", + "fromSide": "bottom", + "toSide": "right", + "via": [[625, 450]] }, { "id": "to-open-pr", - "from": "report", - "to": "open-pr" + "from": "push-branch", + "to": "open-pr", + "label": "pass" }, { "id": "to-gate", @@ -252,9 +301,11 @@ "dot": "cyan", "title": "Evidence gate", "items": [ - "evidence writes evidence.json only on a pass verdict", - "open-pr runs whatever evidence concluded", - "evidence_policy ends the run completed only when evidence.json exists" + "evidence writes evidence.json on every verdict", + "evidence reads the pre-check. A hard finding is fail, and a pre-check that did not run is unresolved when nothing else failed", + "report, commit-session and push-branch run on every verdict, so a red build still reaches its branch", + "open-pr runs only on a pass, and guard-not-pass cancels the run on any other verdict", + "A failed push ends the run failed, and evidence_policy is only a backstop" ] } ] diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.light.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.light.png index ae3aa914..9a6c1e7a 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.light.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-build-pipeline.light.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.dark.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.dark.png index 3e68cf2b..74ecedec 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.dark.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.dark.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.html b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.html index 2c879fa5..81b4b99a 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.html +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-pr-review-pipeline.html @@ -5085,7 +5085,7 @@

unic-dlc-pr-review pipeline

- + - + diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.json b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.json index 0828249b..27722f23 100644 --- a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.json +++ b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.json @@ -29,11 +29,27 @@ "focus": ["bootstrap", "guard-not-ready"], "note": "A run whose preconditions fail is cancelled before any check runs." }, + { + "id": "no-merge", + "label": "Cancel paths", + "focus": [ + "bootstrap", + "guard-not-ready", + "pr-check", + "guard-no-open-pr", + "verify-pr-base", + "guard-not-mergeable", + "merge", + "guard-not-merged" + ], + "note": "A path that does not merge on a result ends cancelled. A failed node ends the run failed. No path without an open PR reaches merge." + }, { "id": "happy-path", "label": "Happy path", "focus": [ "bootstrap", + "pr-check", "test", "e2e", "coverage-gate", @@ -70,12 +86,28 @@ "pos": [40, 40], "size": [150, 48] }, + { + "id": "pr-check", + "type": "backend", + "label": "pr-check", + "sublabel": "reads the PR state", + "size": [190, 48], + "pos": [530, 40] + }, + { + "id": "guard-no-open-pr", + "type": "external", + "label": "guard-no-open-pr", + "sublabel": "cancels the run", + "size": [150, 48], + "pos": [40, 126] + }, { "id": "test", "type": "backend", "label": "test", "sublabel": "runs the tests", - "pos": [530, 40], + "pos": [800, 40], "size": [190, 48] }, { @@ -83,7 +115,7 @@ "type": "backend", "label": "e2e", "sublabel": "runs e2e if declared", - "pos": [800, 40], + "pos": [1070, 40], "size": [190, 48] }, { @@ -91,7 +123,7 @@ "type": "backend", "label": "coverage-gate", "sublabel": "checks coverage", - "pos": [260, 190], + "pos": [260, 260], "size": [190, 48] }, { @@ -99,7 +131,7 @@ "type": "backend", "label": "uat-prep", "sublabel": "writes qa-checks.md", - "pos": [530, 190], + "pos": [530, 260], "size": [190, 48] }, { @@ -107,39 +139,55 @@ "type": "security", "label": "uat-gate", "sublabel": "gates.qa: hitl", - "pos": [1070, 190], + "pos": [1070, 260], "size": [190, 48] }, { "id": "verify-pr-base", "type": "backend", "label": "verify-pr-base", - "sublabel": "checks the PR base", - "pos": [260, 340], + "sublabel": "re-reads PR state, base", + "pos": [260, 410], "size": [190, 48] }, + { + "id": "guard-not-mergeable", + "type": "external", + "label": "guard-not-mergeable", + "sublabel": "cancels the run", + "size": [150, 48], + "pos": [40, 410] + }, { "id": "merge-gate", "type": "security", "label": "merge-gate", "sublabel": "gates.qa: hitl", - "pos": [1070, 340], + "pos": [1070, 410], "size": [190, 48] }, { "id": "merge", "type": "backend", "label": "merge", - "sublabel": "merges the PR", - "pos": [260, 430], + "sublabel": "merges, reports merged", + "pos": [260, 500], "size": [190, 48] + }, + { + "id": "guard-not-merged", + "type": "external", + "label": "guard-not-merged", + "sublabel": "cancels the run", + "size": [150, 48], + "pos": [40, 500] } ], "boundaries": [ { "kind": "region", "label": "Prepare + check", - "wraps": ["bootstrap", "test", "e2e"], + "wraps": ["bootstrap", "pr-check", "test", "e2e"], "pad": 16 }, { @@ -157,11 +205,27 @@ ], "connections": [ { - "id": "bootstrap-test", + "id": "bootstrap-pr-check", "from": "bootstrap", - "to": "test", + "to": "pr-check", "label": "ready" }, + { + "id": "pr-check-test", + "from": "pr-check", + "to": "test", + "label": "open" + }, + { + "id": "no-open-pr", + "from": "pr-check", + "to": "guard-no-open-pr", + "label": "not open", + "variant": "security", + "fromSide": "bottom", + "toSide": "right", + "via": [[625, 150]] + }, { "id": "not-ready", "from": "bootstrap", @@ -180,7 +244,7 @@ "to": "coverage-gate", "fromSide": "bottom", "toSide": "top", - "via": [[895, 138], [355, 138]] + "via": [[1165, 210], [355, 210]] }, { "id": "coverage-gate-uat-prep", @@ -201,13 +265,20 @@ "label": "all_done", "fromSide": "bottom", "toSide": "top", - "via": [[1165, 288], [355, 288]] + "via": [[1165, 358], [355, 358]] }, { "id": "verify-pr-base-merge-gate", "from": "verify-pr-base", "to": "merge-gate", - "label": "hitl, base_ok", + "label": "hitl, mergeable", + "variant": "security" + }, + { + "id": "not-mergeable", + "from": "verify-pr-base", + "to": "guard-not-mergeable", + "label": "not mergeable", "variant": "security" }, { @@ -217,7 +288,14 @@ "label": "all_done", "fromSide": "bottom", "toSide": "right", - "via": [[1165, 454]] + "via": [[1165, 524]] + }, + { + "id": "not-merged", + "from": "merge", + "to": "guard-not-merged", + "label": "not merged", + "variant": "security" } ], "cards": [ @@ -225,8 +303,9 @@ "dot": "emerald", "title": "Merge condition", "items": [ - "merge runs only when test is pass, e2e and coverage-gate are not fail, and base_ok is true", - "merge marks the linked items resolved" + "merge runs only when test is pass, e2e and coverage-gate are not fail, the PR is open, and base_ok is true", + "guard-not-mergeable cancels on the exact negation of that condition, and merge-gate asks only when merge would run", + "merge marks the linked items resolved, and guard-not-merged cancels when the merge did not land" ] }, { diff --git a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.light.png b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.light.png index d3be4f6f..b48d2901 100644 Binary files a/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.light.png and b/apps/claude-code/unic-archon-dlc/docs/architecture/20260925-unic-dlc-qa-pipeline.light.png differ diff --git a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-build.md b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-build.md index a6a3da56..5a3e0e82 100644 --- a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-build.md +++ b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-build.md @@ -17,7 +17,7 @@ each slice carrying its `acceptance_criteria` + `test_command`). There is **no g ## What this workflow does 1. **bootstrap** — parse the slug from `$ARGUMENTS`, read `.archon/unic-dlc.config.yaml` - (`artifacts_dir`, `gates.build`, `build.*`, `project.branching`, and the whole `sdlc_needs` block) + (`artifacts_dir`, `gates.build`, `build.*`, and the whole `sdlc_needs` block) and confirm `issues.json` exists. It also runs `sdlc_needs.install` once for the whole run and reports whether it did. It resolves **no** repository: `docs/agents/issue-tracker.md` § Addressing names it, and the nodes @@ -42,6 +42,16 @@ each slice carrying its `acceptance_criteria` + `test_command`). There is **no g There is no REFACTOR phase: the `tdd` Method puts refactoring in the review stage, so it reaches the code through `/unic-archon-dlc:pr-review`'s Standards axis instead (ADR-0023 §7, #281). + After the loop, **implement-review-precheck** runs the `code-review` Method once over the whole + diff and posts nothing. Beside its prose `findings`, it reports `status` (`ran` or `blocked`), + `blocked_reason`, `hard_findings` and `notes`. A hard finding is a breached documented standard, + or an acceptance criterion that is missing or implemented wrong, and it quotes the standard or the + spec line, or carries an empty citation when it has none. A baseline smell, scope creep and any + other judgement call are notes. The pre-check reports `blocked` when the review could not run: + no merge-base resolves, the `Agent` tool is unavailable, the diff is empty, or one of the two + axes returned no result. `evidence` reads these fields, so the pre-check is a gate + input. + 4. **verification** — the `sdlc_needs.test` suite, the `sdlc_needs.e2e` suite when that key is declared, a stub scan (TODO/FIXME/empty-return/`pass`) on the diff, and `sdlc_needs.coverage` against the threshold when one is configured. `test` reports `pass`, `fail` or `unresolved`, and a @@ -53,29 +63,57 @@ each slice carrying its `acceptance_criteria` + `test_command`). There is **no g implementation evidence. 6. **evidence** — prints a three-state verdict (`pass` / `fail` / `unresolved`) as JSON, and writes - `$ARTIFACTS_DIR/evidence.json` only when `verification` and `goals-check` both report - `passed: true` with an empty `failures` list **and** the test outcome is `pass`. An unresolved test - is not an absence of failures. It mirrors the file to - `//evidence.json`. The workflow-level `evidence_policy: { required: true }` - refuses terminal `completed` when that file is absent, so a red suite or an uncovered acceptance - criterion fails the run closed. A script node, never a prompt - ([ADR-0034](../adr/0034-evidence-gate-deterministic-writer.md)). The run still continues - to `report` and `open-pr` on a withheld verdict, so you get the report and the PR — it is the run - _status_ the engine refuses. + `$ARTIFACTS_DIR/evidence.json` on **every** verdict. The verdict is `pass` only when + `verification` and `goals-check` both report `passed: true` with an empty `failures` list, the + test outcome is `pass`, **and** the pre-check reports `ran` with no hard finding. An unresolved + test is not an absence of failures. A pre-check hard finding makes the verdict `fail`, even when + its citation is empty or its category is unknown. A pre-check that is `blocked`, reports any other + status, or cannot be read makes it `unresolved` when nothing else failed. The file names the + verdict and lists the failures behind it, so it tells you what failed. It also carries + `precheck_status`, `precheck_blocked_reason`, `hard_findings`, `precheck_notes` and + `precheck_read_error`, which names why the node could not read the pre-check, or is empty. The + node reads the pre-check's four fields through `with:` bindings, never through its source. A + measurement in ADR-0034 on Archon 0.12.1 put a quote, a backslash, a backtick, a newline and + `${` in the pre-check's text, and none of them stopped the node. ADR-0034 records the size limit + of a binding. The node mirrors `evidence.json` to + `//evidence.json`. A script node, never a prompt + ([ADR-0034](../adr/0034-evidence-gate-deterministic-writer.md)). The workflow-level + `evidence_policy: { required: true }` still refuses terminal `completed` without the file. That is + now a backstop for `evidence` ending without writing it + ([ADR-0040](../adr/0040-build-verdict-decides-the-pull-request.md)). 7. **report** — writes `//report.md`, led by a block naming every check that came - back `unresolved` and whether an install ran, then what was built, the matrix, test outcomes, - decisions/ADRs and tech debt. - -8. **open-pr → build-pr-gate** — stages an explicit list of **named paths** (source, tests, `PRD.md`, - `issues.json`, `report.md`, `build-state.json`, `evidence.json` when the gate wrote it, and any - drafted ADR), confirms with `git status --porcelain` that nothing else is staged, opens a PR - against the derived repository with - base `develop`, then gates it. `build-state.json` is committed here and **only** here — never during - a loop iteration — so the loop's anti-cheat record survives `/cleanup`. The gate is **HITL by default** - and honours `gates.build`: skipped when `afk` (the PR is still opened). On **reject**, a - verify-and-fix pass runs from the reviewer's feedback and the gate re-pauses — it does **not** rebuild - from scratch. + back `unresolved`, whether an install ran, and a pre-check that did not run. What was built + follows, led by a warning when the pre-check reported hard findings. Then come the matrix, test + outcomes, decisions/ADRs and tech debt. It runs on every verdict. + +8. **commit-session → push-branch** run on **every** verdict. `commit-session` stages an explicit + list of **named paths** (source, tests, `PRD.md`, `issues.json`, `report.md`, `build-state.json`, + `evidence.json`, and any drafted ADR), confirms with `git status --porcelain` that nothing else is + staged, and commits. `build-state.json` is committed in `commit-session` and **only** there, never + during a loop iteration, so the loop's anti-cheat record survives `/cleanup`. `push-branch` pushes + the branch to `origin` and reports the branch and the commit. It refuses to push from a detached + HEAD or from the run's base branch, and when a Session file `commit-session` staged (`PRD.md`, `issues.json`, `report.md`, `build-state.json`, `evidence.json`) is + not committed at HEAD. If it refuses or the push fails, the run ends `failed`, because no work + reached the remote. `evidence` declares `always_run` and `commit-session` does not, so a resumed + run re-runs `evidence` after the commit. `push-branch` refuses that run when the rewritten + `evidence.json` differs from the committed one. Start a new run instead. + +9. **guard-not-pass** or **open-pr → build-pr-gate** are two siblings after `push-branch`, and the + verdict picks one: + - A verdict other than `pass` (`fail` or `unresolved`): `guard-not-pass` ends the run + `cancelled`, and /build opens no pull request. Its message names the verdict, the pushed branch and + commit, and the path of `report.md` and `evidence.json` on that branch. Read those to see what + failed. + - A `pass` verdict: `open-pr` opens a PR from the pushed branch against the repository the tracker + contract names, with the run's base branch, Archon's `$BASE_BRANCH`, as its target. + `/unic-archon-dlc:setup` reports which branch that is. It links the PR to the slices' work items. + Its description lists `precheck_notes` from `evidence.json` under `## Pre-check notes`. + If the contract's repository is not `origin`, the pushed branch is not there, and `open-pr` + prints `BLOCKED:` with the tool's error. The gate is + **HITL by default** and honours `gates.build`: skipped when `afk`, and the PR stays open. On + **reject**, a verify-and-fix pass runs from the reviewer's feedback and the gate re-pauses. It + does **not** rebuild from scratch. ## Prerequisites @@ -84,7 +122,9 @@ each slice carrying its `acceptance_criteria` + `test_command`). There is **no g - `.archon/unic-dlc.config.yaml` is present (from `/unic-archon-dlc:setup`). - `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` are present — this repository's tracker contract, which names the server, the repository, the work-item scope and every role. -- Archon ≥ 0.7.0 ([ADR-0033](../adr/0033-archon-070-schema-target.md)). +- Archon ≥ 0.7.0 ([ADR-0033](../adr/0033-archon-070-schema-target.md)). `/build` needs Archon ≥ 0.10.0, + because `evidence` reads the pre-check through a script node's `with:` bindings + ([ADR-0034](../adr/0034-evidence-gate-deterministic-writer.md)). On an older engine the binding does not arrive, so no build ends `pass`. Whether such an engine loads the Box at all is not measured. ## Runs diff --git a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-explore.md b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-explore.md index 4b806fe2..c5bf747a 100644 --- a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-explore.md +++ b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-explore.md @@ -54,7 +54,7 @@ a prior baton. findings.md and the verdicts, with the AI disclaimer. It reads the tracker contract in `docs/agents/` for the server, the repository, the work-item scope its idempotency search filters on, and the value and axis of every role - ([ADR-0024](../adr/0024-triage-intake-on-ramp.md), amended). + ([ADR-0024](../adr/0024-triage-intake-on-ramp.md)). Runs **before** the gate so the durable output survives a "discard". 6. **spike-branch-gate** — **HITL by default** (`gates.explore`); skipped in AFK. APPROVE → preserve the diff --git a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-pr-review.md b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-pr-review.md index 91204407..3fb831bd 100644 --- a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-pr-review.md +++ b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-pr-review.md @@ -9,8 +9,8 @@ docs pages, the PR description, and `PRD.md`), runs **one review node** that hos Method's own **two-axis fan-out** — **Standards** (repo standards plus the twelve-item Fowler smell baseline) and **Spec** (the diff against the originating intent) — synthesises the findings, **reconciles them against the prior iteration** -(new / still-present / fixed / regressed), and — after a config-gated human confirm — posts or updates a -single structured **summary comment** plus **inline comments** on the current PR. +(new / still-present / fixed / unverified / regressed). After a config-gated human confirm, it posts or +updates a single structured **summary comment** plus **inline comments** on the current PR. Generic and **self-contained**: it harvests `unic-pr-review`'s review learnings (confidence rubric, structured summary, hidden-marker idempotency, conditional spawn table, two-surface posting) **without @@ -33,17 +33,26 @@ precondition** — intent is composed from whatever sources resolve. 1. **bootstrap** — parse the slug from `$ARGUMENTS`, read `.archon/unic-dlc.config.yaml` (`artifacts_dir`, `gates.pr-review`, `pr-review.confidence_threshold`, `pr-review.inline_comments`, - `docs.*`, `project.branching`, and the whole `sdlc_needs` block). It runs `sdlc_needs.install` once + `docs.*`, and the whole `sdlc_needs` block). It runs `sdlc_needs.install` once for the whole run and reports whether it did — this Box runs no check by instruction, and it installs anyway, so that a sub-agent deciding it needs to run something fails on that decision rather than on which worktree the run drew. It resolves **no** repository: `docs/agents/issue-tracker.md` § Addressing names it, and `prep` and `post` read that file themselves. Missing slug or config cancels cleanly. -2. **prep** — identify the open PR + its description; compute the diff and **categorise** the changed - files (reported for context; the categories gate nothing); compose **one Intent Brief** from the linked work items, the referenced - docs pages, the PR body, and `PRD.md` (recording any **contradictions across sources**); and detect the - **prior review iteration** by its hidden marker. Writes everything to `//pr-review/`. +2. **prep** — identify the open PR + its description. Compute the diff from the merge-base with the + PR's own target branch. When a query confirms there is no PR, or the tracker exposes none, use + Archon's `$BASE_BRANCH` instead. `/unic-archon-dlc:setup` reports which branch that is. When the two + differ, the summary carries a warning. prep fetches the diff base first. When the tracker query fails, + or the diff base still does not resolve, `guard-no-diff-base` cancels the run. prep also + **categorises** the changed files. The categories are reported for context and gate nothing. It + composes **one Intent Brief** from the linked work items, the referenced docs pages, the PR body and + `PRD.md`, and records any **contradictions across sources**. It detects the **prior review + iteration** by its hidden marker. It records the commit this run reviews, from `git rev-parse HEAD`, + and reads back the head the prior iteration recorded in its summary. It reports no prior head when + the prior summary carries none, which is the case for one an older Box posted, or when the clone + cannot reach that commit after a fetch, as after a rebase or a force push. It writes everything to + `//pr-review/`. 3. **review** (one node, fresh) — hosts the `code-review` Method's own two-axis fan-out. The Method spawns its two sub-agents itself: **Standards** (repo standards + the twelve-item Fowler smell @@ -54,22 +63,90 @@ precondition** — intent is composed from whatever sources resolve. merged or reranked. This replaced seven hand-written aspect nodes and their spawn gates: re-implementing the Method's own step 4 as Archon nodes is what [ADR-0030](../adr/0030-harness-hosts-methods.md)'s structural bar forbids. + `review` first deletes the `review.json` an earlier iteration left, and every stop after that + writes a new one. Each sub-agent ends its report with every command it ran other than reading files + and git, and the outcome it observed. A check is a command that runs the project's toolchain to + verify the code or starts a dev server. `review` records only the checks and drops every other + command. Each check goes into the `reported_checks` array of `review.json` as one entry per need it + serves, with the axis, the need and the reported outcome, which names no command or tool. `review` + judges the need from what the command does, never by matching the command's text against a value in + `sdlc_needs`, and records a check that serves no declared need as `undeclared`. It never records + `install`. It then removes the commands section from both reports. The Intent Check keeps an + acceptance-criterion verdict but removes a check result from it, because a check result appears in + the summary only in the checks block. The array is empty when no sub-agent reported a check. + `review.json` also carries `subagents_ran`, which is true when at least one review sub-agent + started, on every path, a stop included. It also carries `blocked`, which is empty for a review + that ran to the end and holds the printed sentence for a stop. A review is blocked when `blocked` + is not empty or `subagents_ran` is not true. 4. **synthesize** — merge + dedupe this run's findings, bucket by severity, assemble the summary - sections + Intent Check + "What's good". + sections + Intent Check + "What's good". It states no check result as a fact in any section, except + in the body of a finding raised from a failed check. When `review.json` is missing, it stops and + writes no findings, and `reconcile` then stops without a summary. When the review is blocked, it + writes an empty findings list and a callout that names the `blocked` sentence, with no severity + section, so the reader sees that the review stopped. `reconcile` then rules no prior finding + `fixed`, and every unmatched open or unverified prior is `unverified`. Its reply on such a thread + says that the review stopped before it finished, so the finding could not be checked. 5. **reconcile** — the re-review coordinator: classify each finding against the prior iteration - (new / still-present / fixed / regressed), compute the "since iteration N−1" delta, and finalise the - summary comment (with the `Iteration N` marker + footer) and the inline-comment plan. + (new / still-present / fixed / unverified / regressed), compute the "since iteration N−1" delta, and + finalise the summary comment (with the `Iteration N` marker, the head marker and the footer) and the + inline-comment plan. A prior finding that this run's review omits is `fixed` only when its file + changed between the prior iteration's recorded head and this one, by `git diff --name-only` between + the two heads. Otherwise it is `unverified`. That happens when its file did not change, when it is + scoped to the pull request, a work item or the repository, or when there is no usable prior head. + The PR's diff against its base is never evidence for a `fixed`. When there is no usable prior head, + the summary says why and asks the developer to check each `unverified` finding by hand. An anchored + finding that has retired stays retired on later iterations. `prep` reads it as fixed when the prior + summary does not show its hash and its thread is resolved or carries a `fixed` reply marker. + Above every finding, in every iteration, the summary carries the block "Checks this review was not + asked to run". The block states that this Box instructed none of the project's declared checks, so + nothing in the review verifies a claim that a build, lint, type-check or test passes. It names the + declared `sdlc_needs` keys, `install` excluded, by key and never by value, or says that the project + declares no check. It says that a review sub-agent may still have run one of them on its own, and + leaves that sentence out only when `review.json` says `subagents_ran: false`. It renders each + `reported_checks` entry as that sub-agent's claim, naming the need and the outcome and never the + command, and it ends with `bootstrap`'s install report, verbatim. When `reconcile` cannot read + `review.json`, or the file lacks `subagents_ran` or `reported_checks`, the block keeps the sentence + on sub-agents, renders no reported check, and says that `review` left no readable report of what its + sub-agents ran. The block states what the Box instructed, not what ran + ([ADR-0037](../adr/0037-config-declares-sdlc-needs.md) §8). 6. **review-gate** — **HITL by default** (`gates.pr-review`); skipped in AFK. Shows the composed summary, the finding counts, and any contradiction warnings; APPROVE to post, REJECT to halt without posting. + When `reconcile` stopped without a summary, the gate message names the sentence it printed, from + its `stopped` output, and says that its counts are zeros and that APPROVE posts nothing. When the + review is blocked and `reconcile` wrote a summary, `stopped` holds the `blocked` sentence, and the + gate message says that the summary carries a stop callout and rules no prior finding fixed. 7. **post** — post/update the **summary comment** (matched by the `` marker, never author identity) and, when `inline_comments` and the tracker supports inline threads, - reconcile **inline comments** per finding (still-present → update, fixed → resolve, regressed → - reopen, new → create). A tracker whose registered skill cannot comment on a file and line degrades - to summary-only. + reconcile **inline comments** per finding. It updates a still-present thread, resolves a fixed one, + reopens a regressed one and opens a thread for a new finding. On an unverified thread it posts one + reply asking the author to check the finding by hand, and leaves the status as it is. It reads each + thread's status first. When a thread is resolved and its finding is reported again, `post` reopens + it first and then replies, whoever resolved it. Every reply carries a hidden + marker with the thread's finding hash and the verdict. `post` skips a reply when the thread's latest + marker already holds both. So a rerun after a `post` that failed halfway, or another iteration on the + same commit, repeats no reply. `post` posts nothing when `SESSION/comment.md` is missing or belongs + to another run. A tracker whose registered skill cannot comment on a file and line + degrades to summary-only. + + These limits apply: + + - On the first iteration after an upgrade, the prior summary carries no head marker. `post` then + posts nothing on a thread whose finding retires, because the older Box's resolved reply carries no + marker and a second reply could resolve a thread a person reopened on purpose. + - A thread that an older Box resolved, and whose hash the prior summary no longer shows, reads as + fixed, because it is resolved. It stays retired and gets no further reply. If a person reopened such a + thread, it carries no `reply=` marker, and `prep` reads it as open. + - A killed run can post the fixed reply and die before it resolves the thread. The rerun cannot tell + that thread from one a person reopened on purpose, so it leaves the thread open. A false open is + the safe direction, because it costs the author one click. + - A killed run can reopen a thread and die before it replies. The thread is then open with no new + reply. + - A renamed file appears in the change between the two heads under its new path only. A finding on + the old path is `unverified`. ## Gates & AFK @@ -98,7 +175,6 @@ Read from `.archon/unic-dlc.config.yaml`: | `pr-review.inline_comments` | boolean | `true` | Post inline per-finding comments in addition to the summary (where supported) | | `artifacts_dir` | string | `workflows` | Session artefact home (`//pr-review/`) | | `docs.*` | object | — | Composed to fetch the docs pages an intent source cites | -| `project.branching` | string | `gitflow` | `gitflow` → base `develop`; else `main` (for the merge-base diff) | The tracker itself is **not** configured here. `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` carry the server, the repository, the work-item scope and the roles. diff --git a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-qa.md b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-qa.md index 5b708e34..489ef212 100644 --- a/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-qa.md +++ b/apps/claude-code/unic-archon-dlc/docs/boxes/unic-dlc-qa.md @@ -1,11 +1,13 @@ --- -description: Run the unic-archon-dlc QA workflow — test suite, e2e, coverage gate, UAT sign-off, PR-base check, and merge. UAT findings become agent-ready tracker issues. +description: Run the unic-archon-dlc QA workflow — PR check, test suite, e2e, coverage gate, UAT sign-off, PR-base check, and merge. UAT findings become agent-ready tracker issues. --- # /unic-dlc-qa -Runs the `qa` box: test suite → end-to-end tests → coverage gate → UAT sign-off → PR-base +Runs the `qa` box: PR check → test suite → end-to-end tests → coverage gate → UAT sign-off → PR-base verification → merge. The test suite is the floor: the merge gate cannot advance without a pass. +Every run that does not merge on a result ends `cancelled`, with a message that names why. Only a +merge that landed ends `completed`. A node that fails ends the run `failed`. It is also an **issue-producing on-ramp** — when UAT surfaces a defect, the workflow files it directly as an **agent-ready tracker issue** (feeding `/tickets`) instead of only halting. Ported to the key-discriminated Archon node schema ([ADR-0011](../adr/0011-archon-schema-target.md)); design in @@ -30,44 +32,89 @@ from), which `/build` implemented against. nodes that reach the tracker read that file themselves. Missing preconditions cancel cleanly with a "run /build first" message. -2. **test** — run `sdlc_needs.test`. Reports `pass`, `fail` or `unresolved`; a need this project does +2. **pr-check** — read the state of the PR for the current branch through the tracker contract, + before any test runs. It reports `pr_state`: `open`, `merged`, `closed`, `none` or `unknown`. On + any state but `open`, `guard-no-open-pr` cancels the run before `test` starts, with the message for + its case: + + | `pr_state` | Message | + | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | `none` | No PR was ever opened for this branch. Fix the build and re-run /build. | + | `merged` | The PR for this branch is already merged. Nothing to do. | + | `closed` | The PR for this branch was closed without merging. Reopen it, or abandon the work. | + | `unknown` | The PR state could not be read, with the cause. A failed query gives the error as received and says to check access to the tracker and re-run /qa. A missing or unusable `docs/agents/issue-tracker.md` names the file and says to add it with `/unic-archon-dlc:setup`. | + +3. **test** — run `sdlc_needs.test`. Reports `pass`, `fail` or `unresolved`; a need this project does not declare is `unresolved`, never a skip. This is the **floor**: the merge below cannot advance without a `pass` here. -3. **e2e** — run `sdlc_needs.e2e`. Same three states, and `unresolved` here does not hold the merge. +4. **e2e** — run `sdlc_needs.e2e`. Same three states, and `unresolved` here does not hold the merge. Reports a verdict (does not hard-fail). -4. **coverage-gate** — run `sdlc_needs.coverage` and compare the figure to the resolved threshold. +5. **coverage-gate** — run `sdlc_needs.coverage` and compare the figure to the resolved threshold. Same three states: a null key or a null threshold is `unresolved`, not a skip, and it does not hold the merge either. -5. **uat-prep** — build a numbered UAT checklist (`AC-1`, `AC-2`, …) from the PRD acceptance criteria + +6. **uat-prep** — build a numbered UAT checklist (`AC-1`, `AC-2`, …) from the PRD acceptance criteria + `/build`'s goals-check matrix in `report.md`. It **writes** the checklist to `//qa-checks.md` as well as printing it, led by a block naming every check that came back `unresolved` and whether an install ran. This Box has no report node, so a block only printed would die with the worktree. -6. **uat-gate** — **HITL by default** (`gates.qa`); skipped in AFK. The human reviews the checklist and +7. **uat-gate** — **HITL by default** (`gates.qa`); skipped in AFK. The human reviews the checklist and the test/e2e/coverage results. On **reject**, each failing item is **filed as an agent-ready tracker item** (through the tracker contract in `docs/agents/`, with the inlined finding-capture brief shape, blocked-by honesty, and the `> *This was generated by AI during QA.*` disclaimer), then the - gate re-pauses so a second reject halts the run. - -7. **verify-pr-base** — confirm the open PR targets the expected base (`develop` on Gitflow, else - `main`), through the tracker contract and naming the repository it gives - explicitly. Reports `base_ok`. - -8. **merge-gate → merge** — **HITL by default** (`gates.qa`); skipped in AFK. On approval (or in AFK) - the PR is merged via the configured tracker and the feature branch is cleaned up on Gitflow. The - merge **fail-closes**: it never runs on a failed test, e2e or coverage, on a wrong base, or on a - `test` need that could not run — `test` is the floor and its clause requires a `pass`, while e2e and - coverage only have to avoid a `fail`. + gate re-pauses so a second reject halts the run. A red check still reaches this gate, so the human + can file it as a ticket. + +8. **verify-pr-base** — read the PR state again, because a PR can be merged or closed during UAT, and + confirm the open PR targets the run's base branch, Archon's `$BASE_BRANCH`, through the tracker + contract and naming the repository it gives explicitly. `/unic-archon-dlc:setup` reports which + branch that is. It reports `pr_state` and `base_ok`. `base_ok` means only "the open PR targets + `$BASE_BRANCH`", so it is `false` whenever no PR is open, on a mismatch, and on a failed query. + It also reports `blockers`, with one sentence for every reason the merge cannot run. Its prompt + references the output of `uat-prep`, so a failed `uat-prep` fails this node and the run. `merge-gate` + never asks for a merge without a UAT. + + `guard-not-mergeable` then cancels the run when any of these holds: `test` is not `pass`, `e2e` or + `coverage-gate` is `fail`, the PR is not open, or `base_ok` is not `true`. Its message joins the + sentence for every cause that held. A PR that is not open gets + the message for its case from the table in step 2. A wrong base reads "The PR targets `` + but `` was expected", never that the PR is missing. The message ends with the raw + results. An `unresolved` e2e or coverage result is not a cause and does not block the merge. + +9. **merge-gate → merge** — **HITL by default** (`gates.qa`); skipped in AFK. `merge-gate` asks for + approval only when `merge` would run. Its `when` is the HITL check plus every condition of the + `when` on `merge`. On approval (or in AFK) the PR is merged via the configured tracker and the + feature branch is cleaned up on Gitflow. The merge **fail-closes**: it never runs on a failed test, + e2e or coverage, on a PR that is not open, on a wrong base, or on a `test` need that could not run. + `test` is the floor and its clause requires a `pass`, while e2e and coverage only have to avoid a + `fail`. + + `merge` reports `merged`. When `merge` cannot confirm a merge, `guard-not-merged` cancels the run + with the reason. The reasons are: + + - The tracker contract is missing. + - The tracker cannot merge. The reason gives the manual steps. + - The branch policy refuses the merge. + - The merge call errors. + - The merge call returned success, but the PR state could not be read again. The reason tells + the operator to check the PR and, if it merged, to write `resolved` by hand. A re-run of `/qa` + reads the PR as merged and does nothing. + - The merge call returned success, but the PR is not merged yet, as under a merge queue or + auto-merge. The reason tells the operator to watch the PR and to write `resolved` by hand once + it merges. + + In every one of these cases no item takes `resolved`. The line `merge: QA for "" complete … ✓` appears only after a merge that + landed. If writing `resolved` failed on some items, the line names them and does not count them. + Such a run still ends `completed`, because `/qa` never reverts a merge that landed. ## Gates & AFK `gates.qa` (default `hitl`) governs **both** the UAT gate and the merge gate. Set it to `afk` to run the -whole pipeline unattended — the gates are skipped and a clean build auto-merges. An unresolved or red test, a red e2e or coverage, or a -mismatched PR base blocks the merge even in AFK (fail-closed `when`). +whole pipeline unattended — the gates are skipped and a clean build auto-merges. An unresolved or red test, a red e2e or coverage, a PR +that is not open, or a mismatched PR base blocks the merge even in AFK (fail-closed `when`), and the run ends `cancelled`. ## Prerequisites @@ -89,7 +136,7 @@ Read from `.archon/unic-dlc.config.yaml`: | `qa.coverage_threshold` | number/null | `null` | Minimum coverage %; falls back to `build.coverage_threshold`; null → unresolved | | `sdlc_needs` | object | nine nulls | What this project's development process needs. A key names a **need**, never a tool. `test`, `e2e` and `coverage` are the check keys this Box reads, and a null one is an **unresolved** check. `install` is not a check: `bootstrap` runs it once if it is declared and reports either way through `install_report`, which has no `unresolved` state | | `artifacts_dir` | string | `workflows` | Session artefact home (`//`) | -| `project.branching` | string | `gitflow` | `gitflow` → base `develop` + branch cleanup; else base `main` | +| `project.branching` | string | `gitflow` | `gitflow` → delete the merged `feature/*` branch after the merge. It names no base branch. The run's base is Archon's `$BASE_BRANCH`, which `/unic-archon-dlc:setup` reports | The tracker itself is **not** configured here. `docs/agents/issue-tracker.md` and `docs/agents/triage-labels.md` carry the server, the repository, the work-item scope and the roles. diff --git a/apps/claude-code/unic-archon-dlc/package.json b/apps/claude-code/unic-archon-dlc/package.json index 9755d0d1..aa7a5401 100644 --- a/apps/claude-code/unic-archon-dlc/package.json +++ b/apps/claude-code/unic-archon-dlc/package.json @@ -1,6 +1,6 @@ { "name": "unic-archon-dlc", - "version": "0.29.0", + "version": "0.36.1", "private": true, "license": "LGPL-3.0-or-later", "type": "module", diff --git a/apps/claude-code/unic-archon-dlc/vendor/mattpocock-skills/README.md b/apps/claude-code/unic-archon-dlc/vendor/mattpocock-skills/README.md index de869521..cbdba21a 100644 --- a/apps/claude-code/unic-archon-dlc/vendor/mattpocock-skills/README.md +++ b/apps/claude-code/unic-archon-dlc/vendor/mattpocock-skills/README.md @@ -15,9 +15,7 @@ the one path every Box and every command reads a Method from. | Commit | `d574778f94cf620fcc8ce741584093bc650a61d3` | | Licence | MIT — see [`LICENSE`](LICENSE) | -**This table is the provenance.** It used to mirror a `METHODS_BUNDLE` constant in -`lib/methods-manifest.mjs`, with a test holding the two together; #381 deleted the plugin's code, so -the mirror and the test are gone and these four values live here alone. Update them in the same commit +**This table is the provenance.** These four values live here alone. Update them in the same commit that replaces the vendored files — nothing else records the tag, and nothing checks that this file still describes what sits beside it. @@ -25,30 +23,28 @@ still describes what sits beside it. `skills/` mirrors the upstream directory layout (`skills///`). Mirroring rather than flattening is what makes a re-vendoring diffable against upstream by eye — which is now the only check -there is. The closure test that caught an upstream relocation went with the manifest (#381), so +there is. The closure test that caught an upstream relocation went with the manifest, so **upgrading this bundle means diffing this tree against the new upstream tag by hand**, in the commit -that moves the pin above. The root `AGENTS.md` § "The quality bar for a prose Box" names that as the -moment the check happens. +that moves the pin above. That diff is the only moment the check happens. Eleven Methods and their sub-files are vendored. `handoff` and `prototype` are deliberately absent: the Boxes name them in prose for a human to run and never read their files. Which Box reads which Method is recorded once, in the table under [`../../README.md` § Dependencies](../../README.md#dependencies). -`skills/**` is listed in the repository's root `.prettierignore` so the pinned text stays -byte-identical to upstream. This file is not, so it is Prettier-formatted like the rest of the repo. +Keep `skills/**` out of every formatter, so the pinned text stays byte-identical to upstream. This +file is not pinned text, so a formatter may touch it. ## Licence -Upstream is MIT. [`LICENSE`](LICENSE) is the upstream licence file, verbatim; `/setup` verifies its -SHA-256 against `METHODS_BUNDLE.licenceSha256` before installing anything. MIT requires only that +Upstream is MIT. [`LICENSE`](LICENSE) is the upstream licence file, verbatim; `/setup` checks that +it is present before installing anything. MIT requires only that the copyright notice and the permission notice accompany copies, and that file carries both, so no `NOTICE` file is needed. The `LICENSE` file is maintainer-owned: never create, copy, or delete it. ## Re-vendoring Fetch with `git clone --depth 1 --branch https://github.com/mattpocock/skills` into a temp -directory and copy each manifest entry's directory across. Never use `npx skills` — its +directory and copy each vendored Method's directory across. Never use `npx skills` — its `owner/repo@tag` form silently resolves `main`. -Then update `METHODS_BUNDLE` (tag, commit, and `licenceSha256` if upstream's `LICENSE` changed) and -this file's provenance table, and re-run `pnpm --filter unic-archon-dlc test`. +Then update this file's provenance table. diff --git a/docs/research/2026-10-21-archon-workflow-extensibility.md b/docs/research/2026-10-21-archon-workflow-extensibility.md new file mode 100644 index 00000000..b6cbeca3 --- /dev/null +++ b/docs/research/2026-10-21-archon-workflow-extensibility.md @@ -0,0 +1,173 @@ +# Archon workflow extensibility: contributing to a workflow you do not own, 2026-10-21 + +Research for [Practice Packs must stack](https://github.com/unic/unic-agents-plugins/issues/520), under the map [#373](https://github.com/unic/unic-agents-plugins/issues/373). + +Researched 2026-10-21. Question: how independently installable Practice Packs can contribute steps, prompts, skills, MCP servers or context to `unic-dlc-*` workflow runs in a Consumer repository, when one plugin owns and installs those YAML files verbatim. + +## Sources and versions + +- Upstream: `coleam00/Archon` on GitHub. Releases listed with `gh release list -R coleam00/Archon`. +- Source and docs read from a clone at tag `v0.12.1` (commit of the tag), with spot checks at `v0.10.1`. Paths below are relative to the repository root. + - Docs: `packages/docs-web/src/content/docs/` (published at https://archon.diy). Abbreviated `docs/` below. + - Source: `packages/workflows/src/`, `packages/providers/src/claude/`, `packages/paths/src/`. + - Changelog: `CHANGELOG.md`, and the GitHub release notes for v0.11.0, v0.11.1, v0.12.0 and v0.12.1. +- Local CLI: `archon` v0.10.1 (Homebrew, git commit `9820785c`). +- Measurements: run on the local v0.10.1 binary in a fresh scratch directory, with `ARCHON_HOME` pointed at a scratch folder so `~/.archon` was not touched. Only `bash:` nodes ran, no AI node. + +Evidence labels used below: **documented** (official docs or changelog), **source** (seen in code at the tag), **measured on 0.10.1**, **not found**, **needs measurement**. + +The repo-local skill at `.claude/skills/archon/` was used as a lead only. One repo fact is now stale, see the last section. + +## 1. Newest release and what changed since v0.10.1 + +The newest release is **v0.12.1, published 2026-10-17** (marked Latest). Releases after v0.10.1 (2026-08-30): v0.11.0 and v0.11.1 (2026-09-25), v0.12.0 and v0.12.1 (2026-10-17). Source: GitHub releases, `CHANGELOG.md` headers `## [0.12.1] - 2026-10-17` and following. + +Changes that touch workflow YAML, nodes, skills, MCP, variables or discovery: + +| Version | Change | Relevance | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| 0.11.0 | `archon plugin install owner/repo[/path][@tag]` installs a **workflow pack** from GitHub; entrypoints run as `owner/pack:entrypoint`; `archon plugin copy` makes an editable project copy (#2767, #3485, #2835, #3486) | New third-party distribution channel for workflows, see Q6 | +| 0.11.0 | `assistants.` in `.archon/config.yaml` is validated on load; a bad `settingSources` entry stops the load (#2582, #3461) | `settingSources` accepts only `project` and `user` | +| 0.11.0 | `thinking:` removed, `effort:` only (#3094) | YAML breakage, not extensibility | +| 0.11.0 | A `workflow:` node can no longer declare `output_format` (#3148) | Sub-run contract owned by the child | +| 0.11.0 | Loop nodes honour provider, model, tool and timeout settings (#3248, #3284) | Matches the repo note on loop `allowed_tools` | +| 0.11.0 | Workflows in a symlinked scope root such as `~/.archon/workflows` run (#3233) | Discovery fix | +| 0.11.1 | A directory holding `archon-plugin.json` is read only by the pack loader (#3505, #3512) | Discovery | +| 0.12.0 | **Claude workflow nodes no longer load the Claude Code plugins installed on the machine.** A node names plugins with `plugins: [name@marketplace]`; it fails before its first turn if a named plugin is missing, or if Claude Code loads a plugin the node did not name. Settings, hooks, permissions and `CLAUDE.md` keep loading (#3574, #3578) | Major: ambient Claude plugins no longer reach nodes | +| 0.12.0 | Same for Codex: only `mcp:` servers and `plugins:` named plugins (#1118, #2843) | | +| 0.12.0 | Optional MCP: `mcp: { path: , optional: true }` runs without servers when the file is missing or the provider lacks MCP (#3994) | A missing MCP file no longer has to fail | +| 0.12.0 | A relative `mcp:` path in a **home** workflow also resolves next to that YAML file (#1604) | Home-scope only | +| 0.12.0 | Nested output refs `$node.output.a.b` (#3616, #3675) | Prompt-text substitution | +| 0.12.0 | Legacy bundled workflows and `.archon/commands/defaults/` removed; only the `sdlc` pack ships. A project or global `archon-review-block.yaml` now replaces the pack's own review block (#3526, #4311) | Shows that `include:` targets are shadowable by name, see Q3 and Q4 | +| 0.12.0 | Old workflow marketplace retired; `archon workflow search/install` removed; use `archon plugin add` (#2376) | | +| 0.12.0 | `plugin install` / `skill install` renamed to `plugin add` / `skill add`; old verbs are deprecated aliases (#4133) | | +| 0.12.0 | Release binaries ship without Claude, Codex, Pi providers; `archon provider add claude` after every upgrade (#3645) | Upgrade step for Consumers | +| 0.12.0 | A run resolves AI configuration from the checkout it was launched from, including uncommitted content (#3827, #3899) | Config-layer behaviour | +| 0.12.0 | `archon state` durable JSON documents; `worktree.postCreate` commands (#3981, #3973) | Possible side channels, see Q6 | +| 0.12.1 | Security only: release binaries no longer load `.env`/`bunfig.toml` from the working directory (#4426, #4427) | None for this question | + +No release since v0.10.1 adds workflow-level or config-level defaults for `skills:` or `mcp:`, an `optional` include, or interpolation in structural fields (**documented**, absent from changelog; **source**, see Q2 and Q5). + +## 2. Templating and interpolation in node fields + +**Prompt-text substitution exists. Structural fields take literals only.** + +Prompt-text surfaces (**documented**, `docs/reference/variables.md`, "Workflow Variables"): + +- Substituted in `command:` file bodies, `prompt:`, `bash:`, `script:`, `loop:`, `loop_group:`, and a `workflow:` node's `input:`. +- Also in AI-configuration text: `systemPrompt:` and `agents..prompt` / `agents..description`. +- Sources: engine variables (`$ARGUMENTS`, `$USER_MESSAGE`, `$WORKFLOW_ID`, `$ARTIFACTS_DIR`, `$TYPED_ARTIFACTS_FILE`, `$STATE_DIR`, `$BASE_BRANCH`, `$DOCS_DIR`, `$CONTEXT` and aliases, loop variables), node outputs `$nodeId.output[.a.b]`, and `$INPUTS.` from a workflow's `inputs:` or an `include:`/`workflow:` node's `with:`. +- Config feeds two of them: `$DOCS_DIR` from `docs.path` and `$BASE_BRANCH` from `worktree.baseBranch` in `.archon/config.yaml`. +- No `$ENV_VAR` substitution in prompt text (**not found** in `variables.md`). In `bash:`/`script:` nodes, user-controlled values and `INPUTS_*` arrive as process environment variables, not spliced text. Variables from `.archon/.env`, `~/.archon/.env` or `config.yaml`'s `env:` section reach `bash:`/`script:` through the process environment (**documented**, 0.12.0 changelog, #3509, #3514). +- `include:` `with:` values are inserted verbatim at load time and are never expressions (**documented**, `docs/guides/authoring-workflows.md`, "Passing values into an included block"). + +Structural fields (**source**, v0.12.1): + +- `skills`, `plugins` and `allowed_tools` pass straight from the parsed node to the provider: `packages/workflows/src/dag-executor.ts`, around lines 2125-2131 (`skills: node.skills`, `plugins: node.plugins`, `allowed_tools: node.allowed_tools`). Every `substituteWorkflowVariables` call in that file targets prompt, script or input text, none of these fields. +- The `skills` schema is `z.array(z.string().min(1))` (`packages/workflows/src/schemas/dag-node.ts:228`). +- `mcp:` is a literal path or `{ path, optional }`. Relative paths resolve against the run's checkout; only home-scoped workflows fall back to the YAML's own directory (**documented**, `docs/guides/mcp-servers.md`, lines 95-100). Inside the MCP JSON file, only `env` and `headers` values expand `$VAR`/`${VAR}` from Archon's process environment; `command`, `args` and `url` are untouched (**documented**, same file, "Environment Variable Expansion"). Whether `config.yaml` `env:` entries feed that expansion: **needs measurement**. +- `include:` takes a literal workflow name, "no `include: $something`" (**documented**, "Non-goals (Phase 1)"). +- `model:` has an indirection layer: tiers (`large`/`medium`/`small`) and aliases (`@name`) defined in `.archon/config.yaml` or a per-run `--config` file (**documented**, `docs/reference/configuration.md`). This is the only structural field with a config-driven indirection. Nothing equivalent exists for skills or MCP. + +## 3. Composition and override mechanisms + +| Mechanism | Exists | Notes | Evidence | +| ---------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `include: ` | Yes, since before 0.10.1 | Inlines another workflow's nodes at load time as `__`. Target resolves **by `name:`** with precedence bundled < `~/.archon/workflows/` < repo `.archon/workflows/`. `with:` passes values to `$INPUTS.`. Nesting up to 3 levels, cycle-checked. A missing target is a load error that drops the includer | documented (`authoring-workflows.md` "Composing Another Workflow with `include:`"); source (`include-expander.ts:1318`, "include target '…' not found"); measured on 0.10.1 | +| `workflow: ` child run | Yes | Separate governed run at execution time, its own record, gates, artifacts and cost. `input:` or `with:` | documented ("Launching a Separate Governed Run") | +| Extending a base workflow, overriding one named node | No | A composer contributes ordering and gates; it "does not reach inside". No deep access to an included block's nodes | documented ("No deep access"); not found in schema | +| Optional or conditional include target | No | `when:` on an include node gates the block's entry nodes at runtime, but the target must still exist at load | source; documented | +| Named command files | Yes | `command: ` in a legacy (flat) workflow resolves at runtime: repo `.archon/commands/` → `commands.folder` from `.archon/config.yaml` → `~/.archon/commands/` → bundled. First match wins. Duplicate basenames inside one scope are a user error. Packaged workflows (`.archon/workflows///`) resolve commands only from their own `commands/`, with no fallback | source (`executor-shared.ts` `loadCommandPrompt`, `paths/src/archon-paths.ts:362`, same order at v0.10.1); documented (`global-workflows.md`, `configuration.md` "Command Folder Detection") | +| Workflow packs (`archon plugin add`) | Since 0.11.0 | See Q6 | documented | + +Two consequences for Practice Packs: + +1. **An include slot is shadowable, but holds one occupant.** If a `unic-dlc-*` Box contains `include: practice-hook`, whichever single file supplies `name: practice-hook` at the highest scope is inlined. Measured on 0.10.1: a home stub (`~/.archon/workflows/practice-hook.yaml`, one node) ran when alone; a project file with the **same filename** replaced it (both project nodes ran, the home stub did not). +2. **Two contributors cannot share a slot.** Measured on 0.10.1: a project file `acme-practice.yaml` declaring `name: practice-hook` next to the home `practice-hook.yaml` produced `Duplicate workflow name 'practice-hook' … same-name files do not override each other (overrides are by filename)`. Both files were dropped, and the includer then failed to load with `include target 'practice-hook' not found`. A Pack that picks a colliding `name:` disables the Box that includes it. + +## 4. Discovery precedence and shadowing + +Documented (`docs/guides/global-workflows.md`, "Load Priority"; `docs/reference/configuration.md`, line 33): + +1. Bundled (lowest). +2. Home: `~/.archon/workflows/`, `~/.archon/commands/`, `~/.archon/scripts/` (or `$ARCHON_HOME/…`). +3. Repo: `/.archon/workflows/`, `.archon/commands/`, `.archon/scripts/` (highest). + +Details from source (`packages/workflows/src/workflow-discovery.ts`, header comment lines 10-21 and discovery around lines 1295-1385): + +- **Workflows override by filename** (basename, e.g. `unic-dlc-build.yaml`), not by `name:`. Two surviving files with the same `name:` but different filenames are both dropped with a duplicate-name error (lines 911-940). Measured on 0.10.1, see Q3. +- Packaged layouts: a filename collision across packaged folders inside one scope drops both (lines 470-490). +- Commands: first match in repo → `commands.folder` → home → bundled (Q3). +- Installed packs sit outside this precedence. A project or global workflow whose `name:` equals an installed qualified name is not loaded ("that name belongs to an installed plugin"), so no project or global file can shadow an installed workflow (lines 1170-1186; test `installed-packs.test.ts`, "a project workflow cannot shadow an installed name"). + +For the `unic-dlc-*` Boxes: `unic-archon-dlc`'s `/setup` installs them into the Consumer's repo `.archon/workflows/` (`apps/claude-code/unic-archon-dlc/commands/setup.md:217`), which is the highest legacy scope. So: + +- A home-scope file cannot shadow a Box. +- A Consumer-local file with the same basename is the same path, so writing it overwrites the Box. +- A Consumer-local file with a different basename and the same `name:` drops **both** (duplicate-name error). +- A Box's command files, if it used named `command:` references in flat layout, could be shadowed by a repo `.archon/commands/.md`. The current Boxes use inline prompts. + +## 5. Project-level defaults for node skills or MCP; `skills: all` + +**Not found.** No project, repo or workflow-level default for `skills:`, `mcp:` or `plugins:` exists at v0.12.1. + +- The workflow-level schema (`packages/workflows/src/schemas/workflow.ts`, lines 200-286) has `provider`, `model`, `effort`, `fallbackModel`, `betas`, `sandbox`, `worktree`, `container`, `budget`, `inputs`, `returns` and similar, but no `skills`, `mcp` or `plugins` (**source**). The docs list workflow-level defaults only for `effort`, `fallbackModel`, `betas`, `sandbox` (**documented**, `authoring-workflows.md`, "Workflow-level defaults"). +- `.archon/config.yaml` offers `assistants.claude.settingSources` (eligibility of `project`/`user` sources), not activation. "In workflow nodes, discovery does not activate ambient skills: the node's `skills:` list remains the exact active set, and omission/`[]` selects none" (**documented**, `configuration.md`, "Claude settingSources"). +- **`skills: all` is not fixed.** The Claude provider sets `options.skills = nodeConfig.skills ?? []` (`packages/providers/src/claude/provider.ts:651`, same at v0.10.1 line 511), and the schema accepts only a string array (**source**). The skills guide calls the per-node list "an exact allowlist" (**documented**, `docs/guides/skills.md`). +- Unknown skill names still warn and are ignored: "Absent from every skills directory → Warning, and the run continues", because built-in and `plugin:skill` names live outside skill directories. A name installed only outside an enabled `.claude/skills/` root is now an **error before spend** (**documented**, `skills.md`, "How Claude handles an unresolved name"; **source**, `provider.ts` lines 661-710). + +## 6. Other routes for third-party contribution + +| Route | What it lets a third party add | Constraint | Evidence | +| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Project `CLAUDE.md` and `.claude/` content through `settingSources` | Context (`CLAUDE.md`), agents, commands, native hooks from `.claude/settings.json` | Loads by default (`['project', 'user']`) on Claude nodes, also after 0.12.0. Skills under `.claude/skills/` are eligible but still need the node's `skills:` list | documented (`authoring-workflows.md` table row `settingSources`; `skills.md`; `hooks.md` "Native Claude hooks and `settingSources`") | +| Native Claude hooks in `/.claude/settings.json` | Input-aware hooks; a `SessionStart` or `PostToolUse` hook can inject `additionalContext` | Claude only. Runs under `settingSources` `project` | documented (`hooks.md` lines 64, 79-95, 162, 201). Whether a `SessionStart` hook's `additionalContext` reaches a workflow node session: **needs measurement** | +| `.claude/rules/*.md` loaded through the `project` source | Path-scoped instructions | Archon docs do not mention rules files | **not found** in Archon docs; **needs measurement** | +| Named skill slots | A Box names `skills: [x]`; a Pack later installs `.claude/skills/x/SKILL.md` | The name is fixed in YAML. Before the Pack is installed, the node warns and runs on (absent from disk) | documented (`skills.md`); matches the 0.10.1 measurement in the brief | +| `plugins: [name@marketplace]` plus `skills: [plugin:skill]` | A Claude Code plugin's agents, hooks, LSP servers, commands; its skills one by one | Since 0.12.0. Hard failure if the named plugin is not installed, and also if Claude Code loads a plugin the node did not name | documented (`authoring-workflows.md` "Plugins"); source (`providers/src/claude/plugins.ts`) | +| MCP config file content | A Pack edits the JSON file a node's `mcp:` path points at | Path is literal. Since 0.12.0 `optional: true` lets the file be absent. Gitignored files need `worktree.copyFiles` to reach a worktree run | documented (`mcp-servers.md`) | +| Archon workflow packs (`archon plugin add owner/repo[/path][@tag]`) | Whole workflows, frozen at a commit | Installed per Archon install, not per repo. Support workflows are reachable only from their own pack; a project workflow cannot `include:` them (test `installed-packs.test.ts:383`). Whether a project workflow may `workflow:`-launch or `include:` another pack's entrypoint by qualified name: **needs measurement** on ≥0.11.0 | documented (`global-workflows.md` "Installed workflow packs", `publishing-plugins.md`); source | +| `inputs:` and `with:` | Caller-supplied values into prompt text | Values only, not structure | documented | +| Runtime file conventions | A prompt tells the agent to read a known path (for example `$DOCS_DIR/…` or a fixed `.archon/…` file) | Not an Archon feature; it is prompt text the Box owner writes | documented (`$DOCS_DIR` from `docs.path`) | +| `archon state` documents, `$STATE_DIR` | Durable data a `bash:`/`script:` node can read | Since 0.12.0 for `archon state`. Data, not structure | documented (0.12.0 changelog #3981; `variables.md`) | +| `worktree.postCreate` in `.archon/config.yaml` | Shell commands in each new worktree | Since 0.12.0. Workflow variables unavailable. Provisioning, not prompts | documented (`configuration.md` lines 352-362) | +| Provider, forge and chat plugins | New providers or forges | Not a route for steps or prompts | documented (0.12.0 changelog) | + +Not found: an extension-point or hook API that lets a separate file add nodes to, or alter nodes of, a workflow it does not own. Composition always runs from the owning workflow outward (`include:`, `workflow:`). + +## Items that need a live measurement + +1. On 0.12.x: whether a `SessionStart` native hook in project `.claude/settings.json` injects `additionalContext` into an Archon Claude node. +2. On 0.12.x: whether `.claude/rules/*.md` load into a node under `settingSources: ['project']`. +3. On ≥0.11.0: whether a project workflow can `include:` or `workflow:` an installed pack's entrypoint by `owner/pack:entry`. +4. Whether `config.yaml` `env:` entries feed `$VAR` expansion inside MCP JSON `env`/`headers`. +5. On 0.12.x: the include-slot behaviour measured on 0.10.1 (filename override, duplicate-name drop). Source at v0.12.1 shows the same logic, so a change is unlikely. + +## Repo facts this research contradicts + +- `.claude/rules/learned.md` (2026-10-01) says "By default a node also loads user-scope plugins". True on 0.10.1. False from **0.12.0**: Claude nodes load no installed Claude Code plugins unless the node names them in `plugins:`, and they fail if one loads unnamed (0.12.0 changelog, #3574, #3578). +- `docs/guides/skills.md` at v0.12.1 confirms the 0.10.1 measurement that an unknown skill name warns and is ignored, and adds an error case for a skill found only outside an enabled `.claude/skills/` root. + +## Source map + +| Claim | Source | +| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| v0.12.1 is latest, 2026-10-17 | `gh release list -R coleam00/Archon`; `CHANGELOG.md` line 10 | +| 0.11.0, 0.12.0, 0.12.1 change list | GitHub release notes for each tag; `CHANGELOG.md` sections `[0.12.1]`, `[0.12.0]`, `[0.11.1]`, `[0.11.0]` | +| Node field table, `skills`, `plugins`, `mcp`, `settingSources` semantics | `docs/guides/authoring-workflows.md` "Node Fields", "Claude SDK Advanced Options", "Plugins" (v0.12.1) | +| No workflow-level `skills`/`mcp`/`plugins` | `packages/workflows/src/schemas/workflow.ts` lines 200-286 (v0.12.1) | +| `skills` raw pass-through | `packages/workflows/src/dag-executor.ts` lines 2125-2131 (v0.12.1) | +| `options.skills = nodeConfig.skills ?? []` | `packages/providers/src/claude/provider.ts:651` (v0.12.1); line 511 (v0.10.1) | +| Unknown-skill warning, unreachable-skill error | `docs/guides/skills.md` "How Claude handles an unresolved name"; `provider.ts` lines 661-710 | +| Prompt-text variables and substitution surfaces | `docs/reference/variables.md` "Workflow Variables", "Node Output References" | +| MCP path resolution and env expansion | `docs/guides/mcp-servers.md` lines 85-110, 188-218, 300-330 | +| `include:` semantics, precedence, literal-only, depth 3 | `docs/guides/authoring-workflows.md` lines 1711-1925 | +| Missing include target error string | `packages/workflows/src/include-expander.ts:1318` | +| Filename override, duplicate-name drop, installed-name reservation | `packages/workflows/src/workflow-discovery.ts` lines 10-21, 911-940, 1170-1186, 1295-1385 | +| Legacy command lookup order | `packages/workflows/src/executor-shared.ts` `loadCommandPrompt`; `packages/paths/src/archon-paths.ts:362`; same at v0.10.1 | +| Load priority bundled < home < repo | `docs/guides/global-workflows.md` "Load Priority" | +| Installed packs, entrypoints, support workflows | `docs/guides/global-workflows.md` "Installed workflow packs"; `docs/guides/publishing-plugins.md`; `packages/workflows/src/installed-packs.test.ts` lines 383-454 | +| Native hooks follow `settingSources` | `docs/guides/hooks.md` lines 60-150 | +| Config `env:`, `worktree.postCreate`, `docs.path`, tiers, aliases | `docs/reference/configuration.md` | +| Boxes installed into Consumer `.archon/workflows/` | `apps/claude-code/unic-archon-dlc/commands/setup.md:217` (this repo) | +| Include slot override and duplicate-name drop | Measured on 0.10.1, scratch directory, `ARCHON_HOME` isolated, `bash:` nodes only |