forgedock 1.8.1 → 1.8.3

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 (90) 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/design-bench.md +1 -0
  11. package/commands/design-render-critique-loop.md +1 -0
  12. package/commands/design.md +1 -0
  13. package/commands/forge-stats.md +1 -0
  14. package/commands/geo-audit.md +3 -1
  15. package/commands/incident-response.md +3 -1
  16. package/commands/issue.md +4 -2
  17. package/commands/milestone.md +8 -6
  18. package/commands/orchestrate/config.md +3 -2
  19. package/commands/orchestrate/phase-1-resolve.md +57 -12
  20. package/commands/orchestrate/phase-2-triage.md +1 -0
  21. package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
  22. package/commands/orchestrate/phase-3-dependency.md +17 -5
  23. package/commands/orchestrate/phase-4-execution.md +625 -78
  24. package/commands/orchestrate/phase-5-cleanup.md +5 -2
  25. package/commands/orchestrate/phase-6-report.md +8 -3
  26. package/commands/orchestrate/safety.md +1 -0
  27. package/commands/orchestrate.md +13 -9
  28. package/commands/pipeline-health/config.md +1 -0
  29. package/commands/pipeline-health/phase-1-context.md +1 -0
  30. package/commands/pipeline-health/phase-2-metrics.md +1 -0
  31. package/commands/pipeline-health/phase-3-analyze.md +1 -0
  32. package/commands/pipeline-health/phase-4-proposals.md +7 -4
  33. package/commands/pipeline-health/phase-5-report.md +5 -2
  34. package/commands/pipeline-health/phase-6-summary.md +1 -0
  35. package/commands/pipeline-health.md +9 -7
  36. package/commands/pipeline-resume.md +4 -2
  37. package/commands/qa-sweep.md +3 -1
  38. package/commands/quality-gate.md +4 -3
  39. package/commands/recover-orphans.md +219 -13
  40. package/commands/review-pr-agents/api.md +1 -0
  41. package/commands/review-pr-agents/auth.md +1 -0
  42. package/commands/review-pr-agents/billing.md +1 -0
  43. package/commands/review-pr-agents/concurrency.md +1 -0
  44. package/commands/review-pr-agents/database.md +1 -0
  45. package/commands/review-pr-agents/frontend.md +1 -0
  46. package/commands/review-pr-agents/infra.md +1 -0
  47. package/commands/review-pr-agents/protocols.md +1 -0
  48. package/commands/review-pr-agents/scraper.md +1 -0
  49. package/commands/review-pr-agents/security.md +1 -0
  50. package/commands/review-pr-agents/spec-cli.md +1 -0
  51. package/commands/review-pr-agents.md +10 -4
  52. package/commands/review-pr-staging.md +14 -5
  53. package/commands/review-pr.md +187 -19
  54. package/commands/rollback.md +3 -1
  55. package/commands/security-audit.md +3 -1
  56. package/commands/signal-planner.md +6 -4
  57. package/commands/sync-ecosystem.md +4 -2
  58. package/commands/test-gate.md +4 -2
  59. package/commands/upgrade-deps.md +3 -1
  60. package/commands/validate.md +3 -1
  61. package/commands/work-on/build/architect.md +82 -19
  62. package/commands/work-on/build/context.md +62 -18
  63. package/commands/work-on/build/implement.md +63 -18
  64. package/commands/work-on/build/validate.md +241 -60
  65. package/commands/work-on/build.md +345 -146
  66. package/commands/work-on/close.md +618 -246
  67. package/commands/work-on/decompose.md +240 -41
  68. package/commands/work-on/investigate.md +378 -61
  69. package/commands/work-on/remediate.md +43 -9
  70. package/commands/work-on/review.md +241 -29
  71. package/commands/work-on.md +180 -2014
  72. package/docs/CODEX.md +2 -0
  73. package/docs/CONFIG.md +11 -0
  74. package/package.json +1 -1
  75. package/scripts/build-spec-graph.mjs +11 -4
  76. package/scripts/check-spec-markers.sh +21 -1
  77. package/scripts/checkbox-sections.test.sh +8 -23
  78. package/scripts/extract-affected-files.sh +5 -2
  79. package/scripts/extract-affected-files.test.sh +27 -0
  80. package/scripts/forge-root.test.sh +176 -0
  81. package/scripts/is-docs-only.sh +31 -0
  82. package/scripts/is-docs-only.test.sh +64 -0
  83. package/scripts/lint-dispatch-prompt.sh +217 -0
  84. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  85. package/scripts/validate-spec-graph.sh +3 -2
  86. package/scripts/verify-phase-trail.sh +282 -0
  87. package/scripts/verify-phase-trail.test.sh +298 -0
  88. package/scripts/wait-ci-green.sh +98 -0
  89. package/scripts/wait-ci-green.test.sh +99 -0
  90. package/commands/work-on-monolithic.md +0 -326
