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.
Files changed (76) hide show
  1. package/bin/engine/phases-args.test.mjs +2 -1
  2. package/bin/engine/phases.mjs +349 -90
  3. package/bin/engine/runlog.mjs +25 -2
  4. package/bin/engine-cli.mjs +4 -1
  5. package/bin/engine.mjs +72 -19
  6. package/bin/hooks/interactive-engine.mjs +135 -209
  7. package/bin/hooks/pre-tool-use.mjs +16 -4
  8. package/bin/labels.json +10 -0
  9. package/bin/opencode-adapter.mjs +1 -1
  10. package/bin/orchestrate-preflight.mjs +1 -1
  11. package/bin/runner.mjs +258 -23
  12. package/bin/watch-utils.mjs +1 -0
  13. package/commands/autopilot.md +6 -6
  14. package/commands/cleanup.md +8 -5
  15. package/commands/orchestrate/phase-1-resolve.md +1 -1
  16. package/commands/orchestrate/phase-3-dependency.md +14 -19
  17. package/commands/orchestrate/phase-4-execution.md +80 -53
  18. package/commands/orchestrate/phase-6-report.md +6 -0
  19. package/commands/orchestrate/safety.md +1 -1
  20. package/commands/pipeline-health/phase-2-metrics.md +39 -0
  21. package/commands/pipeline-health/phase-5-report.md +1 -0
  22. package/commands/pipeline-resume.md +1 -1
  23. package/commands/pipeline-status.md +2 -2
  24. package/commands/recover-orphans.md +5 -5
  25. package/commands/review-pr-agents/api.md +1 -1
  26. package/commands/review-pr-agents/auth.md +1 -1
  27. package/commands/review-pr-agents/infra.md +3 -3
  28. package/commands/review-pr-agents/protocols.md +15 -6
  29. package/commands/review-pr-agents/scraper.md +1 -1
  30. package/commands/review-pr-agents/security.md +5 -5
  31. package/commands/review-pr-agents.md +12 -12
  32. package/commands/review-pr-staging.md +226 -8
  33. package/commands/review-pr.md +394 -53
  34. package/commands/work-on/build/architect.md +36 -0
  35. package/commands/work-on/build/implement.md +1 -1
  36. package/commands/work-on/build/validate.md +88 -4
  37. package/commands/work-on/build.md +106 -7
  38. package/commands/work-on/decompose.md +2 -2
  39. package/commands/work-on/investigate.md +14 -4
  40. package/commands/work-on/remediate.md +137 -25
  41. package/commands/work-on/review.md +25 -9
  42. package/commands/work-on.md +15 -15
  43. package/docs/CONFIG.md +29 -0
  44. package/docs/RELEASE-NOTES.md +45 -0
  45. package/docs/spec/forge-protocol-v1.md +22 -1
  46. package/docs/spec/label-state-machine.md +6 -1
  47. package/docs/spec/review-protocol.md +20 -5
  48. package/package.json +2 -1
  49. package/packages/protocol/src/phases.js +21 -0
  50. package/scripts/check-blast-radius.sh +164 -0
  51. package/scripts/check-blast-radius.test.sh +183 -0
  52. package/scripts/check-contract-scope.sh +137 -0
  53. package/scripts/check-contract-scope.test.sh +83 -0
  54. package/scripts/check-spec-bash.sh +60 -3
  55. package/scripts/check-spec-bash.test.sh +56 -0
  56. package/scripts/check-spec-markers.sh +29 -0
  57. package/scripts/check-test-wiring.sh +57 -0
  58. package/scripts/check-test-wiring.test.sh +70 -0
  59. package/scripts/classify-finding.sh +72 -8
  60. package/scripts/classify-finding.test.sh +77 -8
  61. package/scripts/close-dossier-no-main-commit.test.sh +12 -12
  62. package/scripts/diff-size.test.sh +3 -2
  63. package/scripts/doctor-pipeline-state.sh +35 -0
  64. package/scripts/forge-annotation.sh +3 -0
  65. package/scripts/forge-root.test.sh +103 -13
  66. package/scripts/forge-run.sh +2 -2
  67. package/scripts/recovery-claim-live.test.sh +10 -7
  68. package/scripts/remediate-basesync.test.sh +103 -0
  69. package/scripts/review-delta.sh +206 -0
  70. package/scripts/review-delta.test.sh +213 -0
  71. package/scripts/review-pr-domain-map.test.sh +87 -0
  72. package/scripts/review-pr-gate2.test.sh +84 -0
  73. package/scripts/review-pr-staging-scope.test.sh +99 -0
  74. package/scripts/run-shell-tests.sh +115 -0
  75. package/scripts/select-fix-targets.sh +17 -5
  76. package/scripts/transition-label.sh +3 -1
@@ -2,6 +2,51 @@
2
2
 
3
3
  Operator-facing notes for behavior changes that need action or awareness. Newest first.
4
4
 
