forgedock 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/geo-audit.md +3 -1
  11. package/commands/incident-response.md +3 -1
  12. package/commands/issue.md +4 -2
  13. package/commands/milestone.md +8 -6
  14. package/commands/orchestrate/config.md +2 -2
  15. package/commands/orchestrate/phase-1-resolve.md +56 -12
  16. package/commands/orchestrate/phase-3-dependency.md +15 -5
  17. package/commands/orchestrate/phase-4-execution.md +593 -78
  18. package/commands/orchestrate/phase-5-cleanup.md +4 -2
  19. package/commands/orchestrate/phase-6-report.md +7 -3
  20. package/commands/orchestrate.md +13 -9
  21. package/commands/pipeline-health/phase-4-proposals.md +6 -4
  22. package/commands/pipeline-health/phase-5-report.md +4 -2
  23. package/commands/pipeline-health.md +9 -7
  24. package/commands/pipeline-resume.md +4 -2
  25. package/commands/qa-sweep.md +3 -1
  26. package/commands/quality-gate.md +4 -3
  27. package/commands/recover-orphans.md +172 -13
  28. package/commands/review-pr-agents.md +6 -4
  29. package/commands/review-pr-staging.md +13 -5
  30. package/commands/review-pr.md +187 -19
  31. package/commands/rollback.md +3 -1
  32. package/commands/security-audit.md +3 -1
  33. package/commands/signal-planner.md +6 -4
  34. package/commands/sync-ecosystem.md +4 -2
  35. package/commands/test-gate.md +4 -2
  36. package/commands/upgrade-deps.md +3 -1
  37. package/commands/validate.md +3 -1
  38. package/commands/work-on/build/architect.md +81 -19
  39. package/commands/work-on/build/context.md +61 -18
  40. package/commands/work-on/build/implement.md +62 -18
  41. package/commands/work-on/build/validate.md +240 -60
  42. package/commands/work-on/build.md +344 -146
  43. package/commands/work-on/close.md +617 -246
  44. package/commands/work-on/decompose.md +239 -41
  45. package/commands/work-on/investigate.md +377 -61
  46. package/commands/work-on/remediate.md +42 -9
  47. package/commands/work-on/review.md +240 -29
  48. package/commands/work-on-monolithic.md +48 -7
  49. package/commands/work-on.md +180 -2014
  50. package/docs/CODEX.md +2 -0
  51. package/docs/CONFIG.md +11 -0
  52. package/package.json +1 -1
  53. package/scripts/build-spec-graph.mjs +11 -4
  54. package/scripts/check-spec-markers.sh +11 -1
  55. package/scripts/checkbox-sections.test.sh +8 -23
  56. package/scripts/extract-affected-files.sh +5 -2
  57. package/scripts/extract-affected-files.test.sh +27 -0
  58. package/scripts/forge-root.test.sh +176 -0
  59. package/scripts/is-docs-only.sh +31 -0
  60. package/scripts/is-docs-only.test.sh +64 -0
  61. package/scripts/lint-dispatch-prompt.sh +217 -0
  62. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  63. package/scripts/validate-spec-graph.sh +3 -2
  64. package/scripts/verify-phase-trail.sh +282 -0
  65. package/scripts/verify-phase-trail.test.sh +298 -0
  66. package/scripts/wait-ci-green.sh +98 -0
  67. package/scripts/wait-ci-green.test.sh +99 -0
@@ -1,51 +1,140 @@
1
1
  ---
2
2
  description: Build subcommand — create worktree, post contract, sequence context/architect/implement/validate
3
- argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
3
+ context: fork
4
+ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BASE}"
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
7
8
 
8
9
  # work-on/build — Build Phase Orchestrator
9
10
 
11
+ > **Skill Name Resolution (forked phase)**: `{FORGE_SKILL_PREFIX}` is the namespace this skill itself was invoked under — invoked as `forgedock:work-on:…` → `forgedock:` (nesting `:`); as `work-on:…` → empty (`install.sh`); as `forge-work-on-…` → `forge-` (Codex, nesting `-`); OpenCode → empty with `-` nesting. Confirm the target name in the available-skills list before calling it. A forked phase receives no resolved value from its caller; never guess, and if the target skill is not listed return BLOCKED "skill not found: <name>".
12
+
10
13
  **Input**: $ARGUMENTS
11
14
 
12
- **Invoked by**: `work-on.md` Phase 3 — entered when the issue carries label `workflow:ready-to-build` or `workflow:building` (see Universal Phase Dispatcher in work-on.md).
13
- **Output**: Create worktree, post contract, run build phases, return result to work-on.md.
15
+ **Invoked by**: the work-on router, when the issue carries label `workflow:ready-to-build` or `workflow:building`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub and git.
16
+ **Output**: Create worktree, classify complexity, post contract, run the child phases (context, architect, implement, validate) through `Skill()`, run the acceptance gate, and print exactly one `BUILD_RESULT:` block as the final reply.
14
17
 
15
- **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (3B classification, 3D label transitions) stay at this tier because they're interleaved with the reasoning-heavy build steps (3C.5/3C.6/3F) in the same `Skill()` invocation — see `work-on.md` section "Model and Effort Tiering — What Actually Applies". <!-- Added: forge#1827 -->
18
+ **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
16
19
  **NEVER use plan mode (EnterPlanMode).**
17
20
 
18
- **CRITICAL: You MUST execute ALL phases B0–B6 in order. Phases B3 (context) and B4 (architect) are skipped ONLY when COMPLEXITY_BAND: TRIVIAL (read from FORGE:FAST_PATH comment in Phase B0). For STANDARD and COMPLEX tasks they are NOT optional — skipping them degrades build quality.**
21
+ **CRITICAL: You MUST execute ALL phases B0–B6.5 in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
19
22
 
20
- ### Canonical Build Path (STANDARD/fast-lane) <!-- Added: forge#1276 -->
23
+ <!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
21
24
 
22
- **Default execution model: inline.** For STANDARD and fast-lane issues, phases B3 (context gathering) and B4 (architecture planning) run **inline in the current context window** — not as separate `Skill()` sub-agent spawns. B5 (implement) and B6 (validate) also run inline.
25
+ ---
23
26
 
