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
package/commands/work-on.md
CHANGED
|
@@ -19,116 +19,92 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
|
|
|
19
19
|
|
|
20
20
|
## HARD RULES — READ BEFORE ANYTHING ELSE
|
|
21
21
|
|
|
22
|
-
1. **Every
|
|
22
|
+
1. **This file is a router. Every phase runs as a forked sub-skill via `Skill(...)`; the router never executes phase logic itself.** Each phase sub-skill (`work-on:investigate`, `work-on:decompose`, `work-on:build`, `work-on:review`, `work-on:close`, `work-on:remediate`) declares `context: fork`, so Claude Code runs it in an isolated sub-agent context that holds only that phase's spec and the args you pass. The phase re-reads everything else from GitHub/git and returns exactly one `*_RESULT:` block — that block is all the router sees. Do not read phase spec files, do not re-implement a phase step here, and do not "help" a phase by posting its markers or labels. If a skill name does not resolve, STOP with "skill not found" (see Skill Name Resolution) — never run a phase inline.
|
|
23
23
|
|
|
24
|
-
2. **
|
|
24
|
+
2. **Route only on `*_RESULT` blocks and GitHub state.** After each phase returns, apply the routing table for that phase (below). Never infer success from a phase's prose; never continue past a `BLOCKED`/`FAILED` result.
|
|
25
25
|
|
|
26
|
-
3. **Follow the
|
|
26
|
+
3. **Follow the Phase Dispatcher.** Do not skip, reorder, or treat an intermediate completion as terminal. Only the terminal states listed in the Dispatcher allow stopping. Between phases: no narration, no summary, no end of turn — invoke the next phase immediately.
|
|
27
27
|
|
|
28
|
-
4. **PRs NEVER target `main`.** Target `staging` (fast lane) or `milestone/{slug}` (feature lane).
|
|
28
|
+
4. **PRs NEVER target `main`.** Target `staging` (fast lane) or `milestone/{slug}` (feature lane). The router computes and validates the target (Lane Resolution) and passes it to the phases as `--base`.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
5. **`needs-human` is for genuine decisions and external actions only.** A phase that can fix its own problem (failing checks, review findings, a moved PR head, a missing marker it can produce) does so inside its own fork; the router never adds `needs-human` for a condition a phase reported as fixable.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
2. Read full GitHub state (issue body + comments + labels) ONCE, in Phase 0B. Carry those values in-context for the rest of the run — do NOT re-fetch them at later phase boundaries. The only trigger for a fresh re-read of something already read this session is a **detected compaction**: if a later phase needs a value (issue body, a specific FORGE:* comment, labels) that this session does not actually have in its visible context — because compaction dropped the earlier tool output, or this is a brand-new session resuming mid-pipeline — re-fetch ONLY that missing value, not the full state.
|
|
34
|
-
3. After compaction: re-read issue (body + comments + labels) to reconstruct state
|
|
35
|
-
4. Key principle: A NEW session running `/work-on {number}` should pick up where the last left off by reading GitHub state alone
|
|
32
|
+
### Skill Name Resolution (`{FORGE_SKILL_PREFIX}`)
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
ForgeDock skills register under different names per install:
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
| Runtime / install | `{FORGE_SKILL_PREFIX}` | Nesting separator | `work-on` / `work-on` + `build` |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| Claude Code plugin | `forgedock:` | `:` | `forgedock:work-on` / `forgedock:work-on:build` |
|
|
39
|
+
| Claude Code `install.sh` symlinks (`~/.claude/commands/work-on/build.md`) | empty | `:` | `work-on` / `work-on:build` |
|
|
40
|
+
| Codex (`install-codex.sh`) | `forge-` | `-` | `forge-work-on` / `forge-work-on-build` |
|
|
41
|
+
| OpenCode (`forgedock opencode install`) | empty | `-` | `work-on` / `work-on-build` |
|
|
40
42
|
|
|
41
|
-
`
|
|
43
|
+
Every `Skill(skill="...")` call to a ForgeDock skill (any command under `commands/`: `work-on*`, `review-pr*`, `quality-gate`, `issue`, `orchestrate`, `cleanup`, `deploy-pr`, etc.) is written `{FORGE_SKILL_PREFIX}<name>`, with `:` between nesting levels as the canonical spelling. Resolve the prefix AND the nesting separator ONCE per run, before the first Skill dispatch. Substitute the prefix literally; when the separator is `-`, also rewrite every `:` nesting level in the name to `-` (`{FORGE_SKILL_PREFIX}work-on:build` becomes `forge-work-on-build`). Apply the result to every call and every sub-agent prompt:
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
1. If env `FORGE_SKILL_NAMESPACE` is set: `forgedock` → prefix `forgedock:`, separator `:`; `none` → empty prefix, separator `:`; `codex` → prefix `forge-`, separator `-`; `opencode` → empty prefix, separator `-`. Any other value is an error.
|
|
46
|
+
2. Else if env `FORGE_RUNTIME` is `codex` or `opencode`, use that runtime's row above.
|
|
47
|
+
3. Else read the available-skills list: if it contains `forgedock:work-on` → `forgedock:`/`:`; else if it contains `forge-work-on` → `forge-`/`-`; else if it contains `work-on:build` → empty/`:`; else if it contains `work-on-build` → empty/`-`; else if it contains `work-on` → empty/`:`.
|
|
48
|
+
4. If none of these resolves, or a later `Skill(...)` call reports the resolved name unknown, this is a **HARD ERROR**: STOP and report "skill not found: <name>" (post a `needs-human` comment when running against an issue). NEVER fall back to running the phase inline or via the Agent tool — an inline phase has no paper trail.
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
A sub-agent that receives no resolved value applies the same rule itself.
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
### Compaction Resilience
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| 3 | Phase 1D: Route | Investigation complete | No |
|
|
54
|
-
| 4 | Phase 2: Decomposition | decompose: YES | Yes (spawns sub-issues) |
|
|
55
|
-
| 5 | Phase 3: Build (3A–3M) | `workflow:ready-to-build` or `workflow:building` | No |
|
|
56
|
-
| 6 | Phase 4: PR Creation | Builder comment posted, no PR exists | No |
|
|
57
|
-
| 7 | Phase 5: Auto-Review | PR exists, `workflow:in-review` | No |
|
|
58
|
-
| 8 | Phase 6: Close & Cleanup | PR merged | No |
|
|
59
|
-
| 9 | Phase 7: Trajectory | Issue closed | Yes |
|
|
54
|
+
1. Every phase writes its state to GitHub (FORGE annotations + `workflow:*` labels) before returning.
|
|
55
|
+
2. The router keeps only small routing values in context: `NUMBER`, `GH_REPO`, `GH_FLAG`, `PR_BASE`, `CLASSIFIED_LANE`, `BRANCH`, `WORKTREE_PATH`, `PR_NUMBER`, `UNDER_ORCHESTRATION`. Phase results carry the rest.
|
|
56
|
+
3. After a compaction (or in a new session), re-run Phase 0: it reconstructs the resume point from GitHub state alone, and every phase is idempotent (each starts with its own resume check).
|
|
57
|
+
4. **Shell state does not persist between Bash tool calls.** Any bash snippet below that uses `resolve_script`/`FORGE_ROOT` must include the Script resolution block from Phase 0 in the same command; substitute `{PLACEHOLDER}` values literally.
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
### Orchestration Flag
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
`UNDER_ORCHESTRATION` — resolved once in Phase 0A. Defaults to `false` (solo run). Set to `true` when the invocation args include `--under-orchestration` (this is how `/orchestrate` dispatches `/work-on`; see `commands/orchestrate/phase-4-execution.md`). This flag gates the phase-entry heartbeat comments posted by the router (Phases 1, 3 and 5 below, plus Phase 0A.5); solo runs skip them.
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
### Phase Dispatcher
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
- `workflow:merged` label is set
|
|
69
|
-
- `workflow:invalid` label is set
|
|
70
|
-
- `needs-human` label is set
|
|
71
|
-
- `workflow:awaiting-merge` label is set (remediated + re-reviewed, awaiting a human merge decision — see #1810) <!-- Added: forge#1810 -->
|
|
72
|
-
- `workflow:decomposed` label is set (sub-issues spawned)
|
|
73
|
-
- Issue state is CLOSED with terminal label
|
|
65
|
+
<!-- FORGE:DISPATCHER — This is the SINGLE source of truth for phase transitions. -->
|
|
74
66
|
|
|
75
|
-
|
|
67
|
+
| Step | Phase | Skill (forked) | Entry condition |
|
|
68
|
+
|------|-------|----------------|-----------------|
|
|
69
|
+
| 0 | Resolve & resume | — (router) | Always first |
|
|
70
|
+
| 1 | Investigate | `work-on:investigate` | No `FORGE:INVESTIGATOR` comment containing `INVESTIGATION:COMPLETE` or `INVESTIGATION:INVALID` |
|
|
71
|
+
| 2 | Decompose | `work-on:decompose` | `INVESTIGATE_RESULT.decompose: YES` (or resume: investigation says decompose YES and no `FORGE:DECOMPOSED`) |
|
|
72
|
+
| 3 | Build | `work-on:build` | Investigation complete, decompose NO, no `FORGE:BUILDER:COMPLETE` |
|
|
73
|
+
| 4 | Review (push, PR, review, merge) | `work-on:review` | `FORGE:BUILDER:COMPLETE` present and PR not merged |
|
|
74
|
+
| 5 | Close & trajectory | `work-on:close` | PR merged, or a PR-less terminal outcome (investigation deliverables, decomposed, invalid) |
|
|
76
75
|
|
|
77
|
-
**
|
|
76
|
+
**Terminal states** (only these allow stopping): `workflow:merged` with the issue closed; `workflow:invalid`; `workflow:decomposed` (sub-issues own the work); `needs-human`; `workflow:awaiting-merge` (held for a human merge decision on the deploy gate); a `CLOSE_RESULT` with `status: COMPLETE | ALREADY_DONE`. Anything else → run the next phase immediately.
|
|
78
77
|
|
|
79
78
|
---
|
|
80
79
|
|
|
81
80
|
## Spawn-Decision Policy
|
|
82
81
|
|
|
83
|
-
<!-- FORGE:SPAWN_POLICY — Canonical spawn-decision table. Sibling specs (orchestrate.md, review-pr.md) link to this section.
|
|
82
|
+
<!-- FORGE:SPAWN_POLICY — Canonical spawn-decision table. Sibling specs (orchestrate.md, review-pr.md) link to this section. -->
|
|
84
83
|
|
|
85
|
-
**
|
|
84
|
+
**Phases are forked by declaration, not by judgement.** Every `/work-on` phase sub-skill declares `context: fork` in its frontmatter, so each phase (and each build child: context, architect, implement, validate, and the quality gate) runs in its own isolated context with only its own spec loaded. This replaces the former "default inline" rule and its Row (c)/(d) exceptions: phase-at-a-time spec delivery is what keeps every worker's context small and its instructions unambiguous (field evidence: inline delivery put 230k–360k tokens of overlapping spec into each orchestrated worker and produced phase shortcuts).
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
`Agent(...)` sub-agents remain the mechanism for work that is not a phase:
|
|
88
87
|
|
|
89
|
-
| Row | Criterion |
|
|
90
|
-
|
|
91
|
-
| a | **Parallel fan-out** —
|
|
92
|
-
| b | **Fresh-context isolation
|
|
93
|
-
| c | **Parent context near overflow** — the parent agent has made ≥20 Skill invocations OR the build changed ≥10 files, meaning delegating review inline risks a mid-review token overflow | YES — spawn a fresh sub-agent for review | Phase 5C: `Skill(skill="work-on/review", …)` instead of direct `review-pr` invocation |
|
|
94
|
-
| d | **Prompt-cache TTL** — the sub-operation (multi-domain review, multi-iteration quality-gate) is expected to run for several minutes, longer than Anthropic's ~5-minute prompt-cache TTL. Leaving it inline lets the parent's already-large accumulated context (investigation/contract/context/architect/implement annotations) sit idle past the TTL; the parent's next turn then re-hydrates that entire context **uncached**, roughly doubling effective token cost for that turn. This is independent of build size — a 1-file fix idles the parent exactly as long as a 20-file one once the sub-operation starts running | YES — spawn a fresh sub-agent, **unconditionally**, regardless of file count or Skill-invocation count | Phase 5C review (always forks — Row d supersedes Row c's threshold as the controlling reason); Phase 3G quality-gate loop (forks under Row d even though it never qualified under Row c) <!-- Added: forge#1825 --> |
|
|
95
|
-
|
|
96
|
-
**If none of the four rows match: run inline.** Do not fork for convenience, narrative clarity, or to avoid reading a large file. Context cost of a fork (spawning, context reconstruction, result aggregation) is paid every time, even when parallelism, isolation, or cache preservation adds no value.
|
|
88
|
+
| Row | Criterion | Example |
|
|
89
|
+
|-----|-----------|---------|
|
|
90
|
+
| a | **Parallel fan-out** — independent work units run concurrently | `/orchestrate` dispatching one `/work-on` worker per issue; `review-pr` dispatching domain reviewers |
|
|
91
|
+
| b | **Fresh-context isolation for a load-bearing review** | `review-pr` domain reviewers (they must not see the builder's reasoning) |
|
|
97
92
|
|
|
98
93
|
### Depth Budget
|
|
99
94
|
|
|
100
|
-
**Available depth**: 5 levels (since Claude Code v2.1.172).
|
|
101
|
-
|
|
102
|
-
**Target depth for a standard run**: ≤ 3.
|
|
103
|
-
|
|
104
95
|
| Depth | Agent | Notes |
|
|
105
96
|
|-------|-------|-------|
|
|
106
|
-
| 1 | `/orchestrate`
|
|
107
|
-
| 2 | `/work-on`
|
|
108
|
-
| 3 |
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
**Depth 4–5 are reserved** for exceptional cases (e.g. an orchestrate agent that itself spawns an orchestrate agent for a sub-milestone). Agents that reach depth 4 MUST log a justification comment on the relevant issue.
|
|
113
|
-
|
|
114
|
-
### Phase 5C Cross-Reference
|
|
115
|
-
|
|
116
|
-
The Phase 5C review fork is now unconditional, controlled by **Row (d)** (prompt-cache TTL economics — independent of build size). It was previously gated by **Row (c)** alone (≥10 changed files OR ≥20 Skill invocations, i.e. parent-context-overflow risk); that threshold is preserved as a *documented, non-gating* reason the fork is also correct for large builds, but no longer determines *whether* the fork happens — Row (d) means it always does. <!-- Updated: forge#1825 (previously: "quantitative thresholds are not changed") -->
|
|
117
|
-
|
|
118
|
-
### Phase 3G Cross-Reference
|
|
119
|
-
|
|
120
|
-
The Phase 3G quality-gate loop forks unconditionally under **Row (d)**: it scans 14+ domains across up to 3 iterations, long enough to idle the parent's accumulated context past the prompt-cache TTL regardless of how many files changed. Unlike Phase 5C, quality-gate never had a Row (c) (file-count) exception — Row (d) is the sole justification for forking it. <!-- Added: forge#1825 -->
|
|
97
|
+
| 1 | `/orchestrate` (or a solo `/work-on` session) | Never implements directly |
|
|
98
|
+
| 2 | `/work-on` router (an orchestrated worker) | Routes only |
|
|
99
|
+
| 3 | Forked phase (`work-on:<phase>`) | One at a time; `review-pr` runs inside `work-on:review`'s fork |
|
|
100
|
+
| 4 | Forked build child (context, architect, implement, validate); domain reviewers dispatched by `review-pr` | |
|
|
101
|
+
| 5 | Quality gate (forked, inside validate) | Deepest level used |
|
|
121
102
|
|
|
122
103
|
### Model and Effort Tiering — What Actually Applies
|
|
123
104
|
|
|
124
105
|
<!-- FORGE:MODEL_TIER_NOTE — Canonical explanation of the real vs. aspirational tiering mechanism. Every work-on/*.md "Agent model policy" line cross-references this section instead of restating it. -->
|
|
125
106
|
|
|
126
|
-
Every `work-on/*.md`
|
|
127
|
-
|
|
128
|
-
- **`effort` is real and applies per `Skill()` invocation.** It is genuine reasoning-depth tuning on the model already running the session, gated correctly on Claude Code >= 2.1.154. Setting `effort: low` on a sub-phase file that is mechanical end-to-end (deterministic label edits, annotation posting, board sync — no root-cause analysis, no architecture planning) is a real, no-fork-required cost reduction.
|
|
129
|
-
- **`model` overrides are NOT functional for `Skill()`-dispatched sub-phases.** The `Skill` tool executes "within the main conversation" — it has no model parameter and does not fork a new agent/session, so a sub-phase file cannot switch the model that's already generating the current run. The only tool with real model-override semantics is `Agent(model=...)` (see `tool_param_value_permission_rules`, Claude Code >= 2.1.178) — and HARD RULE #2 above explicitly forbids using the `Agent` tool for `/work-on` sub-phase dispatch, to keep the FORGE-annotation paper trail and phase protocol intact. In practice, the only place a `/work-on` run's model is genuinely chosen is `/orchestrate`'s single `Agent(model=..., ...)` spawn for the entire run (see `commands/orchestrate/phase-4-execution.md`) — every internal `Skill()` call inside that run inherits that same model, with no per-sub-phase override.
|
|
130
|
-
|
|
131
|
-
**What this means concretely**: a sub-phase file's "Agent model policy" line documents *effort* tiering that genuinely takes effect, plus a *model* value that is aspirational/no-op unless that file is one day dispatched via `Agent(...)` instead of `Skill(...)`. Because mechanical bits (label transitions, `FORGE:CHECKPOINT` writes, heartbeat posts, task-type classification) are interleaved with reasoning-heavy content within the same `Skill()`-invoked file in most sub-phases, they cannot be selectively downtiered without either degrading the reasoning phases sharing that file, or extracting them into a brand-new fork — which the Spawn-Decision Policy above correctly discourages for operations this small (a single `gh issue edit` call does not clear Row a/b/c/d). Do not add a `model: "haiku"` claim to a sub-phase file expecting it to have effect; only add `effort: low`, and only when the whole file is mechanical end-to-end. <!-- Added: forge#1827 -->
|
|
107
|
+
Every `work-on/*.md` phase file carries an "Agent model policy" line naming a `model` and an `effort`. Phase files now run with `context: fork` (an isolated sub-agent per phase), so the line documents the intended tier for that phase. `effort` applies per invocation on Claude Code >= 2.1.154. A `model` value only takes effect where the runtime honours it for a forked skill or an `Agent(model=...)` dispatch; otherwise the phase runs on the session's model. Mechanical work (labels, annotations, heartbeats) lives in the same phase file as the reasoning it belongs to, so no separate tiering split is needed.
|
|
132
108
|
|
|
133
109
|
---
|
|
134
110
|
|
|
@@ -236,7 +212,7 @@ if [ "$PREFLIGHT_FAILED" -eq 1 ]; then
|
|
|
236
212
|
fi
|
|
237
213
|
```
|
|
238
214
|
|
|
239
|
-
Worktree/branch-already-exists and stale-label conditions are surfaced later (
|
|
215
|
+
Worktree/branch-already-exists and stale-label conditions are surfaced later (by the build phase's worktree step and the `## Error Handling` section) with their own recovery guidance in `docs/site/troubleshooting.md`.
|
|
240
216
|
|
|
241
217
|
### 0A: Parse input
|
|
242
218
|
Extract project prefix and issue number. If `next`/`pick`: list open issues sorted by priority, skip `needs-human`, `workflow:decomposed`, and `workflow:awaiting-merge`, pick highest priority.
|
|
@@ -253,9 +229,9 @@ Extract project prefix and issue number. If `next`/`pick`: list open issues sort
|
|
|
253
229
|
|
|
254
230
|
```bash
|
|
255
231
|
if echo "$ARGUMENTS" | grep -qE -- '--remediate\b'; then
|
|
256
|
-
REMEDIATE_PR_NUMBER=$(echo "$ARGUMENTS" |
|
|
232
|
+
REMEDIATE_PR_NUMBER=$(echo "$ARGUMENTS" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
257
233
|
REMEDIATE_ISSUE_FLAG=""
|
|
258
|
-
REMEDIATE_ISSUE_NUMBER=$(echo "$ARGUMENTS" |
|
|
234
|
+
REMEDIATE_ISSUE_NUMBER=$(echo "$ARGUMENTS" | sed -n 's/.*--issue[[:space:]][[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
259
235
|
[ -n "$REMEDIATE_ISSUE_NUMBER" ] && REMEDIATE_ISSUE_FLAG="--issue ${REMEDIATE_ISSUE_NUMBER}"
|
|
260
236
|
|
|
261
237
|
if [ -z "$REMEDIATE_PR_NUMBER" ]; then
|
|
@@ -270,12 +246,12 @@ fi
|
|
|
270
246
|
If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's normal issue-number resume logic (an issue number is not even known yet; `work-on/remediate.md` Phase M0 resolves it):
|
|
271
247
|
|
|
272
248
|
```
|
|
273
|
-
Skill(skill="work-on
|
|
249
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
274
250
|
```
|
|
275
251
|
|
|
276
|
-
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
252
|
+
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
277
253
|
|
|
278
|
-
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
|
|
254
|
+
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
|
|
279
255
|
|
|
280
256
|
**Skip this entire section if `--remediate` is absent from `$ARGUMENTS`** — proceed to the normal parse below.
|
|
281
257
|
|
|
@@ -293,7 +269,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
|
|
|
293
269
|
**Issue**: #{NUMBER}"
|
|
294
270
|
```
|
|
295
271
|
|
|
296
|
-
**Also
|
|
272
|
+
**Also posted at phase entry points** (Phases 1, 3 and 4) by the router — see "Phase Heartbeat (router-owned)" below. Phases never post heartbeats. <!-- Added: forge#740 -->
|
|
297
273
|
|
|
298
274
|
**Skip if**: Issue already has a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`) — no heartbeat needed on a completed issue. (This is in addition to, not instead of, the `UNDER_ORCHESTRATION` gate above.)
|
|
299
275
|
|
|
@@ -305,7 +281,9 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .u
|
|
|
305
281
|
|
|
306
282
|
**Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid`/`workflow:awaiting-merge` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:DECOMPOSED`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`, `FORGE:DECISION_RECORD`), parent tracker status, sub-issue status.
|
|
307
283
|
|
|
308
|
-
**
|
|
284
|
+
**Resume preflight (MANDATORY on any resume past Phase 1)** <!-- Added: forge#3061 -->: before routing to Phase 3 (build), 4 (review) or 5 (close) from existing state, run `bash "$FORGE_ROOT/scripts/verify-phase-trail.sh" {NUMBER} -R {GH_REPO}` (add `--docs-only` for docs-only diffs, or `--code-diff` when the diff has any non-docs file so an INVESTIGATION band cannot waive requirements), with `FORGE_ROOT` resolved by the bootstrap in "Script resolution" below. If `FORGE_ROOT` is empty or the script is missing, treat it as `PHASE_TRAIL: ERROR` (fail closed: stop and add `needs-human`; never skip the preflight). On `PHASE_TRAIL: FAIL`, go BACK and run each missing phase through its `Skill(...)` (investigate, Phase 3B classification, build contract/context/architect, validate) before continuing — never continue forward over a gap, never hand-post a missing marker, and never treat a recovered uncommitted worktree as a substitute for the skipped phases. The same verifier gates PR creation (`work-on/review.md` Phase R1.5) and auto-merge (`review-pr.md` Phase 8).
|
|
285
|
+
|
|
286
|
+
**Determine resume point**: no `FORGE:INVESTIGATOR` → Phase 1 (investigate). Investigation complete with decompose YES and no `FORGE:DECOMPOSED` → Phase 2 (decompose); with `FORGE:DECOMPOSED` → Phase 5 (`--terminal-state decomposed`). Investigation complete (decompose NO) without `FORGE:BUILDER:COMPLETE` → Phase 3 (build; a partial BUILDER comment is cleaned up by the build phase). `FORGE:BUILDER:COMPLETE` and no merged PR → Phase 4 (review). PR merged and issue open → Phase 5 (close). `workflow:invalid` → STOP.
|
|
309
287
|
|
|
310
288
|
### 0B.5: Read Phase Checkpoint (MANDATORY — executes before any phase-skip decision)
|
|
311
289
|
|
|
@@ -327,13 +305,13 @@ fi
|
|
|
327
305
|
|
|
328
306
|
| `next_phase` value | Resume at |
|
|
329
307
|
|--------------------|-----------|
|
|
330
|
-
| `BUILD` | Phase 3 (skip
|
|
331
|
-
| `DECOMPOSE` | Phase 2 (skip
|
|
332
|
-
| `REVIEW` | Phase 4 (skip
|
|
333
|
-
| `CLOSE` | Phase
|
|
308
|
+
| `BUILD` | Phase 3 — build (skip investigation) |
|
|
309
|
+
| `DECOMPOSE` | Phase 2 — decompose (skip investigation) |
|
|
310
|
+
| `REVIEW` | Phase 4 — review (skip investigation and build) |
|
|
311
|
+
| `CLOSE` | Phase 5 — close (skip everything before it) |
|
|
334
312
|
| *(absent or unrecognized)* | Fall back to prose heuristics above |
|
|
335
313
|
|
|
336
|
-
Note:
|
|
314
|
+
Note: the investigate phase no longer writes `next_phase: BUILD`/`DECOMPOSE` CHECKPOINT comments (removed as redundant with the `workflow:ready-to-build`/`workflow:decomposed` label transition — see Phase 1D). Those two rows remain here only to route older, pre-existing CHECKPOINT comments correctly; new runs land on the prose-heuristic fallback for those two cases instead, which is equally precise. `REVIEW` and `CLOSE` are still written (Phase 3M and Phase 5D) because each covers a real gap before the corresponding label transition.
|
|
337
315
|
|
|
338
316
|
**If no checkpoint exists**: fall back to prose resume heuristics in Phase 0B above — treat as fresh start at Phase 1.
|
|
339
317
|
|
|
@@ -351,7 +329,7 @@ BATCH_MEMBERS_BLOCK=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body'
|
|
|
351
329
|
if [ -n "$BATCH_MEMBERS_BLOCK" ]; then
|
|
352
330
|
IS_BATCH=1
|
|
353
331
|
# Extract member issue numbers (- [ ] #NNN: title lines)
|
|
354
|
-
BATCH_MEMBERS=($(echo "$BATCH_MEMBERS_BLOCK" |
|
|
332
|
+
BATCH_MEMBERS=($(echo "$BATCH_MEMBERS_BLOCK" | sed -n 's/^.*- \[ \] #\([0-9][0-9]*\).*/\1/p' || true))
|
|
355
333
|
echo "Batch issue detected — member issues: ${BATCH_MEMBERS[*]}"
|
|
356
334
|
fi
|
|
357
335
|
```
|
|
@@ -378,7 +356,7 @@ fi
|
|
|
378
356
|
gh issue edit "$MEMBER" {GH_FLAG} --add-label "workflow:merged" 2>/dev/null || true
|
|
379
357
|
done
|
|
380
358
|
```
|
|
381
|
-
- Member issues are closed
|
|
359
|
+
- Member issues are closed by the router after Phase 5 (close) returns COMPLETE — NOT before (see "Batch-member closure").
|
|
382
360
|
|
|
383
361
|
**Source branch for review-findings**: Parse `**Code branch**: \`{branch}\`` from body. Branch from there, not main.
|
|
384
362
|
|
|
@@ -396,11 +374,29 @@ if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
|
396
374
|
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
397
375
|
ADAPTIVE_ENABLED=false
|
|
398
376
|
fi
|
|
399
|
-
|
|
377
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
378
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
379
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
380
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
381
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
382
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
383
|
+
FORGE_ROOT=""
|
|
384
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
385
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
386
|
+
_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")")"
|
|
387
|
+
# 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)
|
|
388
|
+
_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-)"
|
|
389
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
390
|
+
# '${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.
|
|
391
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
392
|
+
while IFS= read -r _c; do
|
|
393
|
+
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
|
|
394
|
+
done <<< "$_k"
|
|
395
|
+
fi
|
|
396
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
400
397
|
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
# Phase 0, so it is the deterministic fallback when FORGEDOCK_HOME is unset.
|
|
398
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
399
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
404
400
|
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
405
401
|
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
406
402
|
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
@@ -415,7 +411,7 @@ resolve_script() {
|
|
|
415
411
|
return
|
|
416
412
|
fi
|
|
417
413
|
# Tier 3: universal script
|
|
418
|
-
if [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
414
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
419
415
|
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
420
416
|
return
|
|
421
417
|
fi
|
|
@@ -467,12 +463,7 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
|
|
|
467
463
|
echo "Learned override: STAGING_BRANCH → $STAGING_BRANCH (from learned.branch_targets.staging)"
|
|
468
464
|
```
|
|
469
465
|
|
|
470
|
-
2. **Test commands** —
|
|
471
|
-
```bash
|
|
472
|
-
# Pass LEARNED_TEST_COMMANDS to Phase 3H as additional commands to run after verification.commands
|
|
473
|
-
# (consumed in 3H — store as env var or carry forward in context)
|
|
474
|
-
echo "Learned test commands: $LEARNED_TEST_COMMANDS"
|
|
475
|
-
```
|
|
466
|
+
2. **Test commands** — Read by the validate phase itself from `forge.yaml → learned.test_commands` (a forked phase cannot see router variables). Nothing to pass.
|
|
476
467
|
|
|
477
468
|
3. **Label map** — If `LEARNED_LABEL_MAP` is non-empty, export it as `FORGE_LABEL_MAP` so that all subsequent `resolve_script 'transition-label'` invocations (which are child processes) can read it. The script performs the substitution internally: if the canonical label (e.g. `workflow:investigating`) appears as a key in the map, it uses the mapped value instead.
|
|
478
469
|
```bash
|
|
@@ -484,7 +475,7 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
|
|
|
484
475
|
echo "Learned override: FORGE_LABEL_MAP active — label_map will be applied by resolve_script 'transition-label'"
|
|
485
476
|
```
|
|
486
477
|
|
|
487
|
-
4. **Commit style** —
|
|
478
|
+
4. **Commit style** — Read by the build children (implement/validate) from `forge.yaml → learned.commit_style`; the router only logs it:
|
|
488
479
|
```bash
|
|
489
480
|
[ -n "$LEARNED_COMMIT_STYLE" ] && COMMIT_STYLE="$LEARNED_COMMIT_STYLE" && \
|
|
490
481
|
echo "Learned override: COMMIT_STYLE → $COMMIT_STYLE"
|
|
@@ -495,1998 +486,173 @@ LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' forge.yaml 2>/dev/null |
|
|
|
495
486
|
### 0C: Sync to Project board
|
|
496
487
|
Add issue to project, set Status=In Progress, Lane, Component, Priority, Workflow=Investigating.
|
|
497
488
|
|
|
498
|
-
### 0C.5:
|
|
499
|
-
|
|
500
|
-
Rather than loading the full ~27-command corpus (~1.1 MB / ~276K tokens) into
|
|
501
|
-
context, resolve the **minimal spec set** this run actually needs from the spec
|
|
502
|
-
knowledge graph. Use the universal-tier `graph-query` script via the canonical
|
|
503
|
-
`resolve_script` tier-dispatch pattern:
|
|
504
|
-
|
|
505
|
-
```bash
|
|
506
|
-
# Forward-transitive reachability: work-on + its reachable sub-phases (CONTAINS)
|
|
507
|
-
# + required devdocs (REQUIRES), as repo-relative file paths.
|
|
508
|
-
RESOLUTION=$(resolve_script 'graph-query')
|
|
509
|
-
TIER="${RESOLUTION%%:*}"
|
|
510
|
-
SCRIPT_PATH="${RESOLUTION#*:}"
|
|
511
|
-
case "$TIER" in
|
|
512
|
-
adaptive|universal)
|
|
513
|
-
SPEC_SET=$(bash "$SCRIPT_PATH" load-set work-on 2>/dev/null || echo '[]')
|
|
514
|
-
echo "$SPEC_SET" | jq -r '.[]'
|
|
515
|
-
;;
|
|
516
|
-
prose)
|
|
517
|
-
# Prose fallback: graph-query.sh unavailable — read specs on demand as each
|
|
518
|
-
# Skill(...) is invoked. Selective loading is an optimization, not required.
|
|
519
|
-
: ;;
|
|
520
|
-
esac
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
**Read ONLY the files returned by `load-set work-on`** when you need full spec
|
|
524
|
-
text during this run — these are the work-on orchestrator plus the sub-phases
|
|
525
|
-
and devdocs reachable from it (e.g. `commands/work-on/build/*`, `commands/work-on/review.md`,
|
|
526
|
-
`commands/review-pr.md`). Do **not** broadly read unrelated command specs
|
|
527
|
-
(`pipeline-health.md`, `audit.md`, `geo-audit.md`, …) that are not in the set.
|
|
528
|
-
Sub-phases are still invoked normally via their existing `Skill(...)` calls; this
|
|
529
|
-
step only narrows what is *pre-read* into context, it does not remove any phase.
|
|
489
|
+
### 0C.5: Spec loading
|
|
530
490
|
|
|
531
|
-
|
|
532
|
-
reachability). It is read-only and auto-builds the graph if the gitignored JSON
|
|
533
|
-
is absent — no committed graph is required. The prose tier above handles older
|
|
534
|
-
installs without the scripts layer: selective loading is an optimization, never
|
|
535
|
-
a hard dependency.
|
|
491
|
+
The router never reads phase spec files. Each phase loads exactly its own spec when it runs, because every phase sub-skill is forked (`context: fork`): the phase's context holds only that phase's instructions plus its args. Do not pre-read `commands/work-on/*.md`, `review-pr.md` or any other command spec into the router's context.
|
|
536
492
|
|
|
537
493
|
---
|
|
538
494
|
|
|
539
|
-
## Phase
|
|
540
|
-
|
|
541
|
-
**Skip if**: `<!-- FORGE:INVESTIGATOR -->` exists with `<!-- INVESTIGATION:COMPLETE -->` OR `<!-- INVESTIGATION:INVALID -->` in the SAME comment. `INVESTIGATION:INVALID` is the terminal sentinel Phase 1C emits for an INVALID verdict (see 1C below) — it signals investigation completion just as much as `INVESTIGATION:COMPLETE` does, not an interrupted state.
|
|
542
|
-
|
|
543
|
-
**Partial investigation**: If investigator comment exists BUT NEITHER `<!-- INVESTIGATION:COMPLETE -->` NOR `<!-- INVESTIGATION:INVALID -->` is present → investigation was interrupted. Delete the partial comment and restart:
|
|
544
|
-
```bash
|
|
545
|
-
COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
546
|
-
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .id')
|
|
547
|
-
gh api repos/{GH_REPO}/issues/comments/$COMMENT_ID -X DELETE
|
|
548
|
-
```
|
|
495
|
+
## Phase Heartbeat (router-owned)
|
|
549
496
|
|
|
550
|
-
|
|
551
|
-
```bash
|
|
552
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
553
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
554
|
-
case "$TIER" in
|
|
555
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
556
|
-
prose)
|
|
557
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" \
|
|
558
|
-
--remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
559
|
-
;;
|
|
560
|
-
esac
|
|
561
|
-
```
|
|
497
|
+
Posted at the entry of Phases 1, 3 and 4 — **only** when `UNDER_ORCHESTRATION` is `true`, and skipped when the issue already carries a terminal label (`workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`). `/orchestrate`'s stall detector reads these timestamps. Phases never post heartbeats.
|
|
562
498
|
|
|
563
|
-
**Post Phase 1 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
|
|
564
499
|
```bash
|
|
565
|
-
PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
566
500
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
|
|
567
|
-
**Phase**:
|
|
568
|
-
**Timestamp**: $
|
|
569
|
-
**Issue**: #{NUMBER}"
|
|
501
|
+
**Phase**: {PHASE_LABEL}
|
|
502
|
+
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
503
|
+
**Issue**: #{NUMBER}" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
570
504
|
```
|
|
571
505
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
Before investigation begins, verify the issue body contains the four mandatory pipeline sections. If any are missing, add placeholder content so the investigator has the correct scaffolding.
|
|
575
|
-
|
|
576
|
-
**Skip if**: All four sections (`## Problem`, `## Affected Files`, `## Expected Behavior`, `## Acceptance Criteria`) are already present.
|
|
577
|
-
|
|
578
|
-
**Reuse `ISSUE_BODY` from Phase 0B** — it was already read in full there. Only re-fetch with the command below if `ISSUE_BODY` is not actually present in this session's context (compaction or a fresh resume).
|
|
579
|
-
|
|
580
|
-
```bash
|
|
581
|
-
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
582
|
-
|
|
583
|
-
MISSING_SECTIONS=""
|
|
584
|
-
echo "$ISSUE_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
|
|
585
|
-
echo "$ISSUE_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
|
|
586
|
-
echo "$ISSUE_BODY" | grep -q "^## Expected Behavior" || MISSING_SECTIONS="$MISSING_SECTIONS EXPECTED_BEHAVIOR"
|
|
587
|
-
echo "$ISSUE_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
|
|
506
|
+
`{PHASE_LABEL}`: `Phase 1 — Investigation`, `Phase 3 — Build`, `Phase 4 — Review`.
|
|
588
507
|
|
|
589
|
-
|
|
590
|
-
echo "Missing sections:$MISSING_SECTIONS — normalizing issue body before investigation"
|
|
591
|
-
|
|
592
|
-
APPEND_TEXT=""
|
|
593
|
-
echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
|
|
594
|
-
## Problem
|
|
595
|
-
|
|
596
|
-
Root cause unknown — investigation needed."
|
|
597
|
-
|
|
598
|
-
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
599
|
-
## Affected Files
|
|
600
|
-
|
|
601
|
-
Files to be identified during investigation."
|
|
602
|
-
|
|
603
|
-
echo "$MISSING_SECTIONS" | grep -q "EXPECTED_BEHAVIOR" && APPEND_TEXT="$APPEND_TEXT
|
|
604
|
-
## Expected Behavior
|
|
508
|
+
---
|
|
605
509
|
|
|
606
|
-
|
|
510
|
+
## Phase 1: Investigate
|
|
607
511
|
|
|
608
|
-
|
|
609
|
-
## Acceptance Criteria
|
|
512
|
+
**Skip if**: a `<!-- FORGE:INVESTIGATOR -->` comment contains `<!-- INVESTIGATION:COMPLETE -->` or `<!-- INVESTIGATION:INVALID -->` (route on the existing verdict as below — the skill's own resume check returns it if invoked).
|
|
610
513
|
|
|
611
|
-
|
|
514
|
+
Post the Phase 1 heartbeat, then:
|
|
612
515
|
|
|
613
|
-
# Append missing sections to the existing body (never replace — only extend)
|
|
614
|
-
NORMALIZED_BODY="${ISSUE_BODY}${APPEND_TEXT}"
|
|
615
|
-
gh issue edit {NUMBER} {GH_FLAG} --body "$NORMALIZED_BODY"
|
|
616
|
-
echo "Issue body normalized — added:$MISSING_SECTIONS"
|
|
617
|
-
else
|
|
618
|
-
echo "Issue body already contains all mandatory sections — skipping normalization"
|
|
619
|
-
fi
|
|
620
516
|
```
|
|
621
|
-
|
|
622
|
-
**Continue to Phase 1B unconditionally.** Normalization is a compensation step — it never blocks investigation.
|
|
623
|
-
|
|
624
|
-
### 1B: Investigate the issue
|
|
625
|
-
|
|
626
|
-
Mission: Validate whether the issue is real. Assume description is wrong until proven otherwise.
|
|
627
|
-
|
|
628
|
-
**Resolve target repo and branch**:
|
|
629
|
-
|
|
630
|
-
Read `forge.yaml → review.tech_stack` and `forge.yaml → review.key_paths` (if present) to identify which files are most relevant for the affected domain. If the `review` section is absent, use the issue labels, title keywords, and the affected files listed in the issue body to determine the domain. Start with the files the issue explicitly names, then expand to callers and related modules.
|
|
631
|
-
|
|
632
|
-
**Workflow pipeline issues** (repo is a ForgeDock installation):
|
|
633
|
-
- Key files: `commands/work-on.md`, `commands/review-pr.md`, `commands/quality-gate.md`, `commands/orchestrate.md`, `forge.yaml`, `bin/forgedock.mjs`
|
|
634
|
-
|
|
635
|
-
**Application issues** (all other repos):
|
|
636
|
-
|
|
637
|
-
Use `forge.yaml → review.tech_stack` and the issue domain labels to identify entry points. If `forge.yaml → review.key_paths` lists domain-to-file mappings, use that table directly. Otherwise, infer key files from the issue body's Affected Files section.
|
|
638
|
-
|
|
639
|
-
**INFRA domain known footguns** (read before writing any `.github/workflows/*.yml` changes):
|
|
640
|
-
- **appleboy/ssh-action Go template preprocessing**: Any `{{` in a `script:` block is interpreted as a Go template directive **before the script reaches SSH**. This means `docker ps --format '{{.Names}}'` and `docker inspect --format '{{index .RepoTags 0}}'` will crash the action with exit 1. Both function calls (`{{index .X Y}}`) AND field accessors (`{{.Names}}`, `{{.Status}}`) fail on the action's empty data context. Shell error handlers (`|| fallback`, `set -e`, `2>/dev/null`) are bypassed because the failure is client-side. Always use `docker inspect IMAGE | jq -r '.[0].RepoTags[0]'` and `docker ps --format json | jq -r '.Names'` patterns in `appleboy/ssh-action` scripts. (Ref: forge#226 — 6-day silent deploy failure masked by `continue-on-error: true`)
|
|
641
|
-
|
|
642
|
-
**Steps**:
|
|
643
|
-
1. Check the right branch — read from branch specified in issue body (`**Code branch**: \`{branch}\``) if present
|
|
644
|
-
2. Read domain files — start with key files for the affected domain
|
|
645
|
-
2.5. **Existing system search (conditional)**: If the issue describes a gap in a functional capability — content not being distributed, notifications not sending, jobs not running, data not being synced — MUST search for an existing automated system before proposing a new one. The issue body may name a specific tool or path (e.g., `reddit-bot/`, `marketing/`) — do NOT anchor on that path alone. Expand the search to all service layers:
|
|
646
|
-
```bash
|
|
647
|
-
# Check all service layers for the capability (adapt paths to your project structure)
|
|
648
|
-
grep -rn "{capability_keyword}" {REPO_PATH}/services/ --include="*.py" -l | head -20
|
|
649
|
-
# Look for scheduled jobs, automated runners, existing integrations
|
|
650
|
-
grep -rn "scheduler\|celery\|cron\|nightly\|periodic" {REPO_PATH}/services/ --include="*.py" -l | head -10
|
|
651
|
-
```
|
|
652
|
-
If an existing system is found that already handles the capability: the fix MUST route through the existing system (fix its config, env var, or gate) — NOT create a new parallel tool. Document the existing system in the investigation report and make it the centerpiece of the recommendation. This check is especially critical when the issue references a standalone tool directory (`reddit-bot/`, `scripts/`, `tools/`) — those directories often duplicate functionality that a service already owns. (Ref: forge#279 — investigator anchored on `reddit-bot/` from issue body, never checked `services/herald/app/scheduler/`, built parallel PRAW integration alongside Herald's existing automated crosspost scheduler)
|
|
653
|
-
3. Verify claims — does the code actually have the problem described?
|
|
654
|
-
3.5. **Type Invariant Verification (MANDATORY)**: Before declaring that a field, key, or parameter has a specific type (e.g. "content is always a dict", "status is always an int"), search for ALL code paths that write to that field across ALL services:
|
|
655
|
-
```bash
|
|
656
|
-
grep -rn '"field_name"\s*:' services/ # Python dict key assignments
|
|
657
|
-
grep -rn 'result\["field_name"\]\s*=' services/ # Direct assignments
|
|
658
|
-
grep -rn '\.field_name\s*=' services/ # Attribute assignments
|
|
659
|
-
```
|
|
660
|
-
If the field is written with different types in different code paths (e.g. dict in the standard path, string in the auth-gated path), document ALL variants. The fix must handle every variant — not just the one on the primary investigated code path. A type guard like `or {}` only protects against falsy values; a non-empty string is truthy and bypasses it.
|
|
661
|
-
4. Git blame — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
|
|
662
|
-
```bash
|
|
663
|
-
# Introducing commit for each affected file (first commit that added it)
|
|
664
|
-
git log --reverse --format='%h %an %ad %s' --date=short -- {affected_file} | head -1
|
|
665
|
-
# Last-touch commit (most recent change)
|
|
666
|
-
git log -1 --format='%h %an %ad %s' --date=short -- {affected_file}
|
|
667
|
-
# Line-level blame for a specific suspect hunk, if the issue names one
|
|
668
|
-
git blame -L {start},{end} -- {affected_file}
|
|
669
|
-
```
|
|
670
|
-
Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
|
|
671
|
-
4.5. **Rogue commit pre-state comparison (conditional)**: If the issue body references a specific commit as rogue, bad, or unintended (e.g., "rogue commit `abc1234`", "bad commit", "this was never intended"), MUST run `git show {commit}^:{file}` to see the file before that commit. Compare the pre-commit state against the current file. Any block present in the current file but absent in the pre-commit state was introduced by that commit chain and is a candidate for full reversion — not just partial editing. Report the delta (pre vs. current) in the investigation report. Do NOT assume surrounding code near a named import/bug is correct simply because the issue only named a specific sub-problem. (Ref: forge#278 — investigator confirmed the broken import but never ran `git show 18a3a2cf3^:batch.py`; the surrounding 50-line feature gate was also rogue and was preserved by the fix PR, causing a P1 access regression for all non-Scale users)
|
|
672
|
-
5. Domain context discovery (narrow scope only, 1–5 files):
|
|
673
|
-
```bash
|
|
674
|
-
git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
|
|
675
|
-
gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
|
|
676
|
-
```
|
|
677
|
-
Keep only file/function-level overlap. Max 5 related issues.
|
|
678
|
-
|
|
679
|
-
**Pickaxe pass (prior fix / regression detection)** — bounded to one pass, capped at 5 hits: search for prior additions/removals of the suspected symbol or literal string named in the issue, independent of whether that fix was ever linked to a filed issue:
|
|
680
|
-
```bash
|
|
681
|
-
git log -S"{suspected_symbol_or_string}" --oneline -- {affected_files} | head -5
|
|
682
|
-
# Use -G instead of -S when the target is a regex pattern rather than a literal string
|
|
683
|
-
git log -G"{pattern}" --oneline -- {affected_files} | head -5
|
|
684
|
-
```
|
|
685
|
-
Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
|
|
686
|
-
6. Determine root cause
|
|
687
|
-
7. Identify affected files — full list of files that need changes
|
|
688
|
-
7.5. **Sibling Pattern Sweep** *(conditional — when the bug is a condition, gated function call, or field presence check)*: After identifying the affected files, grep for the same pattern in sibling files within the same directory. The issue spec may name only the file where the error was first observed — but the same commit or PR that introduced the bug often applied it uniformly across related handlers.
|
|
689
|
-
```bash
|
|
690
|
-
# Identify the broken condition or gated function call from the issue
|
|
691
|
-
# Then search sibling files in the same router/service directory
|
|
692
|
-
AFFECTED_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
|
|
693
|
-
grep -rn "{broken_pattern}" "$AFFECTED_DIR" --include="*.py" | grep -v "{PRIMARY_AFFECTED_FILE}"
|
|
694
|
-
```
|
|
695
|
-
**If identical patterns are found in files NOT listed in the issue spec**, output a scope-gap warning:
|
|
696
|
-
> **Scope-Gap Warning**: The issue spec lists `{PRIMARY_FILE}` but the same pattern exists in `{SIBLING_FILE}:{LINE}`. These were likely introduced together. Recommend widening scope to fix all callers in this PR, or creating follow-up issues for the other files before proceeding.
|
|
697
|
-
|
|
698
|
-
Do NOT silently exclude sibling matches. The appropriate output when sibling files have the same bug is to flag them explicitly — even if the issue spec's silence appears intentional. The fix-approach validation step (step 8) will confirm whether to widen scope or create follow-ups. <!-- Added: forge#383 -->
|
|
699
|
-
8. Fix-approach validation — if issue proposes a fix, don't adopt as spec. Trace through middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
700
|
-
|
|
701
|
-
### 1C: Post investigation comment
|
|
702
|
-
|
|
703
|
-
The comment MUST include a terminal sentinel at the very end. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
|
|
704
|
-
|
|
705
|
-
- **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase checks for it explicitly (ahead of `INVESTIGATION:COMPLETE`) and routes to `terminalReason: "invalid"`; `bin/hooks/interactive-engine.mjs`'s `PHASE_MARKERS` table also already treats it as terminal. Emitting `INVESTIGATION:COMPLETE` for an INVALID verdict is what previously caused every completed investigation to read as `{verdict: "CONFIRMED"}` regardless of actual outcome — do NOT regress this (forge#2350).
|
|
706
|
-
- **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` below — only INVALID gets the distinct terminal sentinel).
|
|
707
|
-
|
|
708
|
-
```bash
|
|
709
|
-
if [ "{VERDICT}" = "INVALID" ]; then
|
|
710
|
-
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:INVALID -->"
|
|
711
|
-
else
|
|
712
|
-
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:COMPLETE -->"
|
|
713
|
-
fi
|
|
714
|
-
|
|
715
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
716
|
-
## Investigation Report
|
|
717
|
-
|
|
718
|
-
**Verdict**: {CONFIRMED|PARTIAL|INVALID}
|
|
719
|
-
**Confidence**: {HIGH|MEDIUM|LOW}
|
|
720
|
-
**Severity**: {CRITICAL|HIGH|MEDIUM|LOW}
|
|
721
|
-
**Task Type**: {Bug Fix|Feature|Refactor|Maintenance|Investigation}
|
|
722
|
-
|
|
723
|
-
### What Was Claimed
|
|
724
|
-
{summary of what the issue describes}
|
|
725
|
-
|
|
726
|
-
### What We Found
|
|
727
|
-
{what the code actually shows}
|
|
728
|
-
|
|
729
|
-
### Root Cause
|
|
730
|
-
{specific root cause, with file:line references where applicable}
|
|
731
|
-
|
|
732
|
-
### Affected Files
|
|
733
|
-
{numbered list of files that need changes}
|
|
734
|
-
|
|
735
|
-
### Evidence
|
|
736
|
-
{specific findings — function names, line numbers, behavior observed}
|
|
737
|
-
|
|
738
|
-
### History Findings
|
|
739
|
-
**Introducing commit**: {hash — author — date — subject, per primary affected file}
|
|
740
|
-
**Last touched**: {hash — author — date — subject}
|
|
741
|
-
**Pickaxe hits (prior fixes / regressions)**: {commit(s) found via \`git log -S\`/\`-G\`, or 'None found' — max 5}
|
|
742
|
-
{This field is MANDATORY — populate from the git blame + pickaxe commands in step 4/5. If a file is newly created (no history), write 'New file — no history.'}
|
|
743
|
-
|
|
744
|
-
### Recommendation
|
|
745
|
-
{what to build/fix, concrete and actionable}
|
|
746
|
-
|
|
747
|
-
### Related Issues
|
|
748
|
-
{if any found via domain context discovery, max 5}
|
|
749
|
-
|
|
750
|
-
### Decomposition Assessment
|
|
751
|
-
**{YES|NO}** — {reason}
|
|
752
|
-
{if YES: proposed sub-issues with titles and dependencies}
|
|
753
|
-
|
|
754
|
-
${INVESTIGATION_SENTINEL}"
|
|
517
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:investigate", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\"")
|
|
755
518
|
```
|
|
756
519
|
|
|
757
|
-
|
|
520
|
+
| `INVESTIGATE_RESULT` | Router action |
|
|
521
|
+
|---|---|
|
|
522
|
+
| `status: COMPLETE` or `ALREADY_DONE`, `decompose: NO` | Marker gate (below) → Lane Resolution → Phase 3 |
|
|
523
|
+
| `status: COMPLETE` or `ALREADY_DONE`, `decompose: YES` | Phase 2 |
|
|
524
|
+
| `status: INVALID` | Terminal (the phase labelled `workflow:invalid` and closed the issue) → Phase 5 with `--terminal-state invalid` |
|
|
525
|
+
| `status: BLOCKED` | Terminal: confirm `needs-human` is on the issue (add it with the blocker as a comment if not) and STOP |
|
|
758
526
|
|
|
759
|
-
|
|
527
|
+
**Marker gate — Phase 1 exit** (decompose NO path only): the issue must have a `FORGE:INVESTIGATOR` comment containing `INVESTIGATION:COMPLETE`. If absent, invoke the skill once more; if still absent, post `<!-- FORGE:GATE_FAILURE -->` ("investigation marker missing after one re-invoke"), add `needs-human`, STOP. <!-- forge#1419 -->
|
|
760
528
|
|
|
761
|
-
|
|
529
|
+
---
|
|
762
530
|
|
|
763
|
-
|
|
764
|
-
```bash
|
|
765
|
-
# Get repo owner login for filtering.
|
|
766
|
-
# Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
|
|
767
|
-
# but comment .user.login is always a personal account login. For org-owned repos
|
|
768
|
-
# these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
|
|
769
|
-
# so using project.owner directly silently disables correction capture for all org repos.
|
|
770
|
-
#
|
|
771
|
-
# Resolution order:
|
|
772
|
-
# 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
|
|
773
|
-
# 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
|
|
774
|
-
# 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
|
|
775
|
-
REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
|
|
776
|
-
if [ -z "$REPO_OWNER" ]; then
|
|
777
|
-
REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
|
|
778
|
-
fi
|
|
779
|
-
if [ -z "$REPO_OWNER" ]; then
|
|
780
|
-
REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
|
|
781
|
-
fi
|
|
531
|
+
## Phase 2: Decompose
|
|
782
532
|
|
|
783
|
-
# Fetch all comments, filter to owner-only, look for correction signals
|
|
784
|
-
CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
785
|
-
| jq -r --arg owner "$REPO_OWNER" \
|
|
786
|
-
'.[] | select(.user.login == $owner) | select(
|
|
787
|
-
(.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
|
|
788
|
-
) | .body' 2>/dev/null || echo '')
|
|
789
533
|
```
|
|
790
|
-
|
|
791
|
-
**If correction signals found** — extract and write each correction:
|
|
792
|
-
|
|
793
|
-
```bash
|
|
794
|
-
# Example: extract branch target correction "use develop not staging"
|
|
795
|
-
# Adjust regex to the correction pattern detected
|
|
796
|
-
|
|
797
|
-
if [ -n "$CORRECTIONS" ]; then
|
|
798
|
-
echo "Correction signals detected — writing to forge.yaml → learned:"
|
|
799
|
-
echo "$CORRECTIONS"
|
|
800
|
-
|
|
801
|
-
# Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
|
|
802
|
-
# Always use env variable injection to avoid YAML injection from comment content
|
|
803
|
-
# Example for branch target correction:
|
|
804
|
-
# BRANCH_VALUE="develop"
|
|
805
|
-
# yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
|
|
806
|
-
|
|
807
|
-
# After writing, emit FORGE:LEARNED annotation
|
|
808
|
-
LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
|
|
809
|
-
CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
810
|
-
|
|
811
|
-
# Update captured_at and captured_by metadata
|
|
812
|
-
CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
|
|
813
|
-
CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
|
|
814
|
-
|
|
815
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEARNED -->
|
|
816
|
-
## Learned Pattern Captured
|
|
817
|
-
|
|
818
|
-
**Source**: Owner correction in comment on issue #{NUMBER}
|
|
819
|
-
**Captured at**: $CAPTURED_AT
|
|
820
|
-
**Keys written**: \`$LEARNED_KEYS\`
|
|
821
|
-
|
|
822
|
-
The following project-specific pattern was detected from owner feedback and written to \`forge.yaml → learned:\`. Future sessions will use this override automatically (read in Phase 0B.1).
|
|
823
|
-
|
|
824
|
-
\`\`\`yaml
|
|
825
|
-
# Written to forge.yaml
|
|
826
|
-
learned:
|
|
827
|
-
# {key}: {value}
|
|
828
|
-
\`\`\`
|
|
829
|
-
|
|
830
|
-
**Idempotency**: yq merge-write — re-running will not duplicate entries."
|
|
831
|
-
|
|
832
|
-
echo "FORGE:LEARNED annotation posted."
|
|
833
|
-
fi
|
|
534
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:decompose", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\"")
|
|
834
535
|
```
|
|
835
536
|
|
|
836
|
-
|
|
537
|
+
| `DECOMPOSE_RESULT` | Router action |
|
|
538
|
+
|---|---|
|
|
539
|
+
| `COMPLETE` / `ALREADY_DONE` | Phase 5 with `--terminal-state decomposed` (the parent stays open as a tracker; each sub-issue runs its own `/work-on`) |
|
|
540
|
+
| `BLOCKED` | Terminal: the phase already added `needs-human` and posted the blocker. STOP. |
|
|
837
541
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
**CONFIRMED or PARTIAL with decompose: NO**:
|
|
841
|
-
```bash
|
|
842
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
843
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
844
|
-
case "$TIER" in
|
|
845
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
846
|
-
prose)
|
|
847
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" \
|
|
848
|
-
--remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
849
|
-
;;
|
|
850
|
-
esac
|
|
851
|
-
```
|
|
542
|
+
---
|
|
852
543
|
|
|
853
|
-
|
|
854
|
-
```bash
|
|
855
|
-
INV_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
856
|
-
--jq '[.[] | select(.body | contains("INVESTIGATION:COMPLETE"))] | length')
|
|
857
|
-
if [ "${INV_MARKER:-0}" -eq 0 ]; then
|
|
858
|
-
echo "MARKER GATE FAIL: INVESTIGATION:COMPLETE absent — re-invoking work-on/investigate once"
|
|
859
|
-
Skill(skill="work-on/investigate", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
860
|
-
INV_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
861
|
-
--jq '[.[] | select(.body | contains("INVESTIGATION:COMPLETE"))] | length')
|
|
862
|
-
if [ "${INV_MARKER:-0}" -eq 0 ]; then
|
|
863
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
|
|
864
|
-
## Marker Gate Failure — Phase 1 (Investigation)
|
|
865
|
-
|
|
866
|
-
**Expected marker**: \`INVESTIGATION:COMPLETE\` inside a \`FORGE:INVESTIGATOR\` comment
|
|
867
|
-
**Status**: Absent after subcommand re-invocation. Human review required.
|
|
868
|
-
|
|
869
|
-
The router re-invoked \`work-on/investigate\` once but the marker was still not posted.
|
|
870
|
-
Inspect the subcommand output above for errors. <!-- forge#1418 -->"
|
|
871
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" \
|
|
872
|
-
--remove-label "workflow:investigating" 2>/dev/null || true
|
|
873
|
-
exit 1
|
|
874
|
-
fi
|
|
875
|
-
fi
|
|
876
|
-
```
|
|
544
|
+
## Lane Resolution (router, before Phase 3 and again before Phase 4)
|
|
877
545
|
|
|
878
|
-
|
|
879
|
-
<!-- FORGE:PHASE_COMPLETE — Investigation routed to build. See Universal Phase Dispatcher: next phase is Phase 3. Not terminal — continue immediately. -->
|
|
880
|
-
→ Continue to Phase 3.
|
|
546
|
+
`PR_BASE` (the branch the PR targets and the worktree is based on) and `CLASSIFIED_LANE` are computed by the router and passed to the phases as `--base`. Include the Phase 0 Script resolution block in the same command.
|
|
881
547
|
|
|
882
|
-
**CONFIRMED or PARTIAL with decompose: YES**:
|
|
883
548
|
```bash
|
|
884
|
-
RESOLUTION=$(resolve_script '
|
|
549
|
+
RESOLUTION=$(resolve_script 'classify-lane')
|
|
885
550
|
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
886
551
|
case "$TIER" in
|
|
887
|
-
adaptive|universal)
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
--
|
|
552
|
+
adaptive|universal)
|
|
553
|
+
if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
|
|
554
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute the PR target — see the script error. Adding needs-human." # allowlist:check-command-side-effects
|
|
555
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
556
|
+
exit 1
|
|
557
|
+
fi
|
|
891
558
|
;;
|
|
892
|
-
esac
|
|
893
|
-
```
|
|
894
|
-
|
|
895
|
-
**No separate CHECKPOINT write here** — the `workflow:decomposed` label set above is itself a terminal state for this run; nothing needs to resume past it, so a `next_phase: DECOMPOSE` CHECKPOINT comment adds no information. <!-- Removed redundant FORGE:CHECKPOINT write: forge#1826 -->
|
|
896
|
-
<!-- FORGE:PHASE_COMPLETE — Investigation routed to decomposition. See Universal Phase Dispatcher: next phase is Phase 2. Not terminal — continue immediately. -->
|
|
897
|
-
→ Continue to Phase 2 (Decomposition).
|
|
898
|
-
|
|
899
|
-
**INVALID**:
|
|
900
|
-
```bash
|
|
901
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
902
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
903
|
-
case "$TIER" in
|
|
904
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
905
559
|
prose)
|
|
906
|
-
gh issue
|
|
907
|
-
|
|
560
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute the PR target. Adding needs-human." # allowlist:check-command-side-effects
|
|
561
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
562
|
+
exit 1
|
|
908
563
|
;;
|
|
909
564
|
esac
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
→ STOP. No checkpoint written — INVALID is terminal.
|
|
913
|
-
|
|
914
|
-
---
|
|
915
|
-
|
|
916
|
-
## Phase 2: Decomposition (Conditional)
|
|
917
|
-
|
|
918
|
-
**Engine coverage** (forge#2379): this phase's dispatch target (`work-on/decompose`) is now a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`/`bin/engine/phases.mjs`, live-wired so `investigate`'s `DECOMPOSE:YES` outcome hands off to it instead of terminating the run in place. This prose Phase 2 remains the interactive/prose dispatch path; the engine path is the headless equivalent, both invoking the same `commands/work-on/decompose.md` subcommand.
|
|
919
|
-
|
|
920
|
-
**Skip if**: Already decomposed, is a sub-issue, or investigation says decompose: NO.
|
|
921
|
-
|
|
922
|
-
**Trigger if**: Investigation assessment says YES — 2+ signals match (at least 1 Strong): multiple task types, 3+ service groups, phased requirements, 6+ files across directories.
|
|
923
|
-
|
|
924
|
-
### 2A: Load state
|
|
925
|
-
```bash
|
|
926
|
-
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
|
|
927
|
-
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
928
|
-
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
**MANDATORY — Owner override detection**: After reading the investigation comment, read ALL comments on the issue to check for owner override signals:
|
|
932
|
-
```bash
|
|
933
|
-
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
934
|
-
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
938
|
-
|
|
939
|
-
1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
|
|
940
|
-
2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
|
|
941
|
-
3. **If override makes a sub-issue obsolete**: Skip creating it. Note the skip reason in the decomposition comment.
|
|
942
|
-
4. **If override changes the sequencing dependency**: Revise the execution order so that the override's primary action (e.g., "strip the feature") completes before any downstream doc/SDK sub-issues are built against it.
|
|
943
|
-
|
|
944
|
-
**Why this matters**: Sub-issues scoped before an override are built against a stale premise. A docs sub-issue scoped as "neutralize liability language" becomes incorrect if the upstream schema sub-issue will fully remove the feature — the docs sub-issue should instead be "remove all references to the deleted feature." Building both in parallel against the pre-override scope produces contradictory staging state.
|
|
945
|
-
|
|
946
|
-
### 2B: Design sub-issues
|
|
947
|
-
From the Decomposition Assessment (adjusted for any owner override detected in 2A), extract sub-issue titles (dependency order — independent first), dependencies, and descriptions.
|
|
948
|
-
|
|
949
|
-
For each sub-issue:
|
|
950
|
-
- **Title**: from investigation report
|
|
951
|
-
- **Body**: brief scope + `**Parent**: #{NUMBER}` + dependency note
|
|
952
|
-
- **Labels**: inherit priority label from parent; do NOT copy workflow labels
|
|
953
|
-
- **Milestone**: same as parent
|
|
954
|
-
|
|
955
|
-
### 2C: Create sub-issues
|
|
956
|
-
|
|
957
|
-
Route through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") instead of calling the raw issue-creation command directly — this gets dedup (Phase 2D) and body validation (Phase 3F) for free on every decomposition-spawned sub-issue.
|
|
958
|
-
|
|
959
|
-
```bash
|
|
960
|
-
SUB_ISSUE_TITLE_FULL="{fix|feat|refactor}: {SUB_ISSUE_TITLE}"
|
|
961
|
-
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
962
|
-
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
963
|
-
# this is no longer required for safety — but strip it anyway so the raw title
|
|
964
|
-
# stays readable if it round-trips through any other eval-based consumer.
|
|
965
|
-
SUB_ISSUE_TITLE_FULL=$(printf '%s' "$SUB_ISSUE_TITLE_FULL" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
966
|
-
SUB_ISSUE_BODY_FILE=$(mktemp)
|
|
967
|
-
cat <<'SUB_BODY_EOF' > "$SUB_ISSUE_BODY_FILE"
|
|
968
|
-
## Problem
|
|
969
|
-
|
|
970
|
-
{1-3 sentences: what this sub-issue specifically addresses. What's wrong or what needs to be built.}
|
|
971
|
-
|
|
972
|
-
## Root Cause (if known)
|
|
973
|
-
|
|
974
|
-
{Specific root cause for this sub-task. Reference the parent investigation findings where applicable. If unknown: "Root cause unknown — investigation needed."}
|
|
975
|
-
|
|
976
|
-
## Affected Files
|
|
977
|
-
|
|
978
|
-
Files that need changes:
|
|
979
|
-
1. `{filepath}` — {what needs to change}
|
|
980
|
-
2. `{filepath}` — {what needs to change}
|
|
981
|
-
|
|
982
|
-
## Acceptance Criteria
|
|
983
|
-
|
|
984
|
-
- [ ] {Specific, testable criterion}
|
|
985
|
-
- [ ] {Specific, testable criterion}
|
|
986
|
-
- [ ] No regression in {related feature}
|
|
987
|
-
|
|
988
|
-
## Context
|
|
989
|
-
|
|
990
|
-
**Parent**: #{NUMBER}
|
|
991
|
-
{If depends on another sub-issue: "**Depends on**: #{SUB_ISSUE_N} — {reason}"}
|
|
992
|
-
SUB_BODY_EOF
|
|
993
|
-
|
|
994
|
-
# --milestone is only passed when the parent has one (see Phase 2B — "Milestone: same as parent").
|
|
995
|
-
MILESTONE_ARG=""
|
|
996
|
-
[ -n "{MILESTONE_TITLE}" ] && MILESTONE_ARG="--milestone \"{MILESTONE_TITLE}\""
|
|
997
|
-
Skill(skill="issue", args="--title \"$SUB_ISSUE_TITLE_FULL\" --body-file \"$SUB_ISSUE_BODY_FILE\" --label \"{PRIORITY_LABEL}\" ${MILESTONE_ARG}")
|
|
998
|
-
rm -f "$SUB_ISSUE_BODY_FILE"
|
|
999
|
-
|
|
1000
|
-
# /issue has no machine-readable return contract — resolve the created issue's number by
|
|
1001
|
-
# exact-title search immediately after the call (used by Phase 2D's tracker checklist).
|
|
1002
|
-
# Retry to absorb GitHub Search API indexing lag.
|
|
1003
|
-
SUB_ISSUE_NUM=""
|
|
1004
|
-
for _resolve_attempt in 1 2 3; do
|
|
1005
|
-
SUB_ISSUE_NUM=$(gh issue list {GH_FLAG} --search "in:title \"${SUB_ISSUE_TITLE_FULL}\"" --state open --limit 1 --json number --jq '.[0].number // empty')
|
|
1006
|
-
[ -n "$SUB_ISSUE_NUM" ] && break
|
|
1007
|
-
sleep 2
|
|
1008
|
-
done
|
|
565
|
+
CLASSIFIED_LANE="$PR_BASE"
|
|
566
|
+
echo "PR_BASE=$PR_BASE"
|
|
1009
567
|
```
|
|
568
|
+
Output is authoritative — no prose fallback. A non-zero exit means `needs-human` and STOP. <!-- Added: forge#669, forge#639 -->
|
|
1010
569
|
|
|
1011
|
-
|
|
1012
|
-
Add tracker checklist with all sub-issues in dependency order.
|
|
1013
|
-
|
|
1014
|
-
### 2E: Post decomposition comment
|
|
1015
|
-
```bash
|
|
1016
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
|
|
1017
|
-
## Decomposition Complete
|
|
1018
|
-
|
|
1019
|
-
### Sub-Issues Created
|
|
1020
|
-
- #{SUB_NUMBER}: {TITLE}
|
|
1021
|
-
|
|
1022
|
-
### Decomposition Rationale
|
|
1023
|
-
{brief summary}
|
|
1024
|
-
|
|
1025
|
-
<!-- FORGE:DECOMPOSED:COMPLETE -->"
|
|
1026
|
-
```
|
|
570
|
+
**Before Phase 4 only — validate the target against the classified lane** (guards against a resumed run or a hand-edited branch targeting the wrong branch):
|
|
1027
571
|
|
|
1028
|
-
### 2F: Update labels
|
|
1029
572
|
```bash
|
|
1030
|
-
RESOLUTION=$(resolve_script '
|
|
573
|
+
RESOLUTION=$(resolve_script 'validate-pr-target')
|
|
1031
574
|
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1032
575
|
case "$TIER" in
|
|
1033
|
-
adaptive|universal)
|
|
576
|
+
adaptive|universal)
|
|
577
|
+
if ! bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}; then
|
|
578
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKING: validate-pr-target.sh — PR base \`{PR_BASE}\` does not match classified lane \`{CLASSIFIED_LANE}\`. Manual intervention required." # allowlist:check-command-side-effects
|
|
579
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" # allowlist:check-command-side-effects
|
|
580
|
+
exit 1
|
|
581
|
+
fi
|
|
582
|
+
;;
|
|
1034
583
|
prose)
|
|
1035
|
-
|
|
1036
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
584
|
+
echo "WARNING: validate-pr-target.sh not installed (prose tier) — lane validation skipped; the review phase still refuses to merge into main." >&2
|
|
1037
585
|
;;
|
|
1038
586
|
esac
|
|
1039
587
|
```
|
|
1040
|
-
|
|
1041
|
-
→ STOP. Each sub-issue runs its own `/work-on`.
|
|
588
|
+
On a mismatch → STOP; do not invoke Phase 4. <!-- Added: forge#671 -->
|
|
1042
589
|
|
|
1043
590
|
---
|
|
1044
591
|
|
|
1045
592
|
## Phase 3: Build
|
|
1046
593
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
**Canonical path**: Sub-phases 3A–3M run **inline** in the current context window for STANDARD and fast-lane issues, with one standing exception: 3G (quality gate) always forks to a sub-agent under Row (d). This is the single authoritative build topology. `work-on/build.md` and `work-on-monolithic.md` ([BENCHMARK]) describe the same inline model with different levels of detail; they are not separate competing paths. `Skill()`/`Agent()` sub-agent spawns for build sub-phases are only permitted under a Spawn-Decision Table Row (c) or Row (d) exception — Row (c): ≥20 Skill invocations or ≥10 files changed before the build; Row (d): the sub-phase's own runtime risks exceeding the prompt-cache TTL regardless of build size (3G's standing exception). <!-- Added: forge#1276, updated: forge#1825 -->
|
|
1050
|
-
|
|
1051
|
-
**Skip if**: `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment. <!-- Added: forge#1305 — require completion marker, not mere presence of BUILDER annotation -->
|
|
1052
|
-
|
|
1053
|
-
**Partial build detection**: If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → the build was interrupted after implement.md Phase I6 (comment posted) but before validate.md Phase V5 (commit). Delete the partial comment and restart Phase 3 from the top: <!-- Added: forge#1305 -->
|
|
1054
|
-
```bash
|
|
1055
|
-
PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1056
|
-
--jq '[.[] | select(.body | contains("FORGE:BUILDER") and (contains("FORGE:BUILDER:COMPLETE") | not))] | last | .id // ""')
|
|
1057
|
-
if [ -n "$PARTIAL_ID" ]; then
|
|
1058
|
-
gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
|
|
1059
|
-
echo "Deleted partial FORGE:BUILDER comment (no FORGE:BUILDER:COMPLETE) — restarting build"
|
|
1060
|
-
fi
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
**CRITICAL: You MUST execute ALL sub-phases 3A–3M in order. Sub-phases 3C.5 (context) and 3C.6 (architect) are skipped ONLY for TRIVIAL tasks and Investigation tasks — see Phase 3B for classification. For STANDARD and COMPLEX tasks they post mandatory `FORGE:CONTEXT` and `FORGE:ARCHITECT` comments that Phase 3F reads as its primary input. Skipping them without a TRIVIAL/Investigation classification degrades build quality and causes review findings. After each sub-phase, continue to the next — no sub-phase is terminal.**
|
|
1064
|
-
|
|
1065
|
-
### 3A: Confirm state (reuse in-context; re-read only on compaction)
|
|
1066
|
-
|
|
1067
|
-
**Post Phase 3 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
|
|
1068
|
-
```bash
|
|
1069
|
-
PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1070
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
|
|
1071
|
-
**Phase**: Phase 3 — Build
|
|
1072
|
-
**Timestamp**: ${PHASE_START_TIMESTAMP}
|
|
1073
|
-
**Issue**: #{NUMBER}"
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
If this is a continuous run (Phase 1 already executed in this same session), you already have the issue view, the FORGE:INVESTIGATOR body, and know whether FORGE:BUILDER:COMPLETE/FORGE:FAST_PATH exist from having posted or checked for them earlier — reuse those values, do not re-fetch. Only run the block below if one of those values is genuinely missing from this session's context (fresh/resumed session after compaction, or checkpoint routing jumped straight here):
|
|
1077
|
-
|
|
1078
|
-
```bash
|
|
1079
|
-
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,milestone
|
|
1080
|
-
|
|
1081
|
-
# Read investigation report
|
|
1082
|
-
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1083
|
-
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
|
|
1084
|
-
|
|
1085
|
-
# Check if build already completed (require FORGE:BUILDER:COMPLETE — not just FORGE:BUILDER)
|
|
1086
|
-
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1087
|
-
--jq '.[] | select(.body | contains("FORGE:BUILDER:COMPLETE")) | .body'
|
|
1088
|
-
|
|
1089
|
-
# Check for existing COMPLEXITY_BAND from a prior run (resume path)
|
|
1090
|
-
EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1091
|
-
--jq '.[] | select(.body | contains("FORGE:FAST_PATH")) | .body' 2>/dev/null | head -1)
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
If no investigation comment with `<!-- INVESTIGATION:COMPLETE -->` → STOP (investigation not complete).
|
|
1095
|
-
|
|
1096
|
-
Extract from investigation: affected files, root cause, recommendation, task type.
|
|
1097
|
-
|
|
1098
|
-
### 3B: Classify task type and complexity
|
|
1099
|
-
|
|
1100
|
-
**Step 1 — Task type classification:**
|
|
594
|
+
**Skip if**: a `FORGE:BUILDER` comment contains `<!-- FORGE:BUILDER:COMPLETE -->` (go to Phase 4).
|
|
1101
595
|
|
|
1102
|
-
|
|
1103
|
-
|--------|------|----------|
|
|
1104
|
-
| Title starts with "Investigate:"/"Audit:"/"Research:" | Investigation | Produce issues as deliverables |
|
|
1105
|
-
| UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
|
|
1106
|
-
| Feature + services/ | Backend Feature | Implement directly |
|
|
1107
|
-
| Feature + both | Full-Stack | Backend first, then frontend-design |
|
|
1108
|
-
| Bug + web/ | Frontend Fix | Direct |
|
|
1109
|
-
| Bug + services/ | Backend Fix | Direct |
|
|
1110
|
-
| Refactor/docs | Maintenance | Direct |
|
|
596
|
+
Post the Phase 3 heartbeat, run Lane Resolution, then:
|
|
1111
597
|
|
|
1112
|
-
**Investigation tasks — early exit (BEFORE Phase 3C):** If task type = Investigation, skip Phases 3C, 3C.5, and 3C.6 entirely. Post `<!-- FORGE:FAST_PATH -->` comment, then jump directly to Phase 3F (implement → issue creation path). Do NOT run the Builder Contract, Context Gathering, or Architecture Plan for investigation tasks.
|
|
1113
|
-
|
|
1114
|
-
```bash
|
|
1115
|
-
# Post fast-path comment for investigation tasks
|
|
1116
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
|
|
1117
|
-
## Fast-Path Classification
|
|
1118
|
-
|
|
1119
|
-
**COMPLEXITY_BAND**: INVESTIGATION
|
|
1120
|
-
**Task type**: Investigation
|
|
1121
|
-
**Rationale**: Title prefix 'Investigate:' (or task type = Investigation from investigator report) — skipping Builder Contract (3C), Context Gathering (3C.5), and Architecture Plan (3C.6). Jumping directly to Phase 3F (issue creation).
|
|
1122
|
-
**Phases skipped**: 3C, 3C.5, 3C.6"
|
|
1123
598
|
```
|
|
1124
|
-
|
|
1125
|
-
→ Jump to Phase 3F immediately. Do not continue to Phase 3C.
|
|
1126
|
-
|
|
1127
|
-
**Step 2 — Complexity classification (for non-Investigation tasks):**
|
|
1128
|
-
|
|
1129
|
-
Classify COMPLEXITY_BAND based on affected file count and task nature:
|
|
1130
|
-
|
|
1131
|
-
| Condition | COMPLEXITY_BAND |
|
|
1132
|
-
|-----------|-----------------|
|
|
1133
|
-
| Single file, doc/config/markdown only, no logic changes expected | TRIVIAL |
|
|
1134
|
-
| 1–5 files, existing patterns, no cross-service impact | STANDARD |
|
|
1135
|
-
| 6+ files, new abstractions, cross-service, migration, schema changes | COMPLEX |
|
|
1136
|
-
|
|
1137
|
-
Post `<!-- FORGE:FAST_PATH -->` comment immediately after classification:
|
|
1138
|
-
|
|
1139
|
-
```bash
|
|
1140
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:FAST_PATH -->
|
|
1141
|
-
## Fast-Path Classification
|
|
1142
|
-
|
|
1143
|
-
**COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
|
|
1144
|
-
**Task type**: {TASK_TYPE}
|
|
1145
|
-
**Affected file count**: {N}
|
|
1146
|
-
**Rationale**: {one-sentence explanation of classification decision}
|
|
1147
|
-
**Phases skipped**: {list phases skipped, or 'none — full pipeline' for STANDARD/COMPLEX}"
|
|
599
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:build", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BASE}")
|
|
1148
600
|
```
|
|
1149
601
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
**TRIVIAL tasks**: After posting FORGE:FAST_PATH, skip Phase 3C.5 (Context Gathering) and Phase 3C.6 (Architecture Plan) only. Phase 3C (Builder Contract) is **retained** — it still runs. Continue: 3C (Builder Contract) → 3D → 3E → 3F → 3F.5 → 3G → 3H onward. When filling in **Phases skipped** in the FORGE:FAST_PATH comment, write: `3C.5, 3C.6`.
|
|
1153
|
-
|
|
1154
|
-
**STANDARD and COMPLEX tasks**: Run full pipeline — 3C → 3C.5 → 3C.6 → 3D onward. No phases skipped.
|
|
1155
|
-
|
|
1156
|
-
### 3C: Builder Contract (MANDATORY)
|
|
1157
|
-
|
|
1158
|
-
Post `<!-- FORGE:CONTRACT -->` comment with: task type, proposed approach, deliverables table (file/change/why), acceptance criteria, quality considerations (auth model, new env vars, SQL safety, security surface), out of scope, alternatives.
|
|
1159
|
-
|
|
1160
|
-
```bash
|
|
1161
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
|
|
1162
|
-
## Builder Contract
|
|
1163
|
-
|
|
1164
|
-
**Task type**: {TASK_TYPE}
|
|
1165
|
-
|
|
1166
|
-
### Proposed Approach
|
|
1167
|
-
{BRIEF_APPROACH_DESCRIPTION}
|
|
1168
|
-
|
|
1169
|
-
### Deliverables
|
|
1170
|
-
| File | Change | Why |
|
|
1171
|
-
|------|--------|-----|
|
|
1172
|
-
{DELIVERABLES_ROWS}
|
|
1173
|
-
|
|
1174
|
-
### Acceptance Criteria
|
|
1175
|
-
{ACCEPTANCE_CRITERIA_CHECKLIST}
|
|
1176
|
-
|
|
1177
|
-
### Quality Considerations
|
|
1178
|
-
{AUTH_MODEL_NEW_ENV_VARS_SQL_SAFETY_SECURITY_SURFACE}
|
|
602
|
+
The build phase owns classification (`FORGE:FAST_PATH`), the worktree, the `workflow:building` label, the contract, and runs its children — context, architect, implement, validate (with the quality gate) — each as its own forked skill, then the acceptance gate and the build checkpoint.
|
|
1179
603
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
604
|
+
| `BUILD_RESULT` | Router action |
|
|
605
|
+
|---|---|
|
|
606
|
+
| `COMPLETE` | Marker gate (below) → record `BRANCH` and `WORKTREE_PATH` from the result → Phase 4 |
|
|
607
|
+
| `ALREADY_DONE` | Record `BRANCH`/`WORKTREE_PATH` (from the result, or `git worktree list` for the issue branch) → Phase 4 |
|
|
608
|
+
| `INVESTIGATION_COMPLETE` | Investigation-type task: deliverable issues were filed and the original closed by the phase → Phase 5 with `--terminal-state investigation` (no review) |
|
|
609
|
+
| `BLOCKED` | Terminal: confirm `needs-human` is present (the phase adds it with the blocker) and STOP |
|
|
1185
610
|
|
|
1186
|
-
|
|
611
|
+
**Marker gate — Phase 3 exit**: a `FORGE:BUILDER` comment must contain `FORGE:BUILDER:COMPLETE`. If absent, invoke `work-on:build` once more with the same args; if still absent, post `<!-- FORGE:GATE_FAILURE -->` ("builder completion marker missing after one re-invoke"), add `needs-human`, remove `workflow:building`, STOP. <!-- forge#1418 -->
|
|
1187
612
|
|
|
1188
|
-
|
|
613
|
+
---
|
|
1189
614
|
|
|
1190
|
-
|
|
615
|
+
## Phase 4: Review (push → PR → /review-pr → merge)
|
|
1191
616
|
|
|
1192
|
-
|
|
617
|
+
Post the Phase 4 heartbeat, run Lane Resolution including the lane validation, then:
|
|
1193
618
|
|
|
1194
|
-
```bash
|
|
1195
|
-
FUNCTION_NAMES=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1196
|
-
--jq '.[] | select(.body | contains("FORGE:CONTRACT")) | .body' \
|
|
1197
|
-
| awk '/^### Deliverables/{p=1; next} /^### /{p=0} p' \
|
|
1198
|
-
| grep -oE '`[A-Za-z_][A-Za-z0-9_]*`' \
|
|
1199
|
-
| tr -d '`' | sort -u | tr '\n' ' ' | xargs)
|
|
1200
619
|
```
|
|
1201
|
-
|
|
1202
|
-
**The ONLY acceptable skip conditions** (all must be true): Issue is a 1-file config/docs edit with no code logic AND affected files have zero git history. In all other cases, run context gathering.
|
|
1203
|
-
|
|
1204
|
-
**Batch execution (MANDATORY)**: C1, C2 (including the direct commit-body read and the pickaxe pass), C3, and C4 below are mutually independent — none consumes another's output. Issue ALL of their underlying `gh`/`git`/`grep` calls as a single batch of parallel tool calls in one message, not as four sequential steps. This is the same rule as the "independent tool calls → same message" convention used elsewhere in the pipeline; it turns a ~4x serial round-trip chain (each `gh api` call is 0.5-2s) into one wall-clock round-trip. The per-file loop inside C1 and the per-function loop inside C3 are themselves independent across iterations — include every iteration's call in the same batch rather than looping turn-by-turn. Total budget is still 20s per query / 2 min overall; batching only removes serialization, it does not change the timeout.
|
|
1205
|
-
|
|
1206
|
-
Queries to batch (20s timeout each, 2 min total budget):
|
|
1207
|
-
|
|
1208
|
-
**C1: Past Review Findings on These Files**
|
|
1209
|
-
```bash
|
|
1210
|
-
# {AFFECTED_FILES} is a space-separated argument (see --files contract) — split
|
|
1211
|
-
# explicitly on IFS=' ' into an array instead of a bare `for file in {AFFECTED_FILES}`,
|
|
1212
|
-
# which word-splits on the shell's default IFS (space, tab, AND newline) and
|
|
1213
|
-
# would corrupt any path containing a space.
|
|
1214
|
-
IFS=' ' read -ra AFFECTED_FILES_ARR <<< "{AFFECTED_FILES}"
|
|
1215
|
-
for file in "${AFFECTED_FILES_ARR[@]}"; do
|
|
1216
|
-
basename=$(basename "$file" .py)
|
|
1217
|
-
gh issue list -R {GH_REPO} --state closed --label "review-finding" \
|
|
1218
|
-
--search "$basename" --limit 10 \
|
|
1219
|
-
--json number,title,body \
|
|
1220
|
-
--jq '.[] | {number, title,
|
|
1221
|
-
pattern: (.body | capture("\\*\\*Pattern\\*\\*: *(?<p>[^\\n]+)").p // null),
|
|
1222
|
-
prevention: (.body | capture("\\*\\*Prevention\\*\\*: *(?<v>[^\\n]+)").v // null),
|
|
1223
|
-
root_cause: (.body | capture("\\*\\*Root cause\\*\\*: *(?<rc>[^\\n]+)").rc // (.body | capture("Root Cause[^\\n]*\\n(?<rc>[^\\n]+)").rc // "see body"))
|
|
1224
|
-
}'
|
|
1225
|
-
done
|
|
620
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:review", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
|
|
1226
621
|
```
|
|
1227
622
|
|
|
1228
|
-
|
|
1229
|
-
```bash
|
|
1230
|
-
git log --oneline -30 -- {AFFECTED_FILES} | grep -oE '#[0-9]+' | sort -u | head -8
|
|
1231
|
-
# For each issue: fetch title + root cause, keep only bug/fix/review-finding labeled. Max 5.
|
|
1232
|
-
```
|
|
623
|
+
The review phase owns: ancestry and empty-branch guards, push, PR creation, `workflow:in-review`, the phase-trail preflight (and the single re-dispatch of any missing phase via `Skill`), `/review-pr --auto-merge` (CI gate and reviewed-head guard included), stale-review re-review, and the REVIEW checkpoint. It never closes the issue.
|
|
1233
624
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
625
|
+
| `REVIEW_RESULT` | Router action |
|
|
626
|
+
|---|---|
|
|
627
|
+
| `COMPLETE` (PR merged) | Record `PR_NUMBER` → Phase 5 with `--terminal-state merged` |
|
|
628
|
+
| `ALREADY_MERGED` | Same as COMPLETE |
|
|
629
|
+
| `BLOCKED` | Terminal: the phase already added `needs-human` (or `workflow:awaiting-merge` for a deploy-gate hold) with the blocker. Do NOT merge, re-run phases, or add labels here. STOP. |
|
|
1239
630
|
|
|
1240
|
-
|
|
1241
|
-
```bash
|
|
1242
|
-
git log -S"{suspected_symbol_or_string}" --oneline -- {AFFECTED_FILES} | head -5
|
|
1243
|
-
# Use -G instead of -S for regex patterns
|
|
1244
|
-
git log -G"{pattern}" --oneline -- {AFFECTED_FILES} | head -5
|
|
1245
|
-
```
|
|
1246
|
-
Any hit is a candidate prior fix or reintroduced defect for this exact code area — read `git show {hash}` to confirm scope before including it in the output. This catches regressions the issue-number harvest above misses (e.g. a defect fixed via a squashed commit with no `#NNN` reference).
|
|
631
|
+
---
|
|
1247
632
|
|
|
1248
|
-
|
|
1249
|
-
```bash
|
|
1250
|
-
for fn in {FUNCTION_NAMES}; do
|
|
1251
|
-
grep -r "$fn" {WORKTREE_PATH} --include="*.py" -l | grep -v __pycache__ | head -5
|
|
1252
|
-
grep -r "$fn" {WORKTREE_PATH}/web/src --include="*.ts" --include="*.tsx" -l 2>/dev/null | head -5
|
|
1253
|
-
done
|
|
1254
|
-
```
|
|
633
|
+
## Phase 5: Close & trajectory
|
|
1255
634
|
|
|
1256
|
-
**C4: Successful Similar Implementations**
|
|
1257
|
-
```bash
|
|
1258
|
-
gh pr list -R {GH_REPO} --state merged --search "{domain_keywords}" --limit 5 \
|
|
1259
|
-
--json number,title,files --jq '.[] | {number, title, file_count: (.files | length)}'
|
|
1260
635
|
```
|
|
1261
|
-
|
|
1262
|
-
Post `<!-- FORGE:CONTEXT -->` comment with findings:
|
|
1263
|
-
```bash
|
|
1264
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTEXT -->
|
|
1265
|
-
## Implementation Context for #{NUMBER}
|
|
1266
|
-
|
|
1267
|
-
### Known Pitfalls for This Area
|
|
1268
|
-
{prevention rules from past review-findings}
|
|
1269
|
-
|
|
1270
|
-
### Historical Findings on These Files
|
|
1271
|
-
{past review-finding issues}
|
|
1272
|
-
|
|
1273
|
-
### Past Bugs in This Module
|
|
1274
|
-
{closed bug issues from git log mining, PLUS pickaxe-derived findings (commits with no linked issue, or commit
|
|
1275
|
-
bodies read directly per the C2 direct-commit-body step)}
|
|
1276
|
-
|
|
1277
|
-
### Related Code Paths (must stay consistent)
|
|
1278
|
-
{files that import/call changed functions}
|
|
1279
|
-
|
|
1280
|
-
### Patterns That Cause Bugs Here
|
|
1281
|
-
{recurring bug types synthesized from C1+C2}
|
|
1282
|
-
|
|
1283
|
-
### Successful Similar Implementations
|
|
1284
|
-
{positive patterns from C4}
|
|
1285
|
-
|
|
1286
|
-
<!-- FORGE:CONTEXT:COMPLETE -->"
|
|
636
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on:close", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --pr {PR_NUMBER} --base {PR_BASE} --branch {BRANCH} --worktree {WORKTREE_PATH} --terminal-state {merged|investigation|decomposed|invalid}")
|
|
1287
637
|
```
|
|
638
|
+
Omit `--pr/--branch/--worktree` when the value is unknown (PR-less terminals).
|
|
1288
639
|
|
|
1289
|
-
|
|
640
|
+
| `CLOSE_RESULT` | Router action |
|
|
641
|
+
|---|---|
|
|
642
|
+
| `COMPLETE` / `ALREADY_DONE` | Pipeline done. If `IS_BATCH` (Phase 0B), run the batch-member closure below. Print the phase's report/card verbatim and stop. |
|
|
643
|
+
| `PHASE_COMPLETE` | Multi-phase issue: this phase merged, more remain. The issue is open again at `workflow:investigating` → re-run Phase 0 and continue from the next phase. |
|
|
644
|
+
| `FAILED` | Invoke `work-on:close` once more with the same args (it is idempotent). If it fails again, post the blocker as a comment, add `needs-human`, STOP. |
|
|
1290
645
|
|
|
1291
|
-
|
|
646
|
+
**Batch-member closure** (only when `IS_BATCH` and the batch PR merged): for each member in `BATCH_MEMBERS`, re-read its live state and labels; leave open (and report as a split outcome) any member that is closed, `needs-human`, `blocked` or `operator-only`; otherwise close it with "Resolved as part of batch PR #{PR_NUMBER} (#{NUMBER})" and add `workflow:merged`.
|
|
1292
647
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
**For STANDARD and COMPLEX tasks**: This phase is NOT optional. Always run it. Even a 1-file STANDARD fix benefits from cross-path consistency checks. Do NOT skip without a TRIVIAL classification from Phase 3B.
|
|
1296
|
-
|
|
1297
|
-
Trace ALL affected code paths before writing code.
|
|
1298
|
-
|
|
1299
|
-
**Additional skip condition** (STANDARD tasks only): Issue creates ONLY new files with no existing callers AND title starts with "docs:" or "chore:".
|
|
648
|
+
---
|
|
1300
649
|
|
|
1301
|
-
|
|
650
|
+
## Remediation entry (`--remediate`)
|
|
1302
651
|
|
|
1303
|
-
|
|
1304
|
-
1. **Authenticated routes**: Callers under `app/dashboard/`, `app/(authenticated)/`, or any layout that wraps children with an auth provider (e.g., `UserProvider`, `SessionProvider`).
|
|
1305
|
-
2. **Public routes**: Callers under `app/(public)/`, `app/playground/`, `app/(marketing)/`, or any layout without the relevant provider.
|
|
1306
|
-
3. **If both categories have callers AND the new hook throws when its provider is absent**: add an explicit implementation step to either (a) guard the hook call with a null-context check, (b) make the hook return a safe default when called outside its provider, or (c) remove the hook from the shared component and move it to the authenticated-route-only caller. Document the split in the FORGE:ARCHITECT affected paths table. Do NOT leave a shared component that crashes public routes to be discovered by the FE review agent. <!-- Added: forge#381 -->
|
|
1307
|
-
|
|
1308
|
-
**A2: Trace Data Flow** — From each entry point: Entry → Transform → Persist/Relay → Exit. Check if the proposed change needs to propagate to each step. **For every field or key read by the changed code, enumerate ALL write paths first** — search across all services for assignments to that field. If multiple code paths write different types to the same field (e.g. dict in the standard path, string in the auth-gated path), the implementation must handle all variants. Do not assume the type you see on the primary code path is the only possible type.
|
|
1309
|
-
|
|
1310
|
-
**A2.1: Runtime UID × Volume Ownership Check** *(conditional — skip if no Dockerfile or entrypoint is affected)* — When the PR changes the container's runtime user (Dockerfile `USER` directive, `su-exec`, `gosu`, `setuid`), the architect MUST trace the full write-path chain before writing the implementation plan:
|
|
1311
|
-
1. **Enumerate volume mounts**: Read all `docker-compose*.yml` files for the affected service. List every named volume and its container mount point (e.g. `storage_shared:/app/storage`). Docker named volumes are created as root-owned by default — any UID change without a corresponding ownership fix will silently break writes.
|
|
1312
|
-
2. **Grep for filesystem writes**: Search the affected service's codebase for all filesystem write operations (`mkdir`, `Path.mkdir`, `write_bytes`, `open(`, `os.makedirs`, `shutil.copy`, `shutil.move`). For each write operation, identify the target path.
|
|
1313
|
-
3. **Cross-reference**: For each write path that falls under a named volume mount point, add an explicit implementation step to ensure ownership compatibility before the privilege drop — typically `chown -R <user>:<group> <mount_point>` in the entrypoint script before the `exec su-exec` / `exec gosu` call.
|
|
1314
|
-
4. **Add to FORGE:ARCHITECT deliverables table**: List the entrypoint or docker-compose change as an explicit deliverable. Do NOT leave volume ownership to be discovered by the builder or reviewer. <!-- Added: forge#323 -->
|
|
1315
|
-
|
|
1316
|
-
**A2.2: Gate-Condition Caller Sweep** *(conditional — when the fix changes a gate condition that guards a function call or restricts a field)*: Before finalizing the affected-paths table, grep for all callers of the gated function across sibling files in the same service directory. For each caller, verify that the gate condition applied at the call site is semantically correct.
|
|
1317
|
-
```bash
|
|
1318
|
-
# Identify the gated function from the issue/contract
|
|
1319
|
-
GATED_FUNCTION="{function_being_called_inside_the_gate}"
|
|
1320
|
-
SERVICE_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
|
|
1321
|
-
# Find all call sites
|
|
1322
|
-
grep -rn "$GATED_FUNCTION" "$SERVICE_DIR" --include="*.py" | grep -v "#"
|
|
1323
|
-
```
|
|
1324
|
-
For each call site found: read the surrounding gate condition (±10 lines). If the condition includes fields that do NOT require the gated resource (e.g., `extraction_schema` gated behind an LLM key check when `extraction_schema` is processed without an LLM), add that caller file to the FORGE:ARCHITECT affected-paths table with a note explaining the incorrect gate condition.
|
|
1325
|
-
|
|
1326
|
-
**Do NOT** omit sibling callers from the affected-paths table simply because they were not listed in the issue spec. The architect's scope is determined by code correctness, not by the issue spec's file list. A gate-condition bug that exists identically in 3 router files must be fixed in all 3 — even if the issue only named 1. <!-- Added: forge#383 -->
|
|
1327
|
-
|
|
1328
|
-
**A2.5: Pipeline Phase-Dependency Check** *(Forge pipeline changes only — skip if no `commands/*.md` file is affected)* — Identify artefact contracts the changed file produces (comment markers, structured output blocks, Skill() invocation signatures, label transitions). Find downstream pipeline phases that consume those artefacts. Flag any invocation signature change and verify all callers. Emit a checklist of downstream phases that must be updated — appended to the Consistency Checks block in the FORGE:ARCHITECT comment.
|
|
1329
|
-
|
|
1330
|
-
**A3: Consistency Rules** — Identify invariants all paths must satisfy: null handling, validation, logging, error response shape, auth checks.
|
|
1331
|
-
|
|
1332
|
-
**A4: Sequence Implementation** — Order: schema/type changes first → core logic → secondary paths → tests → config/env. Files imported by others change before importers. Higher risk first.
|
|
1333
|
-
|
|
1334
|
-
**A5: Risk Assessment** — Rate each non-obvious interaction: HIGH/MEDIUM/LOW with mitigation.
|
|
1335
|
-
|
|
1336
|
-
Post `<!-- FORGE:ARCHITECT -->` comment:
|
|
1337
|
-
```bash
|
|
1338
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
1339
|
-
## Implementation Plan for #{NUMBER}
|
|
1340
|
-
|
|
1341
|
-
### Affected Paths (ALL must be updated)
|
|
1342
|
-
| # | File | Function/Class | Change Required | Why |
|
|
1343
|
-
|---|------|----------------|-----------------|-----|
|
|
1344
|
-
{rows}
|
|
1345
|
-
|
|
1346
|
-
### Implementation Order
|
|
1347
|
-
1. {FIRST_CHANGE} — {WHY_FIRST}
|
|
1348
|
-
2. {SECOND_CHANGE} — {WHY_SECOND}
|
|
1349
|
-
|
|
1350
|
-
### Consistency Checks
|
|
1351
|
-
- [ ] {INVARIANT_1}
|
|
1352
|
-
- [ ] {INVARIANT_2}
|
|
1353
|
-
|
|
1354
|
-
### Risk Assessment
|
|
1355
|
-
| Risk | Severity | Mitigation |
|
|
1356
|
-
|------|----------|------------|
|
|
1357
|
-
{rows}
|
|
1358
|
-
|
|
1359
|
-
### Files to Read Before Coding
|
|
1360
|
-
- \`{FILE}\` — {WHY_READ_IT}
|
|
1361
|
-
|
|
1362
|
-
<!-- FORGE:ARCHITECT:COMPLETE -->"
|
|
1363
|
-
```
|
|
1364
|
-
|
|
1365
|
-
If budget exceeded (3 min), use `<!-- FORGE:ARCHITECT:PARTIAL -->`.
|
|
1366
|
-
|
|
1367
|
-
### 3D: Set building label
|
|
1368
|
-
```bash
|
|
1369
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
1370
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1371
|
-
case "$TIER" in
|
|
1372
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} building ;;
|
|
1373
|
-
prose)
|
|
1374
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
|
|
1375
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
1376
|
-
;;
|
|
1377
|
-
esac
|
|
1378
|
-
```
|
|
1379
|
-
|
|
1380
|
-
### 3E: Create worktree
|
|
1381
|
-
|
|
1382
|
-
Branch slug from title (lowercase, hyphenated, max 40 chars). Prefix: `fix/` (bugs) or `feat/` (features).
|
|
1383
|
-
|
|
1384
|
-
**Compute `PR_BASE` before worktree creation** — the source branch for the worktree MUST match the PR target. Compute `PR_BASE` now using `classify-lane.sh` so both worktree creation and Phase 4C use the same deterministic value. <!-- Added: forge#639 -->
|
|
1385
|
-
|
|
1386
|
-
```bash
|
|
1387
|
-
# Compute PR_BASE deterministically from issue milestone — no LLM interpretation
|
|
1388
|
-
RESOLUTION=$(resolve_script 'classify-lane')
|
|
1389
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1390
|
-
case "$TIER" in
|
|
1391
|
-
adaptive|universal)
|
|
1392
|
-
if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
|
|
1393
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to compute PR target — see script error above. Adding needs-human."
|
|
1394
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
1395
|
-
exit 1
|
|
1396
|
-
fi
|
|
1397
|
-
;;
|
|
1398
|
-
prose)
|
|
1399
|
-
# classify-lane has no valid prose fallback — the script output is authoritative.
|
|
1400
|
-
# Without it, PR target cannot be determined safely. Add needs-human and stop.
|
|
1401
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot compute PR target deterministically. Adding needs-human."
|
|
1402
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
1403
|
-
exit 1
|
|
1404
|
-
;;
|
|
1405
|
-
esac
|
|
1406
|
-
```
|
|
1407
|
-
|
|
1408
|
-
**Determine source branch**:
|
|
1409
|
-
- Review-finding → parse `**Code branch**: \`{branch}\`` from issue body; branch from `origin/{branch}`
|
|
1410
|
-
- **Milestone review-finding hybrid lane** (Code branch matches `milestone/*`): High-risk lane. NEVER use `git merge` to resolve conflicts — use `git rebase` or `git cherry-pick` only. If conflicts can't be resolved without merge, post comment, add `needs-human`, STOP.
|
|
1411
|
-
- **Missing ref fallback**: After parsing, verify the Code branch still exists on remote. If not (e.g., the source PR's head branch was deleted post-merge before a corrected stamp took effect), fall back to `PR_BASE` (the lane default) and note the fallback:
|
|
1412
|
-
```bash
|
|
1413
|
-
SOURCE_BRANCH="{CODE_BRANCH_FROM_ISSUE_BODY}"
|
|
1414
|
-
if ! git ls-remote --exit-code origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
|
|
1415
|
-
echo "WARNING: Code branch '$SOURCE_BRANCH' not found on remote — falling back to PR_BASE '$PR_BASE'"
|
|
1416
|
-
SOURCE_BRANCH="$PR_BASE"
|
|
1417
|
-
fi
|
|
1418
|
-
```
|
|
1419
|
-
- Feature lane (has milestone) → branch from `origin/{PR_BASE}` (PR_BASE now set above)
|
|
1420
|
-
- Fast lane (no milestone) → branch from `origin/{PR_BASE}` (PR_BASE = `{STAGING_BRANCH}`)
|
|
1421
|
-
|
|
1422
|
-
```bash
|
|
1423
|
-
cd {REPO_PATH}
|
|
1424
|
-
git fetch origin
|
|
1425
|
-
BRANCH="fix/{slug}-{NUMBER}"
|
|
1426
|
-
WORKTREE_ROOT="{REPO_PATH}/.claude/worktrees"
|
|
1427
|
-
if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
|
|
1428
|
-
[ -n "${OPENCODE_SESSION_ID:-}" ] ||
|
|
1429
|
-
[ -n "${OPENCODE_PID:-}" ] ||
|
|
1430
|
-
[ -n "${OPENCODE:-}" ]; then
|
|
1431
|
-
WORKTREE_ROOT="{REPO_PATH}/.opencode/worktrees"
|
|
1432
|
-
elif [ "${FORGE_RUNTIME:-}" = "codex" ]; then
|
|
1433
|
-
WORKTREE_ROOT="{REPO_PATH}/.codex/worktrees"
|
|
1434
|
-
fi
|
|
1435
|
-
WORKTREE_PATH="${WORKTREE_ROOT}/{BRANCH_SLUG}"
|
|
1436
|
-
git worktree add {WORKTREE_PATH} -b {BRANCH} origin/{PR_BASE}
|
|
1437
|
-
```
|
|
1438
|
-
|
|
1439
|
-
If worktree already exists: verify correct branch, reuse or remove and recreate.
|
|
1440
|
-
|
|
1441
|
-
### 3F: Implement
|
|
1442
|
-
|
|
1443
|
-
**Load context chain** — Read these from GitHub BEFORE writing code:
|
|
1444
|
-
1. `FORGE:ARCHITECT` comment (primary implementation guide — if present, follow its ordered list exactly)
|
|
1445
|
-
2. `FORGE:INVESTIGATOR` comment (root cause, affected files)
|
|
1446
|
-
3. `FORGE:CONTRACT` comment (deliverables, acceptance criteria)
|
|
1447
|
-
4. `FORGE:CONTEXT` comment (pitfalls, related paths, past bugs)
|
|
1448
|
-
|
|
1449
|
-
If `FORGE:ARCHITECT` is absent, fall back to investigation report + contract.
|
|
1450
|
-
|
|
1451
|
-
**Route by task type**: Bug Fix → implement directly. Feature (backend) → implement directly. Feature (UI/UX) → invoke `frontend-design` skill. Full-Stack → backend first, then frontend-design. Investigation → create issues, skip to Phase 7.
|
|
1452
|
-
|
|
1453
|
-
**Implementation rules**:
|
|
1454
|
-
- Work in `{WORKTREE_PATH}` — all file reads, writes, git ops happen here
|
|
1455
|
-
- Read the current file before modifying it — never assume its state
|
|
1456
|
-
- Read related files identified in context briefing before touching changed code
|
|
1457
|
-
- Follow architect plan's implementation order exactly (when present)
|
|
1458
|
-
- For each acceptance criterion: implement it, then verify it's met
|
|
1459
|
-
- Do NOT add unrequested scope — contract out-of-scope stays out
|
|
1460
|
-
- **Library callback verification**: When writing a lambda/callable passed to a library parameter, MUST verify expected calling convention BEFORE writing it. Check library's default value, documentation, or source code. Wrong arity causes runtime `TypeError` invisible to static analysis. (Ref: PR #14391 — `lambda _: ""` passed where SQLAlchemy expects 0 args)
|
|
1461
|
-
- **Cross-lane import guard**: Before adding any `import` or `from X import Y` statement for a service-internal module (`app.*`), verify the module exists on the PR's base branch — NOT just on your local disk or a milestone branch. Run `git show origin/{base_branch}:{module_path}.py` (replacing dots with slashes) to confirm. If the module only exists on a milestone branch, do NOT import it — find an alternative implementation or make the import conditional with a `try/except ImportError` fallback. A milestone-only import on a fast-lane PR will crash production on every request with `ModuleNotFoundError`. (Ref: forge#277 — builder imported `app.billing.subscriptions` from `milestone/subscription-model`, which doesn't exist on `staging`/`main`, causing P1 production crash for paying customer)
|
|
1462
|
-
- **Deliverable-type consistency check**: Before committing, compare the actual output against the CONTRACT's deliverable list. If the CONTRACT explicitly states "no code changes required", "docs only", or "configuration update only" AND the diff introduces new executable files (`.py`, `.js`, `.ts`, `.sh` — not test, config, or documentation files), STOP. Do NOT commit. Re-read the CONTRACT, the investigation recommendation, and the ARCHITECT plan. If all three agree that code is needed, update the CONTRACT comment to reflect the new deliverable type before proceeding. If only the builder decided to add code without contract support, discard the code change and implement the contracted deliverable instead. <!-- Added: forge#279 -->
|
|
1463
|
-
- **Pipeline check documentation — generalization rule**: When writing or updating pipeline check documentation (in `commands/*.md`), describe the **bug class**, not a specific incident. Do NOT embed: PR numbers, issue numbers, run IDs, timestamps, function names, dollar amounts, or multi-sentence incident timelines in check prose or `**Evidence**:` blocks. One brief HTML comment `<!-- Added: forge#NNN -->` is acceptable per check for traceability. `**Evidence**:` blocks must describe the vulnerability pattern (what the class of bug looks like, why it's dangerous) — not narrate a single historical occurrence. CHANGELOG entries may reference originating issues; command prompt text must not.
|
|
1464
|
-
- **Endpoint response contract consumer tracing**: When changing an endpoint's response body shape or status field values (e.g., changing `"status": "healthy"` to `"status": "ok"`, renaming response keys, removing fields), grep the full repo for ALL consumers of that response body before committing. Consumers are not limited to the service being changed — they include deploy scripts, CI health checks, monitoring configs, docker-compose healthcheck definitions, Traefik probes, and any script that parses or pattern-matches on the response body. Run: `grep -rn "{old_value}\|{endpoint_path}" scripts/ infra/ .github/ docker-compose*.yml traefik/ 2>/dev/null`. All consumers whose behavior depends on the old response format MUST be updated in the same PR — a response contract change that updates only one consumer while leaving others on the old format is a deploy-time breakage. <!-- Added: forge#321 -->
|
|
1465
|
-
- If contract is wrong (file doesn't exist, function has different signature): STOP, post comment, add `needs-human`, EXIT
|
|
1466
|
-
|
|
1467
|
-
### 3F.5: Env/Config Completeness Check
|
|
1468
|
-
|
|
1469
|
-
Run BEFORE committing. Read-only scan of working changes.
|
|
1470
|
-
|
|
1471
|
-
**Trigger**: Run whenever diff introduces env vars, touches infra/deploy configs, or adds literal IPs.
|
|
1472
|
-
|
|
1473
|
-
**Check 1 — New env var sync**: Scan for `os.getenv`/`process.env.` references. For each, verify present in `.env.example`, `ENV_VARS.md`, `env_validation.py`. Add if missing.
|
|
1474
|
-
|
|
1475
|
-
**Check 2 — Deploy/infra restart risk**: If docker-compose/deploy/infra files changed, scan for restart-inducing changes. Annotate commit with `[restart: <service>]`.
|
|
1476
|
-
|
|
1477
|
-
**Check 3 — Hardcoded IPs and credentials**: Scan for bare IPv4 literals and credential-like assignments. HARD BLOCKER — replace with env vars before staging.
|
|
1478
|
-
|
|
1479
|
-
**Check 4 — SDK/API Literal sync advisory** (trigger: diff contains `Literal[` in a schema file):
|
|
1480
|
-
```bash
|
|
1481
|
-
# Detect Literal type changes in API schema files
|
|
1482
|
-
cd {WORKTREE_PATH}
|
|
1483
|
-
LITERAL_CHANGES=$(git diff HEAD -- | grep -E '^\+.*Literal\[' | grep -v '^\+\+\+')
|
|
1484
|
-
if [ -n "$LITERAL_CHANGES" ]; then
|
|
1485
|
-
echo "SDK SYNC ADVISORY: Literal type changed in schema."
|
|
1486
|
-
echo "Changed Literal lines:"
|
|
1487
|
-
echo "$LITERAL_CHANGES"
|
|
1488
|
-
echo ""
|
|
1489
|
-
echo "ACTION REQUIRED — verify SDK method/type lists match new API schema:"
|
|
1490
|
-
echo " - sdk/python/*/client.py: check _valid_methods or equivalent list"
|
|
1491
|
-
echo " - sdk/node/src/index.ts: check JSDoc @param Literal type annotation"
|
|
1492
|
-
echo " - web/public/openapi*.json: check enum arrays for affected field"
|
|
1493
|
-
echo " - web/public/openapi-versions/*.json: check all versioned specs"
|
|
1494
|
-
echo ""
|
|
1495
|
-
echo "Inconsistency example: API schema narrows Literal['GET','POST','PUT','PATCH','DELETE']"
|
|
1496
|
-
echo "to Literal['GET','POST'] but SDK JSDoc still lists all 5 methods — API returns 422"
|
|
1497
|
-
echo "for callers following SDK docs. This produces silent user-facing failures."
|
|
1498
|
-
fi
|
|
1499
|
-
```
|
|
1500
|
-
|
|
1501
|
-
This advisory is informational — it does NOT block the commit. But the implementer MUST check each listed file and add to the implementation scope if any SDK/spec file still documents the removed/changed Literal values. If SDK files need changes, add them to the current PR rather than leaving the inconsistency for review to catch.
|
|
1502
|
-
|
|
1503
|
-
### 3G: Quality Gate
|
|
1504
|
-
|
|
1505
|
-
Skip for 1-file config/docs edits.
|
|
1506
|
-
|
|
1507
|
-
**Fork the loop unconditionally under Spawn-Decision Table Row (d)** (see Phase 3G Cross-Reference above): quality-gate scans 14+ domains across up to 3 iterations — long enough to idle the parent's already-large accumulated context past the prompt-cache's ~5-minute TTL, forcing an uncached re-hydration on the parent's next turn. This applies regardless of changed-file count; there is no small-build exception. Dispatch with the `Agent` tool, not a nested `Skill()` call — a nested `Skill()` call still executes inside the parent's own context and would not stop the parent from idling past the cache TTL while the loop runs. The sub-agent needs nothing from the parent's context: it works directly against files on disk at `{WORKTREE_PATH}`. <!-- Added: forge#1825 -->
|
|
1508
|
-
|
|
1509
|
-
```
|
|
1510
|
-
Agent(
|
|
1511
|
-
subagent_type="general-purpose",
|
|
1512
|
-
model="{SUBAGENT_MODEL}",
|
|
1513
|
-
description="Quality gate for #{NUMBER}",
|
|
1514
|
-
run_in_background=false,
|
|
1515
|
-
prompt="Run the quality gate loop for issue #{NUMBER} in worktree {WORKTREE_PATH}.
|
|
1516
|
-
NEVER use plan mode (EnterPlanMode).
|
|
1517
|
-
Changed files: {CHANGED_FILES}
|
|
1518
|
-
|
|
1519
|
-
iteration = 0
|
|
1520
|
-
max_iterations = 3
|
|
1521
|
-
while iteration < max_iterations:
|
|
1522
|
-
iteration += 1
|
|
1523
|
-
Skill('quality-gate', args='{CHANGED_FILES} --worktree {WORKTREE_PATH}')
|
|
1524
|
-
if result == 'QUALITY GATE: PASS':
|
|
1525
|
-
GATE_PASSED = true
|
|
1526
|
-
break
|
|
1527
|
-
else:
|
|
1528
|
-
Fix each HIGH and MEDIUM finding directly in the files at {WORKTREE_PATH}
|
|
1529
|
-
Re-stage the fixes (do not commit, do not push)
|
|
1530
|
-
|
|
1531
|
-
Do not touch GitHub issue state — the caller handles labels/comments.
|
|
1532
|
-
Return exactly one line: 'GATE_RESULT: passed={true|false} iterations={N} summary={one-line summary of remaining findings if failed, else \"clean\"}'"
|
|
1533
|
-
)
|
|
1534
|
-
```
|
|
1535
|
-
|
|
1536
|
-
Parse the returned `GATE_RESULT` line:
|
|
1537
|
-
- `passed=true` → `GATE_PASSED = true` → continue to sub-phase 3H.
|
|
1538
|
-
- `passed=false` (loop exhausted 3 iterations) → post "Quality Gate Failed After 3 Iterations" comment using the returned `summary` → add `needs-human` label → STOP.
|
|
1539
|
-
|
|
1540
|
-
**Fallback**: if the `Agent` tool is unavailable in the current runtime (partial install), run the loop inline as before —
|
|
1541
|
-
|
|
1542
|
-
```
|
|
1543
|
-
iteration = 0
|
|
1544
|
-
max_iterations = 3
|
|
1545
|
-
|
|
1546
|
-
while iteration < max_iterations:
|
|
1547
|
-
iteration += 1
|
|
1548
|
-
Skill("quality-gate", args="{CHANGED_FILES} --worktree {WORKTREE_PATH}")
|
|
1549
|
-
if result == "QUALITY GATE: PASS":
|
|
1550
|
-
GATE_PASSED = true
|
|
1551
|
-
break
|
|
1552
|
-
else:
|
|
1553
|
-
Fix each HIGH and MEDIUM finding in {WORKTREE_PATH}
|
|
1554
|
-
Re-stage fixes
|
|
1555
|
-
|
|
1556
|
-
if iteration == max_iterations AND not PASS:
|
|
1557
|
-
Post "Quality Gate Failed After 3 Iterations" comment
|
|
1558
|
-
Add needs-human label → STOP
|
|
1559
|
-
```
|
|
1560
|
-
|
|
1561
|
-
— and note in the builder comment that context isolation was degraded for this run.
|
|
1562
|
-
|
|
1563
|
-
# MUST CONTINUE to sub-phase 3H (Format and verify) — quality gate PASS is intermediate, NOT terminal. <!-- Added: forge#220 -->
|
|
1564
|
-
|
|
1565
|
-
**After the sub-agent returns `passed=true`: proceed immediately to sub-phase 3H below. Quality gate is an intermediate check — "PASS" means the code is clean, NOT that the build is done. Do NOT stop.**
|
|
1566
|
-
|
|
1567
|
-
**After PASS: Do NOT re-read GitHub state, issue body, labels, or any file beyond what the sub-agent already changed on disk. Do NOT run any gh commands. Do NOT check PR status. Proceed directly to Phase 3H (Format and verify) below.** <!-- Added: forge#93 -->
|
|
1568
|
-
|
|
1569
|
-
### 3H: Format and verify
|
|
1570
|
-
|
|
1571
|
-
All tool commands are read from `forge.yaml → verification.commands`. When a key is absent, the step logs `SKIPPED — not configured in verification.commands` and continues rather than silently passing. Before any test command executes (`learned.test_commands`, or a future `verification.commands.*.test` invocation), it is first checked against `verification.known_slow_tests` — a repo-declared list of patterns for suites known to hang or make live network/LLM calls — and skipped or narrowed to a safe subset on a match. When that config is absent, this gate is a no-op and behavior is unchanged. <!-- Added: forge#1861 -->
|
|
1572
|
-
|
|
1573
|
-
**Track skipped checks** — initialize before any check runs:
|
|
1574
|
-
```bash
|
|
1575
|
-
VERIFICATION_SKIPPED_CHECKS=""
|
|
1576
|
-
```
|
|
1577
|
-
|
|
1578
|
-
**Python**:
|
|
1579
|
-
```bash
|
|
1580
|
-
cd {WORKTREE_PATH}
|
|
1581
|
-
|
|
1582
|
-
PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
1583
|
-
if [ -n "$PYTHON_FORMAT" ]; then
|
|
1584
|
-
eval "$PYTHON_FORMAT" 2>&1
|
|
1585
|
-
else
|
|
1586
|
-
echo "SKIPPED — python.format not configured in verification.commands"
|
|
1587
|
-
VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }python.format"
|
|
1588
|
-
fi
|
|
1589
|
-
|
|
1590
|
-
# Compile check always runs (no config needed — catches syntax errors)
|
|
1591
|
-
python -m py_compile {PYTHON_FILES}
|
|
1592
|
-
```
|
|
1593
|
-
`py_compile` failures are BLOCKING.
|
|
1594
|
-
|
|
1595
|
-
**TypeScript**:
|
|
1596
|
-
```bash
|
|
1597
|
-
cd {WORKTREE_PATH}
|
|
1598
|
-
|
|
1599
|
-
TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
|
|
1600
|
-
TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
|
|
1601
|
-
TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
|
|
1602
|
-
|
|
1603
|
-
if [ -n "$TS_FORMAT" ]; then
|
|
1604
|
-
eval "$TS_FORMAT" 2>&1
|
|
1605
|
-
else
|
|
1606
|
-
echo "SKIPPED — typescript.format not configured in verification.commands"
|
|
1607
|
-
VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }typescript.format"
|
|
1608
|
-
fi
|
|
1609
|
-
|
|
1610
|
-
if [ -n "$TS_TYPECHECK" ]; then
|
|
1611
|
-
eval "$TS_TYPECHECK" 2>&1
|
|
1612
|
-
TS_EXIT=$?
|
|
1613
|
-
elif [ -n "$TS_BUILD" ]; then
|
|
1614
|
-
eval "$TS_BUILD" 2>&1 | tail -30
|
|
1615
|
-
TS_EXIT=$?
|
|
1616
|
-
else
|
|
1617
|
-
echo "SKIPPED — typescript.typecheck and typescript.build not configured in verification.commands"
|
|
1618
|
-
VERIFICATION_SKIPPED_CHECKS="${VERIFICATION_SKIPPED_CHECKS:+$VERIFICATION_SKIPPED_CHECKS, }typescript.typecheck/build"
|
|
1619
|
-
TS_EXIT=0
|
|
1620
|
-
fi
|
|
1621
|
-
```
|
|
1622
|
-
Typecheck or build failures are BLOCKING.
|
|
1623
|
-
|
|
1624
|
-
**Known-slow test gate** — Before running any test command below, check it against `verification.known_slow_tests` (a repo-declared list of test patterns known to hang or make live network/LLM calls). When absent or empty, this is a no-op and behavior is unchanged. <!-- Added: forge#1861 -->
|
|
1625
|
-
|
|
1626
|
-
```bash
|
|
1627
|
-
# KNOWN_SLOW_TESTS read directly from verification.known_slow_tests (a static,
|
|
1628
|
-
# operator-declared config — unlike learned.test_commands, it is NOT part of the
|
|
1629
|
-
# agent-writable `learned:` section, so it is read inline here rather than
|
|
1630
|
-
# pre-loaded in Phase 0B.1).
|
|
1631
|
-
KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' forge.yaml 2>/dev/null || echo '[]')
|
|
1632
|
-
|
|
1633
|
-
# apply_known_slow_filter <cmd> — echoes the command to actually run, or "" to
|
|
1634
|
-
# skip it entirely. Matching is substring match of `pattern` against the full
|
|
1635
|
-
# command text. Exactly one of skip/subset is expected per matched entry.
|
|
1636
|
-
apply_known_slow_filter() {
|
|
1637
|
-
local cmd="$1"
|
|
1638
|
-
local out="$cmd"
|
|
1639
|
-
if [ -n "$KNOWN_SLOW_TESTS" ] && [ "$KNOWN_SLOW_TESTS" != "[]" ] && [ "$KNOWN_SLOW_TESTS" != "null" ]; then
|
|
1640
|
-
while IFS= read -r entry; do
|
|
1641
|
-
[ -z "$entry" ] && continue
|
|
1642
|
-
pattern=$(echo "$entry" | yq '.pattern // ""')
|
|
1643
|
-
skip=$(echo "$entry" | yq '.skip // false')
|
|
1644
|
-
subset=$(echo "$entry" | yq '.subset // ""')
|
|
1645
|
-
reason=$(echo "$entry" | yq '.reason // "no reason given"')
|
|
1646
|
-
[ -z "$pattern" ] && continue
|
|
1647
|
-
case "$cmd" in
|
|
1648
|
-
*"$pattern"*)
|
|
1649
|
-
if [ "$skip" = "true" ]; then
|
|
1650
|
-
echo "SKIPPED — known-slow test matched pattern '$pattern' ($reason)" >&2
|
|
1651
|
-
out=""
|
|
1652
|
-
elif [ -n "$subset" ]; then
|
|
1653
|
-
echo "SUBSTITUTED — known-slow test matched pattern '$pattern' ($reason); running safe subset instead" >&2
|
|
1654
|
-
out="$subset"
|
|
1655
|
-
fi
|
|
1656
|
-
;;
|
|
1657
|
-
esac
|
|
1658
|
-
done < <(echo "$KNOWN_SLOW_TESTS" | yq -o=json -I=0 '.[]' 2>/dev/null)
|
|
1659
|
-
fi
|
|
1660
|
-
echo "$out"
|
|
1661
|
-
}
|
|
1662
|
-
```
|
|
1663
|
-
|
|
1664
|
-
**Learned test commands** — After all `verification.commands` steps complete, run any commands from `learned.test_commands` (captured from owner corrections in Phase 1D or set manually in forge.yaml), filtered through the known-slow gate above:
|
|
1665
|
-
|
|
1666
|
-
```bash
|
|
1667
|
-
# LEARNED_TEST_COMMANDS was set in Phase 0B.1 from forge.yaml → learned.test_commands
|
|
1668
|
-
# If empty/null, this block is a no-op
|
|
1669
|
-
if [ -n "$LEARNED_TEST_COMMANDS" ] && [ "$LEARNED_TEST_COMMANDS" != "[]" ]; then
|
|
1670
|
-
echo "Running learned test commands..."
|
|
1671
|
-
# yq outputs each entry on its own line with -r flag
|
|
1672
|
-
echo "$LEARNED_TEST_COMMANDS" | yq '.[]' | while IFS= read -r cmd; do
|
|
1673
|
-
[ -z "$cmd" ] && continue
|
|
1674
|
-
FILTERED_CMD=$(apply_known_slow_filter "$cmd")
|
|
1675
|
-
if [ -z "$FILTERED_CMD" ]; then
|
|
1676
|
-
continue
|
|
1677
|
-
fi
|
|
1678
|
-
echo "Running learned command: $FILTERED_CMD"
|
|
1679
|
-
eval "$FILTERED_CMD" 2>&1 | tail -30
|
|
1680
|
-
CMD_EXIT=$?
|
|
1681
|
-
if [ $CMD_EXIT -ne 0 ]; then
|
|
1682
|
-
echo "FAILED (exit $CMD_EXIT): $FILTERED_CMD"
|
|
1683
|
-
exit $CMD_EXIT
|
|
1684
|
-
fi
|
|
1685
|
-
done
|
|
1686
|
-
else
|
|
1687
|
-
echo "No learned test commands configured — skipping"
|
|
1688
|
-
fi
|
|
1689
|
-
```
|
|
1690
|
-
Learned test command failures are BLOCKING (same as verification.commands failures). A command matched and skipped by the known-slow gate is never executed and never counted as a failure. <!-- Added: forge#667, forge#1861 -->
|
|
1691
|
-
|
|
1692
|
-
### 3I: Frontend proxy wiring check (MANDATORY)
|
|
1693
|
-
|
|
1694
|
-
Skip if no TS/TSX files changed.
|
|
1695
|
-
|
|
1696
|
-
All client-side `fetch`/`useSWR`/`apiFetch`/`axios` MUST use `/api/...` proxy routes, NEVER `/api/v1/...` or hardcoded host:port. Scan and fix violations.
|
|
1697
|
-
|
|
1698
|
-
### 3I.5: Database Configuration Change Advisory
|
|
1699
|
-
|
|
1700
|
-
Skip if no changed Python files contain DB engine/session/pool patterns.
|
|
1701
|
-
|
|
1702
|
-
```bash
|
|
1703
|
-
cd {WORKTREE_PATH}
|
|
1704
|
-
while IFS= read -r f; do
|
|
1705
|
-
[ -z "$f" ] && continue
|
|
1706
|
-
grep -qE "create_async_engine|AsyncSession|connect_args|pool_size|prepared_statement|engine_from_config|sessionmaker" "$f" 2>/dev/null && \
|
|
1707
|
-
echo "DB CONFIG CHANGE DETECTED in: $f"
|
|
1708
|
-
done < <(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '\.py$')
|
|
1709
|
-
```
|
|
1710
|
-
|
|
1711
|
-
Advisory only — does not block build. Check for lambda/callable in connect_args (the exact bug class from PR #14391).
|
|
1712
|
-
|
|
1713
|
-
### 3J: Deployment completeness check (MANDATORY)
|
|
1714
|
-
|
|
1715
|
-
Skip if no new env vars introduced.
|
|
1716
|
-
|
|
1717
|
-
**Config variables used by this phase** (set in `forge.yaml`):
|
|
1718
|
-
- `{deploy.secrets_backend}` — secrets delivery method (`sops`, `aws-sm`, `vault`, `ci-env`, `none`). When absent or not `sops`, SOPS-specific checks below are skipped with an explicit log message.
|
|
1719
|
-
- `{verification.services[name].container}` — container name for post-deploy verification. Resolved by matching the service name; falls back to `{service}` (bare name) when not configured.
|
|
1720
|
-
|
|
1721
|
-
For each new env var, verify present in ALL required locations:
|
|
1722
|
-
|
|
1723
|
-
| Location | Required for |
|
|
1724
|
-
|----------|-------------|
|
|
1725
|
-
| `.env.example` | All new vars |
|
|
1726
|
-
| Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
|
|
1727
|
-
| `app/env_validation.py` | API service vars (if project has one) |
|
|
1728
|
-
| `docker-compose.prod.yml` | Vars needing explicit injection (if project uses Docker Compose) |
|
|
1729
|
-
|
|
1730
|
-
**Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
|
|
1731
|
-
|
|
1732
|
-
If the project uses SOPS, verify the new var is present in all SOPS chain locations:
|
|
1733
|
-
- `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
|
|
1734
|
-
- `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
|
|
1735
|
-
- Deploy chain: SOPS → `decrypt-secrets.sh` (ENV_MAPPING) → `.env.secrets` → `merge-env-secrets.sh` → `.env.production` → docker-compose `env_file`
|
|
1736
|
-
|
|
1737
|
-
If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
|
|
1738
|
-
> `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
|
|
1739
|
-
|
|
1740
|
-
**Operator-set var classification** *(trigger: new env var is NOT in the configured secrets backend)*: <!-- Added: forge#380 -->
|
|
1741
|
-
|
|
1742
|
-
Some env vars are operator-set (non-secret, not sourced from the secrets backend) — they must be manually added to the runtime environment on the production server. When a new env var has no entry in the secrets backend, classify it as operator-set and add a **HARD BLOCKER** item to the Testing Checklist.
|
|
1743
|
-
|
|
1744
|
-
Resolve the container name for the verification command:
|
|
1745
|
-
1. Look up the service in `forge.yaml → verification.services[]` by name — use the `container` field if present.
|
|
1746
|
-
2. If no matching entry, fall back to the bare service name: `{service}` (no suffix).
|
|
1747
|
-
|
|
1748
|
-
```
|
|
1749
|
-
- [ ] HARD BLOCKER: Add {VAR_NAME} to the runtime environment on the production server.
|
|
1750
|
-
This var is operator-set — it does NOT flow through the automated secrets chain.
|
|
1751
|
-
It must be added manually before or after deploy.
|
|
1752
|
-
Verify with: docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
|
|
1753
|
-
(CONTAINER_NAME resolved from verification.services[{service}].container in forge.yaml,
|
|
1754
|
-
or bare service name if not configured)
|
|
1755
|
-
```
|
|
1756
|
-
|
|
1757
|
-
**`env_file` re-read warning** *(trigger: any new env var added to `.env.production` path)*:
|
|
1758
|
-
|
|
1759
|
-
> **Docker `env_file` re-read behavior**: New entries in `.env.production` are only read when a container is **recreated** (e.g., `docker compose up --force-recreate`). A plain `docker restart` restarts the existing container with its frozen env — new `env_file` entries are silently absent. The standard deploy workflow uses `--force-recreate` and handles this correctly. If any out-of-band restart is used, new env vars will not take effect.
|
|
1760
|
-
|
|
1761
|
-
Add this warning to the Testing Checklist whenever a new env var is introduced (whether secret or operator-set).
|
|
1762
|
-
|
|
1763
|
-
**Post-deploy in-container verification** *(trigger: any new env var)*:
|
|
1764
|
-
|
|
1765
|
-
Add the following to the Testing Checklist so the deployer can confirm delivery after deploy.
|
|
1766
|
-
|
|
1767
|
-
Resolve `{CONTAINER_NAME}` from `forge.yaml → verification.services[{service}].container`; use `{service}` (bare) if the field is absent.
|
|
1768
|
-
|
|
1769
|
-
```bash
|
|
1770
|
-
# Verify env var reached the running container (run post-deploy)
|
|
1771
|
-
docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
|
|
1772
|
-
# Expected: {VAR_NAME}={value}
|
|
1773
|
-
# If blank: container was not recreated — run: docker compose up --no-deps --force-recreate {service}
|
|
1774
|
-
```
|
|
1775
|
-
|
|
1776
|
-
### 3K: Commit
|
|
1777
|
-
|
|
1778
|
-
Stage all changes and commit:
|
|
1779
|
-
|
|
1780
|
-
```bash
|
|
1781
|
-
cd {WORKTREE_PATH}
|
|
1782
|
-
git add -u
|
|
1783
|
-
git commit -s -m "fix({SCOPE}): {description} (#{NUMBER})"
|
|
1784
|
-
```
|
|
1785
|
-
|
|
1786
|
-
Conventional prefix: `fix`/`feat`/`refactor`/`docs`. Reference `#{NUMBER}` in message.
|
|
1787
|
-
|
|
1788
|
-
**Post-commit ancestry audit (MANDATORY)**:
|
|
1789
|
-
```bash
|
|
1790
|
-
cd {WORKTREE_PATH}
|
|
1791
|
-
if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
|
|
1792
|
-
MERGE_COMMITS=$(git log --merges HEAD ^origin/{PR_BASE} 2>/dev/null)
|
|
1793
|
-
if [ -n "$MERGE_COMMITS" ]; then
|
|
1794
|
-
# Post ancestry audit failure comment, add needs-human → STOP
|
|
1795
|
-
fi
|
|
1796
|
-
fi
|
|
1797
|
-
```
|
|
1798
|
-
|
|
1799
|
-
### 3L: Update issue body (MANDATORY)
|
|
1800
|
-
Check off completed items, mark phases complete, add PR references.
|
|
1801
|
-
|
|
1802
|
-
### 3M: Post implementation comment
|
|
1803
|
-
```bash
|
|
1804
|
-
# Compute verification status from VERIFICATION_SKIPPED_CHECKS (set in Phase 3H)
|
|
1805
|
-
if [ -z "$VERIFICATION_SKIPPED_CHECKS" ]; then
|
|
1806
|
-
VERIFICATION_STATUS="✅ All configured verification commands passed"
|
|
1807
|
-
else
|
|
1808
|
-
VERIFICATION_STATUS="⚠ Verification NOT run: ${VERIFICATION_SKIPPED_CHECKS} — verification.commands not configured for these checks"
|
|
1809
|
-
fi
|
|
1810
|
-
|
|
1811
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
|
|
1812
|
-
## Implementation Complete
|
|
1813
|
-
|
|
1814
|
-
**Branch**: \`{BRANCH}\`
|
|
1815
|
-
**Commits**: {COMMIT_SHA(S)}
|
|
1816
|
-
**Files changed**: {COUNT}
|
|
1817
|
-
**Verification Status**: ${VERIFICATION_STATUS}
|
|
1818
|
-
**Cost (build phase)**: ${PHASE_COST_USD:-unavailable} (best-effort — session telemetry; omit if unavailable)
|
|
1819
|
-
|
|
1820
|
-
### Approach
|
|
1821
|
-
{what was built, key decisions}
|
|
1822
|
-
|
|
1823
|
-
### Changes
|
|
1824
|
-
{bulleted list of file changes}
|
|
1825
|
-
|
|
1826
|
-
### Acceptance Criteria Status
|
|
1827
|
-
{checklist from contract, marked pass/fail}
|
|
1828
|
-
|
|
1829
|
-
### Testing Checklist
|
|
1830
|
-
- [ ] {scenario 1} [type:api]
|
|
1831
|
-
- [ ] {scenario 2} [type:unit]
|
|
1832
|
-
|
|
1833
|
-
> **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each checklist item. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback.
|
|
1834
|
-
|
|
1835
|
-
<!-- FORGE:BUILDER:COMPLETE -->"
|
|
1836
|
-
```
|
|
1837
|
-
|
|
1838
|
-
Write machine-readable phase checkpoint (MUST execute immediately after FORGE:BUILDER comment is posted, before Phase 4):
|
|
1839
|
-
```bash
|
|
1840
|
-
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1841
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
1842
|
-
\`\`\`json
|
|
1843
|
-
{\"phase\": \"BUILD\", \"status\": \"COMPLETE\", \"next_phase\": \"REVIEW\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
|
|
1844
|
-
\`\`\`"
|
|
1845
|
-
```
|
|
1846
|
-
|
|
1847
|
-
**Marker gate — Phase 3 exit** (see Marker Gate table in Universal Phase Dispatcher): <!-- forge#1419, forge#1418 -->
|
|
1848
|
-
```bash
|
|
1849
|
-
BUILD_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1850
|
-
--jq '[.[] | select(.body | contains("FORGE:BUILDER:COMPLETE"))] | length')
|
|
1851
|
-
if [ "${BUILD_MARKER:-0}" -eq 0 ]; then
|
|
1852
|
-
echo "MARKER GATE FAIL: FORGE:BUILDER:COMPLETE absent — re-invoking work-on/build once"
|
|
1853
|
-
Skill(skill="work-on/build", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}")
|
|
1854
|
-
BUILD_MARKER=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1855
|
-
--jq '[.[] | select(.body | contains("FORGE:BUILDER:COMPLETE"))] | length')
|
|
1856
|
-
if [ "${BUILD_MARKER:-0}" -eq 0 ]; then
|
|
1857
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:GATE_FAILURE -->
|
|
1858
|
-
## Marker Gate Failure — Phase 3 (Build)
|
|
1859
|
-
|
|
1860
|
-
**Expected marker**: \`FORGE:BUILDER:COMPLETE\` inside a \`FORGE:BUILDER\` comment
|
|
1861
|
-
**Status**: Absent after subcommand re-invocation. Human review required.
|
|
1862
|
-
|
|
1863
|
-
The router re-invoked \`work-on/build\` once but the marker was still not posted.
|
|
1864
|
-
Inspect the subcommand output above for errors. <!-- forge#1418 -->"
|
|
1865
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" \
|
|
1866
|
-
--remove-label "workflow:building" 2>/dev/null || true
|
|
1867
|
-
exit 1
|
|
1868
|
-
fi
|
|
1869
|
-
fi
|
|
1870
|
-
```
|
|
652
|
+
Handled in Phase 0A.1: `/work-on <pr> --remediate [--issue N]` dispatches `Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", ...)` (forked) and STOPS after its `REMEDIATE_RESULT`.
|
|
1871
653
|
|
|
1872
654
|
---
|
|
1873
655
|
|
|
1874
|
-
## Phase 4: PR Creation
|
|
1875
|
-
|
|
1876
|
-
### 4A: Pre-push ancestry guard
|
|
1877
|
-
|
|
1878
|
-
```bash
|
|
1879
|
-
cd {WORKTREE_PATH}
|
|
1880
|
-
if git ls-remote --exit-code origin {PR_BASE} >/dev/null 2>&1; then
|
|
1881
|
-
MERGE_COMMITS=$(git log --merges {BRANCH} ^origin/{PR_BASE} 2>/dev/null)
|
|
1882
|
-
if [ -n "$MERGE_COMMITS" ]; then
|
|
1883
|
-
# Post ancestry guard failure, add needs-human → STOP
|
|
1884
|
-
fi
|
|
1885
|
-
fi
|
|
1886
|
-
```
|
|
1887
|
-
|
|
1888
|
-
### 4B: Push branch
|
|
1889
|
-
```bash
|
|
1890
|
-
cd {WORKTREE_PATH} && git push -u origin {BRANCH}
|
|
1891
|
-
```
|
|
1892
|
-
If fails: try `--force-with-lease`. If still fails: post comment, add `needs-human`, STOP.
|
|
1893
|
-
|
|
1894
|
-
### 4C: Determine PR target
|
|
1895
|
-
`PR_BASE` was computed in Phase 3E. If somehow unset (e.g., resumed session after compaction), recompute:
|
|
1896
|
-
```bash
|
|
1897
|
-
RESOLUTION=$(resolve_script 'classify-lane')
|
|
1898
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1899
|
-
case "$TIER" in
|
|
1900
|
-
adaptive|universal)
|
|
1901
|
-
if ! PR_BASE=$(bash "$SCRIPT_PATH" {NUMBER} -R {GH_REPO}); then
|
|
1902
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh failed to recompute PR target — see script error above. Adding needs-human."
|
|
1903
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
1904
|
-
exit 1
|
|
1905
|
-
fi
|
|
1906
|
-
;;
|
|
1907
|
-
prose)
|
|
1908
|
-
# No valid prose fallback — see Phase 3E note.
|
|
1909
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKER: classify-lane.sh not installed (prose tier). Cannot recompute PR target. Adding needs-human."
|
|
1910
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
1911
|
-
exit 1
|
|
1912
|
-
;;
|
|
1913
|
-
esac
|
|
1914
|
-
```
|
|
1915
|
-
Output is authoritative — no prose fallback. Script exits 1 on error (invalid issue, `gh` auth failure, or milestone branch absent on remote); treat non-zero exit as `needs-human` and STOP. <!-- Added: forge#669, forge#639 -->
|
|
1916
|
-
|
|
1917
|
-
### 4C.5: Validate PR target against classified lane
|
|
1918
|
-
```bash
|
|
1919
|
-
RESOLUTION=$(resolve_script 'validate-pr-target')
|
|
1920
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1921
|
-
case "$TIER" in
|
|
1922
|
-
adaptive|universal)
|
|
1923
|
-
bash "$SCRIPT_PATH" {PR_BASE} {CLASSIFIED_LANE}
|
|
1924
|
-
;;
|
|
1925
|
-
prose)
|
|
1926
|
-
# validate-pr-target has no safe prose fallback — silently skipping validation risks
|
|
1927
|
-
# merging to the wrong branch. Log a warning but do NOT block the pipeline; the PR
|
|
1928
|
-
# review step will catch a mismatched target before merge.
|
|
1929
|
-
echo "WARNING: validate-pr-target.sh not installed (prose tier) — skipping lane validation. Confirm PR base manually." >&2
|
|
1930
|
-
;;
|
|
1931
|
-
esac
|
|
1932
|
-
```
|
|
1933
|
-
`{CLASSIFIED_LANE}` is the value returned by `classify-lane.sh` in Phase 4C. `{PR_BASE}` is the branch the PR will target. If exit code is 1 (mismatch):
|
|
1934
|
-
```bash
|
|
1935
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "BLOCKING: validate-pr-target.sh — PR base \`{PR_BASE}\` does not match classified lane \`{CLASSIFIED_LANE}\`. Manual intervention required."
|
|
1936
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
1937
|
-
```
|
|
1938
|
-
→ STOP. Do NOT proceed to Phase 4D. <!-- Added: forge#671 -->
|
|
1939
|
-
|
|
1940
|
-
### 4D: Create PR
|
|
1941
|
-
```bash
|
|
1942
|
-
PR_URL=$(gh pr create {GH_FLAG} --base {PR_BASE} --head {BRANCH} \
|
|
1943
|
-
--title "{Fix|Feat|Refactor}: {description}" \
|
|
1944
|
-
--body "## Summary
|
|
1945
|
-
{BRIEF_DESCRIPTION}
|
|
1946
|
-
|
|
1947
|
-
## Changes
|
|
1948
|
-
{CHANGES_LIST}
|
|
1949
|
-
|
|
1950
|
-
## Testing
|
|
1951
|
-
{TESTING_CHECKLIST}
|
|
1952
|
-
|
|
1953
|
-
---
|
|
1954
|
-
Closes #{NUMBER}
|
|
1955
|
-
**Implementation branch**: \`{BRANCH}\`
|
|
1956
|
-
**Base**: \`{PR_BASE}\`")
|
|
1957
|
-
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
|
|
1958
|
-
```
|
|
1959
|
-
|
|
1960
|
-
`Closes #{NUMBER}` documents intent but does NOT auto-close for non-default-branch PRs. Capture `PR_NUMBER` here — Phase 5A reuses it instead of re-querying `gh pr list`.
|
|
1961
|
-
|
|
1962
|
-
If PR already exists for this branch, use the existing PR number.
|
|
1963
|
-
|
|
1964
|
-
### 4E: Update labels
|
|
1965
|
-
```bash
|
|
1966
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
1967
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1968
|
-
case "$TIER" in
|
|
1969
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
1970
|
-
prose)
|
|
1971
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" \
|
|
1972
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
1973
|
-
;;
|
|
1974
|
-
esac
|
|
1975
|
-
```
|
|
1976
|
-
|
|
1977
|
-
---
|
|
1978
|
-
|
|
1979
|
-
## Phase 5: Auto-Review
|
|
1980
|
-
|
|
1981
|
-
### 5A: Confirm state (reuse in-context; re-read only on compaction)
|
|
1982
|
-
|
|
1983
|
-
**Post Phase 5 heartbeat** (skip unless `UNDER_ORCHESTRATION` is `true`; also skip if issue already has a terminal label — `workflow:merged`, `workflow:invalid`, `needs-human`, `workflow:awaiting-merge`):
|
|
1984
|
-
```bash
|
|
1985
|
-
PHASE_START_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
1986
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
|
|
1987
|
-
**Phase**: Phase 5 — Review
|
|
1988
|
-
**Timestamp**: ${PHASE_START_TIMESTAMP}
|
|
1989
|
-
**Issue**: #{NUMBER}"
|
|
1990
|
-
```
|
|
1991
|
-
|
|
1992
|
-
`PR_NUMBER` was already captured from `gh pr create`'s output in Phase 4D — reuse it. Only run the lookup below if `PR_NUMBER` is genuinely unset (resumed session after compaction, or checkpoint routing jumped straight to Phase 4/5):
|
|
1993
|
-
```bash
|
|
1994
|
-
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
|
|
1995
|
-
PR_NUMBER=$(gh pr list {GH_FLAG} --head {BRANCH} --json number --jq '.[0].number')
|
|
1996
|
-
```
|
|
1997
|
-
|
|
1998
|
-
### 5B: Post progress comment
|
|
1999
|
-
```bash
|
|
2000
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "## Submitting for Review
|
|
2001
|
-
|
|
2002
|
-
PR #${PR_NUMBER} created targeting \`{PR_BASE}\`. Invoking /review-pr with --auto-merge.
|
|
2003
|
-
|
|
2004
|
-
<!-- FORGE:REVIEW_STARTED -->"
|
|
2005
|
-
```
|
|
2006
|
-
|
|
2007
|
-
### 5C: Invoke /review-pr with --auto-merge
|
|
2008
|
-
|
|
2009
|
-
**Always fork — Row (d) supersedes Row (c) as the controlling reason** (run before invoking review-pr): <!-- Updated: forge#1825 (previously threshold-gated by Row (c) alone — forge#93) -->
|
|
2010
|
-
|
|
2011
|
-
`/review-pr` spawns domain review agent(s) (observed at `effort: xhigh`) that run for minutes — long enough to idle the parent's accumulated context past the prompt cache's ~5-minute TTL under **Spawn-Decision Policy Row (d)**, regardless of build size — see the [Spawn-Decision Table](#spawn-decision-table). There is no small-build exception: review ALWAYS forks.
|
|
2012
|
-
|
|
2013
|
-
- ALWAYS invoke `work-on/review` as a fresh sub-agent (via `Skill(skill="work-on/review", args="...")`) rather than calling review-pr directly. The sub-agent starts with a clean context window and re-reads all needed state from GitHub (Phase R0) — it does not depend on anything the parent accumulated.
|
|
2014
|
-
- **Fallback only**: if `work-on/review` is not available (partial install), invoke review-pr directly and add a note in the progress comment that context isolation was degraded for this run.
|
|
2015
|
-
|
|
2016
|
-
**Sub-agent invocation** (always, regardless of changed-file count or Skill-invocation count):
|
|
2017
|
-
|
|
2018
|
-
Before spawning, build a distilled hot-copy of the key annotations the review sub-agent would otherwise re-fetch from GitHub. This reduces gh round-trips in the child without replacing the durable FORGE annotation record. <!-- Added: forge#1277 -->
|
|
2019
|
-
|
|
2020
|
-
```bash
|
|
2021
|
-
# Hot-copy: extract CONTRACT and ARCHITECT annotation bodies for inline injection
|
|
2022
|
-
HOT_CONTRACT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2023
|
-
--jq '[.[] | select(.body | contains("<!-- FORGE:CONTRACT -->"))] | last | .body // ""' 2>/dev/null \
|
|
2024
|
-
| head -60) # Scope: first 60 lines — captures Proposed Approach + Deliverables table
|
|
2025
|
-
|
|
2026
|
-
HOT_ARCHITECT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2027
|
-
--jq '[.[] | select(.body | contains("<!-- FORGE:ARCHITECT -->"))] | last | .body // ""' 2>/dev/null \
|
|
2028
|
-
| head -40) # Scope: first 40 lines — captures Affected Paths + Implementation Order
|
|
2029
|
-
|
|
2030
|
-
# Build inline context block (omit sections where annotation was not found)
|
|
2031
|
-
HOT_COPY_BLOCK=""
|
|
2032
|
-
if [ -n "$HOT_CONTRACT" ]; then
|
|
2033
|
-
HOT_COPY_BLOCK="${HOT_COPY_BLOCK}
|
|
2034
|
-
**HOT COPY — FORGE:CONTRACT** (do not re-fetch; durable record is on the issue):
|
|
2035
|
-
${HOT_CONTRACT}"
|
|
2036
|
-
fi
|
|
2037
|
-
if [ -n "$HOT_ARCHITECT" ]; then
|
|
2038
|
-
HOT_COPY_BLOCK="${HOT_COPY_BLOCK}
|
|
2039
|
-
|
|
2040
|
-
**HOT COPY — FORGE:ARCHITECT** (do not re-fetch; durable record is on the issue):
|
|
2041
|
-
${HOT_ARCHITECT}"
|
|
2042
|
-
fi
|
|
2043
|
-
```
|
|
2044
|
-
|
|
2045
|
-
```
|
|
2046
|
-
Skill(skill="work-on/review", args="{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE}", context="{HOT_COPY_BLOCK}")
|
|
2047
|
-
```
|
|
2048
|
-
|
|
2049
|
-
The `{HOT_COPY_BLOCK}` is an optimization that avoids the child re-discovering context already held by the parent. The FORGE annotations on GitHub remain the durable, compaction-safe record. If the hot-copy block is empty (annotations not yet posted), the sub-agent falls back to reading them from GitHub as before.
|
|
2050
|
-
|
|
2051
|
-
**Fallback invocation** (only when `work-on/review` is unavailable):
|
|
2052
|
-
```
|
|
2053
|
-
Skill(skill="review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
|
|
2054
|
-
```
|
|
2055
|
-
|
|
2056
|
-
Review-pr handles: full domain-agent review → post findings as separate issues → merge PR. It does NOT close the issue or clean up the worktree — those run in Phase 6.
|
|
2057
|
-
|
|
2058
|
-
### 5D: Verify merge and close (recovery)
|
|
2059
|
-
|
|
2060
|
-
```bash
|
|
2061
|
-
gh pr view {PR_NUMBER} {GH_FLAG} --json state,mergedAt --jq '{state: .state, mergedAt: .mergedAt}'
|
|
2062
|
-
gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
2063
|
-
```
|
|
2064
|
-
|
|
2065
|
-
- PR MERGED + issue CLOSED → write checkpoint, then proceed to Phase 6
|
|
2066
|
-
- PR MERGED + issue OPEN → close issue manually, write checkpoint, proceed to Phase 6
|
|
2067
|
-
- PR NOT MERGED → `gh pr merge {PR_NUMBER} {GH_FLAG} --merge --auto`. If fails → post comment, add `needs-human`, STOP.
|
|
2068
|
-
|
|
2069
|
-
**When PR is MERGED — write machine-readable phase checkpoint (MANDATORY)**:
|
|
2070
|
-
```bash
|
|
2071
|
-
CHECKPOINT_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2072
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
2073
|
-
\`\`\`json
|
|
2074
|
-
{\"phase\": \"REVIEW\", \"status\": \"COMPLETE\", \"next_phase\": \"CLOSE\", \"timestamp\": \"${CHECKPOINT_TIMESTAMP}\"}
|
|
2075
|
-
\`\`\`"
|
|
2076
|
-
```
|
|
2077
|
-
|
|
2078
|
-
<!-- FORGE:PHASE_COMPLETE — Review done, PR merged. See Universal Phase Dispatcher: next phase is Phase 6 (Close & Cleanup). Not terminal — continue immediately. -->
|
|
2079
|
-
|
|
2080
|
-
**After /review-pr returns and the PR is confirmed merged: immediately proceed to Phase 6 (Close & Cleanup). Do NOT stop here. `REVIEW_RESULT: status: COMPLETE` is an intermediate result — the pipeline is NOT done. Invoke Phase 6 now to close the issue, update labels, post the trajectory log, and clean up the worktree.**
|
|
2081
|
-
|
|
2082
|
-
**Do NOT output any text describing this transition. Do NOT write phrases like "returning to work-on", "proceeding to close", "now invoking Phase 6", or any narrative summary of what comes next. Do NOT emit end_turn. Execute Phase 6 code immediately.** <!-- Added: forge#93 -->
|
|
2083
|
-
|
|
2084
|
-
---
|
|
2085
|
-
|
|
2086
|
-
## Phase 6: Close & Cleanup
|
|
2087
|
-
|
|
2088
|
-
### 6A: Final issue body update
|
|
2089
|
-
|
|
2090
|
-
**Multi-phase guard**: Detect whether the issue has multiple phases. Only check off items belonging to the current completed phase.
|
|
2091
|
-
|
|
2092
|
-
**This read is intentionally NOT covered by the session-state-cache rule** — the body is about to be rewritten below, and Phase 5's `/review-pr` invocation is an external process that can post comments/edits between the last read and here. Writing back a stale cached body would silently revert any concurrent change, so always fetch fresh immediately before a body mutation.
|
|
2093
|
-
|
|
2094
|
-
```bash
|
|
2095
|
-
BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
2096
|
-
REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
|
|
2097
|
-
|
|
2098
|
-
# Structural test: count heading-delimited sections that contain checkbox items.
|
|
2099
|
-
# Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
|
|
2100
|
-
# single-phase regardless of how many prose headings surround it.
|
|
2101
|
-
#
|
|
2102
|
-
# - Fenced code blocks are stripped first: issue bodies routinely embed fenced
|
|
2103
|
-
# blocks whose lines start with '#' or contain a literal '- [ ]'. An
|
|
2104
|
-
# unterminated fence keeps the original body so later work is never hidden.
|
|
2105
|
-
# - ATX headings and setext underlines both delimit sections. The ATX pattern
|
|
2106
|
-
# avoids awk interval-quantifier variance across awk implementations.
|
|
2107
|
-
# - grep -E / awk only — no PCRE. '^#+ ' needs none.
|
|
2108
|
-
FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
|
|
2109
|
-
if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
|
|
2110
|
-
BODY_STRIPPED="$BODY"
|
|
2111
|
-
else
|
|
2112
|
-
BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
|
|
2113
|
-
fi
|
|
2114
|
-
|
|
2115
|
-
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
2116
|
-
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2117
|
-
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2118
|
-
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
|
|
2119
|
-
END { if (in_section && has) n++; print n+0 }
|
|
2120
|
-
')
|
|
2121
|
-
|
|
2122
|
-
# Sub-issue-tracker guard: a decompose parent whose only checkbox group is
|
|
2123
|
-
# '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
|
|
2124
|
-
# sub-issues done and close the tracker, so any unchecked GFM task item for an
|
|
2125
|
-
# issue forces multi-phase.
|
|
2126
|
-
SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
|
|
2127
|
-
```
|
|
2128
|
-
|
|
2129
|
-
**Sync invariant:** Keep the structural computation above and its consuming multi-phase guard in `commands/work-on/close.md` Phase C1 synchronized. The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`; this Phase 6A path is reached only when `REMAINING_BEFORE > 0`. <!-- Fixed: forge#2840, #2874 -->
|
|
2130
|
-
|
|
2131
|
-
If multi-phase (`CHECKBOX_SECTIONS >= 2` OR a `- [ ] #NNN` sub-issue item is present): do NOT check off future phase items. Add PR reference only. A heading count is **not** the test — every templated issue carries `## Problem`/`## Evidence`/`## Context`, so it is `> 0` universally.
|
|
2132
|
-
|
|
2133
|
-
If single-phase or final phase: check off all `[ ]` items, add PR reference.
|
|
2134
|
-
|
|
2135
|
-
### 6B: Project board update (Status=Done, Workflow=Merged)
|
|
2136
|
-
|
|
2137
|
-
Resolve `PROJECT_BOARD_OWNER` and `PROJECT_BOARD_NUMBER` from `forge.yaml → project_board` (fields: `owner`, `project_number`). Fall back to `forge.yaml → project.owner` and project number `1` if `project_board` section is absent.
|
|
2138
|
-
|
|
2139
|
-
```bash
|
|
2140
|
-
ISSUE_URL="https://github.com/{GH_REPO}/issues/{NUMBER}"
|
|
2141
|
-
ITEM_ID=$(gh project item-list {PROJECT_BOARD_NUMBER} --owner {PROJECT_BOARD_OWNER} --format json --limit 200 \
|
|
2142
|
-
--jq ".items[] | select(.content.url == \"$ISSUE_URL\") | .id" 2>/dev/null | head -1)
|
|
2143
|
-
```
|
|
2144
|
-
|
|
2145
|
-
If found: set Status=Done, Workflow=Merged using project field IDs from `forge.yaml → project_board.field_ids`.
|
|
2146
|
-
|
|
2147
|
-
### 6C: Ensure issue is closed
|
|
2148
|
-
|
|
2149
|
-
**Multi-phase guard**: If `REMAINING_AFTER > 0`, uncompleted phases remain. Post phase-complete comment, leave issue open, EXIT — router picks up next phase on next iteration.
|
|
2150
|
-
|
|
2151
|
-
If all phases complete:
|
|
2152
|
-
```bash
|
|
2153
|
-
gh issue close {NUMBER} {GH_FLAG} \
|
|
2154
|
-
--comment "Closed: PR #{PR_NUMBER} merged to \`{PR_BASE}\`. Closes #{NUMBER}."
|
|
2155
|
-
RESOLUTION=$(resolve_script 'transition-label')
|
|
2156
|
-
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
2157
|
-
case "$TIER" in
|
|
2158
|
-
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} merged ;;
|
|
2159
|
-
prose)
|
|
2160
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:merged" \
|
|
2161
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
2162
|
-
;;
|
|
2163
|
-
esac
|
|
2164
|
-
```
|
|
2165
|
-
|
|
2166
|
-
### 6D: Parent tracker update (sub-issues only)
|
|
2167
|
-
|
|
2168
|
-
**Skip if**: Not a sub-issue (no parent reference in body).
|
|
2169
|
-
|
|
2170
|
-
Markdown emphasis markers (`**bold**`, `__bold__`, `*italic*`) are stripped before matching, since sub-issue bodies commonly render the label as `**Parent**: #NNN` and the bare label alternation below would otherwise fail to match past the emphasis characters:
|
|
2171
|
-
```bash
|
|
2172
|
-
PARENT_REF=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
|
|
2173
|
-
| sed -E 's/[*_]+//g' \
|
|
2174
|
-
| grep -iE '(part of|spawned from|sub-issue of|parent issue:?|parent:)\s*#[0-9]+' \
|
|
2175
|
-
| sed -n 's/.*#\([0-9][0-9]*\).*/\1/p' | head -1)
|
|
2176
|
-
```
|
|
2177
|
-
|
|
2178
|
-
If parent found: check off this sub-issue in parent body. If ALL sub-issues checked off → close parent with `workflow:merged`.
|
|
2179
|
-
|
|
2180
|
-
### 6E: Worktree & branch cleanup
|
|
2181
|
-
|
|
2182
|
-
```bash
|
|
2183
|
-
if [ -n "{WORKTREE_PATH}" ] && [ -d "{WORKTREE_PATH}" ]; then
|
|
2184
|
-
GIT_COMMON=$(git -C {WORKTREE_PATH} rev-parse --git-common-dir 2>/dev/null)
|
|
2185
|
-
REPO_ROOT=$(dirname "$(realpath "$GIT_COMMON" 2>/dev/null || echo "$GIT_COMMON")")
|
|
2186
|
-
git -C "$REPO_ROOT" worktree remove {WORKTREE_PATH} --force 2>/dev/null || true
|
|
2187
|
-
if [ -n "{BRANCH}" ]; then
|
|
2188
|
-
git -C "$REPO_ROOT" branch -D {BRANCH} 2>/dev/null || true
|
|
2189
|
-
fi
|
|
2190
|
-
fi
|
|
2191
|
-
```
|
|
2192
|
-
|
|
2193
|
-
---
|
|
2194
|
-
|
|
2195
|
-
## Phase 7: Summary & Trajectory
|
|
2196
|
-
|
|
2197
|
-
### 7A: Report + Pipeline Summary Card
|
|
2198
|
-
|
|
2199
|
-
Output the terse report, then render the shareable **Pipeline Summary Card** — the shareable
|
|
2200
|
-
moment a developer screenshots. Gather real stats (commits, additions/deletions, PR target,
|
|
2201
|
-
review summary, elapsed time) and render exactly as specified in `work-on/close.md` Phase C4.5
|
|
2202
|
-
(`C4.5a` stats gathering → `C4.5b` box-drawing card to stdout → `C4.5c` machine-readable twin).
|
|
2203
|
-
This inline path and the delegated `close.md` path MUST produce an identical card.
|
|
2204
|
-
|
|
2205
|
-
```
|
|
2206
|
-
## Done: #{NUMBER} — {TITLE}
|
|
2207
|
-
- Investigation: {VERDICT} ({CONFIDENCE})
|
|
2208
|
-
- Lane: {FAST/FEATURE}
|
|
2209
|
-
- Fix: {BRANCH} → PR #{PR_NUMBER} → merged to `{PR_BASE}`
|
|
2210
|
-
- Files changed: {COUNT}
|
|
2211
|
-
```
|
|
2212
|
-
|
|
2213
|
-
Then print the card to stdout (inner width 51; truncate long titles with `…`; missing stats
|
|
2214
|
-
render `—`; pipeline line reflects the actual terminal state — merged / decomposed / invalid /
|
|
2215
|
-
blocked; draft PRs append `(draft)`):
|
|
2216
|
-
|
|
2217
|
-
```
|
|
2218
|
-
╔═══════════════════════════════════════════════════╗
|
|
2219
|
-
║ ForgeDock Pipeline Complete ║
|
|
2220
|
-
╠═══════════════════════════════════════════════════╣
|
|
2221
|
-
║ ║
|
|
2222
|
-
║ Issue: #{NUMBER} — {TITLE} ║
|
|
2223
|
-
║ Pipeline: investigate → architect → build → ║
|
|
2224
|
-
║ review → merge ✓ ║
|
|
2225
|
-
║ Commits: {COMMITS} ({ADDITIONS} additions, {DELETIONS} deletions) ║
|
|
2226
|
-
║ PR: #{PR_NUMBER} (merged to {PR_BASE}) ║
|
|
2227
|
-
║ Review: {REVIEW_SUMMARY} ║
|
|
2228
|
-
║ Time: {ELAPSED} ║
|
|
2229
|
-
║ ║
|
|
2230
|
-
╚═══════════════════════════════════════════════════╝
|
|
2231
|
-
```
|
|
2232
|
-
|
|
2233
|
-
**Gather real stats** (C4.5a — this block MUST run on the inline path to populate card variables;
|
|
2234
|
-
do NOT rely on the cross-reference to `close.md` alone):
|
|
2235
|
-
|
|
2236
|
-
```bash
|
|
2237
|
-
PR_STATS=$(gh pr view {PR_NUMBER} {GH_FLAG} --json commits,additions,deletions,baseRefName,isDraft 2>/dev/null)
|
|
2238
|
-
COMMITS=$(echo "$PR_STATS" | jq -r '(.commits | length) // empty' 2>/dev/null); COMMITS=${COMMITS:-—}
|
|
2239
|
-
ADDITIONS=$(echo "$PR_STATS" | jq -r '.additions // empty' 2>/dev/null); ADDITIONS=${ADDITIONS:-—}
|
|
2240
|
-
DELETIONS=$(echo "$PR_STATS" | jq -r '.deletions // empty' 2>/dev/null); DELETIONS=${DELETIONS:-—}
|
|
2241
|
-
PR_TARGET=$(echo "$PR_STATS" | jq -r '.baseRefName // empty' 2>/dev/null); PR_TARGET=${PR_TARGET:-{PR_BASE}}
|
|
2242
|
-
IS_DRAFT=$(echo "$PR_STATS" | jq -r '.isDraft // false' 2>/dev/null)
|
|
2243
|
-
|
|
2244
|
-
REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
2245
|
-
--jq '[.reviews[].body // ""] + [.comments[].body // ""] | .[]' 2>/dev/null)
|
|
2246
|
-
# NOTE: `grep -c` already prints `0` on no match (and exits non-zero) — do NOT add
|
|
2247
|
-
# `|| echo 0`, which would append a second line ("0\n0") and break the arithmetic
|
|
2248
|
-
# and `--argjson` below. Swallow the non-zero exit with `|| true`, then default.
|
|
2249
|
-
APPROVED=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED=${APPROVED:-0}
|
|
2250
|
-
CHANGES=$(echo "$REVIEW_BODIES" | grep -cE 'CHANGES REQUESTED:' 2>/dev/null || true); CHANGES=${CHANGES:-0}
|
|
2251
|
-
TOTAL_AGENTS=$((APPROVED + CHANGES))
|
|
2252
|
-
BLOCKERS=$(echo "$REVIEW_BODIES" | grep -ciE 'blocker|merge.?block' 2>/dev/null || true); BLOCKERS=${BLOCKERS:-0}
|
|
2253
|
-
if [ "$TOTAL_AGENTS" -gt 0 ]; then
|
|
2254
|
-
REVIEW_SUMMARY="${APPROVED}/${TOTAL_AGENTS} agents passed, ${BLOCKERS} blockers"
|
|
2255
|
-
else
|
|
2256
|
-
REVIEW_SUMMARY="—" # review data unavailable (e.g. review skipped)
|
|
2257
|
-
fi
|
|
2258
|
-
|
|
2259
|
-
FIRST_TS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2260
|
-
--jq '[.[] | select(.body | contains("FORGE:")) | .created_at] | sort | .[0] // empty' 2>/dev/null)
|
|
2261
|
-
if [ -n "$FIRST_TS" ]; then
|
|
2262
|
-
START_EPOCH=$(date -u -d "$FIRST_TS" +%s 2>/dev/null \
|
|
2263
|
-
|| python3 -c "import sys,datetime; ts=sys.argv[1].rstrip('Z'); print(int(datetime.datetime.fromisoformat(ts+'+00:00').timestamp()))" "$FIRST_TS" 2>/dev/null \
|
|
2264
|
-
|| echo "")
|
|
2265
|
-
NOW_EPOCH=$(date -u +%s)
|
|
2266
|
-
if [ -n "$START_EPOCH" ]; then
|
|
2267
|
-
ELAPSED_SECS=$((NOW_EPOCH - START_EPOCH))
|
|
2268
|
-
ELAPSED=$(printf '%dm %02ds' $((ELAPSED_SECS / 60)) $((ELAPSED_SECS % 60)))
|
|
2269
|
-
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
2270
|
-
else ELAPSED="—"; ELAPSED_SECS=0; fi
|
|
2271
|
-
|
|
2272
|
-
case "{TERMINAL_STATE}" in
|
|
2273
|
-
decomposed) PIPELINE_LINE="investigate → decompose ⏹"; CARD_STATUS="decomposed" ;;
|
|
2274
|
-
invalid) PIPELINE_LINE="investigate → invalid ✗"; CARD_STATUS="invalid" ;;
|
|
2275
|
-
blocked) PIPELINE_LINE="investigate → build → blocked ⚠"; CARD_STATUS="blocked" ;;
|
|
2276
|
-
*) PIPELINE_LINE="investigate → architect → build → review → merge ✓"; CARD_STATUS="merged" ;;
|
|
2277
|
-
esac
|
|
2278
|
-
[ "$IS_DRAFT" = "true" ] && PIPELINE_LINE="${PIPELINE_LINE} (draft)"
|
|
2279
|
-
```
|
|
2280
|
-
|
|
2281
|
-
**Build the machine-readable twin** (C4.5c — MUST run this block to assign `CARD_JSON` before
|
|
2282
|
-
Phase 7B embeds it; the cross-reference to `close.md` above is insufficient on the inline path): <!-- forge#1178 -->
|
|
2283
|
-
|
|
2284
|
-
```bash
|
|
2285
|
-
CARD_JSON=$(jq -nc \
|
|
2286
|
-
--argjson issue {NUMBER} \
|
|
2287
|
-
--arg title "{TITLE}" \
|
|
2288
|
-
--arg status "$CARD_STATUS" \
|
|
2289
|
-
--arg pipeline "$PIPELINE_LINE" \
|
|
2290
|
-
--arg pr "{PR_NUMBER}" \
|
|
2291
|
-
--arg target "$PR_TARGET" \
|
|
2292
|
-
--arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
|
|
2293
|
-
--arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
|
|
2294
|
-
--argjson elapsed "${ELAPSED_SECS:-0}" \
|
|
2295
|
-
'{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
|
|
2296
|
-
pr:($pr|tonumber? // null), pr_target:$target,
|
|
2297
|
-
commits:($commits|tonumber? // null),
|
|
2298
|
-
additions:($adds|tonumber? // null),
|
|
2299
|
-
deletions:($dels|tonumber? // null),
|
|
2300
|
-
review:$review, blockers:$blockers, elapsed_seconds:$elapsed}')
|
|
2301
|
-
```
|
|
2302
|
-
|
|
2303
|
-
`CARD_JSON` is now set and embedded in the trajectory comment by 7B.
|
|
2304
|
-
|
|
2305
|
-
### 7B: Trajectory Log (MANDATORY)
|
|
2306
|
-
|
|
2307
|
-
**Review-presence check** (run before filling in Phase 4-5 row): <!-- Added: forge#381 -->
|
|
2308
|
-
```bash
|
|
2309
|
-
# Check whether /review-pr was actually invoked — look for review agent comments on the PR
|
|
2310
|
-
REVIEW_PRESENT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
2311
|
-
--jq '([.reviews[].body // ""] + [.comments[].body // ""]) |
|
|
2312
|
-
map(select(test("APPROVED:|CHANGES REQUESTED:|FORGE:REVIEWER|review-pr";"i"))) |
|
|
2313
|
-
length > 0')
|
|
2314
|
-
# Set Phase 4-5 row: ✅ Merged if review present, ⚠ Skipped (no review) if not
|
|
2315
|
-
REVIEW_ROW=$([ "$REVIEW_PRESENT" = "true" ] && echo "✅ Merged" || echo "⚠ Skipped (no review)")
|
|
2316
|
-
```
|
|
2317
|
-
|
|
2318
|
-
This check is **audit-only** — it annotates the trajectory for visibility. It cannot retroactively block a merged PR. If `⚠ Skipped (no review)` is emitted, log it in the Anomalies field so the skip is surfaced during pipeline health review.
|
|
2319
|
-
|
|
2320
|
-
Post `<!-- FORGE:TRAJECTORY -->` comment with phase-by-phase results table:
|
|
2321
|
-
|
|
2322
|
-
```bash
|
|
2323
|
-
# Compute verification row from VERIFICATION_SKIPPED_CHECKS (set in Phase 3H)
|
|
2324
|
-
if [ -z "$VERIFICATION_SKIPPED_CHECKS" ]; then
|
|
2325
|
-
VERIFICATION_ROW="✅ Ran"
|
|
2326
|
-
else
|
|
2327
|
-
VERIFICATION_ROW="⚠ Skipped — verification.commands not configured for: ${VERIFICATION_SKIPPED_CHECKS}"
|
|
2328
|
-
fi
|
|
2329
|
-
|
|
2330
|
-
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
|
|
2331
|
-
## Pipeline Trajectory — #{NUMBER}
|
|
2332
|
-
|
|
2333
|
-
| Phase | Result | Notes |
|
|
2334
|
-
|-------|--------|-------|
|
|
2335
|
-
| Phase 0: Context Load | ✅ Complete | {lane} → \`{PR_BASE}\` |
|
|
2336
|
-
| Phase 1: Investigation | ✅ {VERDICT} ({CONFIDENCE}) | Task type: {TASK_TYPE} |
|
|
2337
|
-
| Phase 2: Decomposition | ⏭ Skipped | {reason} |
|
|
2338
|
-
| Phase 3: Build | ✅ Complete | Branch: \`{BRANCH}\` |
|
|
2339
|
-
| Phase 3G: Quality Gate | ✅ Gate passed | {iterations} iterations |
|
|
2340
|
-
| Phase 3H: Verification | ${VERIFICATION_ROW} | |
|
|
2341
|
-
| Phase 4–5: Review + PR | {REVIEW_ROW} | PR #{PR_NUMBER} → \`{PR_BASE}\` |
|
|
2342
|
-
| Phase 6: Close | ✅ Complete | Issue closed |
|
|
2343
|
-
|
|
2344
|
-
**Decisions**: {key decisions}
|
|
2345
|
-
**Anomalies**: {anomalies or None}
|
|
2346
|
-
**Pipeline completed**: {TIMESTAMP}
|
|
2347
|
-
|
|
2348
|
-
<!-- FORGE:CARD ${CARD_JSON} -->"
|
|
2349
|
-
```
|
|
2350
|
-
|
|
2351
|
-
Append the `<!-- FORGE:CARD {...} -->` block (machine-readable twin from 7A / close.md C4.5c)
|
|
2352
|
-
as the last line of the trajectory comment. It is HTML-comment-wrapped so it stays hidden in
|
|
2353
|
-
the rendered view but greppable for platform consumption (`/orchestrate` Phase 6 reads it for
|
|
2354
|
-
per-issue cards). Additive — does not affect existing `FORGE:TRAJECTORY` table consumers.
|
|
2355
|
-
|
|
2356
|
-
### 7C: Graph Decision Record (MANDATORY when PR exists)
|
|
2357
|
-
|
|
2358
|
-
**Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks with no PR) OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
|
|
2359
|
-
|
|
2360
|
-
**Purpose**: Post a single consolidated provenance artifact to the PR that proves the merge was backed by citable evidence. Enables downstream benchmarking queries (repeated-mistake rate, stale-edge hit rate, review escape rate) by making every pipeline run queryable via `gh api`.
|
|
2361
|
-
|
|
2362
|
-
**Idempotency check**:
|
|
2363
|
-
```bash
|
|
2364
|
-
GDR_EXISTS=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
|
|
2365
|
-
--jq '[.[] | select(.body | contains("FORGE:DECISION_RECORD"))] | length > 0' 2>/dev/null || echo "false")
|
|
2366
|
-
```
|
|
2367
|
-
|
|
2368
|
-
**Extract context edge counts** from FORGE:CONTEXT comment (already posted on issue):
|
|
2369
|
-
```bash
|
|
2370
|
-
CONTEXT_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2371
|
-
--jq '.[] | select(.body | contains("FORGE:CONTEXT")) | .body' 2>/dev/null | head -1)
|
|
2372
|
-
|
|
2373
|
-
# Count historical review-finding issue references (#NNN patterns in Context comment)
|
|
2374
|
-
REVIEW_FINDING_COUNT=$(echo "$CONTEXT_COMMENT" | grep -oE '#[0-9]+' | wc -l | tr -d ' ')
|
|
2375
|
-
REVIEW_FINDING_COUNT=${REVIEW_FINDING_COUNT:-0}
|
|
2376
|
-
```
|
|
2377
|
-
|
|
2378
|
-
**Extract review verdict and findings count** from PR review summary (Phase 9 of review-pr):
|
|
2379
|
-
```bash
|
|
2380
|
-
REVIEW_SUMMARY=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
|
|
2381
|
-
--jq '[.[] | select(.body | contains("FORGE:REVIEWER") or (.body | test("APPROVED:|CHANGES REQUESTED:"; "i")))] | last | .body // ""' 2>/dev/null || echo '')
|
|
2382
|
-
|
|
2383
|
-
REVIEW_VERDICT=$(echo "$REVIEW_SUMMARY" | sed -n 's/.*Verdict: \(APPROVED\|CHANGES REQUESTED\).*/\1/p' | head -1 || echo "APPROVED")
|
|
2384
|
-
REVIEW_VERDICT="${REVIEW_VERDICT:-APPROVED}"
|
|
2385
|
-
FINDINGS_COUNT=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ findings' | grep -oE '[0-9]+' | head -1 || echo "0")
|
|
2386
|
-
FINDINGS_COUNT="${FINDINGS_COUNT:-0}"
|
|
2387
|
-
AGENTS_RUN=$(echo "$REVIEW_SUMMARY" | grep -oE '[0-9]+ agents' | grep -oE '[0-9]+' | head -1 || echo "0")
|
|
2388
|
-
AGENTS_RUN="${AGENTS_RUN:-0}"
|
|
2389
|
-
```
|
|
2390
|
-
|
|
2391
|
-
**Capture best-effort cost signal** from session telemetry before posting GDR. This is best-effort — if the signal is unavailable, the cost block is omitted rather than blocking the pipeline or fabricating a number. Field names align with `bin/runner.mjs` usage accounting from #1295 so downstream tooling shares one schema:
|
|
2392
|
-
```bash
|
|
2393
|
-
# Best-effort: read per-stage usage from FORGE:BUILDER/FORGE:CONTEXT/FORGE:ARCHITECT phase annotations
|
|
2394
|
-
# Source: session telemetry when available (e.g. OTEL_LOG_TOOL_DETAILS, Claude Code usage reporting).
|
|
2395
|
-
# If unavailable, set COST_BLOCK to null — the field is omitted from the GDR rather than fabricated.
|
|
2396
|
-
COST_INVESTIGATION=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2397
|
-
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
|
|
2398
|
-
| grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
|
|
2399
|
-
COST_BUILD=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
2400
|
-
--jq '[.[] | select(.body | contains("FORGE:BUILDER")) | .body] | last // ""' 2>/dev/null \
|
|
2401
|
-
| grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
|
|
2402
|
-
COST_REVIEW=$(gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments \
|
|
2403
|
-
--jq '[.[] | select(.body | contains("FORGE:REVIEWER")) | .body] | last // ""' 2>/dev/null \
|
|
2404
|
-
| grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
|
|
2405
|
-
|
|
2406
|
-
# Build cost block JSON only if at least one stage value is present; otherwise null
|
|
2407
|
-
if [ -n "$COST_INVESTIGATION" ] || [ -n "$COST_BUILD" ] || [ -n "$COST_REVIEW" ]; then
|
|
2408
|
-
COST_INV_JSON="${COST_INVESTIGATION:-null}"
|
|
2409
|
-
COST_BUILD_JSON="${COST_BUILD:-null}"
|
|
2410
|
-
COST_REVIEW_JSON="${COST_REVIEW:-null}"
|
|
2411
|
-
COST_BLOCK="\"cost\": {
|
|
2412
|
-
\"stages\": {
|
|
2413
|
-
\"investigation\": $COST_INV_JSON,
|
|
2414
|
-
\"build\": $COST_BUILD_JSON,
|
|
2415
|
-
\"review\": $COST_REVIEW_JSON
|
|
2416
|
-
},
|
|
2417
|
-
\"total_usd\": null,
|
|
2418
|
-
\"source\": \"session-telemetry\"
|
|
2419
|
-
},"
|
|
2420
|
-
else
|
|
2421
|
-
COST_BLOCK=""
|
|
2422
|
-
fi
|
|
2423
|
-
```
|
|
2424
|
-
|
|
2425
|
-
**Post GDR to PR** (not to issue — PR comment survives as permanent artifact on the merged diff):
|
|
2426
|
-
```bash
|
|
2427
|
-
if [ "$GDR_EXISTS" != "true" ] && [ -n "{PR_NUMBER}" ]; then
|
|
2428
|
-
GDR_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2429
|
-
HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || echo "")
|
|
2430
|
-
MERGE_COMMIT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json mergeCommit --jq '.mergeCommit.oid // ""' 2>/dev/null || echo "")
|
|
2431
|
-
|
|
2432
|
-
gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:DECISION_RECORD -->
|
|
2433
|
-
## Graph Decision Record — Issue #${NUMBER} / PR #${PR_NUMBER}
|
|
2434
|
-
|
|
2435
|
-
\`\`\`json
|
|
2436
|
-
{
|
|
2437
|
-
\"schema_version\": \"1\",
|
|
2438
|
-
\"issue\": ${NUMBER},
|
|
2439
|
-
\"pr\": ${PR_NUMBER},
|
|
2440
|
-
\"repo\": \"{GH_REPO}\",
|
|
2441
|
-
\"lane\": \"{lane}\",
|
|
2442
|
-
\"pr_base\": \"{PR_BASE}\",
|
|
2443
|
-
\"branch\": \"{BRANCH}\",
|
|
2444
|
-
\"head_sha\": \"${HEAD_SHA}\",
|
|
2445
|
-
\"merge_commit\": \"${MERGE_COMMIT}\",
|
|
2446
|
-
\"investigation\": {
|
|
2447
|
-
\"verdict\": \"{VERDICT}\",
|
|
2448
|
-
\"confidence\": \"{CONFIDENCE}\",
|
|
2449
|
-
\"task_type\": \"{TASK_TYPE}\"
|
|
2450
|
-
},
|
|
2451
|
-
${COST_BLOCK}
|
|
2452
|
-
\"context\": {
|
|
2453
|
-
\"historical_edges_referenced\": ${REVIEW_FINDING_COUNT},
|
|
2454
|
-
\"forge_annotations_read\": [\"FORGE:INVESTIGATOR\", \"FORGE:CONTRACT\", \"FORGE:CONTEXT\", \"FORGE:ARCHITECT\", \"FORGE:BUILDER\"]
|
|
2455
|
-
},
|
|
2456
|
-
\"build\": {
|
|
2457
|
-
\"files_changed\": {FILES_CHANGED},
|
|
2458
|
-
\"quality_gate\": \"{pass|fail}\",
|
|
2459
|
-
\"quality_gate_iterations\": {GATE_ITERATIONS}
|
|
2460
|
-
},
|
|
2461
|
-
\"review\": {
|
|
2462
|
-
\"verdict\": \"${REVIEW_VERDICT:-APPROVED}\",
|
|
2463
|
-
\"findings_created\": ${FINDINGS_COUNT},
|
|
2464
|
-
\"agents_run\": ${AGENTS_RUN}
|
|
2465
|
-
},
|
|
2466
|
-
\"merge\": {
|
|
2467
|
-
\"merged_at\": \"${GDR_TIMESTAMP}\",
|
|
2468
|
-
\"justification\": \"Investigation confirmed ({VERDICT}/{CONFIDENCE}), quality gate passed, review ${REVIEW_VERDICT:-approved}\"
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
\`\`\`
|
|
2472
|
-
|
|
2473
|
-
**Queryable**: \`gh api repos/{GH_REPO}/issues/{PR_NUMBER}/comments --jq '[.[] | select(.body | contains(\"FORGE:DECISION_RECORD\"))] | .[0].body\`"
|
|
2474
|
-
fi
|
|
2475
|
-
```
|
|
2476
|
-
|
|
2477
|
-
**Benchmarking**: Query all GDRs for a repo to compute pipeline metrics:
|
|
2478
|
-
```bash
|
|
2479
|
-
# Fetch all merged PRs and extract their GDR JSON blocks for metric computation
|
|
2480
|
-
# (used by /pipeline-health to measure repeated-mistake rate, stale-edge hit rate, etc.)
|
|
2481
|
-
gh pr list -R {GH_REPO} --state merged --limit 100 --json number \
|
|
2482
|
-
--jq '.[].number' | while read pr; do
|
|
2483
|
-
gh api repos/{GH_REPO}/issues/$pr/comments \
|
|
2484
|
-
--jq '.[] | select(.body | contains("FORGE:DECISION_RECORD")) | .body' 2>/dev/null
|
|
2485
|
-
done
|
|
2486
|
-
```
|
|
2487
|
-
|
|
2488
|
-
<!-- Added: forge#776 -->
|
|
2489
|
-
|
|
2490
656
|
---
|
|
2491
657
|
|
|
2492
658
|
## Error Handling
|