5
+ ## Review damper: LOW needs keyword + CONFIRMED; a missing classifier blocks the review (#3639)
6
+
7
+ `scripts/classify-finding.sh` is stricter for LOW findings. A safety keyword now files a LOW finding only when it is also CONFIRMED (LOW LIKELY/POSSIBLE with a keyword is a NOTE), and a dedicated domain agent (Auth, Billing, Concurrency, Database) rescues MEDIUM findings only, no longer LOW CONFIRMED. MEDIUM behaviour is unchanged.
8
+
9
+ `/review-pr` §6B.5 and `/review-pr-staging` §7B.5 no longer let the model hand-classify (`classifier=manual`) when the script cannot be resolved. They return `REVIEW_RESULT: status: BLOCKED` with blocker `classifier unavailable (classify-finding.sh not resolvable through the trusted resolver)`, file nothing and do not merge or approve a deploy. The staging per-finding runtime-failure fallback is renamed `ISSUE classifier-error`. The `classifier=` marker stays and is always `script`.
10
+
11
+ Action: if reviews now report `classifier unavailable`, fix the ForgeDock install so the trusted resolver finds `scripts/classify-finding.sh`, then re-run the review.
12
+
13
+ ## Introduced-line gate no longer demotes CRITICAL/HIGH (#3597, #3598)
14
+
15
+ `/review-pr` §6B.4 gate 2 never routes a CRITICAL/HIGH finding (any confidence) as `pre-existing`: a new caller of an unchanged unsafe helper is an interaction defect (`protocols.md` §5), so it stays a blocking `review-finding` under §7B criterion 2, including with `--auto-merge`. The pre-existing route now covers MEDIUM and lower only. A file with no `patch` in the PR files API (large or truncated diff) is treated as UNKNOWN and its findings are kept, never routed as pre-existing. Expect slightly more blocking findings on PRs that touch unsafe code through unchanged helpers.
16
+
17
+ ## Contract out-of-scope items now typed; review demotes matching findings (#3447)
18
+
19
+ The `### Out of Scope` section of `FORGE:CONTRACT` is no longer free text. It is `None.` or bullets that each name a backticked path and one disposition: `deferred → #N`, `not-affected: <evidence>` or `accepted-risk: <reason>`. A contract that keeps a known-flawed design must list it as `deferred → #N`.
20
+
21
+ - **Build** (B2.0) files a follow-up issue for each `deferred` item before the contract is posted (idempotent through a `FORGE:DEFERRED_FROM: #N` body marker) and validates the section with the new `scripts/check-contract-scope.sh`. An untyped item blocks the build after one retry.
22
+ - **Review** (`/review-pr` §6B.5) reads the latest trusted contract and passes it to `scripts/classify-finding.sh --contract-scope`. A finding on a `deferred` path (follow-up still open) or an `accepted-risk` path becomes a note instead of a new `review-finding`; deferred matches are commented on the existing issue. `not-affected` items, HIGH/CRITICAL findings, safety-exempt `accepted-risk` findings and files the PR itself changed are never demoted. `FORGE:NOTE_DISPOSITION` gains `contract_demoted=N`.
23
+ - The review scope-creep check now reads only the contract's `### Deliverables`, so backticked out-of-scope paths no longer count as in-scope files.
24
+
25
+ Action: none. Older contracts with free-text Out of Scope sections are not parsed and demote nothing.
26
+
27
+ ## Review provenance gates and noise share (#3452)
28
+
29
+ `/review-pr` §6B.4 now checks each finding before it can become a `review-finding` issue: the cited line must exist at the reviewed head SHA (else dropped as stale), must be added or changed by the PR diff (else routed as `pre-existing`), and must carry a concrete `**Failure scenario**` (else demoted to a note; CRITICAL/HIGH, any confidence, are exempt). Pre-existing defects are never counted as this PR's findings or in the amplification ratio; only MEDIUM or lower ones are routed this way, and the safety-domain ones are filed once with the new `pre-existing` label. The `FORGE:NOTE_DISPOSITION` record gains `findings_dropped_stale`, `findings_preexisting`, `notes_demoted_no_scenario`, and `/pipeline-health` reports a noise share (2E.5).
30
+
31
+ Action: run `npx forgedock labels setup` to create the `pre-existing` label (the review also creates it on demand).
32
+
33
+ ## Diff-size gate in `/work-on` build (#3450)
34
+
35
+ `/work-on` build now measures the changed lines (added plus deleted) after implement stages its changes and before validate commits them. The gate is **on by default at 1000 lines**. Review findings per PR rise steeply with diff size, so an oversized build is split instead of validated.
36
+
37
+ - **Over the threshold**: the build posts a `FORGE:DIFF_SIZE` comment with a split proposal and exits `NEEDS_DECOMPOSE`; the router then runs `work-on:decompose`.
38
+ - **Tune or disable**: set `build.diff_size.threshold` in `forge.yaml` (`0` disables the gate) and add exclusions with `build.diff_size.exclude_globs`. Lockfiles, `*.min.*`, `*.snap` and vendored/generated directories are excluded by default.
39
+ - **Per-issue bypass**: post a `FORGE:SIZE_OVERRIDE` comment with a non-empty justification on the line after the marker. Only comments from trusted authors count.
40
+ - **No re-decomposition**: an issue that is already a decomposed child, or has `FORGE:DECOMPOSED`, blocks until an override is posted instead of being split again.
41
+
42
+ Action: if your builds routinely exceed 1000 changed lines, raise `build.diff_size.threshold` or set it to `0`. See the `build` section of [CONFIG.md](CONFIG.md).
43
+
44
+ ## Stricter `review-pr` / `review-pr-staging` argument rejection (#3466)
45
+
46
+ `/review-pr` and `/review-pr-staging` now reject an argument string as a whole if it contains a quote, backtick, `$`, backslash, newline or tab. Nothing is parsed from a rejected string, so no PR number, repo, merge value or flag survives, and `--auto-merge` is not honoured. `/review-pr-staging` stops with nothing reviewed and nothing posted.
47
+
48
+ Action: callers must pass `--gh-flag -R owner/repo` unquoted, and must not forward untrusted text (issue titles, comment bodies) into the argument string.
49
+
5
50
  ## Spec bash no longer corrupted by Claude Code argument substitution