24
- `Skill()` invocations for context/architect sub-phases are only permitted when the Spawn-Decision Table (work-on.md `##Spawn-Decision Policy`) explicitly applies — specifically Row (c) (parent context near overflow: ≥20 Skill invocations or ≥10 files already changed before the build sub-phase). For most issues, the Skill() forms shown in B3 and B4 below are **reference documentation** describing the sub-phase contract, not mandatory sub-agent invocations.
27
+ ## Inputs
25
28
 
26
- **Build topology summary**:
29
+ Parse from $ARGUMENTS:
30
+ - `{NUMBER}` — issue number (required)
31
+ - `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`) (required)
32
+ - `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`) (required)
33
+ - `--base {PR_BASE}` — PR target branch, computed by the router (e.g. `milestone/modular-pipeline-architecture` or `staging`) (required)
34
+ - `--worktree {WORKTREE_PATH}` / `--branch {BRANCH}` — optional hints from a router re-invocation; when present and consistent with the issue they are reused, otherwise they are re-derived in B1
27
35
 
28
- | Path | When | Phases |
29
- |------|------|--------|
30
- | **STANDARD/fast-lane (default)** | All issues not matching exceptions below | B0 → B1 → B2 → B2.5 → [B3] → [B4] → B5 → B6 — all inline |
31
- | **Spawn exception (Row c)** | ≥20 Skill invocations OR ≥10 files changed before build | Spawn B3/B4 as fresh sub-agents via `Skill()` |
32
- | **TRIVIAL fast-path** | COMPLEXITY_BAND: TRIVIAL | Skip B3 and B4 entirely |
36
+ **Fail closed**: if `{NUMBER}`, `--repo` or `--gh-flag` is missing, or `--base` is missing or empty, take the Blocked exit below with blocker "missing required arg: <name>" — never guess a base branch (no fallback to `staging`/`main`).
33
37
 
34
- This resolves the three-topology conflict: `work-on.md` Phase 3 (inline 3A–3M), `work-on/build.md` (this file), and `work-on-monolithic.md` ([BENCHMARK]) all describe the **same canonical inline path**. `work-on/build.md` adds worktree lifecycle management (B1) and the FORGE:CONTRACT handoff (B2) that the monolithic variant omits for brevity. The `Skill()` forms in B3/B4 below document the sub-phase contract and serve as the exception path only. <!-- Added: forge#1276 -->
38
+ ---
35
39
 
36
- <!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
40
+ ## Script resolution
41
+
42
+ **Shell state does not persist between Bash tool calls** (each call is a fresh shell): paste the block below at the top of every bash command in this skill that calls `resolve_script` or uses `FORGE_ROOT`/`UNIVERSAL_DIR`.
43
+
44
+ ```bash
45
+ # Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
46
+ # Shell state does NOT persist between Bash tool calls: include this whole block at the top of
47
+ # every command that uses resolve_script or FORGE_ROOT.
48
+ REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
49
+ [ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
50
+ ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
51
+ ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
52
+ ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
53
+ # Bounds check: reject adaptive_scripts.directory values that escape the repo root.
54
+ # Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
55
+ # slash in paths.root does not inject a '//' into the glob and trigger a false positive.
56
+ REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
57
+ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
58
+ echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
59
+ ADAPTIVE_ENABLED=false
60
+ fi
61
+ # ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
62
+ # below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
63
+ # repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
64
+ # Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
65
+ # ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
66
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
67
+ FORGE_ROOT=""
68
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
69
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
70
+ _l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
71
+ # newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
72
+ _v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
73
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
74
+ # '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
75
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
76
+ while IFS= read -r _c; do
77
+ case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
78
+ done <<< "$_k"
79
+ fi
80
+ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
81
+ # NOTE: never resolve this via `which` or `find` — universal scripts are
82
+ # install-relative, not installed on $PATH, so a PATH lookup always misses.
83
+ # FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
84
+ # Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
85
+ # locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
86
+ # does not exist, resolve_script() falls through to Tier 4 (prose) below,
87
+ # which is always safe and available. A missing script is never a reason
88
+ # to search the filesystem. <!-- Added: forge#1984 -->
89
+
90
+ resolve_script() {
91
+ local operation="$1"
92
+ # Tier 2: per-repo adaptive (skip if disabled)
93
+ if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
94
+ echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
95
+ return
96
+ fi
97
+ # Tier 3: universal script
98
+ if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
99
+ echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
100
+ return
101
+ fi
102
+ # Tier 4: prose fallback
103
+ echo "prose:"
104
+ }
105
+ ```
37
106
 
38
107
  ---
39
108
 
40
- ## Inputs
109
+ ## Result emission and Blocked exit
41
110
 
42
- Parse from $ARGUMENTS:
43
- - `{NUMBER}` — issue number (required)
44
- - `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}` — resolved from `forge.yaml → project`)
45
- - `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
46
- - `--base {PR_BASE}` — PR target branch (e.g. `milestone/modular-pipeline-architecture` or `staging`)
111
+ Every exit path of this skill — COMPLETE, ALREADY_DONE, INVESTIGATION_COMPLETE, every guard, every failure — ends by printing exactly one block as the final reply:
47
112
 
48
- **Phase notation**: This file uses **B0–B6** for its own phases. The calling orchestrator (`work-on.md`) uses **3A–3M** for its sub-phases. Mapping: work-on.md Phase 3A = B0 (load state), Phase 3B = complexity classification (posts `FORGE:FAST_PATH` before invoking build), Phase 3C onward maps to B1+ in this file. When cross-references mention "Phase 3B", they refer to work-on.md's Phase 3B, not a phase in this file. <!-- Added: forge#1380 -->
113
+ ```
114
+ BUILD_RESULT:
115
+ status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
116
+ branch: {BRANCH}
117
+ worktree: {WORKTREE_PATH}
118
+ blocker: {description if status=BLOCKED, else empty}
119
+ ```
120
+
121
+ `branch` / `worktree` are empty when the build stopped before B1 created them.
122
+
123
+ Every `BLOCKED` exit first runs this procedure (set `BLOCKER` to the reason text), then prints the block with `status: BLOCKED`:
124
+
125
+ ```bash
126
+ BLOCKER="{reason}"
127
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
128
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true
129
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILD_BLOCKED -->
130
+ ## Build Blocked
131
+
132
+ ${BLOCKER}
133
+
134
+ Human attention required (needs-human)." 2>/dev/null || true
135
+ ```
136
+
137
+ (When the arguments themselves are missing and no `gh` call is possible, skip the procedure and print the block only.)
49
138
 
50
139
  ---
51
140
 
@@ -57,109 +146,193 @@ Re-read current state before doing anything:
57
146
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
58
147
 
59
148
  # Check investigation report
60
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
149
+ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
61
150
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
62
151
 
63
- # Check if build already completed
64
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
152
+ # Check if build already completed (require FORGE:BUILDER:COMPLETE — not just FORGE:BUILDER)
153
+ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
65
154
  --jq '.[] | select(.body | contains("FORGE:BUILDER")) | .body'
155
+
156
+ # Existing classification from a prior run (resume path)
157
+ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
158
+ --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
66
159
  ```