@@ -1,51 +1,141 @@
1
1
  ---
2
+ user-invocable: false
2
3
  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]"
4
+ context: fork
5
+ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BASE}"
4
6
  ---
5
7
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
8
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
7
9
 
8
10
  # work-on/build — Build Phase Orchestrator
9
11
 
12
+ > **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>".
13
+
10
14
  **Input**: $ARGUMENTS
11
15
 
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.
16
+ **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.
17
+ **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
18
 
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 -->
19
+ **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
20
  **NEVER use plan mode (EnterPlanMode).**
17
21
 
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.**
22
+ **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
23
 
20
- ### Canonical Build Path (STANDARD/fast-lane) <!-- Added: forge#1276 -->
24
+ <!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
21
25
 
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.
26
+ ---
23
27
 
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.
28
+ ## Inputs
25
29
 
26
- **Build topology summary**:
30
+ Parse from $ARGUMENTS:
31
+ - `{NUMBER}` — issue number (required)
32
+ - `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`) (required)
33
+ - `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`) (required)
34
+ - `--base {PR_BASE}` — PR target branch, computed by the router (e.g. `milestone/modular-pipeline-architecture` or `staging`) (required)
35
+ - `--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
36
 
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 |
37
+ **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
38
 
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 -->
39
+ ---
35
40
 
36
- <!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
41
+ ## Script resolution
42
+
43
+ **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`.
44
+
45
+ ```bash
46
+ # Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
47
+ # Shell state does NOT persist between Bash tool calls: include this whole block at the top of
48
+ # every command that uses resolve_script or FORGE_ROOT.
49
+ REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
50
+ [ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
51
+ ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
52
+ ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
53
+ ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
54
+ # Bounds check: reject adaptive_scripts.directory values that escape the repo root.
55
+ # Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
56
+ # slash in paths.root does not inject a '//' into the glob and trigger a false positive.
57
+ REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
58
+ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
59
+ echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
60
+ ADAPTIVE_ENABLED=false
61
+ fi
62
+ # ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
63
+ # below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
64
+ # repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
65
+ # Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
66
+ # ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
67
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
68
+ FORGE_ROOT=""
69
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
70
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
71
+ _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")")"
72
+ # 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)
73
+ _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-)"
74
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
75
+ # '${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.
76
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
77
+ while IFS= read -r _c; do
78
+ 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
79
+ done <<< "$_k"
80
+ fi
81
+ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
82
+ # NOTE: never resolve this via `which` or `find` — universal scripts are
83
+ # install-relative, not installed on $PATH, so a PATH lookup always misses.
84
+ # FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
85
+ # Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
86
+ # locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
87
+ # does not exist, resolve_script() falls through to Tier 4 (prose) below,
88
+ # which is always safe and available. A missing script is never a reason
89
+ # to search the filesystem. <!-- Added: forge#1984 -->
90
+
91
+ resolve_script() {
92
+ local operation="$1"
93
+ # Tier 2: per-repo adaptive (skip if disabled)
94
+ if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
95
+ echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
96
+ return
97
+ fi
98
+ # Tier 3: universal script
99
+ if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
100
+ echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
101
+ return
102
+ fi
103
+ # Tier 4: prose fallback
104
+ echo "prose:"
105
+ }
106
+ ```
37
107
 
38
108
  ---
39
109
 