6
51
 
7
52
  Claude Code replaces `$0`..`$9` in a skill body with the invocation's arguments (0-based), so spec bash loaded via `Skill(...)` with args was silently rewritten: `awk '{print $2}'` became `awk '{print --issue}'`, `local AGENT="$1"` became `local AGENT="--auto-merge"`, which broke `/review-pr` agent selection and the CI/deploy comparisons with no error. `${N}`, `$(N)`, `$NF`, `$@`, `$#` and `$10`+ are not substituted.
@@ -251,9 +251,30 @@ findings and what will actually be built.
251
251
  {auth model, new configuration, data-safety, security surface, migration risk}
252
252
 
253
253
  ### Out of Scope
254
- {items explicitly excluded}
254
+ None.
255
255
  ```
256
256
 
257
+ `### Out of Scope` is either the single line `None.` or a list of typed bullets. Each bullet names at
258
+ least one backticked path or symbol followed by exactly one disposition:
259
+
260
+ ```
261
+ - `path` — deferred → #N: <why>
262
+ - `path` — not-affected: <evidence>
263
+ - `path` — accepted-risk: <reason>
264
+ ```
265
+
266
+ - `deferred → #N`: a follow-up issue `#N` was filed before the contract was posted (`->` is accepted
267
+ for `→`). A contract that keeps a known-flawed design must list the flaw as a `deferred` item.
268
+ - `not-affected: <evidence>`: the path was checked and is not reached by the change.
269
+ - `accepted-risk: <reason>`: a known gap that is knowingly kept.
270
+ - Free-text items, a `deferred` item without an issue number, or a missing/empty section are invalid.
271
+ `scripts/check-contract-scope.sh validate|list` is the reference parser (TSV
272
+ `disposition<TAB>path<TAB>issue`).
273
+ - Review demotion: a finding on a `deferred` path (follow-up issue still open) or an `accepted-risk`
274
+ path becomes a note instead of a new `review-finding`; `not-affected` items, HIGH/CRITICAL findings,
275
+ safety-exempt `accepted-risk` findings and files the PR changed are never demoted. Only a trusted
276
+ author's contract is read. The count is recorded as `contract_demoted` in `FORGE:NOTE_DISPOSITION`.
277
+
257
278
  #### `FORGE:CONTEXT`
258
279
 
259
280
  - **Written by**: a context-gathering agent
@@ -14,6 +14,7 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
14
14
  | `workflow:ready-to-build` | Investigation complete, build not started | Investigator agent |
15
15
  | `workflow:building` | Build phase active | Builder agent |
16
16
  | `workflow:in-review` | PR created, review active | Orchestrator agent |
17
+ | `workflow:remediating` | Autonomous remediation pending: review handed a ci-gate refusal, an in-PR fix request or a pure base conflict to `work-on:remediate`, which fixes it with no human action. NOT a human gate, NOT terminal; orchestrate classifies it IN_PROGRESS (never GATED) and `work-on:remediate` clears it on every exit | Review phase (`work-on/review.md` R4); cleared by `work-on/remediate.md` |
17
18
  | `workflow:awaiting-merge` | Remediated + re-reviewed, awaiting a human merge decision | Review-pr Phase 8 (auto-merge guard) |
18
19
  | `workflow:merged` | PR merged, issue closed | Close phase agent |
19
20
  | `workflow:invalid` | Issue closed as invalid | Investigator agent |
@@ -21,6 +22,8 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
21
22
  | `needs-human` | Pipeline blocked, human intervention required | Any agent on error |
22
23
  | `workflow:engine-error` | Pipeline blocked by the engine/tool itself breaking (CLI crash, missing SDK/API key, exhausted retries with no attempt ever reaching a real outcome) — not a genuine content-level judgment call | `terminate()` in `bin/engine.mjs` |
23
24
 