67
160
 
68
161
  **Resume check**:
69
- - If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → build already complete. Return `BUILD_RESULT: status: ALREADY_DONE` to router.
70
- - If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate.md V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
162
+ - If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
163
+ - If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
71
164
  ```bash
72
- PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
165
+ PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
73
166
  --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
74
167
  if [ -n "$PARTIAL_ID" ]; then
75
168
  gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
76
169
  echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build from Phase B2"
77
170
  fi
78
171
  ```
79
- - If no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → EXIT with `BUILD_RESULT: status: BLOCKED`, blocker: "Investigation not complete — run investigate first".
172
+ - If there is no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → Blocked exit with blocker "Investigation not complete — run investigate first".
80
173
 
81
- Extract from investigation report:
82
- - Affected files list
174
+ Extract from the investigation report:
175
+ - Affected files list → `{AFFECTED_FILES}` (space-separated repo-relative paths; this is what B3/B4 receive via `--files`)
83
176
  - Root cause
84
177
  - Recommendation
85
178
  - Task type (Bug Fix / Feature / Refactor / Maintenance / UI/UX / Full-Stack)
86
179
 
87
- **Read COMPLEXITY_BAND** (from `FORGE:FAST_PATH` comment posted by Phase 3B of `work-on.md` — the complexity classification step that runs before invoking this file): <!-- Fixed: forge#1380 -->
180
+ ---
181
+
182
+ ## Phase B0.5: Classify Task Type and Complexity (MANDATORY — build owns this) <!-- Added: forge#1380 -->
183
+
184
+ Build classifies the task and posts the `FORGE:FAST_PATH` comment. `scripts/verify-phase-trail.sh` fails the review preflight if it is missing.
185
+
186
+ **Resume path**: If `EXISTING_FAST_PATH` is non-empty, extract COMPLEXITY_BAND from it and skip re-classification (do not post a second comment):
187
+
88
188
  ```bash
89
- COMPLEXITY_BAND=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
90
- --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null \
91
- | sed -n 's/.*\*\*COMPLEXITY_BAND\*\*: \([A-Z_]*\).*/\1/p' | head -1)
92
- # Default to STANDARD if not found (conservative — runs full pipeline)
93
- COMPLEXITY_BAND="${COMPLEXITY_BAND:-STANDARD}"
94
- echo "COMPLEXITY_BAND: $COMPLEXITY_BAND"
189
+ COMPLEXITY_BAND=$(printf '%s\n' "$EXISTING_FAST_PATH" \
190
+ | sed -n 's/.*\*\*COMPLEXITY_BAND\*\*: *\([A-Za-z_]*\).*/\1/p' | head -1 | tr '[:lower:]' '[:upper:]')
191
+ echo "COMPLEXITY_BAND (existing): ${COMPLEXITY_BAND:-<none>}"
95
192
  ```
96
193
 
194
+ If `COMPLEXITY_BAND` is empty after this (no prior classification, or an unparseable one), classify now as below.
195
+
196
+ **Step 1 — Task type classification:**
197
+
198
+ | Signal | Type | Approach |
199
+ |--------|------|----------|
200
+ | Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
201
+ | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
202
+ | Feature + services/ | Backend Feature | Implement directly |
203
+ | Feature + both | Full-Stack | Backend first, then frontend-design |
204
+ | Bug + web/ | Frontend Fix | Direct |
205
+ | Bug + services/ | Backend Fix | Direct |
206
+ | Refactor/docs | Maintenance | Direct |
207
+
208
+ **Investigation tasks — early exit (skip B2, B2.1, B2.5, B3, B4):** If task type = Investigation (title prefix, or task type = Investigation in the investigator report), the Builder Contract, Context Gathering and Architecture Plan are NOT run. Post the `<!-- FORGE:FAST_PATH -->` comment below, still run B1 (worktree and `building` label), then go straight to B5 (implement → issue-creation path). Do NOT run B2/B2.1/B2.5/B3/B4 for investigation tasks.
209
+
210
+ ```bash
211
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
212
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
213
+ ## Fast-Path Classification
214
+
215
+ **COMPLEXITY_BAND**: INVESTIGATION
216
+ **Task type**: Investigation
217
+ **Rationale**: Title prefix 'Investigate:' (or task type = Investigation from investigator report) — skipping Builder Contract, Context Gathering and Architecture Plan. Jumping directly to implement (issue creation).
218
+ **Phases skipped**: contract, context, architect"
219
+ ```
220
+
221
+ **Step 2 — Complexity classification (for non-Investigation tasks):**
222
+
223
+ Classify COMPLEXITY_BAND based on affected file count and task nature:
224
+
225
+ | Condition | COMPLEXITY_BAND |
226
+ |-----------|-----------------|
227
+ | Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
228
+ | 1–5 files, existing patterns, no cross-service impact | STANDARD |
229
+ | 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
230
+
231
+ Post `<!-- FORGE:FAST_PATH -->` immediately after classification:
232
+
233
+ ```bash
234
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
235
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
236
+ ## Fast-Path Classification
237
+
238
+ **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
239
+ **Task type**: {TASK_TYPE}
240
+ **Affected file count**: {N}
241
+ **Rationale**: {one-sentence explanation of classification decision}
242
+ **Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
243
+ ```
244
+
245
+ **TRIVIAL tasks**: B3 (context) and B4 (architect) are skipped only. B2 (Builder Contract) is **retained** — it still runs. When filling in **Phases skipped**, write: `context, architect`. (Each skipped child still posts its own skip marker when invoked — see B3/B4.)
246
+
247
+ **STANDARD and COMPLEX tasks**: Run the full pipeline. No phases skipped.
248
+
249
+ Whatever path was taken, set `COMPLEXITY_BAND` (uppercase) for the rest of this run and echo it. A missing or unparseable band at this point is a bug in this phase — re-classify, never default silently.
250
+
97
251
  ---
98
252
 