40
- ## Inputs
110
+ ## Result emission and Blocked exit
41
111
 
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`)
112
+ 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
113
 
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 -->
114
+ ```
115
+ BUILD_RESULT:
116
+ status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
117
+ branch: {BRANCH}
118
+ worktree: {WORKTREE_PATH}
119
+ blocker: {description if status=BLOCKED, else empty}
120
+ ```
121
+
122
+ `branch` / `worktree` are empty when the build stopped before B1 created them.
123
+
124
+ Every `BLOCKED` exit first runs this procedure (set `BLOCKER` to the reason text), then prints the block with `status: BLOCKED`:
125
+
126
+ ```bash
127
+ BLOCKER="{reason}"
128
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
129
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true
130
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILD_BLOCKED -->
131
+ ## Build Blocked
132
+
133
+ ${BLOCKER}
134
+
135
+ Human attention required (needs-human)." 2>/dev/null || true
136
+ ```
137
+
138
+ (When the arguments themselves are missing and no `gh` call is possible, skip the procedure and print the block only.)
49
139
 
50
140
  ---
51
141
 
@@ -57,109 +147,193 @@ Re-read current state before doing anything:
57
147
  gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
58
148
 
59
149
  # Check investigation report
60
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
150
+ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
61
151
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
62
152
 
63
- # Check if build already completed
64
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
153
+ # Check if build already completed (require FORGE:BUILDER:COMPLETE — not just FORGE:BUILDER)
154
+ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
65
155
  --jq '.[] | select(.body | contains("FORGE:BUILDER")) | .body'
156
+
157
+ # Existing classification from a prior run (resume path)
158
+ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
159
+ --jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
66
160
  ```
67
161
 
68
162
  **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 -->
163
+ - 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`.
164
+ - 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
165
  ```bash
72
- PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
166
+ PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
73
167
  --jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
74
168
  if [ -n "$PARTIAL_ID" ]; then
75
169
  gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
76
170
  echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build from Phase B2"
77
171
  fi
78
172
  ```
79
- - If no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → EXIT with `BUILD_RESULT: status: BLOCKED`, blocker: "Investigation not complete — run investigate first".
173
+ - If there is no `<!-- FORGE:INVESTIGATOR -->` comment with `<!-- INVESTIGATION:COMPLETE -->` → Blocked exit with blocker "Investigation not complete — run investigate first".
80
174
 
81
- Extract from investigation report:
82
- - Affected files list
175
+ Extract from the investigation report:
176
+ - Affected files list → `{AFFECTED_FILES}` (space-separated repo-relative paths; this is what B3/B4 receive via `--files`)
83
177
  - Root cause
84
178
  - Recommendation
85
179
  - Task type (Bug Fix / Feature / Refactor / Maintenance / UI/UX / Full-Stack)
86
180
 
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 -->
181
+ ---
182
+
183
+ ## Phase B0.5: Classify Task Type and Complexity (MANDATORY — build owns this) <!-- Added: forge#1380 -->
184
+
185
+ Build classifies the task and posts the `FORGE:FAST_PATH` comment. `scripts/verify-phase-trail.sh` fails the review preflight if it is missing.
186
+
187
+ **Resume path**: If `EXISTING_FAST_PATH` is non-empty, extract COMPLEXITY_BAND from it and skip re-classification (do not post a second comment):
188
+
88
189
  ```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"
190
+ COMPLEXITY_BAND=$(printf '%s\n' "$EXISTING_FAST_PATH" \
191
+ | sed -n 's/.*\*\*COMPLEXITY_BAND\*\*: *\([A-Za-z_]*\).*/\1/p' | head -1 | tr '[:lower:]' '[:upper:]')
192
+ echo "COMPLEXITY_BAND (existing): ${COMPLEXITY_BAND:-<none>}"
95
193
  ```
96
194
 
195
+ If `COMPLEXITY_BAND` is empty after this (no prior classification, or an unparseable one), classify now as below.
196
+
197
+ **Step 1 — Task type classification:**
198
+
199
+ | Signal | Type | Approach |
200
+ |--------|------|----------|
201
+ | Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
202
+ | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
203
+ | Feature + services/ | Backend Feature | Implement directly |
204
+ | Feature + both | Full-Stack | Backend first, then frontend-design |
205
+ | Bug + web/ | Frontend Fix | Direct |
206
+ | Bug + services/ | Backend Fix | Direct |
207
+ | Refactor/docs | Maintenance | Direct |
208
+
209
+ **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.
210
+
211
+ ```bash
212
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
213
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
214
+ ## Fast-Path Classification
215
+
216
+ **COMPLEXITY_BAND**: INVESTIGATION
217
+ **Task type**: Investigation
218
+ **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).
219
+ **Phases skipped**: contract, context, architect"
220
+ ```
221
+
222
+ **Step 2 — Complexity classification (for non-Investigation tasks):**
223
+
224
+ Classify COMPLEXITY_BAND based on affected file count and task nature:
225
+
226
+ | Condition | COMPLEXITY_BAND |
227
+ |-----------|-----------------|
228
+ | Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
229
+ | 1–5 files, existing patterns, no cross-service impact | STANDARD |
230
+ | 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
231
+
232
+ Post `<!-- FORGE:FAST_PATH -->` immediately after classification:
233
+
234
+ ```bash
235
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
236
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
237
+ ## Fast-Path Classification
238
+
239
+ **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
240
+ **Task type**: {TASK_TYPE}
241
+ **Affected file count**: {N}
242
+ **Rationale**: {one-sentence explanation of classification decision}
243
+ **Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
244
+ ```
245
+
246
+ **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.)
247
+
248
+ **STANDARD and COMPLEX tasks**: Run the full pipeline. No phases skipped.
249
+
250
+ 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.
251
+
97
252
  ---
98
253
 
99
254
  ## Phase B1: Create Worktree & Branch
100
255
 
101
256
  ### B1A: Derive branch name
102
257
 
103
- From issue title: lowercase, hyphenated, max 40 chars (truncate if needed).
258
+ 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
259
  - Bug / fix issues → prefix `fix/`
105
260
  - Feature issues → prefix `feat/`
106
261
  - Refactor / maintenance → prefix `fix/` or `refactor/`
107
262
 
108
- Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85`.
263
+ Append `-{NUMBER}` to ensure uniqueness: e.g. `fix/work-on-build-landing-file-85`. Then `BRANCH_SLUG` is `{BRANCH}` with every `/` replaced by `-`.
109
264
 
