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 66890ef5..9ddec287 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 @@ -28,13 +28,24 @@ 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.e2e_command` (may be null), `build.coverage_threshold` (may be null). + `build.e2e_command` (may be null), `build.coverage_threshold` (may be null), + `tracker.type`, and `project.repo_ref` (OPTIONAL, may be absent or null → emit ""). 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". - Emit ONLY the structured object. `test_command` is the repo default test command you can infer - (e.g. "pnpm test") for the verification node; leave it null if you cannot infer one. + `repo_ref` is the repository the PR is opened against — "/" (or + "//") for github, the repository name or ID for ado. Read it from CONFIG, + never from `git remote get-url origin` and never from `gh repo view`: both follow the host's own + remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork + clone — the exact defect the pin exists to close. Emit "" when the key is absent, null, or blank; + `guard-no-repo-ref` treats "" as missing (a blank `--repo ""` exits 0 and silently falls back to + the host's own resolution, so an empty value must be caught here, not by the CLI). + + Emit ONLY the structured object. ALWAYS emit every required field, including in the non-ready + branches, so the object validates before the guards can cancel. `test_command` is the repo + default test command you can infer (e.g. "pnpm test") for the verification node; leave it null if + you cannot infer one. output_format: type: object properties: @@ -49,8 +60,10 @@ nodes: e2e_command: { type: [string, 'null'] } coverage_threshold: { type: [number, 'null'] } test_command: { type: [string, 'null'] } + tracker_type: { type: string } + repo_ref: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, message] + required: [status, slug, artifacts_dir, gate, tracker_type, repo_ref, message] # --------------------------------------------------------------------------------------------- # guard — expected precondition failures cancel cleanly (ADR-0011 / schema: cancel vs fail). @@ -63,6 +76,22 @@ nodes: issues.json is missing. Run /unic-archon-dlc:tickets first to produce //issues.json. + # --------------------------------------------------------------------------------------------- + # guard-no-repo-ref — open-pr pins the PR to a repository, so an unset `project.repo_ref` is an + # expected precondition failure: CANCEL, never fail (ADR-0011). It runs BEFORE the 60-iteration + # build loop so the run stops in seconds rather than after a full implementation it cannot ship. + # Scoped to the `ready` case so it never competes with guard-not-ready above. + # --------------------------------------------------------------------------------------------- + - id: guard-no-repo-ref + depends_on: [bootstrap] + when: "$bootstrap.output.status == 'ready' && $bootstrap.output.repo_ref == ''" + cancel: > + /build needs a pinned repository: `project.repo_ref` is missing from + `.archon/unic-dlc.config.yaml`. Without it the host CLI infers the repository from the checkout + and opens the PR against the upstream parent on a fork clone. Set `project.repo_ref` to + "/" (github) or the repository name (ado) — run /unic-archon-dlc:setup, or add the + key by hand — then re-run /unic-archon-dlc:build . + # --------------------------------------------------------------------------------------------- # slopcheck — verify every NEW package against the npm registry before any code runs. # Self-contained (ADR-0023 §5): mirrors lib/slopcheck.mjs, imports nothing from the plugin. @@ -165,6 +194,25 @@ nodes: Issues = //issues.json (dependency-ordered, read-only) State = //build-state.json (you create/update it) + ## Staging rule (explicit only — never blind; applies to EVERY commit below) + Stage paths you have NAMED. NEVER `git add -A`, `git add .`, or `git add -u`: you run in an + isolated worktree where other nodes and the runner leave files on disk, and a blind stage + sweeps them into the commit. + NEVER stage, whatever else you name: + - `build-state.json` — it is committed exactly ONCE, by the open-pr node at the end of the + run. It is the anti-cheat proof (`red_exit`, `red_unexpected_pass`, per-slice phase order — + ADR-0012), and committing it per iteration would rewrite it up to 60 times in the history. + Write it to disk each iteration as usual; just never stage it here. + - `pr-body.md` (or `.pr-body.md`), `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root + - anything under Archon's per-run artifacts dir — the `$ARTIFACTS_DIR` **environment + variable**, which resolves OUTSIDE the repo under + `~/.archon/workspaces//artifacts/`. This is NOT the in-repo ARTIFACTS_DIR constant + above (the config `artifacts_dir`, e.g. `workflows//`), whose session files the + open-pr node does commit. + After every `git add`, run `git status --porcelain` and confirm each staged (first-column + `M`/`A`/`R`/`D`) entry is a path you named. `git restore --staged ""` anything else + before you commit. + ## Step 1 — Load state Read issues.json (an ordered array; blockers come first — honour that order). Read build-state.json if it exists; otherwise treat every slice as phase "pending". @@ -194,11 +242,15 @@ nodes: Do NOT write or modify implementation code in this phase. 2. Run the slice's `test_command`. Capture the exit code. 3. If exit code != 0 (test fails as required): - - `git add` the test file(s) and commit: `test(): failing test for ` - - Update build-state.json: phase "red-done", red_exit = <code>. + - Stage the test file(s) BY NAME (`git add "<test-path>" …` — the staging rule above; + no other path, and never build-state.json) and commit: + `test(<SLUG>): failing test for <id> — <title>` + - Update build-state.json on disk (do NOT stage it): phase "red-done", + red_exit = <code>. 4. If exit code == 0 (test unexpectedly PASSES before any impl): - Do NOT commit. The test does not capture new behaviour. - - Update build-state.json: red_unexpected_pass = true, notes = why. + - Update build-state.json on disk (do NOT stage it): red_unexpected_pass = true, + notes = why. - Print "RED FAILED TO FAIL: <id>" and end the iteration. ### GREEN (minimum implementation) @@ -208,17 +260,19 @@ nodes: no extra abstractions, no unrelated files. 2. Run `test_command`. It MUST now pass (exit 0). If it does not, iterate on the impl only (never weaken the test) until green, within this iteration. - 3. `git add` the implementation and commit: `feat(<SLUG>): implement <id> — <title>` + 3. Stage the implementation file(s) BY NAME (`git add "<impl-path>" …`; no other path, and + never build-state.json) and commit: `feat(<SLUG>): implement <id> — <title>` (use `fix(...)` when the slice `type` is bug). - 4. Update build-state.json: phase "green-done". + 4. Update build-state.json on disk (do NOT stage it): phase "green-done". ### REFACTOR (clean up under a green suite) 1. Read the committed implementation for this slice. Improve clarity/duplication/naming ONLY; change no observable behaviour and add no features. 2. Run `test_command`. It MUST stay green. If a change breaks it, revert that change. - 3. If you made changes, `git add` and commit: `refactor(<SLUG>): tidy <id> — <title>`. + 3. If you made changes, stage the files you touched BY NAME (`git add "<path>" …`; no other + path, and never build-state.json) and commit: `refactor(<SLUG>): tidy <id> — <title>`. If nothing needed changing, make NO commit. - 4. Update build-state.json: phase "refactor-done". + 4. Update build-state.json on disk (do NOT stage it): phase "refactor-done". ## Step 4 — Never emit COMPLETE unless Step 2 found every slice "refactor-done". Do exactly one phase per iteration, then stop. @@ -298,10 +352,51 @@ nodes: prompt: | You are the open-pr node of /build for slug "$bootstrap.output.slug". - 1. Stage everything changed by the build: source + tests, the session report - (`$bootstrap.output.artifacts_dir/$bootstrap.output.slug/report.md`), and any new - `docs/adr/NNNN-*.md` drafted by the report node. - 2. Open a PR targeting `develop`: + Constants: + SLUG = $bootstrap.output.slug + ARTIFACTS_DIR = $bootstrap.output.artifacts_dir (in-repo session dir, from config) + SESSION = <ARTIFACTS_DIR>/<SLUG>/ + TRACKER = $bootstrap.output.tracker_type + REPO_REF = $bootstrap.output.repo_ref + + ## Staging rule (explicit only — never blind) + Stage paths you have NAMED. NEVER `git add -A`, `git add .`, or `git add -u`: you run in an + isolated worktree where other nodes and the runner leave files on disk, and a blind stage sweeps + them into the PR diff. + NEVER stage: `pr-body.md` (or `.pr-body.md`), `*.tmp.md`, `*.scratch.md`, `*-report.md` at the + repo root, or anything under Archon's per-run artifacts dir — the `$ARTIFACTS_DIR` **environment + variable**, which resolves OUTSIDE the repo under `~/.archon/workspaces/<name>/artifacts/`. That + is NOT the in-repo ARTIFACTS_DIR constant above (the config `artifacts_dir`), whose session + files are named in the list below and ARE committed. If you need a PR body file, write it + outside the repo tree (e.g. under the `$ARTIFACTS_DIR` env dir) and pass it with `--body-file`. + + ## Repository pinning rule (host-agnostic) + Pin the PR command to REPO_REF; never let the CLI infer the repository from the checkout, which + resolves to the upstream parent on a fork clone: + - github → `gh pr create --repo "<REPO_REF>" --base develop …` + - ado → `az repos pr create --repository "<REPO_REF>" --target-branch develop …` + Take REPO_REF from bootstrap (it read `project.repo_ref` from config). Do NOT recompute it from + `git remote get-url origin` or `gh repo view` — both follow the host's own remote precedence and + return the wrong repository in exactly the fork case this pin closes. Shell state does not + persist between separate bash invocations, so pass the value on every command rather than + exporting it once. `guard-no-repo-ref` already cancelled the run if REPO_REF were empty. + + 1. Build an explicit stage list, then `git add "<path1>" "<path2>" …` those paths only: + - the source and test paths changed by the build — list the candidates with + `git diff --name-only $BASE_BRANCH...HEAD` and `git status --porcelain`, then name them + - `<SESSION>/PRD.md` + - `<SESSION>/issues.json` + - `<SESSION>/report.md` + - `<SESSION>/build-state.json` — committed HERE, exactly once per run. It is ADR-0012's + anti-cheat proof (`red_exit`, `red_unexpected_pass`, per-slice phase order); the loop + deliberately never commits it, so without this line the proof dies when /cleanup prunes + the worktree. + - any new `docs/adr/NNNN-*.md` drafted by the report node — name each file, not the + directory + Drop every path matching the deny list above. Then run `git status --porcelain` and confirm + each staged (first-column `M`/`A`/`R`/`D`) entry is a path you named; `git restore --staged + "<path>"` anything else. Commit the result. + 2. Open a PR targeting `develop`, pinned per the rule above: Title: "build($bootstrap.output.slug): implementation" Body: ## Summary (TDD implementation for the slug; goals-check status; report link) ## Review Checklist @@ -310,7 +405,7 @@ nodes: - [ ] Coverage meets the configured threshold - [ ] Tech-debt section in report.md is accurate - [ ] Drafted ADRs (if any) are ready to record - Compose the host from config (gh / az / the azure-devops-cli skill per the tracker); do NOT + Compose the host from config (gh / az / the azure-devops-cli skill per TRACKER); do NOT hardcode a host. Print the PR URL. # --------------------------------------------------------------------------------------------- @@ -333,5 +428,10 @@ nodes: 1. Address the feedback with the minimum change (fix impl and/or tests). 2. Re-run the test suite and the stub scan; confirm green. 3. Update the session report.md if outcomes changed, and push the fix to the PR branch. + Stage the fix BY NAME (`git add "<path>" …`). NEVER `git add -A`, `git add .`, or + `git add -u`, and never stage `pr-body.md`, `*.tmp.md`, `*.scratch.md`, `*-report.md` at + the repo root, or anything under Archon's per-run artifacts dir (the `$ARTIFACTS_DIR` + environment variable, outside the repo). Confirm with `git status --porcelain` that only + paths you named are staged before you commit. Then the gate re-pauses for another review. max_attempts: 3 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 cb6825bb..055760f9 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 @@ -39,16 +39,25 @@ nodes: `gates.explore` (default "hitl") `tracker.type` (e.g. github / ado / jira / local-markdown) `project.branching` (default "gitflow") + `project.repo_ref` (OPTIONAL, may be absent or null → emit "") 3. If slug and config are both present → status "ready". + `repo_ref` is the repository every host CLI call is pinned to — "<owner>/<repo>" (or + "<host>/<owner>/<repo>") for github, the repository name or ID for ado. Read it from CONFIG, + never from `git remote get-url origin` and never from `gh repo view`: both follow the host's own + remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork + clone — the exact defect the pin exists to close. Emit "" when the key is absent, null, or blank; + `guard-no-repo-ref` treats "" as missing (a blank `--repo ""` exits 0 and silently falls back to + the host's own resolution, so an empty value must be caught here, not by the CLI). + /explore has no PRD or findings precondition — it is an off-line precursor that PRODUCES findings.md, so `ready` needs only a slug + a readable config. Emit ONLY the structured object. ALWAYS emit EVERY required field, including in the no-slug / no-config branches, so the object validates before `guard-not-ready` can cancel. When a value cannot be read, fall back to: slug "" (if no-slug), artifacts_dir "workflows", gate "hitl", - tracker_type "", branching "gitflow". Set `message` to a human-readable reason for any - non-ready status. + tracker_type "", branching "gitflow", repo_ref "". Set `message` to a human-readable reason for + any non-ready status. output_format: type: object properties: @@ -62,8 +71,9 @@ nodes: enum: [hitl, afk] tracker_type: { type: string } branching: { type: string } + repo_ref: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, tracker_type, message] + required: [status, slug, artifacts_dir, gate, tracker_type, repo_ref, message] # --------------------------------------------------------------------------------------------- # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). @@ -76,6 +86,21 @@ nodes: (`/unic-archon-dlc:explore <slug>`) and run /unic-archon-dlc:setup first so `.archon/unic-dlc.config.yaml` exists. + # --------------------------------------------------------------------------------------------- + # guard-no-repo-ref — the spike ticket is filed against a pinned repository, so an unset + # `project.repo_ref` is an expected precondition failure: CANCEL, never fail (ADR-0011). Scoped to + # the `ready` case so it never competes with guard-not-ready above. + # --------------------------------------------------------------------------------------------- + - id: guard-no-repo-ref + depends_on: [bootstrap] + when: "$bootstrap.output.status == 'ready' && $bootstrap.output.repo_ref == ''" + cancel: > + /explore needs a pinned repository: `project.repo_ref` is missing from + `.archon/unic-dlc.config.yaml`. Without it the host CLI infers the repository from the checkout + and targets the upstream parent on a fork clone. Set `project.repo_ref` to "<owner>/<repo>" + (github) or the repository name (ado) — run /unic-archon-dlc:setup, or add the key by hand — + then re-run /unic-archon-dlc:explore <slug>. + # --------------------------------------------------------------------------------------------- # research-stack — parallel research dimension 1 of 4. Read-only, fresh context. Emits its findings # as structured text so `synthesize` can compose them without re-deriving. @@ -319,8 +344,20 @@ nodes: SLUG = $bootstrap.output.slug ARTIFACTS_DIR = $bootstrap.output.artifacts_dir TRACKER = $bootstrap.output.tracker_type + REPO_REF = $bootstrap.output.repo_ref Verdicts = $spike.output.verdict_summary + ## Repository pinning rule (host-agnostic — applies to EVERY host CLI call below) + Pin every call to REPO_REF; never let the CLI infer the repository from the checkout, which + resolves to the upstream parent on a fork clone: + - github → pass `--repo "<REPO_REF>"` (e.g. `gh issue create --repo "<REPO_REF>" …`) + - ado → pass `--repository "<REPO_REF>"` (e.g. `az repos … --repository "<REPO_REF>" …`) + Take REPO_REF from bootstrap (it read `project.repo_ref` from config). Do NOT recompute it from + `git remote get-url origin` or `gh repo view` — both follow the host's own remote precedence and + return the wrong repository in exactly the fork case this pin closes. Shell state does not + persist between separate bash invocations, so pass the value on every command rather than + exporting it once. `guard-no-repo-ref` already cancelled the run if REPO_REF were empty. + 1. Read `.archon/unic-dlc.config.yaml` for `tracker` (type / access / coords) and `classification.labels`. Compose the tracker MCP-first, CLI-fallback (gh / az / jira, or the azure-devops-cli skill) — never hardcode a host (ADR-0016). Labels come ONLY from @@ -370,9 +407,32 @@ nodes: You are the preserve-spike node of /explore for slug "$bootstrap.output.slug". The reviewer approved preserving the spike code. + Constants: + SLUG = $bootstrap.output.slug + ARTIFACTS_DIR = $bootstrap.output.artifacts_dir (in-repo session dir, from config) + + ## Staging rule (explicit only — never blind) + Stage paths you have NAMED. NEVER `git add -A`, `git add .`, or `git add -u`: this node runs in + an isolated worktree where other nodes and the runner leave files on disk, and a blind stage + sweeps them into the commit. + NEVER stage, whatever else you name: + - `pr-body.md` (or `.pr-body.md`), `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root + - anything under Archon's per-run artifacts dir — the `$ARTIFACTS_DIR` **environment + variable**, which resolves OUTSIDE the repo under `~/.archon/workspaces/<name>/artifacts/`. + This is NOT the in-repo `ARTIFACTS_DIR` above (the config `artifacts_dir`, e.g. + `workflows/<SLUG>/`), whose session files ARE committed here. + 1. Create the branch (quote the branch name — the slug is user-controlled): git checkout -b "spike/$bootstrap.output.slug" - 2. Stage and commit the spike code and findings: - git add -A + 2. List the candidates with `git status --porcelain`, then build an explicit path list of the + spike code and findings this run produced: + - the source/test files the spike node created or modified + - `<ARTIFACTS_DIR>/<SLUG>/findings.md` + Drop every path matching the deny list above. Stage the survivors BY NAME: + git add "<path1>" "<path2>" … + 3. Verify nothing else came along: run `git status --porcelain` again and read the staged + (first-column `M`/`A`/`R`/`D`) entries. Every one must be a path you named in step 2. If any + other path is staged, `git restore --staged "<path>"` it before committing. + 4. Commit: git commit -m "spike($bootstrap.output.slug): preserve exploration artifacts from /explore" - 3. Print: "Spike code committed on branch spike/$bootstrap.output.slug." + 5. Print: "Spike code committed on branch spike/$bootstrap.output.slug (<N> paths staged)." 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 ea2950f3..600176a0 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 @@ -39,14 +39,24 @@ nodes: `tracker.type` (e.g. github / ado / jira / local-markdown) `docs.type` / `docs.access` (for intent composition from docs) `project.branching` (default "gitflow") + `project.repo_ref` (OPTIONAL, may be absent or null → emit "") 3. If slug and config are both present → status "ready". (There is no PRD precondition: intent is composed from whatever sources resolve — see prep.) + `repo_ref` is the repository every host CLI call is pinned to — "<owner>/<repo>" (or + "<host>/<owner>/<repo>") for github, the repository name or ID for ado. Read it from CONFIG, + never from `git remote get-url origin` and never from `gh repo view`: both follow the host's own + remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork + clone — the exact defect the pin exists to close. Emit "" when the key is absent, null, or blank; + `guard-no-repo-ref` treats "" as missing (a blank `--repo ""` exits 0 and silently falls back to + the host's own resolution, so an empty value must be caught here, not by the CLI). + Compute `expected_base` from branching: "develop" when branching == "gitflow", else "main". ALWAYS emit EVERY required field, including in the no-slug / no-config branches, so the object validates before `guard-not-ready` 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", tracker_type "", branching "gitflow", expected_base "develop". `inline_comments` is emitted + "true", tracker_type "", branching "gitflow", expected_base "develop", repo_ref "". + `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. output_format: @@ -67,8 +77,9 @@ nodes: tracker_type: { type: string } branching: { type: string } expected_base: { type: string } + repo_ref: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, confidence_threshold, inline_comments, tracker_type, branching, expected_base, message] + required: [status, slug, artifacts_dir, gate, confidence_threshold, inline_comments, tracker_type, branching, expected_base, repo_ref, message] # --------------------------------------------------------------------------------------------- # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). @@ -81,6 +92,23 @@ nodes: .archon/unic-dlc.config.yaml is missing. Pass a slug (`/unic-dlc-pr-review <slug>`) and run /unic-archon-dlc:setup first if there is no config. + # --------------------------------------------------------------------------------------------- + # guard-no-repo-ref — prep and post pin their host CLI calls to a repository, so an unset + # `project.repo_ref` is an expected precondition failure: CANCEL, never fail (ADR-0011). It runs + # before the seven-aspect fan-out so the run stops in seconds rather than after a full review it + # would post to the wrong repository. Scoped to the `ready` case so it never competes with + # guard-not-ready above. + # --------------------------------------------------------------------------------------------- + - id: guard-no-repo-ref + depends_on: [bootstrap] + when: "$bootstrap.output.status == 'ready' && $bootstrap.output.repo_ref == ''" + cancel: > + /pr-review needs a pinned repository: `project.repo_ref` is missing from + `.archon/unic-dlc.config.yaml`. Without it the host CLI infers the repository from the checkout + and reads — or comments on — a PR on the upstream parent on a fork clone. Set `project.repo_ref` + to "<owner>/<repo>" (github) or the repository name (ado) — run /unic-archon-dlc:setup, or add + the key by hand — then re-run /unic-dlc-pr-review <slug>. + # --------------------------------------------------------------------------------------------- # prep — the shared context builder. Identifies the open PR, computes the diff, categorises the # changed files (for the spawn gates), composes ONE Intent Brief from every available source, detects @@ -102,13 +130,25 @@ nodes: ARTIFACTS_DIR = $bootstrap.output.artifacts_dir SESSION = <ARTIFACTS_DIR>/<SLUG>/pr-review/ TRACKER = $bootstrap.output.tracker_type + REPO_REF = $bootstrap.output.repo_ref Create SESSION if it does not exist. + ## Repository pinning rule (host-agnostic — applies to EVERY host CLI call below) + Pin every call to REPO_REF; never let the CLI infer the repository from the checkout, which + resolves to the upstream parent on a fork clone — you would then review somebody else's PR: + - github → pass `--repo "<REPO_REF>"` + - ado → pass `--repository "<REPO_REF>"` + Take REPO_REF from bootstrap (it read `project.repo_ref` from config). Do NOT recompute it from + `git remote get-url origin` or `gh repo view` — both follow the host's own remote precedence and + return the wrong repository in exactly that fork case. Shell state does not persist between + separate bash invocations, so pass the value on every command rather than exporting it once. + ## 1. Identify the open PR + read its description Compose the configured tracker (MCP-first, CLI-fallback — never hardcode a host, ADR-0016), reading `tracker` from `.archon/unic-dlc.config.yaml`: - - github → `gh pr view --json number,title,body,headRefName,baseRefName` - - ado → `az repos pr list --status active` then `az repos pr show` for the current branch + - github → `gh pr view --repo "<REPO_REF>" --json number,title,body,headRefName,baseRefName` + - ado → `az repos pr list --repository "<REPO_REF>" --status active` then + `az repos pr show --repository "<REPO_REF>"` for the current branch - jira / local-markdown / other → there may be no queryable PR; note it and continue. Store PR_ID, PR_TITLE, and the PR DESCRIPTION BODY (an intent source). @@ -418,17 +458,30 @@ nodes: You are the post node of /pr-review for slug "$bootstrap.output.slug". SESSION = <artifacts_dir>/<slug>/pr-review/. ITERATION = $reconcile.output.iteration. PR_ID = $prep.output.pr_id. inline_comments = $bootstrap.output.inline_comments. + REPO_REF = $bootstrap.output.repo_ref. Compose the configured tracker from `.archon/unic-dlc.config.yaml` (`tracker.type`; MCP-first, CLI-fallback — never hardcode a host, ADR-0016). + ## Repository pinning rule (host-agnostic — applies to EVERY host CLI call below) + Pin every call to REPO_REF; never let the CLI infer the repository from the checkout, which + resolves to the upstream parent on a fork clone — you would then post this review on somebody + else's PR: + - github → pass `--repo "<REPO_REF>"` + - ado → pass `--repository "<REPO_REF>"` + Take REPO_REF from bootstrap (it read `project.repo_ref` from config). Do NOT recompute it from + `git remote get-url origin` or `gh repo view` — both follow the host's own remote precedence and + return the wrong repository in exactly that fork case. Shell state does not persist between + separate bash invocations, so pass the value on every command rather than exporting it once. + ## Summary comment (always) Read SESSION/comment.md. Search the PR's comments for the FIRST comment containing the marker `<!-- unic-dlc-pr-review:iteration=` (match the marker, NEVER the author). - - github → find via `gh pr view "$PR_ID" --json comments`; PATCH the matched comment with - `gh api ... -X PATCH -f body=@SESSION/comment.md`, else `gh pr comment "$PR_ID" --body-file`. - - ado → `az repos pr thread list`; update the matched general-comment thread's first comment, - else create a new general-comment thread. + - github → find via `gh pr view "$PR_ID" --repo "<REPO_REF>" --json comments`; PATCH the + matched comment with `gh api ... -X PATCH -f body=@SESSION/comment.md` (build the api path + from REPO_REF), else `gh pr comment "$PR_ID" --repo "<REPO_REF>" --body-file`. + - ado → `az repos pr thread list --repository "<REPO_REF>"`; update the matched + general-comment thread's first comment, else create a new general-comment thread. - jira / local-markdown → update the marked comment / overwrite SESSION/comment.md as the record. The comment.md already carries the bumped `iteration=<ITERATION>` marker + footer. @@ -442,7 +495,8 @@ nodes: tracker supports a resolved status (best-effort; otherwise just reply). - regressed → reopen the thread (or open a new one) noting the regression. - new → create a new inline thread at file:line. - (github: `gh api` review comments; ado: `az repos pr thread create/update` with threadContext.) + (github: `gh api` review comments, api path built from REPO_REF; ado: `az repos pr thread + create/update --repository "<REPO_REF>"` with threadContext.) Finally print: "pr-review: iteration <ITERATION> posted for \"$bootstrap.output.slug\" — <summary action>, <n> inline (<new> new · <upd> updated · <fixed> resolved). ✓" 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 866abda2..c237eb4c 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 @@ -35,6 +35,7 @@ nodes: `qa.coverage_threshold` (may be null) — else fall back to `build.coverage_threshold` `tracker.type` (e.g. github / ado / jira / local-markdown) `project.branching` (default "gitflow") + `project.repo_ref` (OPTIONAL, may be absent or null → emit "") 3. Confirm the QA baton exists: `<artifacts_dir>/<slug>/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". @@ -44,11 +45,19 @@ nodes: Emit ONLY the structured object. `e2e_command` / `coverage_threshold` are the resolved (qa.* ?? build.*) values — null when neither is set (→ the node skips with a warning). + `repo_ref` is the repository every host CLI call is pinned to — "<owner>/<repo>" (or + "<host>/<owner>/<repo>") for github, the repository name or ID for ado. Read it from CONFIG, + never from `git remote get-url origin` and never from `gh repo view`: both follow the host's own + remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork + clone — the exact defect the pin exists to close. Emit "" when the key is absent, null, or blank; + `guard-no-repo-ref` treats "" as missing (a blank `--repo ""` exits 0 and silently falls back to + the host's own resolution, so an empty value must be caught here, not by the CLI). + ALWAYS emit EVERY required field, including in the no-slug / no-config / no-prd branches, so the object validates before `guard-not-ready` 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", - tracker_type "" , branching "gitflow", expected_base "develop". Set `message` to a human-readable - reason for any non-ready status. + tracker_type "" , branching "gitflow", expected_base "develop", repo_ref "". Set `message` to a + human-readable reason for any non-ready status. output_format: type: object properties: @@ -65,8 +74,9 @@ nodes: tracker_type: { type: string } branching: { type: string } expected_base: { type: string } + repo_ref: { type: string } message: { type: string } - required: [status, slug, artifacts_dir, gate, tracker_type, expected_base, message] + required: [status, slug, artifacts_dir, gate, tracker_type, expected_base, repo_ref, message] # --------------------------------------------------------------------------------------------- # guard — expected precondition failures cancel cleanly (ADR-0011: cancel vs fail). @@ -79,6 +89,22 @@ nodes: <artifacts_dir>/<slug>/PRD.md is missing. Run /unic-archon-dlc:build <slug> first (which in turn needs /specs + /tickets), so the acceptance criteria the UAT checklist reads from exist. + # --------------------------------------------------------------------------------------------- + # guard-no-repo-ref — verify-pr-base and merge pin their host CLI calls to a repository, so an unset + # `project.repo_ref` is an expected precondition failure: CANCEL, never fail (ADR-0011). It runs + # before the e2e suite so the run stops in seconds rather than after a full QA pass it cannot merge. + # Scoped to the `ready` case so it never competes with guard-not-ready above. + # --------------------------------------------------------------------------------------------- + - id: guard-no-repo-ref + depends_on: [bootstrap] + when: "$bootstrap.output.status == 'ready' && $bootstrap.output.repo_ref == ''" + cancel: > + /qa needs a pinned repository: `project.repo_ref` is missing from + `.archon/unic-dlc.config.yaml`. Without it the host CLI infers the repository from the checkout + and reads — or merges — a PR on the upstream parent on a fork clone. Set `project.repo_ref` to + "<owner>/<repo>" (github) or the repository name (ado) — run /unic-archon-dlc:setup, or add the + key by hand — then re-run /unic-archon-dlc:qa <slug>. + # --------------------------------------------------------------------------------------------- # e2e — run the configured end-to-end suite. A prompt node (ADR-0023 §5): it reads the resolved # command from bootstrap and runs it with its own tools. It REPORTS a verdict (does not hard-fail); @@ -210,6 +236,15 @@ nodes: FINDING-CAPTURE (ADR-0025) — 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). + REPO_REF = $bootstrap.output.repo_ref — pin EVERY host CLI call to it (github → + `--repo "<REPO_REF>"`, e.g. `gh issue create --repo "<REPO_REF>" …`; ado → + `--repository "<REPO_REF>"`). Never let the CLI infer the repository from the checkout: it + resolves to the upstream parent on a fork clone. Take the value from bootstrap, which read + `project.repo_ref` from config; do NOT recompute it from `git remote get-url origin` or + `gh repo view` — both follow the host's own remote precedence and return the wrong + repository in exactly that fork case. Shell state does not persist between separate bash + invocations, so pass it on every command rather than exporting it once. + 1. Read `.archon/unic-dlc.config.yaml` for `tracker` (type/access/coords) and `classification.labels`. Compose the tracker MCP-first, CLI-fallback (gh / az / jira, or the azure-devops-cli skill) — never hardcode a host (ADR-0016). Labels come ONLY from @@ -250,11 +285,20 @@ nodes: You are the verify-pr-base node of /qa for slug "$bootstrap.output.slug". Expected base branch: $bootstrap.output.expected_base (tracker: $bootstrap.output.tracker_type) + REPO_REF = $bootstrap.output.repo_ref + + ## Repository pinning rule (host-agnostic) + Pin every host CLI call to REPO_REF; never let the CLI infer the repository from the checkout, + which resolves to the upstream parent on a fork clone — you would then verify the base of + somebody else's PR. Take REPO_REF from bootstrap (it read `project.repo_ref` from config); do NOT + recompute it from `git remote get-url origin` or `gh repo view` — both follow the host's own + remote precedence and return the wrong repository in exactly that fork case. Shell state does not + persist between separate bash invocations, so pass it on every command. Query the open PR for the current feature branch and read its target/base branch, composing the tracker from `.archon/unic-dlc.config.yaml` (MCP-first, CLI-fallback): - - github → `gh pr view --json baseRefName` - - ado → `az repos pr show` (targetRefName, strip refs/heads/) + - github → `gh pr view --repo "<REPO_REF>" --json baseRefName` + - ado → `az repos pr show --repository "<REPO_REF>"` (targetRefName, strip refs/heads/) - jira / local-markdown / other → no queryable PR base: set base_ok "true" and note it is unverifiable for this tracker (warning only, non-blocking). Set `base_ok` to the STRING "true" when the actual base equals expected_base (or is unverifiable @@ -297,11 +341,22 @@ nodes: prompt: | You are the merge node of /qa for slug "$bootstrap.output.slug". + REPO_REF = $bootstrap.output.repo_ref + + ## Repository pinning rule (host-agnostic) + Pin every host CLI call to REPO_REF; never let the CLI infer the repository from the checkout, + which resolves to the upstream parent on a fork clone — merging there would land this work in + the wrong repository. Take REPO_REF from bootstrap (it read `project.repo_ref` from config); do + NOT recompute it from `git remote get-url origin` or `gh repo view` — both follow the host's own + remote precedence and return the wrong repository in exactly that fork case. Shell state does not + persist between separate bash invocations, so pass it on every command. + Merge the open PR for the current feature branch, composing the tracker from `.archon/unic-dlc.config.yaml` (`tracker.type`, `project.pr_strategy`, `project.branching`; MCP-first, CLI-fallback — never hardcode a host, ADR-0016): - - github → `gh pr merge` with `--squash` (default) or `--merge` per `project.pr_strategy`. - - ado → `az repos pr update --status completed`. + - github → `gh pr merge --repo "<REPO_REF>"` with `--squash` (default) or `--merge` per + `project.pr_strategy`. + - ado → `az repos pr update --repository "<REPO_REF>" --status completed`. - jira / local-markdown → print the manual steps (transition the linked issue to Done / set Status: resolved in the issues file) — there is no merge CLI. On Gitflow (`project.branching == "gitflow"`) with a git-hosted tracker (github/ado), after a 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 3a99a5b1..428c530e 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.12.0" + "version": "0.12.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 08c53555..57266ec1 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.12.0", + "version": "0.12.1", "description": "A config-driven 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 fa192401..6114a6a3 100644 --- a/apps/claude-code/unic-archon-dlc/AGENTS.md +++ b/apps/claude-code/unic-archon-dlc/AGENTS.md @@ -60,6 +60,7 @@ Load-bearing invariants. These either originate in a Plugin ADR or are policy de - **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 the `/handoff` command/skill (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). - **`/triage` is the intake on-ramp — a thin wrapper over Matt's method, bound to DLC config.** It turns raw work (bugs, requests, QA findings, external PRs) into agent-ready tracker issues feeding `/tickets`. It **composes Matt's `triage` method but injects `classification.labels` from `.archon/unic-dlc.config.yaml` as the single source of truth** and forbids reading Matt's `docs/agents/triage-labels.md` / `issue-tracker.md`, so labels can't drift from what `/tickets` + `/build` read. Consequently `setup-matt-pocock-skills` is **not** a Plugin dependency — only Matt's skill _methods_ are. It writes only to the tracker + `<triage.out_of_scope_dir>` (human present → no PR gate); it produces no `issues.json`/PRD. See [ADR-0024](docs/adr/0024-triage-intake-on-ramp.md). - **The `## Agent skills` block in a Consumer's `CLAUDE.md` is auto-managed.** Setup writes content between `<!-- unic-archon-dlc:begin -->` and `<!-- unic-archon-dlc:end -->` markers. Everything outside the markers is preserved verbatim across re-runs. +- **Stage explicitly; pin the repository.** Two rules every committing or PR-touching node carries **inline in its own prompt**, never by reference — 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 doctrine in this file is invisible to a running node. (1) **Staging**: name every path; no `git add -A` / `git add .` / `git add -u`, and never stage `pr-body.md`, `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root, or anything under Archon's per-run `$ARTIFACTS_DIR` (the environment variable, which resolves outside the repo — distinct from the in-repo `artifacts_dir` config value). Every stage is followed by a `git status --porcelain` check. `build-state.json` is the named exception: written every loop iteration, committed **once** by `/build`'s `open-pr`, because it is ADR-0012's anti-cheat proof. (2) **Repository pinning**: every host CLI call passes `project.repo_ref` from `.archon/unic-dlc.config.yaml` — `gh --repo` for github, `az repos … --repository` for ado, never a hardcoded host. The value comes from config, never from `git remote get-url origin` or `gh repo view`: both follow the host's remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork clone. `repo_ref` is **optional** in the schema; each Box guards on its absence with a `guard-no-repo-ref` node that **cancels** with an actionable message ([ADR-0011](docs/adr/0011-archon-schema-target.md): expected precondition failures cancel, they do not fail). Promoting the key to mandatory belongs with the Archon 0.7.0 adoption, not here. - **Slopcheck before build.** Every new package referenced in `package.json` is verified against the npm registry before any RED/GREEN/REFACTOR 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)). - **`/qa` gates on `gates.qa` and fail-closes the merge.** The QA Archon pipeline (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 e2e/coverage or a wrong PR base. `/qa` is also an **issue-producing on-ramp**: a UAT rejection files each defect directly as a `ready-for-agent` tracker issue (composing the configured tracker + `classification.labels`, Matt's `qa` brief shape, 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). diff --git a/apps/claude-code/unic-archon-dlc/CHANGELOG.md b/apps/claude-code/unic-archon-dlc/CHANGELOG.md index f894d8d6..965255f3 100644 --- a/apps/claude-code/unic-archon-dlc/CHANGELOG.md +++ b/apps/claude-code/unic-archon-dlc/CHANGELOG.md @@ -11,6 +11,20 @@ ### Fixed - (none) +## [0.12.1] — 2026-08-05 + +### Breaking +- (none) + +### Added +- **`project.repo_ref` — an optional, host-agnostic repository pin** in `lib/config-schema.mjs`, defaulting to `null`. It carries the repository every PR-touching Box node targets: `<owner>/<repo>` (or `<host>/<owner>/<repo>`) for github, the repository name or ID for ado. It is deliberately **not** in `MANDATORY_PATHS` and `/setup` does not demand it — promoting the key belongs with the Archon 0.7.0 adoption. Each of the four Box YAMLs reads it in `bootstrap`, emits it in `output_format`, and threads it downstream as `$bootstrap.output.repo_ref`; a new `guard-no-repo-ref` node **cancels** (never fails) with an actionable message naming the key and `/unic-archon-dlc:setup` when it is absent or blank ([ADR-0011](docs/adr/0011-archon-schema-target.md)). The guard treats an empty string as missing, because a blank `--repo ""` exits 0 and silently falls back to the host's own resolution. + +### Fixed +- **Blind staging in the Boxes and the command gates.** `/explore`'s `preserve-spike` ran `git add -A` and `/build`'s `open-pr` said "stage everything changed by the build" — both sweep whatever else is on disk in the isolated worktree into the commit and the PR diff. Every committing node now stages **named paths** and verifies with `git status --porcelain` that nothing else came along, carrying the same deny list inline: `pr-body.md`, `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root, and anything under Archon's per-run `$ARTIFACTS_DIR` (the environment variable, which resolves outside the repo — explicitly distinguished from the in-repo `artifacts_dir` config value, whose session files _are_ committed). `/build`'s `open-pr` now names its stage list: changed source and tests, `PRD.md`, `issues.json`, `report.md`, `build-state.json`, and each new `docs/adr/NNNN-*.md`. `commands/specs.md` and `commands/tickets.md` apply the same rule in both the `open-pr` and `stage-only` gates; `specs.md` no longer stages the bare `docs/adr/` directory. +- **`build-state.json` is now committed exactly once**, by `/build`'s `open-pr`, and never during a loop iteration. Every RED/GREEN/REFACTOR phase writes it to disk and is told explicitly not to stage it. It is [ADR-0012](docs/adr/0012-fresh-context-red-green-separation.md)'s anti-cheat proof (`red_exit`, `red_unexpected_pass`, per-slice phase order), which previously died when `/cleanup` pruned the worktree. +- **Unpinned host CLI calls.** No `gh` or `az` invocation passed a repository, so on a fork clone the host CLI resolved to the upstream parent — opening, reading, merging, or commenting on a PR in the wrong repository. Every PR-touching node in the four Boxes, plus the PR gates and tracker publishing in `commands/specs.md` / `commands/tickets.md` and the stale-PR closing step in `commands/cleanup.md` (which skips the category outright rather than guess when `repo_ref` is unset — closing a PR is destructive), now pins the call host-agnostically: `--repo "<ref>"` for github, `--repository "<ref>"` for ado, never a hardcoded host. The value comes from config only — nodes are told **not** to recompute it from `git remote get-url origin` or `gh repo view`, both of which follow the host's own remote precedence (`upstream` > `github` > `origin`) and return the wrong repository in exactly that fork case. +- Both rules are stated **inline in each prompt**, not only as doctrine, because 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) — a doctrine document is invisible to a running node. `AGENTS.md` gains a matching doctrine bullet for maintainers. + ## [0.12.0] — 2026-07-03 ### Added diff --git a/apps/claude-code/unic-archon-dlc/commands/cleanup.md b/apps/claude-code/unic-archon-dlc/commands/cleanup.md index 406ebc00..9a8ae497 100644 --- a/apps/claude-code/unic-archon-dlc/commands/cleanup.md +++ b/apps/claude-code/unic-archon-dlc/commands/cleanup.md @@ -92,7 +92,11 @@ EOJS Parse the JSON. Keep `ARTIFACTS_DIR` (default `workflows`), `TRACKER` (`.type`/`.access`/`.coords`, may be `null`), `CLEANUP` (`.stale_days` default `7`, `.dry_run` default `true`, `.prune_slug_dirs` default `false`), and `PROJECT` (`.branching`/`.pr_strategy`; a **hint** for the main branch — it may -be `null`). If `degraded` is `true`, print a one-line warning naming `reason` and note the fallbacks, +be `null`; plus `.repo_ref`, the repository every host CLI call is pinned to — `<owner>/<repo>` for +GitHub, the repository name or ID for Azure DevOps. It is an optional key, so it may be `null`. Take +it from config only — never from `git remote get-url origin` or `gh repo view`, which follow the +host's own remote precedence (`upstream` > `github` > `origin`) and resolve to the upstream parent on +a fork clone). If `degraded` is `true`, print a one-line warning naming `reason` and note the fallbacks, then continue. If `TRACKER` is `null` or its `type` is unset, warn that PR/branch-state detection and slug-dir pruning will be skipped (they need the tracker), then continue. If `PROJECT` is `null` or `PROJECT.branching` is unset (the plugin-load and no-config fallbacks leave it so), warn that the main @@ -190,7 +194,11 @@ items, then ask the user to confirm **that category** (yes/no). On confirmation, - **A specific branch's full lifecycle** (worktree + local/remote branch) → `archon complete <branch>`. Use this for a targeted removal the bulk `isolation cleanup` did not cover. - **Stale/leftover PRs** → close via the composed `TRACKER.access` (e.g. the tracker MCP close tool, - or `gh pr close` / `az repos pr update --status abandoned`), never a hardcoded CLI. Opt-in only. + or `gh pr close --repo "<PROJECT.repo_ref>"` / `az repos pr update --repository +"<PROJECT.repo_ref>" --status abandoned`), never a hardcoded CLI. Pin the repository on every call: + closing a PR is destructive, and an unpinned CLI resolves to the upstream parent on a fork clone. + If `PROJECT.repo_ref` is unset, **skip this category** and warn that `project.repo_ref` is missing + from `.archon/unic-dlc.config.yaml` — never guess the repository. Opt-in only. - **Prunable slug dirs** (only if `CLEANUP.prune_slug_dirs` is `true`) → remove the directory with Node's `node:fs` (`rm` recursive). **Before deleting, scan the dir for any `LICENSE` file; if one is present, skip that dir and warn the maintainer to handle it manually** (repo LICENSE policy). diff --git a/apps/claude-code/unic-archon-dlc/commands/specs.md b/apps/claude-code/unic-archon-dlc/commands/specs.md index 308ef343..26d945ba 100644 --- a/apps/claude-code/unic-archon-dlc/commands/specs.md +++ b/apps/claude-code/unic-archon-dlc/commands/specs.md @@ -58,6 +58,7 @@ try { ok: true, artifacts_dir: config.artifacts_dir, tracker: config.tracker, + repo_ref: g('project.repo_ref'), docs: config.docs, design: config.design, estimations: config.estimations, @@ -76,9 +77,15 @@ EOJS ``` Parse the JSON. If `ok` is `false`, print `message` verbatim and **stop**. Otherwise keep: -`ARTIFACTS_DIR`, `TRACKER` (`.type`/`.access`/`.coords`), `DOCS` (`.type`/`.publish`/`.access`), -`DESIGN` (`.type`/`.access`), `ESTIMATIONS`, `DISCUSS_MODE` (`specs.discuss_mode`), `GATE` -(`specs.gate`), `PRD_TEMPLATE`, and `MATT_SUITE`. +`ARTIFACTS_DIR`, `TRACKER` (`.type`/`.access`/`.coords`), `REPO_REF` (`project.repo_ref`), `DOCS` +(`.type`/`.publish`/`.access`), `DESIGN` (`.type`/`.access`), `ESTIMATIONS`, `DISCUSS_MODE` +(`specs.discuss_mode`), `GATE` (`specs.gate`), `PRD_TEMPLATE`, and `MATT_SUITE`. + +`REPO_REF` is the repository the PR gate pins its host CLI to — `<owner>/<repo>` (or +`<host>/<owner>/<repo>`) for GitHub, the repository name or ID for Azure DevOps. It is an optional +config key, so it may be `null`; Step 8 handles that case. Take it from config only — never from +`git remote get-url origin` or `gh repo view`, which follow the host's own remote precedence +(`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork clone. If `MATT_SUITE.present` is `false`, warn that `/grill-with-docs` + `/to-prd` are declared dependencies and grilling quality will degrade, then continue (non-blocking). @@ -193,22 +200,49 @@ The repo copy at `<ARTIFACTS_DIR>/<SLUG>/PRD.md` is always the floor; publishing The PRD is human-approved via a PR — never merge it yourself. Behaviour follows `GATE`: +**Staging rule (both gates).** Stage paths you have named. Never `git add -A`, `git add .`, +`git add -u`, or a bare directory such as `docs/adr/` — name each new ADR file instead. Never stage +`pr-body.md` (or `.pr-body.md`), `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root, or +anything under Archon's per-run artifacts dir (the `$ARTIFACTS_DIR` environment variable, which +resolves outside the repo under `~/.archon/workspaces/<name>/artifacts/` — not the in-repo +`ARTIFACTS_DIR` config value, whose `PRD.md` you do commit). After staging, run +`git status --porcelain` and confirm every staged entry is a path you named; `git restore --staged +"<path>"` anything else before you commit. + +**Repository pinning rule (`open-pr` gate).** Pass `REPO_REF` explicitly: `--repo "<REPO_REF>"` for +GitHub, `--repository "<REPO_REF>"` for Azure DevOps. If `REPO_REF` is null or empty, do **not** open +the PR: print + +``` +project.repo_ref is not set in .archon/unic-dlc.config.yaml. Without it the host CLI infers the +repository from the checkout and opens the PR against the upstream parent on a fork clone. Set +project.repo_ref (run /unic-archon-dlc:setup, or add the key by hand), then re-run this gate. +``` + +then fall back to `stage-only` behaviour below and **stop**. This mirrors the Archon boxes, which +cancel on the same missing key rather than failing. + - **`open-pr`** (default): create `feature/specs/<SLUG>`, stage the PRD and any new ADRs, commit, and open a PR to `develop`, then **stop** for human review: ```bash git checkout -b feature/specs/<SLUG> - git add <ARTIFACTS_DIR>/<SLUG>/PRD.md docs/adr/ + git add <ARTIFACTS_DIR>/<SLUG>/PRD.md docs/adr/<NNNN-new-adr>.md # name each ADR you added + git status --porcelain # confirm nothing else is staged git commit -m "plan(<SLUG>): PRD and ADRs" git push origin feature/specs/<SLUG> - gh pr create --base develop --title "plan(<SLUG>): PRD and ADRs" --body "<why + summary>" + gh pr create --repo "<REPO_REF>" --base develop --title "plan(<SLUG>): PRD and ADRs" --body "<why + summary>" ``` - (Adapt the tracker/host commands to `TRACKER` if the project is not GitHub.) On **reject**, return - to Step 4 and grill the open points, then re-run from Step 7. + For Azure DevOps the last line becomes + `az repos pr create --repository "<REPO_REF>" --target-branch develop --title "…" --description "…"`. + (Adapt the tracker/host commands to `TRACKER` if the project is not GitHub; the repository is always + pinned, whichever host.) On **reject**, return to Step 4 and grill the open points, then re-run from + Step 7. -- **`stage-only`**: write the PRD (already done in Step 7) and `git add` it plus any new ADRs, print a - suggested PR title/body, and **stop** — leave the branch, commit, push, and PR to the user. +- **`stage-only`**: write the PRD (already done in Step 7) and `git add` it plus any new ADRs **by + name** (same staging rule), print a suggested PR title/body, and **stop** — leave the branch, + commit, push, and PR to the user. ## Step 9 — Summary diff --git a/apps/claude-code/unic-archon-dlc/commands/tickets.md b/apps/claude-code/unic-archon-dlc/commands/tickets.md index 2e1ece4b..967c846a 100644 --- a/apps/claude-code/unic-archon-dlc/commands/tickets.md +++ b/apps/claude-code/unic-archon-dlc/commands/tickets.md @@ -65,6 +65,7 @@ try { ok: true, artifacts_dir: config.artifacts_dir, tracker: config.tracker, + repo_ref: g('project.repo_ref'), estimations: config.estimations, tickets: config.tickets, issue_template: g('templates.issue'), @@ -83,8 +84,14 @@ EOJS ``` Parse the JSON. If `ok` is `false`, print `message` verbatim and **stop**. Otherwise keep: -`ARTIFACTS_DIR`, `TRACKER` (`.type`/`.access`/`.coords`), `ESTIMATIONS`, `GATE` (`tickets.gate`), -`ISSUE_TEMPLATE`, `BUG_TEMPLATE`, `LABELS`, and `MATT_SUITE`. +`ARTIFACTS_DIR`, `TRACKER` (`.type`/`.access`/`.coords`), `REPO_REF` (`project.repo_ref`), +`ESTIMATIONS`, `GATE` (`tickets.gate`), `ISSUE_TEMPLATE`, `BUG_TEMPLATE`, `LABELS`, and `MATT_SUITE`. + +`REPO_REF` is the repository every host CLI call is pinned to — `<owner>/<repo>` (or +`<host>/<owner>/<repo>`) for GitHub, the repository name or ID for Azure DevOps. It is an optional +config key, so it may be `null`; Step 10 handles that case. Take it from config only — never from +`git remote get-url origin` or `gh repo view`, which follow the host's own remote precedence +(`upstream` > `github` > `origin`) and resolve to the upstream parent on a fork clone. If `MATT_SUITE.present` is `false`, warn that `/to-issues` is a declared dependency and slicing quality will degrade, then continue (non-blocking). @@ -240,9 +247,11 @@ dependency-ordered baton `/build` consumes — it carries each slice's `acceptan Publish each slice to the team's tracker by **composing the configured tracker system-skill** (MCP-first) or its CLI (`gh` / `az` / `jira` per `TRACKER.access`; or the `azure-devops-cli` skill) — -read `TRACKER.type` / `TRACKER.coords` from config; never hardcode a tracker. Publish in the -dependency `order` from Step 8 (**blockers first**) so each issue can reference the real tracker IDs -of its blockers. +read `TRACKER.type` / `TRACKER.coords` from config; never hardcode a tracker. Pin every host CLI call +to `REPO_REF` — `gh issue create --repo "<REPO_REF>" …` for GitHub, `--repository "<REPO_REF>"` for +Azure DevOps — so the issues land on the configured repository rather than the one the CLI infers +from the checkout. Publish in the dependency `order` from Step 8 (**blockers first**) so each issue +can reference the real tracker IDs of its blockers. For each slice, build the issue body from `ISSUE_TEMPLATE` (use `BUG_TEMPLATE` for `type: bug`; fall back to Matt's `/to-issues` body template if the config template is null). The body MUST carry @@ -255,22 +264,47 @@ parent issue. The plan is human-approved via a PR — never merge it yourself. Behaviour follows `GATE`: +**Staging rule (both gates).** Stage paths you have named — here, `issues.json` alone. Never +`git add -A`, `git add .`, `git add -u`, or a bare directory. Never stage `pr-body.md` (or +`.pr-body.md`), `*.tmp.md`, `*.scratch.md`, `*-report.md` at the repo root, or anything under +Archon's per-run artifacts dir (the `$ARTIFACTS_DIR` environment variable, which resolves outside the +repo under `~/.archon/workspaces/<name>/artifacts/` — not the in-repo `ARTIFACTS_DIR` config value, +whose `issues.json` you do commit). After staging, run `git status --porcelain` and confirm every +staged entry is a path you named; `git restore --staged "<path>"` anything else before you commit. + +**Repository pinning rule (`open-pr` gate).** Pass `REPO_REF` explicitly: `--repo "<REPO_REF>"` for +GitHub, `--repository "<REPO_REF>"` for Azure DevOps. If `REPO_REF` is null or empty, do **not** open +the PR: print + +``` +project.repo_ref is not set in .archon/unic-dlc.config.yaml. Without it the host CLI infers the +repository from the checkout and opens the PR against the upstream parent on a fork clone. Set +project.repo_ref (run /unic-archon-dlc:setup, or add the key by hand), then re-run this gate. +``` + +then fall back to `stage-only` behaviour below and **stop**. This mirrors the Archon boxes, which +cancel on the same missing key rather than failing. + - **`open-pr`** (default): create `feature/tickets/<SLUG>`, stage `issues.json`, commit, and open a PR to `develop`, then **stop** for human review: ```bash git checkout -b feature/tickets/<SLUG> git add <ARTIFACTS_DIR>/<SLUG>/issues.json + git status --porcelain # confirm nothing else is staged git commit -m "tickets(<SLUG>): vertical-slice issues" git push origin feature/tickets/<SLUG> - gh pr create --base develop --title "tickets(<SLUG>): vertical-slice issues" --body "<why + slice summary + tracker links>" + gh pr create --repo "<REPO_REF>" --base develop --title "tickets(<SLUG>): vertical-slice issues" --body "<why + slice summary + tracker links>" ``` - (Adapt the host commands to `TRACKER` if the project is not GitHub.) On **reject**, return to - Step 4 and revise the breakdown, then re-run from Step 8. + For Azure DevOps the last line becomes + `az repos pr create --repository "<REPO_REF>" --target-branch develop --title "…" --description "…"`. + (Adapt the host commands to `TRACKER` if the project is not GitHub; the repository is always pinned, + whichever host.) On **reject**, return to Step 4 and revise the breakdown, then re-run from Step 8. -- **`stage-only`**: write `issues.json` (already done in Step 8) and `git add` it, print a suggested - PR title/body, and **stop** — leave the branch, commit, push, and PR to the user. +- **`stage-only`**: write `issues.json` (already done in Step 8) and `git add` it **by name** (same + staging rule), print a suggested PR title/body, and **stop** — leave the branch, commit, push, and + PR to the user. ## Step 11 — Summary diff --git a/apps/claude-code/unic-archon-dlc/lib/config-schema.mjs b/apps/claude-code/unic-archon-dlc/lib/config-schema.mjs index ca78540d..f3d20788 100644 --- a/apps/claude-code/unic-archon-dlc/lib/config-schema.mjs +++ b/apps/claude-code/unic-archon-dlc/lib/config-schema.mjs @@ -21,6 +21,13 @@ import { getDefaultLabels } from './labels-config.mjs' /** * Dotted paths that must be present and non-null after a merge for a config to be usable. + * + * `project.repo_ref` is deliberately NOT here. It is an OPTIONAL key that defaults to `null`: the + * config carries it so every PR-touching box node can pin its host CLI to one repository + * (`gh --repo` / `az repos … --repository`) instead of letting the CLI infer the repo from the + * checkout — which resolves to the upstream parent on a fork clone. A box that needs it guards on + * its absence and CANCELS with an actionable message (ADR-0011: expected precondition failures + * cancel, they do not fail); promoting the key to mandatory belongs with the Archon 0.7.0 adoption. * @type {readonly string[]} */ export const MANDATORY_PATHS = ['tracker.type', 'project.pr_strategy', 'project.branching'] @@ -73,7 +80,10 @@ _Generated by unic-archon-dlc /specs._ */ export function defaultConfig() { return { - project: { name: null, repo_layout: 'single-context', branching: null, pr_strategy: null }, + // `repo_ref` is the host-agnostic repository reference every PR-touching node pins to: + // "<owner>/<repo>" (or "<host>/<owner>/<repo>") for github, the repository name or ID for + // ado. Optional — `null` until a team fills it in by hand. See MANDATORY_PATHS. + project: { name: null, repo_layout: 'single-context', branching: null, pr_strategy: null, repo_ref: null }, tracker: { type: null, access: { mcp: null, cli: null }, coords: {} }, docs: { type: 'markdown', publish: false, access: { mcp: null, cli: null } }, design: { type: 'none', access: { mcp: null } }, diff --git a/apps/claude-code/unic-archon-dlc/package.json b/apps/claude-code/unic-archon-dlc/package.json index 547927c9..5bae2abb 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.12.0", + "version": "0.12.1", "private": true, "license": "LGPL-3.0-or-later", "type": "module", diff --git a/apps/claude-code/unic-archon-dlc/test/config-schema.test.mjs b/apps/claude-code/unic-archon-dlc/test/config-schema.test.mjs index de259221..1977d512 100644 --- a/apps/claude-code/unic-archon-dlc/test/config-schema.test.mjs +++ b/apps/claude-code/unic-archon-dlc/test/config-schema.test.mjs @@ -11,6 +11,7 @@ import { defaultConfig, isLegacyConfig, loadConfig, + MANDATORY_PATHS, mergeConfig, migrateLegacy, toYaml, @@ -287,6 +288,44 @@ test('mergeConfig preserves a team override of triage, filling untouched sub-key assert.equal(triage.out_of_scope_dir, '.out-of-scope', 'untouched sub-key filled from default') }) +test('defaultConfig ships project.repo_ref as an optional key defaulting to null', () => { + const project = /** @type {any} */ (defaultConfig().project) + assert.equal(project.repo_ref, null, 'the key exists so the config can carry it, unset by default') + assert.ok( + !MANDATORY_PATHS.includes('project.repo_ref'), + 'repo_ref stays optional — promoting it to mandatory is out of scope (a box guards and cancels instead)' + ) +}) + +test('validateConfig passes with repo_ref unset — the key is optional, not mandatory', () => { + const config = mergeConfig( + { tracker: { type: 'github' }, project: { branching: 'gitflow', pr_strategy: 'merge' } }, + {} + ) + const result = validateConfig(config) + assert.ok('ok' in result, 'a config with no repo_ref is still usable') +}) + +test('mergeConfig auto-fills project.repo_ref for an existing config that predates it', () => { + // A config written before repo_ref existed keeps every answered project key. + const merged = mergeConfig( + { tracker: { type: 'github' }, project: { branching: 'gitflow', pr_strategy: 'merge' } }, + {} + ) + const project = /** @type {any} */ (merged.project) + assert.equal(project.repo_ref, null, 'repo_ref filled from default') + assert.equal(project.branching, 'gitflow', 'answered sibling key untouched') + assert.equal(project.pr_strategy, 'merge', 'answered sibling key untouched') +}) + +test('mergeConfig carries a host-agnostic repo_ref through, for github and for ado', () => { + const github = /** @type {any} */ (mergeConfig({ project: { repo_ref: 'unic/unic-agents-plugins' } }, {}).project) + assert.equal(github.repo_ref, 'unic/unic-agents-plugins', 'github owner/repo form preserved verbatim') + const ado = /** @type {any} */ (mergeConfig({ project: { repo_ref: 'MyRepo' } }, {}).project) + assert.equal(ado.repo_ref, 'MyRepo', 'ado repository name preserved verbatim') + assert.equal(github.repo_layout, 'single-context', 'untouched sub-key filled from default') +}) + test('mergeConfig preserves a team override of specs and templates.prd, filling gaps', () => { const merged = mergeConfig({ specs: { gate: 'stage-only' }, templates: { prd: '# Custom\n## Goal\n' } }, {}) const specs = /** @type {any} */ (merged.specs)