99
253
  ## Phase B1: Create Worktree & Branch
100
254
 
101
255
  ### B1A: Derive branch name
102
256
 
103
- From issue title: lowercase, hyphenated, max 40 chars (truncate if needed).
257
+ If `--branch` was passed, or a local/remote branch already exists whose name ends in `-{NUMBER}` (`git branch -a --list "*-{NUMBER}"`), reuse it (resume). Otherwise derive from the issue title: lowercase, hyphenated, max 40 chars (truncate if needed).
104
258
  - Bug / fix issues → prefix `fix/`
105
259
  - Feature issues → prefix `feat/`
106
260
  - Refactor / maintenance → prefix `fix/` or `refactor/`
107
261
 
108
- Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85`.
262
+ Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85`. Then `BRANCH_SLUG` is `{BRANCH}` with every `/` replaced by `-`.
109
263
 
110
264
  ### B1B: Determine source branch
111
265
 
112
266
  - Review-finding issue → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
113
- - **Milestone review-finding hybrid lane** (ONLY when Code branch matches `milestone/*`): This is a high-risk lane. The worktree will carry the full milestone history. The PR target is `staging` (or the base specified). **DANGER: Agents MUST NOT use `git merge` to resolve any conflicts in this lane.** Merge-based conflict resolution will pull the entire milestone commit tree onto staging, contaminating it with unapproved code. Use `git rebase` or `git cherry-pick` only. If conflicts cannot be resolved without a merge, post a comment on the issue, add `needs-human`, and STOP.
114
- - **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not, fall back to the lane default (`staging` for fast lane, `milestone/{slug}` for feature lane) and note the fallback:
267
+ - **Milestone review-finding hybrid lane** (ONLY when Code branch matches `milestone/*`): This is a high-risk lane. The worktree will carry the full milestone history. The PR target is `{PR_BASE}`. **DANGER: Agents MUST NOT use `git merge` to resolve any conflicts in this lane.** Merge-based conflict resolution will pull the entire milestone commit tree onto the PR target, contaminating it with unapproved code. Use `git rebase` or `git cherry-pick` only. If conflicts cannot be resolved without a merge, post a comment on the issue, add `needs-human`, and STOP (Blocked exit).
268
+ - **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not, fall back to `{PR_BASE}` and note the fallback:
115
269
  ```bash
116
270
  SOURCE_BRANCH="{CODE_BRANCH_FROM_ISSUE_BODY}"
117
271
  if ! git ls-remote --exit-code origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
118
- echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to lane default '$PR_BASE'"
119
- SOURCE_BRANCH="$PR_BASE"
272
+ echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to PR base '{PR_BASE}'"
273
+ SOURCE_BRANCH="{PR_BASE}"
120
274
  fi
121
275
  ```
122
- - Feature lane (has milestone) → branch from `origin/{PR_BASE}`
123
- - Fast lane (no milestone) → branch from `origin/staging`
276
+ - Every other issue (feature lane or fast lane) → `SOURCE_BRANCH` = `{PR_BASE}` (the `--base` arg). There is no hardcoded lane default here: the router already chose `{PR_BASE}` for the lane. <!-- Fixed: forge#639 -->
124
277
 
125
278
  ### B1C: Create worktree
126
279
 
127
280
  ```bash
128
- WORKTREE_ROOT="/path/to/repo/.claude/worktrees"
281
+ REPO_ROOT=$(cd "$(git rev-parse --git-common-dir)/.." && pwd)
282
+ WORKTREE_ROOT="${REPO_ROOT}/.claude/worktrees"
129
283
  if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
130
284
  [ -n "${OPENCODE_SESSION_ID:-}" ] ||
131
285
  [ -n "${OPENCODE_PID:-}" ] ||
132
286
  [ -n "${OPENCODE:-}" ]; then
133
- WORKTREE_ROOT="/path/to/repo/.opencode/worktrees"
287
+ WORKTREE_ROOT="${REPO_ROOT}/.opencode/worktrees"
134
288
  elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
135
- WORKTREE_ROOT="/path/to/repo/.codex/worktrees"
289
+ WORKTREE_ROOT="${REPO_ROOT}/.codex/worktrees"
136
290
  fi
291
+ # An explicit, absolute forge.yaml paths.worktree_base wins over the runtime default, so worktrees
292
+ # land where the project's cleanup and recovery tooling expects them (e.g. a project that keeps worktrees under .forge/worktrees).
293
+ CONFIGURED_BASE=$(yq '.paths.worktree_base // ""' forge.yaml 2>/dev/null || echo "")
294
+ case "$CONFIGURED_BASE" in /*) WORKTREE_ROOT="${CONFIGURED_BASE%/}" ;; esac
295
+ mkdir -p "$WORKTREE_ROOT"
137
296
  WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
138
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
297
+ git fetch origin "{SOURCE_BRANCH}" 2>/dev/null || true
298
+ if [ -d "$WORKTREE_PATH" ]; then
299
+ # Reuse existing worktree only when it is on the correct branch
300
+ CURRENT=$(git -C "$WORKTREE_PATH" branch --show-current 2>/dev/null)
301
+ if [ "$CURRENT" != "{BRANCH}" ]; then
302
+ git worktree remove "$WORKTREE_PATH" --force
303
+ fi
304
+ fi
305
+ if [ ! -d "$WORKTREE_PATH" ]; then
306
+ if git show-ref --verify --quiet "refs/heads/{BRANCH}"; then
307
+ git worktree add "$WORKTREE_PATH" "{BRANCH}" # resume: branch already exists
308
+ else
309
+ git worktree add "$WORKTREE_PATH" -b "{BRANCH}" "origin/{SOURCE_BRANCH}"
310
+ fi
311
+ fi
312
+ echo "WORKTREE_PATH=$WORKTREE_PATH BRANCH={BRANCH} BASE={PR_BASE}"
139
313
  ```
140
314
 
141
- If worktree already exists at that path:
142
- ```bash
143
- # Reuse existing worktree — verify it's on the correct branch
144
- git -C {WORKTREE_PATH} branch --show-current
145
- ```
146
- If wrong branch, remove and recreate:
147
- ```bash
148
- git worktree remove {WORKTREE_PATH} --force
149
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
150
- ```
315
+ If worktree creation fails, take the Blocked exit with the git error as the blocker. Use the resulting `$WORKTREE_PATH` as `{WORKTREE_PATH}` for every later phase and in `BUILD_RESULT`.
151
316
 