110
265
  ### B1B: Determine source branch
111
266
 
112
267
  - 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:
268
+ - **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).
269
+ - **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
270
  ```bash
116
271
  SOURCE_BRANCH="{CODE_BRANCH_FROM_ISSUE_BODY}"
117
272
  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"
273
+ echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to PR base '{PR_BASE}'"
274
+ SOURCE_BRANCH="{PR_BASE}"
120
275
  fi
121
276
  ```
122
- - Feature lane (has milestone) → branch from `origin/{PR_BASE}`
123
- - Fast lane (no milestone) → branch from `origin/staging`
277
+ - 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
278
 
125
279
  ### B1C: Create worktree
126
280
 
127
281
  ```bash
128
- WORKTREE_ROOT="/path/to/repo/.claude/worktrees"
282
+ REPO_ROOT=$(cd "$(git rev-parse --git-common-dir)/.." && pwd)
283
+ WORKTREE_ROOT="${REPO_ROOT}/.claude/worktrees"
129
284
  if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
130
285
  [ -n "${OPENCODE_SESSION_ID:-}" ] ||
131
286
  [ -n "${OPENCODE_PID:-}" ] ||
132
287
  [ -n "${OPENCODE:-}" ]; then
133
- WORKTREE_ROOT="/path/to/repo/.opencode/worktrees"
288
+ WORKTREE_ROOT="${REPO_ROOT}/.opencode/worktrees"
134
289
  elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
135
- WORKTREE_ROOT="/path/to/repo/.codex/worktrees"
290
+ WORKTREE_ROOT="${REPO_ROOT}/.codex/worktrees"
136
291
  fi
292
+ # An explicit, absolute forge.yaml paths.worktree_base wins over the runtime default, so worktrees
293
+ # land where the project's cleanup and recovery tooling expects them (e.g. a project that keeps worktrees under .forge/worktrees).
294
+ CONFIGURED_BASE=$(yq '.paths.worktree_base // ""' forge.yaml 2>/dev/null || echo "")
295
+ case "$CONFIGURED_BASE" in /*) WORKTREE_ROOT="${CONFIGURED_BASE%/}" ;; esac
296
+ mkdir -p "$WORKTREE_ROOT"
137
297
  WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
138
- git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{SOURCE_BRANCH}
298
+ git fetch origin "{SOURCE_BRANCH}" 2>/dev/null || true
299
+ if [ -d "$WORKTREE_PATH" ]; then
300
+ # Reuse existing worktree only when it is on the correct branch
301
+ CURRENT=$(git -C "$WORKTREE_PATH" branch --show-current 2>/dev/null)
302
+ if [ "$CURRENT" != "{BRANCH}" ]; then
303
+ git worktree remove "$WORKTREE_PATH" --force
304
+ fi
305
+ fi
306
+ if [ ! -d "$WORKTREE_PATH" ]; then
307
+ if git show-ref --verify --quiet "refs/heads/{BRANCH}"; then
308
+ git worktree add "$WORKTREE_PATH" "{BRANCH}" # resume: branch already exists
309
+ else
310
+ git worktree add "$WORKTREE_PATH" -b "{BRANCH}" "origin/{SOURCE_BRANCH}"
311
+ fi
312
+ fi
313
+ echo "WORKTREE_PATH=$WORKTREE_PATH BRANCH={BRANCH} BASE={PR_BASE}"
139
314
  ```
