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.
- package/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +11 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- 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
|
-
|
|
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**:
|
|
13
|
-
**Output**: Create worktree, post contract, run
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
23
|
+
<!-- FORGE:SPEC_LOADED — work-on/build.md loaded and active. Agent is bound by this spec. -->
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
---
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
## Inputs
|
|
25
28
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
---
|
|
35
39
|
|
|
36
|
-
|
|
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
|
-
##
|
|
109
|
+
## Result emission and Blocked exit
|
|
41
110
|
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
70
|
-
- If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate
|
|
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 -->` →
|
|
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
|
-
|
|
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=$(
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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 `
|
|
114
|
-
- **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not, fall back to
|
|
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
|
|
119
|
-
SOURCE_BRANCH="
|
|
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
|
-
-
|
|
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
|
-
|
|
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="
|
|
287
|
+
WORKTREE_ROOT="${REPO_ROOT}/.opencode/worktrees"
|
|
134
288
|
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
135
|
-
WORKTREE_ROOT="
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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 -
|
|
220
|
-
| tr -d '`' | sort -u |
|
|
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 -
|
|
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="
|
|
231
|
-
CLAIM_TTL="terminal state of Holder issue
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
- `
|
|
330
|
-
- `
|
|
331
|
-
- `
|
|
332
|
-
- `
|
|
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 (
|
|
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
|
-
- `
|
|
349
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=$(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
719
|
+
## Integration Point
|
|
519
720
|
|
|
520
|
-
|
|
721
|
+
Position in the work-on pipeline (label `workflow:ready-to-build` or `workflow:building`):
|
|
521
722
|
|
|
522
723
|
```
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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).
|