152
317
  ### B1D: Set building label
153
318
 
154
319
  ```bash
155
- gh issue edit {NUMBER} {GH_FLAG} \
156
- --add-label "workflow:building" \
157
- --remove-label "workflow:ready-to-build"
320
+ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
321
+ case "$TIER" in
322
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
323
+ prose)
324
+ [ -n "${DRY_RUN:-}" ] && echo "DRY_RUN: set workflow:building" || \
325
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
326
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
327
+ ;;
328
+ esac
158
329
  ```
159
330
 
160
331
  ---
161
332
 
162
- ## Phase B2: Post Builder Contract
333
+ ## Phase B2: Post Builder Contract (skip for INVESTIGATION)
334
+
335
+ **Skip if COMPLEXITY_BAND: INVESTIGATION** — go to B5. For every other band (TRIVIAL included) the contract is mandatory. If a `FORGE:CONTRACT` comment already exists on the issue (resume), do not post a second one — continue to B2.1.
163
336
 
164
337
  Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why:
165
338
 
@@ -171,7 +344,8 @@ SHOW_ATTRIBUTION=$(yq '.branding.show_attribution // "true"' forge.yaml 2>/dev/n
171
344
  ```
172
345
 
173
346
  ```bash
174
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
347
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
348
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
175
349
  ## Builder Contract
176
350
 
177
351
  **Task type**: {TASK_TYPE}
@@ -209,28 +383,29 @@ Contract must be grounded in the investigation report. Every deliverable file mu
209
383
  **When `FORGE_COORD_ISSUE` is set**: Post a `FORGE:CLAIM` annotation on the coordination issue to advertise this agent's active resource reservation to the orchestrator and peer agents. This enables the claims-board Layer-2/4 relaxation sweep (orchestrate Step 4B) to identify issue-pairs with disjoint file sets and downgrade unnecessary serialization edges.
210
384
 
211
385
  ```bash
386
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
212
387
  if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
213
388
  COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
214
389
  if [ -n "$COORD_NUM" ]; then
215
390
  # Extract file paths from the just-posted FORGE:CONTRACT deliverables table
216
- CLAIMED_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
391
+ CLAIMED_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
217
392
  --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body' 2>/dev/null \
218
393
  | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
219
- | grep -oP '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
220
- | tr -d '`' | sort -u | tr '\n' '\n' | head -20)
394
+ | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
395
+ | tr -d '`' | sort -u | head -20)
221
396
  CLAIMED_FILES="${CLAIMED_FILES:-"(files listed in FORGE:CONTRACT deliverables table)"}"
222
397
 
223
398
  # Extract preserved interfaces from the FORGE:ARCHITECT affected paths table (if present)
224
- CLAIMED_INTERFACES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
399
+ CLAIMED_INTERFACES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
225
400
  --jq '[.[] | select(.body | contains("FORGE:ARCHITECT"))] | last | .body' 2>/dev/null \
226
401
  | awk '/^### Affected Paths/{p=1; next} /^### /{p=0} p' \
227
- | grep -oP 'Function/Class.*\|.*\|' | head -10 || echo "(see FORGE:ARCHITECT for interface details)")
402
+ | grep -oE 'Function/Class.*\|.*\|' | head -10 || true)
228
403
  CLAIMED_INTERFACES="${CLAIMED_INTERFACES:-"(see FORGE:ARCHITECT comment for interface details)"}"
229
404
 
230
- CLAIM_HOLDER="#${NUMBER} / $(date -u +%Y%m%dT%H%M%S)"
231
- CLAIM_TTL="terminal state of Holder issue #${NUMBER}"
405
+ CLAIM_HOLDER="#{NUMBER} / $(date -u +%Y%m%dT%H%M%S)"
406
+ CLAIM_TTL="terminal state of Holder issue #{NUMBER}"
232
407
 
233
- gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM -->
408
+ run gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM -->
234
409
  ## Resource Claim
235
410
 
236
411
  **Holder**: ${CLAIM_HOLDER}
@@ -239,7 +414,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
239
414
  **TTL**: ${CLAIM_TTL}
240
415
 
241
416
  <!-- CLAIM:COMPLETE -->" 2>/dev/null || true
242
- echo "FORGE:CLAIM posted on coordination issue #${COORD_NUM} for #${NUMBER}"
417
+ echo "FORGE:CLAIM posted on coordination issue #${COORD_NUM} for #{NUMBER}"
243
418
  fi
244
419
  fi
245
420
  ```
@@ -248,12 +423,12 @@ fi
248
423
 
249
424
  ---
250
425
 
251
- ## Phase B2.5: Extract FUNCTION_NAMES from Contract
426
+ ## Phase B2.5: Extract FUNCTION_NAMES from Contract (skip for INVESTIGATION)
252
427
 
253
- After posting the Builder Contract, extract the primary function/class names from the contract's deliverables table. These are passed to the context subcommand for Phase C3 caller/importer discovery.
428
+ After posting the Builder Contract, extract the primary function/class names from the contract's deliverables table. These are passed to the context child for its C3 caller/importer discovery.
254
429
 
255
430
  ```bash
256
- FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
431
+ FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
257
432
  --jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
258
433
  | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
259
434
  | grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
@@ -263,56 +438,49 @@ FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
263
438
  | xargs)
264
439
  # Scope is limited to the ### Deliverables section to avoid false matches from FORGE markers,
265
440
  # phase labels (B2, C3), and identifiers mentioned in Acceptance Criteria or Quality sections.