140
315
 
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
- ```
316
+ 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
317
 
152
318
  ### B1D: Set building label
153
319
 
154
320
  ```bash
155
- gh issue edit {NUMBER} {GH_FLAG} \
156
- --add-label "workflow:building" \
157
- --remove-label "workflow:ready-to-build"
321
+ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
322
+ case "$TIER" in
323
+ adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
324
+ prose)
325
+ [ -n "${DRY_RUN:-}" ] && echo "DRY_RUN: set workflow:building" || \
326
+ gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
327
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
328
+ ;;
329
+ esac
158
330
  ```
159
331
 
160
332
  ---
161
333
 
162
- ## Phase B2: Post Builder Contract
334
+ ## Phase B2: Post Builder Contract (skip for INVESTIGATION)
335
+
336
+ **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
337
 
164
338
  Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why:
165
339
 
@@ -171,7 +345,8 @@ SHOW_ATTRIBUTION=$(yq '.branding.show_attribution // "true"' forge.yaml 2>/dev/n
171
345
  ```
172
346
 
173
347
  ```bash
174
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
348
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
349
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
175
350
  ## Builder Contract
176
351
 
177
352
  **Task type**: {TASK_TYPE}
@@ -209,28 +384,29 @@ Contract must be grounded in the investigation report. Every deliverable file mu
209
384
  **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
385
 
211
386
  ```bash
387
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
212
388
  if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
213
389
  COORD_NUM=$(echo "$FORGE_COORD_ISSUE" | grep -oE '[0-9]+$')
214
390
  if [ -n "$COORD_NUM" ]; then
215
391
  # Extract file paths from the just-posted FORGE:CONTRACT deliverables table
216
- CLAIMED_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
392
+ CLAIMED_FILES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
217
393
  --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body' 2>/dev/null \
218
394
  | 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)
395
+ | grep -oE '`[^`]+\.(py|tsx?|jsx?|sql|json|ya?ml|md|mjs|sh)`' \
396
+ | tr -d '`' | sort -u | head -20)
221
397
  CLAIMED_FILES="${CLAIMED_FILES:-"(files listed in FORGE:CONTRACT deliverables table)"}"
222
398
 
223
399
  # Extract preserved interfaces from the FORGE:ARCHITECT affected paths table (if present)
224
- CLAIMED_INTERFACES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
400
+ CLAIMED_INTERFACES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
225
401
  --jq '[.[] | select(.body | contains("FORGE:ARCHITECT"))] | last | .body' 2>/dev/null \
226
402
  | awk '/^### Affected Paths/{p=1; next} /^### /{p=0} p' \
227
- | grep -oP 'Function/Class.*\|.*\|' | head -10 || echo "(see FORGE:ARCHITECT for interface details)")
403
+ | grep -oE 'Function/Class.*\|.*\|' | head -10 || true)
228
404
  CLAIMED_INTERFACES="${CLAIMED_INTERFACES:-"(see FORGE:ARCHITECT comment for interface details)"}"
229
405
 
230
- CLAIM_HOLDER="#${NUMBER} / $(date -u +%Y%m%dT%H%M%S)"
231
- CLAIM_TTL="terminal state of Holder issue #${NUMBER}"
406
+ CLAIM_HOLDER="#{NUMBER} / $(date -u +%Y%m%dT%H%M%S)"
407
+ CLAIM_TTL="terminal state of Holder issue #{NUMBER}"
232
408
 
233
- gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM -->
409
+ run gh issue comment "$COORD_NUM" -R {GH_REPO} --body "<!-- FORGE:CLAIM -->
234
410
  ## Resource Claim
235
411
 
236
412
  **Holder**: ${CLAIM_HOLDER}
@@ -239,7 +415,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ]; then
239
415
  **TTL**: ${CLAIM_TTL}
240
416
 
241
417
  <!-- CLAIM:COMPLETE -->" 2>/dev/null || true
242
- echo "FORGE:CLAIM posted on coordination issue #${COORD_NUM} for #${NUMBER}"
418
+ echo "FORGE:CLAIM posted on coordination issue #${COORD_NUM} for #{NUMBER}"
243
419
  fi
244
420
  fi
245
421
  ```
@@ -248,12 +424,12 @@ fi
248
424
 
249
425
  ---
250
426
 
251
- ## Phase B2.5: Extract FUNCTION_NAMES from Contract
427
+ ## Phase B2.5: Extract FUNCTION_NAMES from Contract (skip for INVESTIGATION)
252
428
 
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.
429
+ 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
430
 
255
431
  ```bash