25
+ `workflow:remediating` vs `needs-human` (forge#3541): `needs-human` is reserved for genuine human decisions and external actions (an exhausted remediation bound, an unresolvable conflict, a policy gate, spend, real-environment validation). Work the pipeline fixes by itself (a pure base-sync, a red CI gate, an in-PR finding round) is marked `workflow:remediating` instead, so operators are not alerted and orchestrate does not hold DAG dependents as GATED. `work-on:remediate` accepts either label (a bare `needs-human` from an older run still works), swaps it for `workflow:in-review` on a FIXABLE run, and on every BLOCKED/UNFIXABLE exit clears `workflow:remediating` and asserts `needs-human`.
26
+
24
27
  `workflow:awaiting-merge` vs `needs-human`: both are terminal (the pipeline stops advancing the issue automatically), but they mean different things. `needs-human` means the pipeline hit a condition it cannot resolve on its own (genuinely blocked — conflicting PR, failed verdict, calibration/trust escalation, etc.) and a human must diagnose and act. `workflow:awaiting-merge` means the opposite: the PR was previously escalated to `needs-human`, has since been remediated and re-reviewed to a clean `APPROVED` verdict with no mergeability blockers, but does not yet meet the automated auto-land bar (see forge#1809 Q1) — a human only needs to click merge, not diagnose a problem. `scripts/transition-label.sh` only clears `needs-human` (best-effort) when the target state is `workflow:awaiting-merge` — every other forward transition leaves a pre-existing `needs-human` label untouched, preserving its sticky/terminal semantics.
25
28
 
26
29
  `workflow:engine-error` vs `needs-human` (taxonomy split — commit `90376f5` / #2261): before this commit, `terminate()` wrote `needs-human` for *every* exhausted-retry blocked outcome, including cases where the engine/CLI tool itself was breaking (crash, missing API key, no SDK) rather than a genuine human-judgment call. As of `90376f5`, `runPhaseWithRetry()` tags an outcome `engine-error` when every exhausted attempt failed via a thrown runner exception (never reached `detectOutcome()`) — the engine/tool is broken, not the issue content — and `terminate()` writes `workflow:engine-error` instead of `needs-human` for that case. A genuine content-level block (unmerged PR, unresolved branch, a fixed-point zero-commits case) still writes `needs-human` unchanged. **Consequence for downstream consumers**: any dashboard, query, or health metric that counts `needs-human` issues computed a different, larger population before `90376f5` than after — a subset of what used to be `needs-human` now shows up as `workflow:engine-error` instead. This is a one-time taxonomy discontinuity, not a real change in pipeline reliability; consumers tracking `needs-human` counts over time should also track `workflow:engine-error` from this commit forward to get a like-for-like comparison. `bin/labels.json` already includes `workflow:engine-error` in this repo; syncing it to consumer/satellite repos is tracked separately (see forge#2346).
@@ -33,6 +36,8 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
33
36
  → workflow:ready-to-build [Phase 2: Investigation complete]
34
37
  → workflow:building [Phase 3: Build started]
35
38
  → workflow:in-review [Phase 4: PR created]
39
+ → workflow:remediating [Review R4: autonomous ci-gate / in-pr-fix / base-sync handoff; no human needed]
40
+ → workflow:in-review [Remediate M1: fix under way] (workflow:in-review → workflow:remediating → workflow:in-review)
36
41
  → workflow:awaiting-merge [TERMINAL] [Phase 5/review-pr: re-reviewed after needs-human, awaiting human merge]
37
42
  → workflow:merged [TERMINAL] [Phase 5: PR merged]
38
43
  → workflow:invalid [TERMINAL] [Any phase: closed as invalid]
@@ -63,7 +68,7 @@ At most one `workflow:*` label should be active on an issue at any time. When tr
63
68
  ```bash
64
69
  gh issue edit {NUMBER} -R {REPO} \
65
70
  --add-label "workflow:building" \
66
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed"
71
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed"
67
72
  ```
68
73
 
69
74
  This is enforced by `scripts/transition-label.sh` — use `resolve_script 'transition-label'` rather than calling `gh issue edit` directly when possible.
@@ -25,6 +25,9 @@ Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to locate pro
25
25
  Every agent MUST follow this protocol:
26
26
 
27
27
  ### 1. Start From the PR Diff
28
+
29
+ **Input scoping**: You have been given a pre-computed diff slice containing only the files relevant to your domain (`[DOMAIN_DIFF_SLICE]`). Do NOT re-fetch `gh pr diff [PR_NUMBER]` in full — use the slice provided. This is capped at ~100K chars, mirroring the runner's tool-result limit.
30
+
28
31
  ```bash
29
32
  # Verify review is still current before reading diff
30
33
  CURRENT_SHA=$(gh pr view [PR_NUMBER] --json headRefOid --jq '.headRefOid')
@@ -34,10 +37,19 @@ if [ "$CURRENT_SHA" != "[REVIEW_SHA]" ]; then
34
37
  echo "Current HEAD: $(echo $CURRENT_SHA | cut -c1-7)"
35
38
  fi
36
39
 
40
+ # List files in your domain slice
37
41
  gh pr diff [PR_NUMBER] --name-only
38
- gh pr diff [PR_NUMBER]
42
+
43
+ # Materialize the pre-computed domain diff slice supplied by the orchestrator.
44
+ # Persona greps below read "$SLICE_FILE" -- they never re-fetch the full diff.
45
+ SLICE_FILE=$(mktemp)
46
+ cat > "$SLICE_FILE" <<'FORGE_SLICE_EOF'
47
+ [DOMAIN_DIFF_SLICE]
48
+ FORGE_SLICE_EOF
39
49
  ```
40
50
 
51
+ **Empty or truncated slice**: if the slice is empty or ends mid-hunk, read the specific changed files with bounded reads (`sed -n 'X,Yp'`, `head -N`) — never fall back to fetching the full PR diff.
52
+
41
53
  **Hot-spot prior**:
42
54
  [CHURN_CONTEXT]
43
55
 
@@ -82,7 +94,7 @@ If a file you are reviewing is listed above as a hot-spot, apply deeper scrutiny
82
94
  3. Will this cause degraded performance? → **MEDIUM**
83
95
  4. Is it genuinely cosmetic with no runtime impact after tracing all consumers? → **LOW**
84
96
 
85
- If you're unsure whether something is cosmetic or a runtime error, **assume it's a runtime error** and flag it for investigation. A false positive costs a minute of review time. A missed runtime error costs production downtime.
97
+ **Precision first.** Report defects that are likely to be real in the code this PR changes and the code it reaches. If you cannot verify a runtime impact after tracing the path, report the finding at **POSSIBLE** confidence — do not inflate its severity or confidence to be safe. A false positive costs reviewer trust and triggers a cascade of follow-up issues and re-reviews; an unverified suspicion belongs at POSSIBLE, where it stays a non-blocking note, not at an inflated severity.
86
98
 
87
99
  ### 5. INTERACTION ANALYSIS — "Pre-existing" Is Not "Safe"
88
100
 
@@ -95,6 +107,8 @@ A redundant import, an unused variable, or a duplicated constant may be harmless
95
107
  2. For each reference, ask: "Does the pre-existing construct cause this new line to fail at runtime?"
96
108
  3. If yes → CONFIRMED finding, not a dismissal
97
109
 
110
+ **Severity rule**: CRITICAL/HIGH findings, any confidence, are never routed as pre-existing by the `/review-pr` introduced-line gate (§6B.4): they stay blocking `review-finding` issues. The `**Scope**` tag does not gate routing.
111
+
98
112
  ### 6. FALSE POSITIVE PREVENTION
99
113
 
100
114
  **Before claiming variable scope issues:** Read the FULL function, count indentation levels, check if/else structure.
@@ -120,7 +134,7 @@ Every finding must include:
120
134
 
121
135
  ## Structured Findings Protocol
122
136
 
123
- **All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot create GitHub issues, and findings die as unread PR comments. Every finding that doesn't become a GitHub issue is a finding that will never be addressed.
137
+ **All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot classify, file, or track what you found. The orchestrator (`/review-pr` §6B.4/§6B.5) decides disposition: CONFIRMED/LIKELY findings that pass its gates become issues, while LOW severity and POSSIBLE confidence findings are non-blocking NOTEs that are not filed as standalone issues.
124
138
 
125
139
  ### Persist Before Post
126
140
 
@@ -138,7 +152,7 @@ Append this block at the very end of your comment (after the `---` footer line,
138
152
 
139
153
  ### Rules
140
154
 
141
- 1. **Include ALL findings at CONFIRMED, LIKELY, and POSSIBLE confidence** — every finding becomes a GitHub issue. Nothing stays as just a PR comment. **POSSIBLE findings are informational advisories (P3/non-blocking)** — they are tracked but do not require a fix PR and do not block merge. CONFIRMED and LIKELY findings are blocking at P1/P2 respectively.
155
+ 1. **Include every finding you can support at CONFIRMED, LIKELY, or POSSIBLE confidence in the block** — the orchestrator needs them all to classify. Inclusion does not mean a GitHub issue is filed: `/review-pr` §6B.4/§6B.5 decide filing. **POSSIBLE confidence and LOW severity findings are non-blocking NOTEs** — reported in the review, never filed as standalone issues, never block merge. CONFIRMED and LIKELY findings are blocking at P1/P2 respectively. Do not pad the block with unverified or speculative items; precision over recall.
142
156
  2. **One line per finding** — sequential numbering (PREFIX-1, PREFIX-2, ...)
143
157
  3. **Confidence**: `CONFIRMED`, `LIKELY`, or `POSSIBLE`
144
158
  4. **Severity**: `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`
@@ -146,8 +160,9 @@ Append this block at the very end of your comment (after the `---` footer line,
146
160
  6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
147
161
  7. **Empty block**: If no findings at all, include just the START/END markers
148
162
  8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
149
- 9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the lowercase dispatched domain.
163
+ 9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
150
164
  10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
165
+ 11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
151
166
 
152
167
  ### Domain Prefixes
153
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -48,6 +48,7 @@
48
48
  ],
49
49
  "scripts": {
50
50
  "test": "node --test \"bin/tests/**/*.test.mjs\" \"bin/engine/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
51
+ "test:sh": "bash scripts/run-shell-tests.sh",
51
52
  "docs:dev": "vitepress dev docs/site",
52
53
  "docs:build": "vitepress build docs/site",
53
54
  "docs:preview": "vitepress preview docs/site"
@@ -54,10 +54,19 @@ export const PHASE_IDS = ['investigate', 'decompose', 'build', 'review', 'remedi
54
54
  * whose presence means this phase has committed.
55
55
  * @property {string} [invalidMarker] - (investigate only) marks the issue INVALID.
56
56
  * @property {string} [decomposedMarker] - (investigate only) marks the issue DECOMPOSED.
57
+ * investigate.md emits it as the HTML comment `<!-- DECOMPOSE:YES -->`.
58
+ * @property {string} [notDecomposedMarker] - (investigate only) the explicit not-decomposed
59
+ * verdict, emitted as `<!-- DECOMPOSE:NO -->`.
60
+ * @property {string} [sizeGateMarker] - (build only) bare annotation-opener of the diff-size gate record.
61
+ * @property {string} [sizeOverrideMarker] - (build only) bare annotation-opener of the size-gate override.
57
62
  * @property {string} [partialMarker] - sentinel for an interrupted/partial annotation.
58
63
  * @property {string} [presenceMarker] - bare annotation-opener substring, used only by
59
64
  * phases whose completion is non-critical (see `context` below) — presence alone
60
65
  * (without `:COMPLETE`) is enough to report a soft/visible skip rather than a hard fail.
66
+ * @property {string} [header] - the typed-comment header (without `<!-- ` / ` -->`) a genuine
67
+ * phase comment must START with (e.g. `FORGE:INVESTIGATOR`). The engine matches the
68
+ * sentinel only inside a trusted comment that leads with this header (forge#3542);
69
+ * additive, the bare marker strings above are unchanged.
61
70
  * @property {string} [completionLabel] - a GitHub issue *label* (not a comment marker)
62
71
  * whose presence means this phase has committed. Only `close` uses this form.
63
72
  */
@@ -66,17 +75,21 @@ export const PHASE_IDS = ['investigate', 'decompose', 'build', 'review', 'remedi
66
75
  export const PHASE_MARKERS = {
67
76
  investigate: {
68
77
  completionMarker: RESERVED_TYPES.INVESTIGATOR.completionSentinel, // 'INVESTIGATION:COMPLETE'
78
+ header: 'FORGE:INVESTIGATOR',
69
79
  invalidMarker: 'INVESTIGATION:INVALID',
70
80
  decomposedMarker: 'DECOMPOSE:YES',
81
+ notDecomposedMarker: 'DECOMPOSE:NO',
71
82
  },
72
83
  // forge#2379: `decompose` actually runs work-on/decompose (sub-issue
73
84
  // fan-out) once `investigate` hands off on `DECOMPOSE:YES` — see
74
85
  // bin/engine/phases.mjs's `decompose` phase entry for the handoff mechanics.
75
86
  decompose: {
76
87
  completionMarker: RESERVED_TYPES.DECOMPOSED.completionSentinel, // 'FORGE:DECOMPOSED:COMPLETE'
88
+ header: 'FORGE:DECOMPOSED',
77
89
  },
78
90
  context: {
79
91
  completionMarker: RESERVED_TYPES.CONTEXT.completionSentinel, // 'FORGE:CONTEXT:COMPLETE'
92
+ header: 'FORGE:CONTEXT',
80
93
  partialMarker: RESERVED_TYPES.CONTEXT.partialSentinel, // 'FORGE:CONTEXT:PARTIAL'
81
94
  // Bare presence marker: context is documented as non-critical (spec §7) — a
82
95
  // missing completion marker is a visible skip, not a hard fail. Only
@@ -87,10 +100,17 @@ export const PHASE_MARKERS = {
87
100
  },
88
101
  architect: {
89
102
  completionMarker: RESERVED_TYPES.ARCHITECT.completionSentinel, // 'FORGE:ARCHITECT:COMPLETE'
103
+ header: 'FORGE:ARCHITECT',
90
104
  partialMarker: RESERVED_TYPES.ARCHITECT.partialSentinel, // 'FORGE:ARCHITECT:PARTIAL'
91
105
  },
92
106
  build: {
93
107
  completionMarker: RESERVED_TYPES.BUILDER.completionSentinel, // 'FORGE:BUILDER:COMPLETE'
108
+ header: 'FORGE:BUILDER',
109
+ // Size-gate (B5.5) records. Neither is a completion sentinel: the gate's
110
+ // NEEDS_DECOMPOSE exit posts no FORGE:BUILDER:COMPLETE, so the engine reads
111
+ // these to route an oversized build to decompose instead of retrying it.
112
+ sizeGateMarker: `FORGE:${RESERVED_TYPES.DIFF_SIZE.type}`, // 'FORGE:DIFF_SIZE'
113
+ sizeOverrideMarker: `FORGE:${RESERVED_TYPES.SIZE_OVERRIDE.type}`, // 'FORGE:SIZE_OVERRIDE'
94
114
  },
95
115
  review: {
96
116
  // No RESERVED_TYPES entry defines this sentinel (REVIEWER has no
@@ -102,6 +122,7 @@ export const PHASE_MARKERS = {
102
122
  // `remediate` phase reads it off the issue, consistent with every other phase here.
103
123
  remediate: {
104
124
  completionMarker: RESERVED_TYPES.REMEDIATION.completionSentinel, // 'FORGE:REMEDIATION:COMPLETE'
125
+ header: 'FORGE:REMEDIATION',
105
126
  },
106
127
  close: {
107
128
  // A GitHub label, not a comment marker — see `completionLabel` above.
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # check-blast-radius.sh — Enforce the FORGE:ARCHITECT blast-radius manifest against the final tree.
6
+ #
7
+ # Usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]
8
+ #
9
+ # <file> is either the extracted manifest block or a whole FORGE:ARCHITECT comment body. Only the text between
10
+ # <!-- FORGE:BLAST_RADIUS:BEGIN --> and <!-- FORGE:BLAST_RADIUS:END --> is parsed (all such regions, concatenated).
11
+ # Grammar, one record per line (blank lines and markdown code-fence lines are the only other lines allowed):
12
+ # SYMBOL: id=<sN> kind=function|field|flag|schema|contract name=<symbol> query="<fixed-string>"
13
+ # HIT: symbol=<sN> file=<path> role=producer|consumer|sibling disposition=change
14
+ # HIT: symbol=<sN> file=<path> role=producer|consumer|sibling disposition=verified-unaffected reason=<one-token>
15
+ # Values contain no spaces (use hyphens in reason). query must match ^[A-Za-z0-9_.:/-]{3,80}$ and not start with "-".
16
+ #
17
+ # For every SYMBOL the query is re-run as a fixed-string search over the final tree (working tree, tracked plus
18
+ # untracked files, so it works before and after the commit). A file that matches must be in the change set
19
+ # (base...HEAD plus anything staged, modified or untracked) or carry a verified-unaffected row.
20
+ # A "change" row whose file is not in the change set is a planned-but-not-done item.
21
+ #
22
+ # Exit codes:
23
+ # 0 covered, or no manifest ("SKIP: no manifest")
24
+ # 1 UNLISTED: <symbol> <file> and/or NOT_DONE: <symbol> <file>
25
+ # 2 malformed manifest, invalid query, unresolvable base/repo, or a git failure — NEVER a pass; callers report DEGRADED
26
+ #
27
+ # Offline: no network, no gh. Portable to bash 3.2: no mapfile, no associative arrays, no sort -V.
28
+
29
+ set -uo pipefail
30
+
31
+ MAX_HITS=200
32
+ MANIFEST=""; BASE=""; REPO="."
33
+ die2() { echo "check-blast-radius: $*" >&2; exit 2; }
34
+
35
+ while [ "$#" -gt 0 ]; do
36
+ case "$1" in
37
+ --manifest) [ "$#" -ge 2 ] || die2 "--manifest needs a value"; MANIFEST="$2"; shift 2 ;;
38
+ --base) [ "$#" -ge 2 ] || die2 "--base needs a value"; BASE="$2"; shift 2 ;;
39
+ --repo) [ "$#" -ge 2 ] || die2 "--repo needs a value"; REPO="$2"; shift 2 ;;
40
+ *) die2 "usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]" ;;
41
+ esac
42
+ done
43
+ [ -n "$MANIFEST" ] || die2 "usage: check-blast-radius.sh --manifest <file> --base <ref> [--repo <path>]"
44
+ [ -f "$MANIFEST" ] || die2 "manifest file not found: $MANIFEST"
45
+
46
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}/check-blast-radius.XXXXXX") || die2 "mktemp failed"
47
+ trap 'rm -rf "$TMP"' EXIT
48
+
49
+ # ---- extract BEGIN..END regions -------------------------------------------------------------------------
50
+ awk -v out="$TMP/region" '
51
+ index($0, "<!-- FORGE:BLAST_RADIUS:BEGIN -->") { if (inb) { bad = 1 } inb = 1; seen = 1; next }
52
+ index($0, "<!-- FORGE:BLAST_RADIUS:END -->") { if (!inb) { bad = 1 } inb = 0; next }
53
+ inb { print > out }
54
+ END { if (inb || bad) exit 3; if (!seen) exit 4 }
55
+ ' "$MANIFEST"
56
+ case $? in
57
+ 0) ;;
58
+ 4) echo "SKIP: no manifest"; exit 0 ;;
59
+ *) die2 "unbalanced FORGE:BLAST_RADIUS:BEGIN/END markers" ;;
60
+ esac
61
+ [ -f "$TMP/region" ] || { echo "SKIP: no manifest"; exit 0; }
62
+
63
+ # has_symbol <id>: awk over the file (no grep -q in a pipeline: SIGPIPE under pipefail misreports the status)
64
+ has_symbol() { awk -F'\t' -v s="$1" '$1==s {ok=1} END{exit ok?0:1}' "$TMP/symbols"; }
65
+
66
+ # ---- parse ----------------------------------------------------------------------------------------------
67
+ : > "$TMP/symbols" # id<TAB>name<TAB>query
68
+ : > "$TMP/hits" # symbol<TAB>file<TAB>disposition
69
+ QRE='^[A-Za-z0-9_.:/-]{3,80}$'
70
+ FRE='^[A-Za-z0-9_./@+=:,-]+$'
71
+ LN=0
72
+ while IFS= read -r line || [ -n "$line" ]; do
73
+ LN=$((LN+1))
74
+ line="${line%$'\r'}"
75
+ trimmed="${line#"${line%%[![:space:]]*}"}"
76
+ case "$trimmed" in
77
+ ""|'```'*) continue ;;
78
+ "SYMBOL: "*) kind_rec=SYMBOL; rest="${trimmed#SYMBOL: }" ;;
79
+ "HIT: "*) kind_rec=HIT; rest="${trimmed#HIT: }" ;;
80
+ *) die2 "line $LN: unrecognised record: $trimmed" ;;
81
+ esac
82
+ id=""; kind=""; name=""; query=""; symbol=""; file=""; role=""; disp=""; reason=""
83
+ set -f
84
+ for tok in $rest; do
85
+ case "$tok" in
86
+ *=*) k="${tok%%=*}"; v="${tok#*=}" ;;
87
+ *) set +f; die2 "line $LN: token without key=value: $tok" ;;
88
+ esac
89
+ case "$k" in
90
+ id) id="$v" ;; kind) kind="$v" ;; name) name="$v" ;;
91
+ query) v="${v#\"}"; v="${v%\"}"; query="$v" ;;
92
+ symbol) symbol="$v" ;; file) file="$v" ;; role) role="$v" ;;
93
+ disposition) disp="$v" ;; reason) reason="$v" ;;
94
+ *) set +f; die2 "line $LN: unknown key: $k" ;;
95
+ esac
96
+ done
97
+ set +f
98
+ if [ "$kind_rec" = SYMBOL ]; then
99
+ case "$id" in s[0-9]*) ;; *) die2 "line $LN: SYMBOL id must look like s1: '$id'" ;; esac
100
+ case "$id" in *[!A-Za-z0-9]*) die2 "line $LN: invalid SYMBOL id: '$id'" ;; esac
101
+ case "$kind" in function|field|flag|schema|contract) ;; *) die2 "line $LN: invalid kind: '$kind'" ;; esac
102
+ [ -n "$name" ] || die2 "line $LN: SYMBOL without name"
103
+ [ -n "$query" ] || die2 "line $LN: empty query"
104
+ case "$query" in -*) die2 "line $LN: query must not start with '-'" ;; esac
105
+ [[ "$query" =~ $QRE ]] || die2 "line $LN: invalid query (want $QRE): $query"
106
+ has_symbol "$id" && die2 "line $LN: duplicate SYMBOL id: $id"
107
+ printf '%s\t%s\t%s\n' "$id" "$name" "$query" >> "$TMP/symbols"
108
+ else
109
+ [ -n "$symbol" ] || die2 "line $LN: HIT without symbol"
110
+ has_symbol "$symbol" || die2 "line $LN: HIT references unknown symbol: $symbol"
111
+ [ -n "$file" ] || die2 "line $LN: HIT without file"
112
+ case "$file" in -*) die2 "line $LN: file must not start with '-'" ;; esac
113
+ [[ "$file" =~ $FRE ]] || die2 "line $LN: invalid file path: $file"
114
+ case "$role" in producer|consumer|sibling) ;; *) die2 "line $LN: invalid role: '$role'" ;; esac
115
+ case "$disp" in
116
+ change) ;;
117
+ verified-unaffected) [ -n "$reason" ] || die2 "line $LN: verified-unaffected requires reason=<token>" ;;
118
+ *) die2 "line $LN: invalid disposition: '$disp'" ;;
119
+ esac
120
+ printf '%s\t%s\t%s\n' "$symbol" "$file" "$disp" >> "$TMP/hits"
121
+ fi
122
+ done < "$TMP/region"
123
+
124
+ if [ ! -s "$TMP/symbols" ]; then
125
+ if [ -s "$TMP/hits" ]; then die2 "HIT records without any SYMBOL"; fi
126
+ echo "SKIP: no manifest"; exit 0
127
+ fi
128
+
129
+ # ---- repo, base, change set -----------------------------------------------------------------------------
130
+ case "$BASE" in ""|-*) die2 "invalid --base: '$BASE'" ;; esac
131
+ git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1 || die2 "not a git work tree: $REPO"
132
+ git -C "$REPO" rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1 || die2 "cannot resolve base ref '$BASE'"
133
+
134
+ : > "$TMP/changed"
135
+ git -C "$REPO" diff -z --name-only "${BASE}...HEAD" -- > "$TMP/c1" 2>/dev/null || die2 "git diff ${BASE}...HEAD failed"
136
+ git -C "$REPO" diff -z --name-only HEAD -- > "$TMP/c2" 2>/dev/null || die2 "git diff HEAD failed"
137
+ git -C "$REPO" ls-files -z --others --exclude-standard > "$TMP/c3" 2>/dev/null || die2 "git ls-files failed"
138
+ cat "$TMP/c1" "$TMP/c2" "$TMP/c3" | tr '\0' '\n' | sed '/^$/d' | sort -u > "$TMP/changed"
139
+
140
+ # ---- check ----------------------------------------------------------------------------------------------
141
+ RC=0
142
+ while IFS="$(printf '\t')" read -r sid sname squery; do
143
+ git -C "$REPO" grep -z --untracked -l -F -e "$squery" -- > "$TMP/g" 2>"$TMP/gerr"
144
+ grc=$?
145
+ if [ "$grc" -gt 1 ]; then die2 "git grep failed for symbol $sid (rc=$grc): $(head -n 1 "$TMP/gerr")"; fi
146
+ tr '\0' '\n' < "$TMP/g" | sed '/^$/d' | sort -u > "$TMP/found"
147
+ n=$(wc -l < "$TMP/found" | tr -d ' ')
148
+ [ "$n" -le "$MAX_HITS" ] || die2 "query too broad for symbol $sid ($n files > $MAX_HITS): use a more distinctive query"
149
+ while IFS= read -r f; do
150
+ [ -n "$f" ] || continue
151
+ if grep -Fxq -- "$f" "$TMP/changed"; then continue; fi
152
+ if awk -F'\t' -v s="$sid" -v f="$f" '$1==s && $2==f && $3=="verified-unaffected" {ok=1} END{exit ok?0:1}' "$TMP/hits"; then continue; fi
153
+ echo "UNLISTED: $sid $f"; RC=1
154
+ done < "$TMP/found"
155
+ # planned-but-not-done: change rows for this symbol whose file is not in the change set
156
+ while IFS="$(printf '\t')" read -r hs hf hd; do
157
+ [ "$hs" = "$sid" ] && [ "$hd" = change ] || continue
158
+ grep -Fxq -- "$hf" "$TMP/changed" || { echo "NOT_DONE: $sid $hf"; RC=1; }
159
+ done < "$TMP/hits"
160
+ done < "$TMP/symbols"
161
+
162
+ # HIT rows whose symbol exists were validated at parse time; a manifest that covers every hit passes.
163
+ [ "$RC" -eq 0 ] && echo "OK: blast-radius manifest covered ($(wc -l < "$TMP/symbols" | tr -d ' ') symbol(s))"
164
+ exit "$RC"