266
- # Fallback: if extraction yields nothing, FUNCTION_NAMES remains empty string
267
- # context.md Phase C3 skips gracefully when FUNCTION_NAMES is empty (for-loop produces zero iterations)
441
+ # Fallback: if extraction yields nothing, FUNCTION_NAMES remains the empty string and
442
+ # context C3 skips gracefully (its for-loop produces zero iterations).
268
443
  ```
269
444
 
270
- If `FUNCTION_NAMES` is non-empty, it will be passed via `--functions` to the context subcommand. If empty, the `--functions` flag is omitted — Phase C3 will naturally skip with zero iterations and no error.
445
+ If `FUNCTION_NAMES` is non-empty, pass it via `--functions` to the context child. If empty, omit the `--functions` flag.
271
446
 
272
447
  ---
273
448
 
274
- ## Phase B3: Context Gathering (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
275
-
276
- **Skip if COMPLEXITY_BAND: TRIVIAL** (read from FORGE:FAST_PATH in Phase B0) — skip this phase entirely. Proceed directly to Phase B4.
277
-
278
- **For STANDARD and COMPLEX tasks**: Always run. Do NOT skip without a TRIVIAL COMPLEXITY_BAND.
449
+ ## Phase B3: Context Gathering (invoke for every band except TRIVIAL / INVESTIGATION)
279
450
 
280
- **Execution model**: Run **inline** (see Canonical Build Path above). Read the `commands/work-on/build/context.md` spec and execute its steps directly in this context window. Only spawn a Skill() sub-agent when the Spawn-Decision Table Row (c) applies (≥20 prior Skill invocations or ≥10 files already changed). <!-- Added: forge#1276 -->
451
+ **Skip only if COMPLEXITY_BAND is TRIVIAL or INVESTIGATION** — proceed directly to Phase B4. (The context child would also detect these bands and return `SKIPPED` itself; for STANDARD and COMPLEX it is NOT optional — it must be invoked.)
281
452
 
282
- Surface historical review findings and bug patterns for the affected files. The full step-by-step logic is defined in `commands/work-on/build/context.md`. Key steps: search closed issues with `review-finding` label on the affected files; check git log for past bug patterns; synthesize a `FORGE:CONTEXT` annotation and post it as a GitHub comment.
453
+ Always invoke it as a forked sub-skill — never inline:
283
454
 
284
- **Spawn exception** (only when Row (c) applies):
285
455
  ```
286
- Skill("work-on:build:context", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE_PATH} {AFFECTED_FILES} --functions {FUNCTION_NAMES}")
456
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:context", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --repo-path {WORKTREE_PATH} --files \"{AFFECTED_FILES}\" --functions \"{FUNCTION_NAMES}\"")
287
457
  ```
288
- If `FUNCTION_NAMES` is empty, omit `--functions`. The Skill() form above is the exception path — not the default. <!-- Added: forge#1276 -->
289
458
 
290
- **After context gathering**:
291
- - Structured context briefing produced (or no relevant history found) → continue to B4
292
- - Context gathering timed out or errored → log warning, continue to B4 with empty context (non-blocking)
459
+ If `FUNCTION_NAMES` is empty, omit `--functions`. The child posts its own `FORGE:CONTEXT` comment (a minimal marker when it skips a non-TRIVIAL issue).
460
+
461
+ **After the child returns** (read its `CONTEXT_RESULT:` block):
462
+ - `status: COMPLETE | PARTIAL | SKIPPED` → continue to B4
463
+ - No `CONTEXT_RESULT:` block, timeout or error → log a warning and continue to B4 (context is advisory and non-blocking)
464
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:context"
293
465
  # MUST CONTINUE to Phase B4 — context result is intermediate, NOT terminal.
294
466
 
295
467
  ---
296
468
 
297
- ## Phase B4: Architecture Planning (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
298
-
299
- **Skip if COMPLEXITY_BAND: TRIVIAL** (read from FORGE:FAST_PATH in Phase B0) — skip this phase entirely. Proceed directly to Phase B5.
469
+ ## Phase B4: Architecture Planning (invoke for every band except TRIVIAL / INVESTIGATION)
300
470
 
301
- **For STANDARD and COMPLEX tasks**: Always run. Even a 1-file STANDARD fix benefits from cross-path consistency checks. Do NOT skip without a TRIVIAL COMPLEXITY_BAND.
471
+ **Skip only if COMPLEXITY_BAND is TRIVIAL or INVESTIGATION** — proceed directly to Phase B5. For STANDARD and COMPLEX it is NOT optional; even a 1-file STANDARD fix benefits from cross-path consistency checks.
302
472
 
303
- **Execution model**: Run **inline** (see Canonical Build Path above). Read the `commands/work-on/build/architect.md` spec and execute its steps directly in this context window. Only spawn a Skill() sub-agent when the Spawn-Decision Table Row (c) applies. <!-- Added: forge#1276 -->
304
-
305
- Trace all affected code paths and produce an ordered implementation plan. The full step-by-step logic is defined in `commands/work-on/build/architect.md`. Key steps: map all callers and importers of changed functions; check consistency rules across paths; post a `FORGE:ARCHITECT` annotation with the ordered plan and a risk table.
306
-
307
- **Spawn exception** (only when Row (c) applies):
308
473
  ```
309
- Skill("work-on:build:architect", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE_PATH} --files {AFFECTED_FILES}")
474
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:architect", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --repo-path {WORKTREE_PATH} --files \"{AFFECTED_FILES}\"")
310
475
  ```
311
- The Skill() form above is the exception path — not the default. <!-- Added: forge#1276 -->
312
476
 
313
- **After architecture planning**:
314
- - Returns ordered implementation plan → continue to B5
315
- - BLOCKED (conflicting constraints that cannot be resolved inline) → post comment, add `needs-human`, return `BUILD_RESULT: status: BLOCKED`
477
+ The child posts its own `FORGE:ARCHITECT` comment (a "Skipped" marker + `:COMPLETE` on every skip, forge#2689).
478
+
479
+ **After the child returns** (read its `ARCHITECT_RESULT:` block):
480
+ - `status: COMPLETE | PARTIAL | SKIPPED` → continue to B5
481
+ - `status: BLOCKED` (conflicting constraints that cannot be resolved) → Blocked exit with the child's `blocker`
482
+ - No `ARCHITECT_RESULT:` block → re-read the issue: if a `FORGE:ARCHITECT:COMPLETE` marker exists continue to B5, otherwise Blocked exit "architect produced no result"
483
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:architect"
316
484
  # MUST CONTINUE to Phase B5 — architect result is intermediate, NOT terminal.
317
485
 
318
486
  ---
@@ -322,14 +490,15 @@ The Skill() form above is the exception path — not the default. <!-- Added: fo
322
490
  Invoke the implement subcommand to write code, stage, and post the builder comment:
323
491
 
324
492
  ```