256
- FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
432
+ FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
257
433
  --jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
258
434
  | awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
259
435
  | grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
@@ -263,56 +439,49 @@ FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
263
439
  | xargs)
264
440
  # Scope is limited to the ### Deliverables section to avoid false matches from FORGE markers,
265
441
  # 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)
442
+ # Fallback: if extraction yields nothing, FUNCTION_NAMES remains the empty string and
443
+ # context C3 skips gracefully (its for-loop produces zero iterations).
268
444
  ```
269
445
 
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.
446
+ If `FUNCTION_NAMES` is non-empty, pass it via `--functions` to the context child. If empty, omit the `--functions` flag.
271
447
 
272
448
  ---
273
449
 
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.
450
+ ## Phase B3: Context Gathering (invoke for every band except TRIVIAL / INVESTIGATION)
279
451
 
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 -->
452
+ **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
453
 
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.
454
+ Always invoke it as a forked sub-skill — never inline:
283
455
 
284
- **Spawn exception** (only when Row (c) applies):
285
456
  ```
286
- Skill("work-on:build:context", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE_PATH} {AFFECTED_FILES} --functions {FUNCTION_NAMES}")
457
+ 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
458
  ```
288
- If `FUNCTION_NAMES` is empty, omit `--functions`. The Skill() form above is the exception path — not the default. <!-- Added: forge#1276 -->
289
459
 
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)
460
+ 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).
461
+
462
+ **After the child returns** (read its `CONTEXT_RESULT:` block):
463
+ - `status: COMPLETE | PARTIAL | SKIPPED` → continue to B4
464
+ - No `CONTEXT_RESULT:` block, timeout or error → log a warning and continue to B4 (context is advisory and non-blocking)
465
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:context"
293
466
  # MUST CONTINUE to Phase B4 — context result is intermediate, NOT terminal.
294
467
 
295
468
  ---
296
469
 
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.
470
+ ## Phase B4: Architecture Planning (invoke for every band except TRIVIAL / INVESTIGATION)
300
471
 
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.
472
+ **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
473
 
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
474
  ```
309
- Skill("work-on:build:architect", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE_PATH} --files {AFFECTED_FILES}")
475
+ 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
476
  ```
311
- The Skill() form above is the exception path — not the default. <!-- Added: forge#1276 -->
312
477
 
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`
478
+ The child posts its own `FORGE:ARCHITECT` comment (a "Skipped" marker + `:COMPLETE` on every skip, forge#2689).
479
+
480
+ **After the child returns** (read its `ARCHITECT_RESULT:` block):
481
+ - `status: COMPLETE | PARTIAL | SKIPPED` → continue to B5
482
+ - `status: BLOCKED` (conflicting constraints that cannot be resolved) → Blocked exit with the child's `blocker`
483
+ - 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"
484
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:architect"
316
485
  # MUST CONTINUE to Phase B5 — architect result is intermediate, NOT terminal.
317
486
 
318
487
  ---
@@ -322,14 +491,15 @@ The Skill() form above is the exception path — not the default. <!-- Added: fo
322
491
  Invoke the implement subcommand to write code, stage, and post the builder comment:
323
492
 
324
493
  ```
325
- Skill("work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH}")
494
+ 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
495
  ```
327
496
 
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`
497
+ **After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
498
+ - `status: COMPLETE` → continue to B6
499
+ - `status: ALREADY_DONE` → continue to B6 (validate what's already there)
500
+ - `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B6/B6.5)
501
+ - `status: BLOCKED` → Blocked exit with the child's `blocker`
502
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
333
503
  # MUST CONTINUE to Phase B6 — implement result is intermediate, NOT terminal (validation still required).
334
504
 
335
505
  ---
@@ -339,25 +509,30 @@ Skill("work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_F
339
509
  Invoke the validate subcommand to run the quality gate loop, formatting, and deploy checks:
340
510
 
341
511
  ```
342
- Skill("work-on:build:validate", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --files {CHANGED_FILES}")
512
+ 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
513
  ```
344
514
 
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).
515
+ Where `{CHANGED_FILES}` is the space-separated list of files changed by the implement subcommand (`files_changed` in `IMPLEMENT_RESULT`, or derive it):
516
+
517
+ ```bash
518
+ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HEAD" 2>/dev/null | tr '\n' ' ' | xargs)
519
+ ```
346
520
 
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`
521
+ **After subcommand returns** (read its `VALIDATE_RESULT:` block):
522
+ - `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)
523
+ - `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)
524
+ - Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
350
525
 
351
526
  ---
352
527
 
353
528
  ## Phase B6.5: Acceptance Gate (MANDATORY — cannot be silently skipped) <!-- Added: forge#1315 -->
354
529
 
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.
530
+ **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
531
 
357
532
  **Read acceptance spec from FORGE:INVESTIGATOR comment**:
358
533
 
359
534
  ```bash
