forgedock 1.12.0 → 1.13.0
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/phases-args.test.mjs +2 -1
- package/bin/engine/phases.mjs +349 -90
- package/bin/engine/runlog.mjs +25 -2
- package/bin/engine-cli.mjs +4 -1
- package/bin/engine.mjs +72 -19
- package/bin/hooks/interactive-engine.mjs +135 -209
- package/bin/hooks/pre-tool-use.mjs +16 -4
- package/bin/labels.json +10 -0
- package/bin/opencode-adapter.mjs +1 -1
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +258 -23
- package/bin/watch-utils.mjs +1 -0
- package/commands/autopilot.md +6 -6
- package/commands/cleanup.md +8 -5
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +14 -19
- package/commands/orchestrate/phase-4-execution.md +80 -53
- package/commands/orchestrate/phase-6-report.md +6 -0
- package/commands/orchestrate/safety.md +1 -1
- package/commands/pipeline-health/phase-2-metrics.md +39 -0
- package/commands/pipeline-health/phase-5-report.md +1 -0
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +2 -2
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/api.md +1 -1
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +3 -3
- package/commands/review-pr-agents/protocols.md +15 -6
- package/commands/review-pr-agents/scraper.md +1 -1
- package/commands/review-pr-agents/security.md +5 -5
- package/commands/review-pr-agents.md +12 -12
- package/commands/review-pr-staging.md +226 -8
- package/commands/review-pr.md +394 -53
- package/commands/work-on/build/architect.md +36 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +88 -4
- package/commands/work-on/build.md +106 -7
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +137 -25
- package/commands/work-on/review.md +25 -9
- package/commands/work-on.md +15 -15
- package/docs/CONFIG.md +29 -0
- package/docs/RELEASE-NOTES.md +45 -0
- package/docs/spec/forge-protocol-v1.md +22 -1
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +20 -5
- package/package.json +2 -1
- package/packages/protocol/src/phases.js +21 -0
- package/scripts/check-blast-radius.sh +164 -0
- package/scripts/check-blast-radius.test.sh +183 -0
- package/scripts/check-contract-scope.sh +137 -0
- package/scripts/check-contract-scope.test.sh +83 -0
- package/scripts/check-spec-bash.sh +60 -3
- package/scripts/check-spec-bash.test.sh +56 -0
- package/scripts/check-spec-markers.sh +29 -0
- package/scripts/check-test-wiring.sh +57 -0
- package/scripts/check-test-wiring.test.sh +70 -0
- package/scripts/classify-finding.sh +72 -8
- package/scripts/classify-finding.test.sh +77 -8
- package/scripts/close-dossier-no-main-commit.test.sh +12 -12
- package/scripts/diff-size.test.sh +3 -2
- package/scripts/doctor-pipeline-state.sh +35 -0
- package/scripts/forge-annotation.sh +3 -0
- package/scripts/forge-root.test.sh +103 -13
- package/scripts/forge-run.sh +2 -2
- package/scripts/recovery-claim-live.test.sh +10 -7
- package/scripts/remediate-basesync.test.sh +103 -0
- package/scripts/review-delta.sh +206 -0
- package/scripts/review-delta.test.sh +213 -0
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/review-pr-gate2.test.sh +84 -0
- package/scripts/review-pr-staging-scope.test.sh +99 -0
- package/scripts/run-shell-tests.sh +115 -0
- package/scripts/select-fix-targets.sh +17 -5
- package/scripts/transition-label.sh +3 -1
|
@@ -495,6 +495,8 @@ grep -rn "$GATED_FUNCTION" "$SERVICE_DIR" --include="*.py" | grep -v "#"
|
|
|
495
495
|
|
|
496
496
|
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.
|
|
497
497
|
|
|
498
|
+
When Phase A2.8 builds the blast-radius manifest, run the sweep over the whole repository (`git grep`), not only `$SERVICE_DIR` — siblings in other directories are the ones a directory-scoped sweep misses.
|
|
499
|
+
|
|
498
500
|
**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.
|
|
499
501
|
|
|
500
502
|
---
|
|
@@ -575,6 +577,30 @@ The comment below passes the table through in a `### Pattern Sweep` section so t
|
|
|
575
577
|
|
|
576
578
|
---
|
|
577
579
|
|
|
580
|
+
## Phase A2.8: Blast-Radius Manifest *(skip when no existing symbol, field, flag, schema or contract changes)* <!-- Added: forge#3446 -->
|
|
581
|
+
|
|
582
|
+
The caller and sibling sweep (A1, A2.2) must leave a record a machine can check. For every **existing** symbol, field, flag, schema or contract the change modifies, emit one `SYMBOL:` record and one `HIT:` record per file that mentions it. `scripts/check-blast-radius.sh` re-runs each query against the final tree during validate (Phase V4.5) and fails when a matching file is neither changed nor marked `verified-unaffected`.
|
|
583
|
+
|
|
584
|
+
1. **Pick a distinctive query per symbol** — a fixed string matching `^[A-Za-z0-9_.:/-]{3,80}$`, not starting with `-` (for example `billing_principal`, not `id`). The checker runs it as `git grep -l -F -e "$Q"`: a literal search, no regex. A query matching more than 200 files is rejected as too broad, so narrow it.
|
|
585
|
+
2. **Run each query repo-wide** and record every hit file:
|
|
586
|
+
```bash
|
|
587
|
+
Q="{SYMBOL_QUERY}"
|
|
588
|
+
git grep -l -F -e "$Q" -- .
|
|
589
|
+
```
|
|
590
|
+
A grep failure (exit above 1) is not "zero hits": report it and stop instead of emitting an empty manifest.
|
|
591
|
+
3. **Give every hit a disposition**: `change` (the plan edits it; it is also a row in `### Affected Paths`) or `verified-unaffected` with `reason=<one-hyphenated-token>` after you read the hit and confirmed it needs no edit. A file the change itself edits needs no row, but listing it as `change` makes the checker confirm it was really edited.
|
|
592
|
+
4. **Record format** — one record per line, no spaces inside any value, nothing else between the markers except blank lines and code fences:
|
|
593
|
+
```
|
|
594
|
+
SYMBOL: id=s1 kind=function|field|flag|schema|contract name=<symbol> query="<fixed-string>"
|
|
595
|
+
HIT: symbol=s1 file=<path> role=producer|consumer|sibling disposition=change
|
|
596
|
+
HIT: symbol=s1 file=<path> role=consumer disposition=verified-unaffected reason=<one-token>
|
|
597
|
+
```
|
|
598
|
+
5. **Skip rule** — emit no manifest (omit the whole section) when the architect step is skipped (see Skip Conditions) or when the change touches no existing symbol with consumers (new files only, docs only). An absent manifest is a SKIP at the checker, not a failure.
|
|
599
|
+
|
|
600
|
+
The block is delimited by `<!-- FORGE:BLAST_RADIUS:BEGIN -->` and `<!-- FORGE:BLAST_RADIUS:END -->`. Never use the architect marker strings as its delimiters: phase-trail and engine detection key on those markers alone, so the manifest stays additive. Builders may add rows (a justified `verified-unaffected`) but must not delete them. The manifest is treated as untrusted data by consumers: only the newest trusted FORGE:ARCHITECT comment is read.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
578
604
|
## Phase A3: Consistency Rules
|
|
579
605
|
|
|
580
606
|
Identify invariants that ALL affected paths must satisfy. These become the builder's consistency checklist.
|
|
@@ -697,6 +723,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
|
697
723
|
|-------|-----|-------------|
|
|
698
724
|
| {QUERY} | {FILE:LINE} | {fix \| not-affected — reason} |
|
|
699
725
|
|
|
726
|
+
### Blast-Radius Manifest
|
|
727
|
+
<!-- Phase A2.8. Omit this whole section when no existing symbol changes (see Skip rule).
|
|
728
|
+
Escape every double quote inside the record as a backslash-quote, because this body is posted inside a double-quoted shell string. -->
|
|
729
|
+
<!-- FORGE:BLAST_RADIUS:BEGIN -->
|
|
730
|
+
SYMBOL: id=s1 kind={function|field|flag|schema|contract} name={SYMBOL} query=\"{FIXED_STRING_QUERY}\"
|
|
731
|
+
HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=change
|
|
732
|
+
HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=verified-unaffected reason={one-hyphenated-token}
|
|
733
|
+
<!-- FORGE:BLAST_RADIUS:END -->
|
|
734
|
+
|
|
700
735
|
### Implementation Order
|
|
701
736
|
1. {FIRST_CHANGE} — {WHY_FIRST}
|
|
702
737
|
2. {SECOND_CHANGE} — {WHY_SECOND}
|
|
@@ -801,6 +836,7 @@ B4 → [THIS MODULE] Architecture Planning (FORGE:ARCHITECT comment)
|
|
|
801
836
|
Phase A2: Trace the Data Flow (A2.1 Runtime UID x Volume Ownership, A2.2 Gate-Condition Caller Sweep)
|
|
802
837
|
Phase A2.5: Pipeline Phase-Dependency Check
|
|
803
838
|
Phase A2.7: Pattern Sweep Intake (review-finding issues only)
|
|
839
|
+
Phase A2.8: Blast-Radius Manifest (enforced by validate V4.5, passed to review-pr)
|
|
804
840
|
Phase A3: Consistency Rules
|
|
805
841
|
Phase A4: Sequence the Implementation
|
|
806
842
|
Phase A5: Risk Assessment
|
|
@@ -42,7 +42,7 @@ Parse from $ARGUMENTS:
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason.
|
|
45
|
+
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
|
|
46
46
|
|
|
47
47
|
## Phase I1: Load Context from GitHub
|
|
48
48
|
|
|
@@ -43,7 +43,7 @@ Parse from $ARGUMENTS:
|
|
|
43
43
|
|
|
44
44
|
## Skip Conditions
|
|
45
45
|
|
|
46
|
-
Skip Phases V0–V4 (set `GATE_PASSED=true`, iterations 0) and go straight to Phase V5 (which posts the skip-path quality-gate marker, commits, and marks the build complete) if:
|
|
46
|
+
Skip Phases V0–V4.5 (set `GATE_PASSED=true`, iterations 0) and go straight to Phase V5 (which posts the skip-path quality-gate marker, commits, and marks the build complete) if:
|
|
47
47
|
- Only 1 file was changed AND the shared predicate classifies it as documentation: `printf '%s\n' "<that file>" | bash "$FORGE_ROOT/scripts/is-docs-only.sh"` exits 0 (resolve `FORGE_ROOT` with the canonical block; unresolvable → the gate runs). A single file under `commands/`, `.claude/`, `.agents/`, `hooks/`, … or any agent-instruction Markdown is **code** for this purpose (it contains executable shell and agent instructions) and is never skipped. <!-- field test: phase-4-execution.md was skipped as "docs" -->
|
|
48
48
|
|
|
49
49
|
In all other cases, the gate MUST run.
|
|
@@ -508,6 +508,86 @@ docker exec {CONTAINER_NAME} env | grep {VAR_NAME}
|
|
|
508
508
|
|
|
509
509
|
---
|
|
510
510
|
|
|
511
|
+
## Phase V4.5: Blast-Radius Check *(runs after V4, before V5 — skipped only on the docs-only skip path)* <!-- Added: forge#3446 -->
|
|
512
|
+
|
|
513
|
+
The architect records the caller and sibling sweep as a machine-readable manifest between `<!-- FORGE:BLAST_RADIUS:BEGIN -->` and `<!-- FORGE:BLAST_RADIUS:END -->` inside its FORGE:ARCHITECT comment. This phase runs `check-blast-radius.sh`, a script and not a prompt, which re-runs every manifest query against the final tree and fails on any matching file that is neither changed nor marked `verified-unaffected`.
|
|
514
|
+
|
|
515
|
+
- **Trust**: the manifest drives a gate, so it is read only from the newest comment that `trusted-comments.sh` accepts (the same trust predicate as the phase-trail verifier). An untrusted comment is never parsed.
|
|
516
|
+
- **Install root only**: both scripts resolve from `FORGE_ROOT` and never from the worktree, because the branch under audit must not supply its own gate. An unresolvable script is DEGRADED, never a pass.
|
|
517
|
+
- **Exit codes**: `0` covered or no manifest (SKIP); `1` an `UNLISTED:` or `NOT_DONE:` line was printed, so set `GATE_PASSED=false`, fix the code (or add a justified `verified-unaffected` row to the plan by editing the FORGE:ARCHITECT comment; never delete a row), and re-run V1 onward; anything else, a failed fetch, or an unresolvable script is DEGRADED: append `blast-radius` to `SKIPPED_CHECKS` so it appears in `verification_skipped`, and print the reason. DEGRADED never passes silently and a failed fetch is never treated as "no manifest".
|
|
518
|
+
- **Carry-forward**: each Bash call is a fresh shell, so V4.5's variables do not reach V5. The block prints its result as explicit lines: `BLAST_RADIUS_STATE: <PASS|SKIP|FAIL|DEGRADED>`, plus `GATE_PASSED: false` on FAIL and `SKIPPED_CHECKS+=blast-radius` on DEGRADED. Record those lines in your notes; V5 Step 1 sets `GATE_PASSED` and V5 Step 4 sets `SKIPPED_CHECKS` from them, alongside the V2 notes.
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
cd {WORKTREE_PATH}
|
|
522
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
523
|
+
FORGE_ROOT=""
|
|
524
|
+
# Windows drive-letter FORGEDOCK_HOME (C:/x or C:\x) is normalized to /c/x (cygpath when present); relative values stay rejected.
|
|
525
|
+
_h="${FORGEDOCK_HOME:-}"; case "$_h" in [A-Za-z]:[/\\]*) _w="$_h"; _h="$(cygpath -u "$_w" 2>/dev/null || true)"; [ -n "$_h" ] || _h="/$(printf %s "$_w" | cut -c1 | tr 'A-Z' 'a-z')$(printf %s "${_w#??}" | tr '\\' '/')" ;; esac
|
|
526
|
+
# Only the official marketplace is trusted (name pinned; override only via the trusted FORGEDOCK_MARKETPLACE env, never repo files).
|
|
527
|
+
_mk="${FORGEDOCK_MARKETPLACE:-forgedock}"; case "$_mk" in ""|.|..|*[!A-Za-z0-9._-]*) _mk="forgedock" ;; esac
|
|
528
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac; else
|
|
529
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match). Every assignment ends in || true so the block survives set -e / pipefail.
|
|
530
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null || true)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
531
|
+
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
532
|
+
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
533
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
534
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{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- || true)"
|
|
535
|
+
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
536
|
+
# '${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.
|
|
537
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
538
|
+
while IFS= read -r _c; do
|
|
539
|
+
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
|
|
540
|
+
done <<< "$_k"
|
|
541
|
+
fi
|
|
542
|
+
BR_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-blast-radius.sh}"; [ -f "$BR_SCRIPT" ] || BR_SCRIPT=""
|
|
543
|
+
TRUST_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/trusted-comments.sh}"; [ -f "$TRUST_SCRIPT" ] || TRUST_SCRIPT=""
|
|
544
|
+
BR_STATE=""; BR_NOTE=""; BR_FILE=$(mktemp "${TMPDIR:-/tmp}/forge-blast-radius.XXXXXX")
|
|
545
|
+
if [ -z "$BR_SCRIPT" ] || [ -z "$TRUST_SCRIPT" ]; then
|
|
546
|
+
BR_STATE="DEGRADED"; BR_NOTE="check-blast-radius.sh or trusted-comments.sh not found under FORGE_ROOT"
|
|
547
|
+
else
|
|
548
|
+
COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null); FETCH_RC=$?
|
|
549
|
+
if [ "$FETCH_RC" -ne 0 ]; then
|
|
550
|
+
BR_STATE="DEGRADED"; BR_NOTE="could not fetch issue comments (rc=${FETCH_RC})"
|
|
551
|
+
else
|
|
552
|
+
BODIES=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUST_SCRIPT" bodies '^<!-- FORGE:ARCHITECT -->'); TRUST_RC=$?
|
|
553
|
+
if [ "$TRUST_RC" -ne 0 ]; then
|
|
554
|
+
BR_STATE="DEGRADED"; BR_NOTE="trusted-comments.sh failed (rc=${TRUST_RC})"
|
|
555
|
+
elif [ -z "$BODIES" ]; then
|
|
556
|
+
BR_STATE="SKIP"; BR_NOTE="no trusted FORGE:ARCHITECT comment"
|
|
557
|
+
else
|
|
558
|
+
# one JSON string per line, newest last
|
|
559
|
+
printf '%s\n' "$BODIES" | tail -n 1 | jq -r . > "$BR_FILE"; JQ_RC=$?
|
|
560
|
+
if [ "$JQ_RC" -ne 0 ]; then
|
|
561
|
+
BR_STATE="DEGRADED"; BR_NOTE="could not decode the FORGE:ARCHITECT comment (rc=${JQ_RC})"
|
|
562
|
+
else
|
|
563
|
+
git fetch origin {PR_BASE} >/dev/null 2>&1 || true
|
|
564
|
+
BR_OUT=$(bash "$BR_SCRIPT" --manifest "$BR_FILE" --base "origin/{PR_BASE}" --repo "{WORKTREE_PATH}" 2>&1); BR_RC=$?
|
|
565
|
+
printf '%s\n' "$BR_OUT"
|
|
566
|
+
case "$BR_RC" in
|
|
567
|
+
0) BR_STATE="PASS" ;;
|
|
568
|
+
1) BR_STATE="FAIL"; GATE_PASSED=false ;;
|
|
569
|
+
*) BR_STATE="DEGRADED"; BR_NOTE="check-blast-radius.sh exited ${BR_RC}" ;;
|
|
570
|
+
esac
|
|
571
|
+
fi
|
|
572
|
+
fi
|
|
573
|
+
fi
|
|
574
|
+
fi
|
|
575
|
+
rm -f "$BR_FILE"
|
|
576
|
+
echo "BLAST_RADIUS: ${BR_STATE}${BR_NOTE:+ — $BR_NOTE}"
|
|
577
|
+
# Carry-forward lines for V5 (fresh shell): read them back in V5 Step 1 and Step 4.
|
|
578
|
+
echo "BLAST_RADIUS_STATE: ${BR_STATE}"
|
|
579
|
+
if [ "$BR_STATE" = "FAIL" ]; then
|
|
580
|
+
echo "GATE_PASSED: false"
|
|
581
|
+
elif [ "$BR_STATE" = "DEGRADED" ]; then
|
|
582
|
+
SKIPPED_CHECKS="${SKIPPED_CHECKS:+$SKIPPED_CHECKS, }blast-radius"
|
|
583
|
+
echo "SKIPPED_CHECKS+=blast-radius"
|
|
584
|
+
fi
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
On `FAIL`, the blocker line for `VALIDATE_RESULT` is `blast-radius: <UNLISTED/NOT_DONE lines>`; the repair loop (build B6.5) fixes the listed files, not the checker or the manifest.
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
511
591
|
## Phase V5: Marker, Commit, Audit, Complete (always — after GATE_PASSED=true)
|
|
512
592
|
|
|
513
593
|
Phase V5 runs in this exact order: **(1)** post the `FORGE:QUALITY_GATE` marker, **(2)** commit, **(3)** ancestry audit, **(4)** append `FORGE:BUILDER:COMPLETE` and the verification status. Do not reorder.
|
|
@@ -518,7 +598,10 @@ The quality gate must leave a checkable artifact. `scripts/verify-phase-trail.sh
|
|
|
518
598
|
|
|
519
599
|
**Skip-path marker**: when the Skip Conditions above set `GATE_PASSED=true` early (single config/docs file), still post the marker with `**Result**: PASS (skipped — single config/docs file)` and `**Iterations**: 0`, so the verifier never has to guess. The verifier's `--docs-only` waiver additionally covers diffs accepted by `scripts/is-docs-only.sh` (allowlisted Markdown only: `docs/**` or root README/CHANGELOG/CONTRIBUTING/SECURITY/GOVERNANCE; nested `AGENTS.md`/`CLAUDE.md`/`SKILL.md`/`GEMINI.md` and the instruction directories `commands/`, `devdocs/`, `templates/`, `skills/`, `agents/`, `hooks/`, `.claude/`, `.claude-plugin/`, `.agents/`, `.codex/`, `.cursor/`, `.github/`, `.opencode/`, `.gemini/`, `.kiro/` excluded; callers feed both sides of renames).
|
|
520
600
|
|
|
601
|
+
Set `GATE_PASSED` from the V1 loop result, then apply V4.5's printed carry-forward line: if V4.5 printed `GATE_PASSED: false` (`BLAST_RADIUS_STATE: FAIL`), `GATE_PASSED` is `false` here even if the V1 loop passed.
|
|
602
|
+
|
|
521
603
|
```bash
|
|
604
|
+
# GATE_PASSED: set from the V1 loop result, and forced to false when V4.5 printed "GATE_PASSED: false"
|
|
522
605
|
GATE_RESULT=$([ "$GATE_PASSED" = "true" ] && echo PASS || echo FAIL)
|
|
523
606
|
# Bind the PASS to what was actually gated: the staged tree is exactly the tree the V5 commit will have.
|
|
524
607
|
# scripts/verify-phase-trail.sh --head-tree (work-on/review.md R1.5) rejects a PASS recorded for a different tree. <!-- Added: forge#3149 -->
|
|
@@ -647,12 +730,12 @@ If the output contains `ANCESTRY_FAILED=1`: do NOT append `:COMPLETE` and do NOT
|
|
|
647
730
|
|
|
648
731
|
After the ancestry audit passes (or is skipped), patch the existing FORGE:BUILDER comment: add the `**Verification Status**` line, the best-effort `cost_usd:` line, and the `<!-- FORGE:BUILDER:COMPLETE -->` marker. This is the **only** place the marker is written — it signals that a real commit exists on the branch and the build is safe to resume-skip. <!-- Added: forge#1305 -->
|
|
649
732
|
|
|
650
|
-
`SKIPPED_CHECKS` comes from Phase V2. Shell state may not persist between Bash calls, so set it here from your V2 notes before running the block (comma-separated check names, empty when every configured check ran).
|
|
733
|
+
`SKIPPED_CHECKS` comes from Phase V2 and Phase V4.5. Shell state may not persist between Bash calls, so set it here from your V2 notes before running the block (comma-separated check names, empty when every configured check ran), and append `blast-radius` when V4.5 printed `SKIPPED_CHECKS+=blast-radius` (`BLAST_RADIUS_STATE: DEGRADED`).
|
|
651
734
|
|
|
652
735
|
**Cost line reconciliation**: the machine-readable `cost_usd:` line (best-effort; only when `PHASE_COST_USD` is available, never blocking) is the single cost signal. Do not add a separate `**Cost (build phase)**` line.
|
|
653
736
|
|
|
654
737
|
```bash
|
|
655
|
-
# SKIPPED_CHECKS: set from the V2 notes, e.g. SKIPPED_CHECKS="python.format,
|
|
738
|
+
# SKIPPED_CHECKS: set from the V2 notes plus V4.5's carry-forward line, e.g. SKIPPED_CHECKS="python.format, blast-radius"
|
|
656
739
|
SKIPPED_CHECKS="${SKIPPED_CHECKS:-}"
|
|
657
740
|
if [ -z "$SKIPPED_CHECKS" ]; then
|
|
658
741
|
VERIFICATION_STATUS="✅ All configured verification commands passed"
|
|
@@ -708,7 +791,7 @@ VALIDATE_RESULT:
|
|
|
708
791
|
blocker: {description if gate_passed=false}
|
|
709
792
|
verification_skipped: [] # empty when all configured checks ran; list of skipped check names otherwise
|
|
710
793
|
# e.g. ["python.format", "typescript.typecheck/build"]
|
|
711
|
-
# populated from SKIPPED_CHECKS in Phase V2
|
|
794
|
+
# populated from SKIPPED_CHECKS in Phase V2 and V4.5
|
|
712
795
|
```
|
|
713
796
|
|
|
714
797
|
---
|
|
@@ -722,6 +805,7 @@ implement (work-on:build:implement) — code written, staged (not committed), FO
|
|
|
722
805
|
→ [THIS MODULE] validate
|
|
723
806
|
V0 self-check → V1 quality-gate loop (Skill quality-gate, forked) → V2 format/verify + known-slow/learned tests
|
|
724
807
|
→ V3 proxy wiring → V3.5 DB advisory → V3.6 browser signals → V4 deploy completeness
|
|
808
|
+
→ V4.5 blast-radius manifest check (check-blast-radius.sh, install-root only)
|
|
725
809
|
→ V5 marker → commit → ancestry audit → BUILDER:COMPLETE
|
|
726
810
|
review (work-on:review) — push, PR, merge
|
|
727
811
|
```
|
|
@@ -23,7 +23,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
23
23
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
|
|
24
24
|
**NEVER use plan mode (EnterPlanMode).**
|
|
25
25
|
|
|
26
|
-
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the one exception is the B0 size-gate resume branch, which enters at B5.5 with its state verified). Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
27
27
|
|
|
28
28
|
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
29
29
|
|
|
@@ -173,6 +173,20 @@ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate
|
|
|
173
173
|
|
|
174
174
|
**Resume check**:
|
|
175
175
|
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
176
|
+
- **Size-gate resume** (checked BEFORE the partial-BUILDER rule below; first match wins) <!-- Added: forge#3463 -->: if a `<!-- FORGE:BUILDER -->` comment exists without `<!-- FORGE:BUILDER:COMPLETE -->` AND the latest trusted `FORGE:DIFF_SIZE` comment has `result: OVER`, the build stopped at a deliberate B5.5 exit (`NEEDS_DECOMPOSE` or the decompose-loop-guard Blocked exit), not in a crash. Keep the BUILDER comment, do not re-run B2-B5, and re-enter at **Phase B5.5**. Read the marker through the same anchored, trust-filtered lookup B5.5 uses (script resolution block verbatim):
|
|
177
|
+
```bash
|
|
178
|
+
# <Script resolution block from above goes here, verbatim>
|
|
179
|
+
set -o pipefail
|
|
180
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
181
|
+
[ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ] || { echo "DIFF_SIZE_LOOKUP=FAILED (trusted-comments.sh unresolved)"; exit 0; }
|
|
182
|
+
DS=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE') \
|
|
183
|
+
|| { echo "DIFF_SIZE_LOOKUP=FAILED"; exit 0; }
|
|
184
|
+
LAST_RESULT=$(printf '%s\n' "$DS" | jq -r 'select(type == "string")' | sed -n 's/^result: *\([A-Z]*\).*/\1/p' | tail -n 1)
|
|
185
|
+
echo "DIFF_SIZE_RESULT=${LAST_RESULT:-<none>}"
|
|
186
|
+
```
|
|
187
|
+
- `DIFF_SIZE_LOOKUP=FAILED` → Blocked exit with blocker `size-gate-unavailable: could not read FORGE:DIFF_SIZE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no DIFF_SIZE".
|
|
188
|
+
- `DIFF_SIZE_RESULT=OVER` → verify, without creating anything, that the branch and worktree exist (derive BRANCH/WORKTREE_PATH as in B1A/B1C), that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH, and that a staged diff is present (`! git -C "$WORKTREE_PATH" diff --cached --quiet`). Recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction. When all hold, skip B0.5-B5 and continue at B5.5 (it re-measures, then applies the override / `NEEDS_DECOMPOSE` / `OK` logic).
|
|
189
|
+
- Anything else (no `FORGE:DIFF_SIZE`, `result` is not `OVER`, worktree or branch missing or mismatched, nothing staged, band unrecoverable) → never skip ahead on missing state; fall through to the next rule.
|
|
176
190
|
- If `<!-- FORGE:BUILDER -->` exists BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
|
|
177
191
|
```bash
|
|
178
192
|
PARTIAL_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate \
|
|
@@ -336,7 +350,7 @@ case "$TIER" in
|
|
|
336
350
|
prose)
|
|
337
351
|
[ -n "${DRY_RUN:-}" ] && echo "DRY_RUN: set workflow:building" || \
|
|
338
352
|
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" \
|
|
339
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
353
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
340
354
|
;;
|
|
341
355
|
esac
|
|
342
356
|
```
|
|
@@ -347,7 +361,83 @@ esac
|
|
|
347
361
|
|
|
348
362
|
**Skip if COMPLEXITY_BAND: INVESTIGATION** — go to B5. For every other band (TRIVIAL included) the contract is mandatory. If a `FORGE:CONTRACT` comment already exists on the issue (resume), do not post a second one — continue to B2.1.
|
|
349
363
|
|
|
350
|
-
Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why:
|
|
364
|
+
Post `<!-- FORGE:CONTRACT -->` comment documenting what will be built and why. Run B2.0 first: it files the follow-up issues for `deferred` out-of-scope items and validates the section, so `{OUT_OF_SCOPE_ITEMS}` below already holds real issue numbers.
|
|
365
|
+
|
|
366
|
+
### B2.0: Out-of-scope dispositions — file deferred follow-ups, validate the section <!-- Added: forge#3447 -->
|
|
367
|
+
|
|
368
|
+
Every item in the contract's `### Out of Scope` section must carry exactly one disposition, or the section is the single line `None.`. Each bullet names at least one backticked path or symbol so review can match findings against it:
|
|
369
|
+
|
|
370
|
+
```
|
|
371
|
+
- `path/or/symbol` — deferred → #N: <why it is deferred>
|
|
372
|
+
- `path/or/symbol` — not-affected: <evidence, e.g. the grep or call-graph check that shows no impact>
|
|
373
|
+
- `path/or/symbol` — accepted-risk: <reason the risk is acceptable>
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- **`deferred → #N`**: a follow-up issue `#N` already exists (filed in this step, before the contract is posted). Use it for a sibling path the work leaves alone but that still needs the change.
|
|
377
|
+
- **`not-affected: <evidence>`**: the path was checked and the change does not reach it. The evidence must be concrete. Review never demotes a finding against a `not-affected` item: a finding that contradicts the evidence means the contract was wrong.
|
|
378
|
+
- **`accepted-risk: <reason>`**: a known gap that is knowingly kept. Review demotes a non-security finding on this path to a note.
|
|
379
|
+
- **Known-flaw rule**: a contract that keeps a known-flawed design (a design the investigation or architect plan identified as flawed, a fail-open caller, an unsound threshold) must state the flaw and list it as a `deferred → #N` item with a linked follow-up. It may not hide behind `accepted-risk` or free text.
|
|
380
|
+
- Free-text exclusions without a disposition are invalid. `->` is accepted for `→`.
|
|
381
|
+
|
|
382
|
+
**File the `deferred` follow-ups** (skip the filing on resume when the contract already exists):
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# <Script resolution block from work-on/build.md, verbatim>
|
|
386
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
387
|
+
# For each deferred item (DEFERRED_PATH = the backticked path, DEFERRED_WHY = one line), repeat:
|
|
388
|
+
DEFERRED_MARKER="<!-- FORGE:DEFERRED_FROM: #{NUMBER} -->"
|
|
389
|
+
LOOKUP=$(gh issue list {GH_FLAG} --state open --search "\"FORGE:DEFERRED_FROM: #{NUMBER}\" in:body" \
|
|
390
|
+
--json number,body --limit 50 2>/dev/null) || LOOKUP="FAILED"
|
|
391
|
+
if [ "$LOOKUP" = "FAILED" ]; then
|
|
392
|
+
echo "DEFERRED_LOOKUP=FAILED" # a failed lookup is not "none found": Blocked exit, never re-file blind
|
|
393
|
+
else
|
|
394
|
+
DEFERRED_NUM=$(printf '%s' "$LOOKUP" | jq -r --arg p "$DEFERRED_PATH" \
|
|
395
|
+
'[.[] | select(.body | contains($p))] | first | .number // empty' 2>/dev/null)
|
|
396
|
+
if [ -z "$DEFERRED_NUM" ]; then
|
|
397
|
+
DEFERRED_URL=$(run gh issue create {GH_FLAG} --title "follow-up: ${DEFERRED_PATH} (deferred from #{NUMBER})" \
|
|
398
|
+
--body "## Problem
|
|
399
|
+
|
|
400
|
+
${DEFERRED_WHY}
|
|
401
|
+
|
|
402
|
+
Deferred from #{NUMBER}: \`${DEFERRED_PATH}\` was declared out of scope in its contract and is tracked here instead of surfacing later as a review finding.
|
|
403
|
+
|
|
404
|
+
## Affected Files
|
|
405
|
+
|
|
406
|
+
- \`${DEFERRED_PATH}\`
|
|
407
|
+
|
|
408
|
+
## Acceptance Criteria
|
|
409
|
+
|
|
410
|
+
- [ ] The concern above is resolved for \`${DEFERRED_PATH}\`
|
|
411
|
+
|
|
412
|
+
${DEFERRED_MARKER}")
|
|
413
|
+
DEFERRED_NUM=$(printf '%s' "$DEFERRED_URL" | grep -oE '[0-9]+$')
|
|
414
|
+
[ -n "${DRY_RUN:-}" ] && DEFERRED_NUM="0"
|
|
415
|
+
fi
|
|
416
|
+
echo "DEFERRED_ISSUE=${DEFERRED_NUM:-}"
|
|
417
|
+
fi
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
- `DEFERRED_LOOKUP=FAILED`, or an empty `DEFERRED_NUM` after a real (non-`DRY_RUN`) create → Blocked exit with blocker `github-unavailable: could not file or look up deferred follow-up for <path>` (a transient failure follows the retry rule above; no `needs-human` for `github-unavailable:`).
|
|
421
|
+
- On resume the lookup reuses the open issue that carries the marker and the same path, so a retried build never files a duplicate.
|
|
422
|
+
- Prefer filing through the `/issue` create-hook's programmatic contract (`Skill(skill="{FORGE_SKILL_PREFIX}issue", ...)`) when it is available; the raw create above is the fallback and must keep the `FORGE:DEFERRED_FROM` marker in the body.
|
|
423
|
+
|
|
424
|
+
**Validate the composed section** with `scripts/check-contract-scope.sh validate` (the exact text that will replace `{OUT_OF_SCOPE_ITEMS}`):
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
# <Script resolution block from work-on/build.md, verbatim>
|
|
428
|
+
SCOPE_FILE=$(mktemp "${TMPDIR:-/tmp}/contract-scope.XXXXXX")
|
|
429
|
+
printf '### Out of Scope\n\n%s\n' "{OUT_OF_SCOPE_ITEMS}" > "$SCOPE_FILE"
|
|
430
|
+
SCRIPT_REF=$(resolve_script 'check-contract-scope')
|
|
431
|
+
case "$SCRIPT_REF" in
|
|
432
|
+
prose:*) echo "SCOPE_CHECK=PROSE" ;; # apply the three-disposition rules above by hand
|
|
433
|
+
*) bash "${SCRIPT_REF#*:}" validate "$SCOPE_FILE"; echo "SCOPE_CHECK_RC=$?" ;;
|
|
434
|
+
esac
|
|
435
|
+
rm -f "$SCOPE_FILE"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
- `SCOPE_CHECK_RC=0` (or `SCOPE_CHECK=PROSE` after a manual check) → post the contract.
|
|
439
|
+
- `SCOPE_CHECK_RC=1` → the stderr lists each bad item. Fix the items (add the missing disposition, file the missing follow-up, name a backticked path), recompose, and re-run once. Still invalid → Blocked exit with blocker `contract-scope-invalid: <first bad item>`.
|
|
440
|
+
- `SCOPE_CHECK_RC=2` → a usage error in this step: fix the invocation and retry once, then Blocked exit.
|
|
351
441
|
|
|
352
442
|
**Before posting, read the attribution config**:
|
|
353
443
|
```bash
|
|
@@ -387,6 +477,8 @@ run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CONTRACT -->
|
|
|
387
477
|
${ATTRIBUTION_LINE}"
|
|
388
478
|
```
|
|
389
479
|
|
|
480
|
+
`{OUT_OF_SCOPE_ITEMS}` is the validated B2.0 section body: `None.`, or typed bullets using the literal dispositions `deferred → #N`, `not-affected:` and `accepted-risk:`. Never post free text there.
|
|
481
|
+
|
|
390
482
|
Contract must be grounded in the investigation report. Every deliverable file must appear in the affected files list from the investigator. Adversarially validate the proposed fix against adjacent system layers before posting.
|
|
391
483
|
|
|
392
484
|
### B2.1: Post FORGE:CLAIM on coordination issue (conditional — when running under orchestration batch) <!-- Added: forge#1736 -->
|
|
@@ -529,7 +621,12 @@ Review findings per PR rise steeply with diff size, so an oversized diff is spli
|
|
|
529
621
|
|
|
530
622
|
```bash
|
|
531
623
|
# <Script resolution block from above goes here, verbatim>
|
|
532
|
-
|
|
624
|
+
# Fresh origin/base for the merge-base measurement: retry with 10s/30s/60s backoff, record whether it ever succeeded
|
|
625
|
+
FETCH_OK=0
|
|
626
|
+
for _delay in 0 10 30 60; do
|
|
627
|
+
[ "$_delay" -gt 0 ] && sleep "$_delay"
|
|
628
|
+
if git -C "{WORKTREE_PATH}" fetch origin "{PR_BASE}" 2>/dev/null; then FETCH_OK=1; break; fi
|
|
629
|
+
done
|
|
533
630
|
THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)
|
|
534
631
|
EXCLUDES=()
|
|
535
632
|
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
@@ -539,12 +636,13 @@ case "$SCRIPT_REF" in
|
|
|
539
636
|
prose:*) echo "SIZE_GATE: scripts/diff-size.sh unresolved (prose tier) — gate skipped with a note in the FORGE:BUILDER comment" ;;
|
|
540
637
|
*)
|
|
541
638
|
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --threshold "$THRESHOLD" ${EXCLUDES[@]+"${EXCLUDES[@]}"}) && SIZE_RC=0 || SIZE_RC=$?
|
|
542
|
-
echo "SIZE_RC=${SIZE_RC}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
639
|
+
echo "SIZE_RC=${SIZE_RC}"; echo "FETCH_OK=${FETCH_OK}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
543
640
|
esac
|
|
544
641
|
```
|
|
545
642
|
|
|
546
643
|
- Prose tier (script unresolvable) → skip the gate, continue to B6 (the script is part of the install; a missing install must not strand every build).
|
|
547
|
-
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over
|
|
644
|
+
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over=`, AND `FETCH_OK=0` (the base fetch failed after the 10s/30s/60s retries, worst case about 100s) → Blocked exit with blocker `github-unavailable: could not fetch origin/{PR_BASE} for diff-size measurement` (transient; the router retry rule applies).
|
|
645
|
+
- The same unmeasurable result with `FETCH_OK=1` (the fetch succeeded, so the script itself failed) → Blocked exit with blocker `size-gate-unavailable: diff-size.sh failed (rc=<N>)`. Either way, do NOT treat an unmeasurable diff as under threshold.
|
|
548
646
|
- Otherwise parse `diff_lines`, `excluded_lines`, `threshold`, `over` and the `top=` lines.
|
|
549
647
|
|
|
550
648
|
**Step 2 — look up an override** (only when `over=true`). Both lookups are anchored, trust-filtered through `scripts/trusted-comments.sh` (trusted association, Bot, or `FORGE_TRAIL_TRUSTED_LOGINS`), and fail closed:
|
|
@@ -602,7 +700,7 @@ BUILD_RESULT:
|
|
|
602
700
|
blocker: diff-size-over-threshold: {diff_lines} > {threshold}; split proposal posted in FORGE:DIFF_SIZE
|
|
603
701
|
```
|
|
604
702
|
|
|
605
|
-
**Resume**: a re-invoked build
|
|
703
|
+
**Resume**: a re-invoked build re-enters here via the B0 size-gate resume branch (partial `FORGE:BUILDER` plus latest trusted `FORGE:DIFF_SIZE` `result: OVER` plus a staged diff in the kept worktree), so it does not re-run implement; this covers both exits that leave BUILDER incomplete, `NEEDS_DECOMPOSE` and the decompose-loop-guard Blocked exit. B5.5 re-measures: if still over with no override it re-emits `NEEDS_DECOMPOSE` without a duplicate comment (the loop-guard case re-emits the same Blocked exit); when a valid `FORGE:SIZE_OVERRIDE` has appeared since, it continues via `OVERRIDDEN`.
|
|
606
704
|
# MUST CONTINUE to Phase B6 — the size gate result is intermediate, NOT terminal (validation still required), except the NEEDS_DECOMPOSE and Blocked exits above.
|
|
607
705
|
|
|
608
706
|
---
|
|
@@ -782,6 +880,7 @@ fi
|
|
|
782
880
|
If `GATE_PASS = false`, the build repairs itself **once** before anything escalates (a failing acceptance check is pipeline work, not a human decision):
|
|
783
881
|
|
|
784
882
|
1. Count `<!-- FORGE:ACCEPTANCE_REPAIR: issue={NUMBER} -->` comments on the issue. If one already exists, skip to step 4.
|
|
883
|
+
1.5. **Decomposition guard** <!-- Added: forge#3543 -->: read the `### Decomposition Assessment` of the latest FORGE:INVESTIGATOR comment. If it is `**YES**`, a failing check that belongs to a sub-item the assessment assigns to a separate issue (named in the check's `description`/`# item-N` tag, or matching a listed sub-issue title) is NOT repaired: exclude it from the `--fix-acceptance` list. When ambiguous, treat the check as decomposed-out (fail safe). If every remaining failure is decomposed-out, do not invoke implement: leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "acceptance check {ID} belongs to decomposed item {N} — repair refuses to implement work the investigator assigned to a separate issue".
|
|
785
884
|
2. Post that marker (with the failed check ids), remove `needs-human` if this gate added it, then invoke
|
|
786
885
|
`Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-acceptance \"<failed check ids and details>\"")`
|
|
787
886
|
then re-derive `{CHANGED_FILES}` from the repaired worktree (never reuse the pre-repair value, and never omit `--files`; an empty list falls back to this diff):
|
|
@@ -20,7 +20,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
|
20
20
|
**Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
|
|
21
21
|
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`. STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
|
|
22
22
|
|
|
23
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on
|
|
23
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on `<!-- DECOMPOSE:YES -->` (or a `**YES**` verdict under `### Decomposition Assessment`) in the newest trusted FORGE:INVESTIGATOR report, instead of terminating in place, so a headless `runIssue()` walk dispatches this subcommand rather than stopping short.
|
|
24
24
|
|
|
25
25
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation. <!-- Added: forge#1827 -->
|
|
26
26
|
**NEVER use plan mode (EnterPlanMode).**
|
|
@@ -513,7 +513,7 @@ case "$TIER" in
|
|
|
513
513
|
adaptive|universal) run bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
|
|
514
514
|
prose)
|
|
515
515
|
run gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
|
|
516
|
-
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
516
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
517
517
|
;;
|
|
518
518
|
esac
|
|
519
519
|
```
|
|
@@ -416,7 +416,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
416
416
|
case "$TIER" in
|
|
417
417
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
418
418
|
prose)
|
|
419
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
419
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
420
420
|
;;
|
|
421
421
|
esac
|
|
422
422
|
```
|
|
@@ -609,7 +609,7 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
609
609
|
|
|
610
610
|
The comment MUST include a terminal sentinel at the very end, AFTER all required sections are present. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
|
|
611
611
|
|
|
612
|
-
- **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
|
|
612
|
+
- **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 reads it as the closing line of the newest trusted `<!-- FORGE:INVESTIGATOR -->` comment (never as a substring of prose or from an untrusted author) 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).
|
|
613
613
|
- **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` in Phase 1D — only INVALID gets the distinct terminal sentinel).
|
|
614
614
|
|
|
615
615
|
Compute the sentinel once, before building the comment body:
|
|
@@ -635,6 +635,9 @@ ANNOTATION_BODY=$(node packages/protocol/src/cli.js emit INVESTIGATOR \
|
|
|
635
635
|
--field "Task Type={TASK_TYPE}" \
|
|
636
636
|
--field "Decomposition Assessment={YES|NO} — {reason}")
|
|
637
637
|
# ANNOTATION_BODY now has opening tag + required fields + INVESTIGATION:COMPLETE sentinel.
|
|
638
|
+
# After appending the body sections, the Decomposition Assessment MUST be followed by exactly one
|
|
639
|
+
# machine-readable marker line (`<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->`) — see the
|
|
640
|
+
# "Decomposition marker" rule below the template. The codec does not add it for you.
|
|
638
641
|
# NOTE: the codec's sentinel is fixed — do not use this path when Verdict=INVALID (see caveat above).
|
|
639
642
|
# Append the Markdown body sections to it before posting.
|
|
640
643
|
```
|
|
@@ -700,6 +703,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
700
703
|
### Decomposition Assessment
|
|
701
704
|
**{YES|NO}** — {reason}
|
|
702
705
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
706
|
+
<!-- DECOMPOSE:{YES|NO} -->
|
|
703
707
|
|
|
704
708
|
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
705
709
|
{For each item in the issue's ## Acceptance Criteria section, emit one machine-checkable check line using the format below. If the issue has no Acceptance Criteria section, derive checks from the Recommendation above. Each check MUST be specific, observable, and testable — not vague prose. Checks are consumed by build/validate Phase B6.5 as the merge gate.}
|
|
@@ -731,6 +735,12 @@ ${ANNOTATION_LINK_FOOTER}
|
|
|
731
735
|
${INVESTIGATION_SENTINEL}"
|
|
732
736
|
```
|
|
733
737
|
|
|
738
|
+
**Rules for the template above (instructions, not template text; never copy them into the posted comment):**
|
|
739
|
+
|
|
740
|
+
**Decomposition marker (MANDATORY, both output paths — template and codec)**: emit exactly one `<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->` line per investigator comment, matching the `**YES**`/`**NO**` verdict, on its own line (nothing else on that line) after the verdict line and any sub-issue list (the `**YES**`/`**NO**` line must stay directly under the heading — the resume parser reads it). The headless engine routes to `work-on/decompose` on this marker (or on the `**YES**` heading in older comments). Never quote or mention the opposite marker anywhere in the comment. Never emit it when Verdict=INVALID. <!-- Added: forge#3543 -->
|
|
741
|
+
|
|
742
|
+
**Decomposition scopes the Acceptance Spec**: when the assessment is YES, the Acceptance Spec MUST cover only the first, in-scope item — or tag each check with the sub-item it belongs to (append `# item-N` to the description). Never emit checks for work the assessment assigns to a separate sub-issue: build's acceptance gate would otherwise fail on them and its repair loop would pull that work into this branch. <!-- Added: forge#3543 -->
|
|
743
|
+
|
|
734
744
|
**Do not hardcode `<!-- INVESTIGATION:COMPLETE -->` as the closing line.** The closing line MUST be the `${INVESTIGATION_SENTINEL}` variable computed above — it resolves to `<!-- INVESTIGATION:INVALID -->` for an INVALID verdict and `<!-- INVESTIGATION:COMPLETE -->` otherwise. `INVESTIGATION:COMPLETE` and `INVESTIGATION:INVALID` are mutually exclusive within a single posted comment — never emit both.
|
|
735
745
|
|
|
736
746
|
---
|
|
@@ -1183,7 +1193,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1183
1193
|
case "$TIER" in
|
|
1184
1194
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
1185
1195
|
prose)
|
|
1186
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1196
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1187
1197
|
;;
|
|
1188
1198
|
esac
|
|
1189
1199
|
```
|
|
@@ -1203,7 +1213,7 @@ RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIP
|
|
|
1203
1213
|
case "$TIER" in
|
|
1204
1214
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
1205
1215
|
prose)
|
|
1206
|
-
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1216
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1207
1217
|
;;
|
|
1208
1218
|
esac
|
|
1209
1219
|
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}" # allowlist:check-command-side-effects
|