325
- Skill("work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH}")
493
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
326
494
  ```
327
495
 
328
- **After subcommand returns**:
329
- - `IMPLEMENT_RESULT: status: COMPLETE` → continue to B6
330
- - `IMPLEMENT_RESULT: status: ALREADY_DONE` → skip to B6 (validate what's already there)
331
- - `IMPLEMENT_RESULT: status: INVESTIGATION_COMPLETE` → issues created as deliverables; return `BUILD_RESULT: status: INVESTIGATION_COMPLETE`
332
- - `IMPLEMENT_RESULT: status: BLOCKED` → post comment with blocker description, add `needs-human`, return `BUILD_RESULT: status: BLOCKED`
496
+ **After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
497
+ - `status: COMPLETE` → continue to B6
498
+ - `status: ALREADY_DONE` → continue to B6 (validate what's already there)
499
+ - `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B6/B6.5)
500
+ - `status: BLOCKED` → Blocked exit with the child's `blocker`
501
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
333
502
  # MUST CONTINUE to Phase B6 — implement result is intermediate, NOT terminal (validation still required).
334
503
 
335
504
  ---
@@ -339,25 +508,30 @@ Skill("work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_F
339
508
  Invoke the validate subcommand to run the quality gate loop, formatting, and deploy checks:
340
509
 
341
510
  ```
342
- Skill("work-on:build:validate", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --files {CHANGED_FILES}")
511
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:validate", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --files \"{CHANGED_FILES}\"")
343
512
  ```
344
513
 
345
- Where `{CHANGED_FILES}` is the space-separated list of files changed by the implement subcommand (read from `IMPLEMENT_RESULT` or from the `<!-- FORGE:BUILDER -->` comment).
514
+ Where `{CHANGED_FILES}` is the space-separated list of files changed by the implement subcommand (`files_changed` in `IMPLEMENT_RESULT`, or derive it):
515
+
516
+ ```bash
517
+ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HEAD" 2>/dev/null | tr '\n' ' ' | xargs)
518
+ ```
346
519
 
347
- **After subcommand returns**:
348
- - `VALIDATE_RESULT: gate_passed: true` → continue to Phase B6.5 (acceptance gate)
349
- - `VALIDATE_RESULT: gate_passed: false` → subcommand has already posted comment and added `needs-human` label; return `BUILD_RESULT: status: BLOCKED`
520
+ **After subcommand returns** (read its `VALIDATE_RESULT:` block):
521
+ - `gate_passed: true` → verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-invoke validate once; a missing marker is not a pass. Then continue to Phase B6.5 (acceptance gate)
522
+ - `gate_passed: false` → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted)
523
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
350
524
 
351
525
  ---
352
526
 
353
527
  ## Phase B6.5: Acceptance Gate (MANDATORY — cannot be silently skipped) <!-- Added: forge#1315 -->
354
528
 
355
- **Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory.
529
+ **Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory. Run the checks from the worktree (`cd "{WORKTREE_PATH}"`) so relative targets resolve against the built code.
356
530
 
357
531
  **Read acceptance spec from FORGE:INVESTIGATOR comment**:
358
532
 
359
533
  ```bash
360
- ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
534
+ ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
361
535
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' \
362
536
  | grep "^ACCEPTANCE_CHECK:" )
363
537
  ```
@@ -365,7 +539,8 @@ ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
365
539
  **If `ACCEPTANCE_CHECKS` is empty** (investigation predates this feature or comment was deleted): post a warning comment and **block** — do not silently pass:
366
540
 
367
541
  ```bash
368
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
542
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
543
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
369
544
  ## Acceptance Gate — No Spec Found
370
545
 
371
546
  No \`ACCEPTANCE_CHECK:\` lines found in the FORGE:INVESTIGATOR comment. This may mean:
@@ -375,14 +550,15 @@ No \`ACCEPTANCE_CHECK:\` lines found in the FORGE:INVESTIGATOR comment. This may
375
550
  **Gate result: BLOCKED** — re-run \`/work-on:investigate {NUMBER}\` to regenerate the acceptance spec, then retry the build.
376
551
 
377
552
  <!-- FORGE:ACCEPTANCE_GATE:BLOCKED -->"
378
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
553
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
379
554
  ```
380
- Return `BUILD_RESULT: status: BLOCKED`, blocker: "No acceptance spec — re-run investigate to emit ACCEPTANCE_CHECK lines".
555
+ Print `BUILD_RESULT: status: BLOCKED`, blocker: "No acceptance spec — re-run investigate to emit ACCEPTANCE_CHECK lines".
381
556
 
382
557
  **If all checks are `type=skipped`**: post a pass comment noting human review is required, then continue to the checkpoint (non-blocking — skip was deliberate):
383
558
 
384
559
  ```bash
385
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
560
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
561
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
386
562
  ## Acceptance Gate — Skipped (No Machine-Checkable Criteria)
387
563
 
388
564
  The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automated checks were run. Human review is required before merge.
@@ -394,6 +570,7 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
394
570
 
395
571
  ```bash
396
572
  GATE_PASS=true
573
+ CORRECTED=""
397
574
  FAILED_CHECKS=""
398
575
 
399
576
  while IFS= read -r check_line; do
@@ -422,9 +599,11 @@ while IFS= read -r check_line; do
422
599
  # only for older ACCEPTANCE_CHECK comments emitted before this fix (still correct for
423
600
  # single-token exists/contains targets; multi-word legacy targets remain truncated until the
424
601
  # issue's investigation is re-run to emit the quoted format).
425
- TARGET=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="\([^"]*\)".*/\1/p')
602
+ # Quoted fields may contain escaped quotes (matcher="return \"$HELD\""): match (\\.|[^"\\])* and unescape.
603
+ # A plain [^"]* stops at the first \" and silently checks a truncated matcher (field test #3167).
604
+ TARGET=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="(([^"\\]|\\.)*)".*/\1/p' | sed 's/\\"/"/g')
426
605
  [ -z "$TARGET" ] && TARGET=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=\([^ ]*\).*/\1/p')