360
- ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
535
+ ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
361
536
  --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' \
362
537
  | grep "^ACCEPTANCE_CHECK:" )
363
538
  ```
@@ -365,7 +540,8 @@ ACCEPTANCE_CHECKS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
365
540
  **If `ACCEPTANCE_CHECKS` is empty** (investigation predates this feature or comment was deleted): post a warning comment and **block** — do not silently pass:
366
541
 
367
542
  ```bash
368
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
543
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
544
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
369
545
  ## Acceptance Gate — No Spec Found
370
546
 
371
547
  No \`ACCEPTANCE_CHECK:\` lines found in the FORGE:INVESTIGATOR comment. This may mean:
@@ -375,14 +551,15 @@ No \`ACCEPTANCE_CHECK:\` lines found in the FORGE:INVESTIGATOR comment. This may
375
551
  **Gate result: BLOCKED** — re-run \`/work-on:investigate {NUMBER}\` to regenerate the acceptance spec, then retry the build.
376
552
 
377
553
  <!-- FORGE:ACCEPTANCE_GATE:BLOCKED -->"
378
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
554
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
379
555
  ```
380
- Return `BUILD_RESULT: status: BLOCKED`, blocker: "No acceptance spec — re-run investigate to emit ACCEPTANCE_CHECK lines".
556
+ Print `BUILD_RESULT: status: BLOCKED`, blocker: "No acceptance spec — re-run investigate to emit ACCEPTANCE_CHECK lines".
381
557
 
382
558
  **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
559
 
384
560
  ```bash
385
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
561
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
562
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
386
563
  ## Acceptance Gate — Skipped (No Machine-Checkable Criteria)
387
564
 
388
565
  The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automated checks were run. Human review is required before merge.
@@ -394,6 +571,7 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
394
571
 
395
572
  ```bash
396
573
  GATE_PASS=true
574
+ CORRECTED=""
397
575
  FAILED_CHECKS=""
398
576
 
399
577
  while IFS= read -r check_line; do
@@ -422,9 +600,11 @@ while IFS= read -r check_line; do
422
600
  # only for older ACCEPTANCE_CHECK comments emitted before this fix (still correct for
423
601
  # single-token exists/contains targets; multi-word legacy targets remain truncated until the
424
602
  # 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')