427
- MATCHER=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="[^"]*" matcher="\([^"]*\)".*/\1/p')
606
+ MATCHER=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("([^"\\]|\\.)*"|[^ ]*) matcher="(([^"\\]|\\.)*)".*/\3/p' | sed 's/\\"/"/g')
428
607
  [ -z "$MATCHER" ] && MATCHER=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=[^ ]* matcher=\([^ ]*\).*/\1/p')
429
608
  DESC=$(echo "$check_line" | sed -n 's/.*description=\(.*\)/\1/p')
430
609
 
@@ -438,14 +617,22 @@ while IFS= read -r check_line; do
438
617
  [ -e "$TARGET" ] || { RESULT="FAIL"; DETAIL="path not found: $TARGET"; }
439
618
  ;;
440
619
  contains)
441
- grep -qE "$MATCHER" "$TARGET" 2>/dev/null || { RESULT="FAIL"; DETAIL="'$MATCHER' not found in $TARGET"; }
620
+ # A matcher is usually literal code; an unescaped $ ( [ . turns it into a different regex. Accept a
621
+ # literal match and record the correction instead of failing the build on a malformed check.
622
+ if ! grep -qE "$MATCHER" "$TARGET" 2>/dev/null; then
623
+ if grep -qF -- "$MATCHER" "$TARGET" 2>/dev/null; then CORRECTED="${CORRECTED}\n- **$ID**: matched as a literal string (the regex form could not match)"
624
+ else RESULT="FAIL"; DETAIL="'$MATCHER' not found in $TARGET"; fi
625
+ fi
442
626
  ;;
443
627
  command|behavior)
444
628
  if [ "$MATCHER" = "exit_0" ]; then
445
629
  eval "$TARGET" >/dev/null 2>&1 || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
446
630
  else
447
631
  OUTPUT=$(eval "$TARGET" 2>&1)
448
- echo "$OUTPUT" | grep -qE "$MATCHER" || { RESULT="FAIL"; DETAIL="output did not match '$MATCHER'. Got: $(echo "$OUTPUT" | head -3)"; }
632
+ if ! printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER"; then
633
+ if printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER"; then CORRECTED="${CORRECTED}\n- **$ID**: output matched as a literal string"
634
+ else RESULT="FAIL"; DETAIL="output did not match '$MATCHER'. Got: $(printf '%s\n' "$OUTPUT" | head -3)"; fi
635
+ fi
449
636
  fi
450
637
  ;;
451
638
  *)
@@ -463,15 +650,17 @@ done <<< "$ACCEPTANCE_CHECKS"
463
650
  **Post gate result comment**:
464
651
 
465
652
  ```bash
653
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
466
654
  if [ "$GATE_PASS" = "true" ]; then
467
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
655
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
468
656
  ## Acceptance Gate — PASSED
469
657
 
470
658
  All machine-checkable acceptance criteria verified against real behavior.
659
+ $( [ -n "$CORRECTED" ] && printf '\n**Checks corrected (malformed matcher, literal text present)**:%b\n' "$CORRECTED" )
471
660
 
472
661
  <!-- FORGE:ACCEPTANCE_GATE:PASSED -->"
473
662
  else
474
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
663
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
475
664
  ## Acceptance Gate — FAILED
476
665
 
477
666
  The following acceptance checks did not pass:
@@ -481,19 +670,26 @@ $(echo -e "$FAILED_CHECKS")
481
670
  Merge is blocked. Fix the failing criteria and re-run the validate phase.
482
671
 
483
672
  <!-- FORGE:ACCEPTANCE_GATE:FAILED -->"
484
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
485
- # Return BLOCKED — merge gate failed
673
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
486
674
  fi
487
675
  ```
488
676
 
489
- If `GATE_PASS = false`: return `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed — see FORGE:ACCEPTANCE_GATE comment".
677
+ If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
678
+
679
+ 1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
680
+ 2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
681
+ `Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
682
+ followed by `work-on:build:validate` with the same args as B6 (the new code gets a fresh quality gate and commit).
683
+ 3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
684
+ 4. Still failing (or already repaired once) → leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed after one repair — see FORGE:ACCEPTANCE_GATE comment".
490
685
 
491
686
  If `GATE_PASS = true`: continue to write the phase checkpoint below.
492
687
 
493
- **When gate_passed is true — write machine-readable phase checkpoint before returning (MANDATORY)**:
688
+ **When the gate passed — write machine-readable phase checkpoint before returning (MANDATORY)**:
494
689
  ```bash
690
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
495
691
  CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
496
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
692
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
497
693
  \`\`\`json
498
694
  {\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
499
695
  \`\`\`"
@@ -503,7 +699,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
503
699
 
504
700
  ## Output
505
701
 
506
- Output this structured block — the routing loop in `work-on.md` will read this result, re-evaluate state, and continue to the next phase. This subcommand is complete; control returns to the router's loop iteration.
702
+ Print this structured block as your final reply (and nothing after it) — the router reads only this block, re-evaluates state, and continues to the next phase. Heartbeats are posted by the router, not here.
507
703
 
508
704
  ```
509
705
  BUILD_RESULT:
@@ -513,18 +709,20 @@ BUILD_RESULT:
513
709
  blocker: {description if status=BLOCKED}
514
710
  ```
515
711
 
712
+ - `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
713
+ - `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
714
+ - `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
715
+ - `BLOCKED` — any guard or failure; `needs-human` is set and the blocker is posted.
716
+
516
717
  ---
517
718
 
518
- ## Integration Point in work-on.md
719
+ ## Integration Point
519
720
 
520
- This module runs during **Phase 3** of the work-on.md pipeline (label: `workflow:ready-to-build` or `workflow:building`). The full sequence is defined by the Universal Phase Dispatcher in work-on.md:
721
+ Position in the work-on pipeline (label `workflow:ready-to-build` or `workflow:building`):
521
722
 
522
723
  ```
523
- Phase 3 (Build) → [THIS MODULE] worktree + contract + context + architect + implement + validate + acceptance-gate
524
- → posts FORGE:BUILDER comment + FORGE:ACCEPTANCE_GATE comment, writes FORGE:CHECKPOINT next_phase=REVIEW
525
- Phase 4 (PR) → work-on:review — push branch, create PR, set workflow:in-review
526
- Phase 5 (Review) → work-on:review — invoke /review-pr --auto-merge
527
- Phase 6 (Close) → work-on:close — trajectory + parent tracker + summary + worktree cleanup
724
+ investigate → [THIS MODULE] worktree + classify + contract + context + architect + implement + validate + acceptance-gate
725
+ → posts FORGE:FAST_PATH, FORGE:CONTRACT, FORGE:BUILDER(:COMPLETE), FORGE:ACCEPTANCE_GATE; writes FORGE:CHECKPOINT next_phase=REVIEW
726
+ → work-on:review (push branch, create PR, review, merge)
727
+ → work-on:close
528
728
  ```
529
-
530
- After this module posts `FORGE:BUILDER` and returns, work-on.md's Universal continuation rule re-reads the issue labels. Since the label is not yet terminal (`workflow:merged` / `workflow:invalid` / `needs-human`), it proceeds immediately to Phase 4 (PR Creation) and then Phase 5 (Auto-Review).