603
+ # Quoted fields may contain escaped quotes (matcher="return \"$HELD\""): match (\\.|[^"\\])* and unescape.
604
+ # A plain [^"]* stops at the first \" and silently checks a truncated matcher (field test #3167).
605
+ TARGET=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target="(([^"\\]|\\.)*)".*/\1/p' | sed 's/\\"/"/g')
426
606
  [ -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')
607
+ MATCHER=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("([^"\\]|\\.)*"|[^ ]*) matcher="(([^"\\]|\\.)*)".*/\3/p' | sed 's/\\"/"/g')
428
608
  [ -z "$MATCHER" ] && MATCHER=$(echo "$check_line" | sed -n 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=[^ ]* matcher=\([^ ]*\).*/\1/p')
429
609
  DESC=$(echo "$check_line" | sed -n 's/.*description=\(.*\)/\1/p')
430
610
 
@@ -438,14 +618,22 @@ while IFS= read -r check_line; do
438
618
  [ -e "$TARGET" ] || { RESULT="FAIL"; DETAIL="path not found: $TARGET"; }
439
619
  ;;
440
620
  contains)
441
- grep -qE "$MATCHER" "$TARGET" 2>/dev/null || { RESULT="FAIL"; DETAIL="'$MATCHER' not found in $TARGET"; }
621
+ # A matcher is usually literal code; an unescaped $ ( [ . turns it into a different regex. Accept a
622
+ # literal match and record the correction instead of failing the build on a malformed check.
623
+ if ! grep -qE "$MATCHER" "$TARGET" 2>/dev/null; then
624
+ if grep -qF -- "$MATCHER" "$TARGET" 2>/dev/null; then CORRECTED="${CORRECTED}\n- **$ID**: matched as a literal string (the regex form could not match)"
625
+ else RESULT="FAIL"; DETAIL="'$MATCHER' not found in $TARGET"; fi
626
+ fi
442
627
  ;;
443
628
  command|behavior)
444
629
  if [ "$MATCHER" = "exit_0" ]; then
445
630
  eval "$TARGET" >/dev/null 2>&1 || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
446
631
  else
447
632
  OUTPUT=$(eval "$TARGET" 2>&1)
448
- echo "$OUTPUT" | grep -qE "$MATCHER" || { RESULT="FAIL"; DETAIL="output did not match '$MATCHER'. Got: $(echo "$OUTPUT" | head -3)"; }
633
+ if ! printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER"; then
634
+ if printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER"; then CORRECTED="${CORRECTED}\n- **$ID**: output matched as a literal string"
635
+ else RESULT="FAIL"; DETAIL="output did not match '$MATCHER'. Got: $(printf '%s\n' "$OUTPUT" | head -3)"; fi
636
+ fi
449
637
  fi
450
638
  ;;
451
639
  *)
@@ -463,15 +651,17 @@ done <<< "$ACCEPTANCE_CHECKS"
463
651
  **Post gate result comment**:
464
652
 
465
653
  ```bash
654
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
466
655
  if [ "$GATE_PASS" = "true" ]; then
467
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
656
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
468
657
  ## Acceptance Gate — PASSED
469
658
 
470
659
  All machine-checkable acceptance criteria verified against real behavior.
660
+ $( [ -n "$CORRECTED" ] && printf '\n**Checks corrected (malformed matcher, literal text present)**:%b\n' "$CORRECTED" )
471
661
 
472
662
  <!-- FORGE:ACCEPTANCE_GATE:PASSED -->"
473
663
  else
474
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
664
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
475
665
  ## Acceptance Gate — FAILED
476
666
 
477
667
  The following acceptance checks did not pass:
@@ -481,19 +671,26 @@ $(echo -e "$FAILED_CHECKS")
481
671
  Merge is blocked. Fix the failing criteria and re-run the validate phase.
482
672
 
483
673
  <!-- FORGE:ACCEPTANCE_GATE:FAILED -->"
484
- gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
485
- # Return BLOCKED — merge gate failed
674
+ run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
486
675
  fi
487
676
  ```
488
677
 
489
- If `GATE_PASS = false`: return `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed — see FORGE:ACCEPTANCE_GATE comment".
678
+ If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
679
+
680
+ 1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
681
+ 2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
682
+ `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>\"")`
683
+ followed by `work-on:build:validate` with the same args as B6 (the new code gets a fresh quality gate and commit).
684
+ 3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
685
+ 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
686
 
491
687
  If `GATE_PASS = true`: continue to write the phase checkpoint below.
492
688
 
493
- **When gate_passed is true — write machine-readable phase checkpoint before returning (MANDATORY)**:
689
+ **When the gate passed — write machine-readable phase checkpoint before returning (MANDATORY)**:
494
690
  ```bash
691
+ run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
495
692
  CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
496
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
693
+ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
497
694
  \`\`\`json
498
695
  {\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
499
696
  \`\`\`"
@@ -503,7 +700,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
503
700
 
504
701
  ## Output
505
702
 
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.
703
+ 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
704
 
508
705
  ```
509
706
  BUILD_RESULT:
@@ -513,18 +710,20 @@ BUILD_RESULT:
513
710
  blocker: {description if status=BLOCKED}
514
711
  ```
515
712
 
713
+ - `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
714
+ - `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
715
+ - `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
716
+ - `BLOCKED` — any guard or failure; `needs-human` is set and the blocker is posted.
717
+
516
718
  ---
517
719
 
518
- ## Integration Point in work-on.md
720
+ ## Integration Point
519
721
 
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:
722
+ Position in the work-on pipeline (label `workflow:ready-to-build` or `workflow:building`):
521
723
 
522
724
  ```
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
725
+ investigate → [THIS MODULE] worktree + classify + contract + context + architect + implement + validate + acceptance-gate
726
+ → posts FORGE:FAST_PATH, FORGE:CONTRACT, FORGE:BUILDER(:COMPLETE), FORGE:ACCEPTANCE_GATE; writes FORGE:CHECKPOINT next_phase=REVIEW
727
+ → work-on:review (push branch, create PR, review, merge)
728
+ → work-on:close
528
729
  ```
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).