forgedock 1.1.6 → 1.1.8

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 (43) hide show
  1. package/README.md +3 -1
  2. package/bin/engine/invariants.mjs +361 -0
  3. package/bin/hooks/pre-tool-use.mjs +182 -0
  4. package/bin/journey.mjs +77 -7
  5. package/bin/labels.json +10 -0
  6. package/bin/recall.mjs +540 -0
  7. package/commands/autopilot.md +959 -78
  8. package/commands/cleanup.md +52 -3
  9. package/commands/optimize.md +585 -0
  10. package/commands/orchestrate/phase-1-resolve.md +35 -25
  11. package/commands/orchestrate/phase-3-dependency.md +465 -34
  12. package/commands/orchestrate/phase-4-execution.md +448 -29
  13. package/commands/orchestrate/phase-6-report.md +198 -9
  14. package/commands/pipeline-health/phase-2-metrics.md +305 -1
  15. package/commands/pipeline-health/phase-4-proposals.md +127 -0
  16. package/commands/quality-gate.md +417 -0
  17. package/commands/review-pr.md +391 -13
  18. package/commands/spec-doctor.md +429 -0
  19. package/commands/validate.md +52 -0
  20. package/commands/work-on/build/architect.md +77 -0
  21. package/commands/work-on/build/context.md +357 -22
  22. package/commands/work-on/build/implement.md +2 -0
  23. package/commands/work-on/build/validate.md +28 -0
  24. package/commands/work-on/build.md +44 -0
  25. package/commands/work-on/close.md +621 -8
  26. package/commands/work-on/investigate.md +221 -0
  27. package/commands/work-on.md +15 -14
  28. package/docs/FORGE-PROTOCOL.md +101 -0
  29. package/docs/design/awesome-list-submissions.md +105 -0
  30. package/docs/design/f5bot-monitoring-setup.md +43 -0
  31. package/docs/design/growth-strategy-2026.md +579 -0
  32. package/docs/spec/forge-protocol-v1.md +97 -5
  33. package/docs/spec/label-state-machine.md +6 -1
  34. package/package.json +2 -2
  35. package/packages/protocol/src/cli.js +374 -84
  36. package/packages/protocol/src/types.js +75 -1
  37. package/scripts/build-knowledge-index.mjs +1761 -0
  38. package/scripts/calibration.mjs +996 -0
  39. package/scripts/danger-zones.mjs +782 -0
  40. package/scripts/doctor-pipeline-state.sh +88 -2
  41. package/scripts/refresh-bot-token.sh +97 -0
  42. package/scripts/select-fix-targets.sh +234 -0
  43. package/scripts/transition-label.sh +157 -6
@@ -21,20 +21,122 @@ done
21
21
 
22
22
  Aggregate into the batch-level analytics for Step 6B.
23
23
 
24
- **Also collect the machine-readable summary cards** (`<!-- FORGE:CARD {json} -->`, embedded in
25
- each issue's `FORGE:TRAJECTORY` comment by `/work-on` close phase). These power the per-issue and
26
- batch cards in Step 6C:
24
+ **Also collect the machine-readable summary cards** (embedded in each issue's `FORGE:TRAJECTORY`
25
+ comment by `/work-on` close phase as a `<!-- FORGE:CARD: v1 sha:... b64:... -->` line). These
26
+ power the per-issue and batch cards in Step 6C.
27
+
28
+ **CODEC PATH (forge#1727)**: Cards are now Base64url-encoded. Use the protocol CLI's `parse`
29
+ subcommand to decode them — do NOT use the old `sed -n 's/.*<!-- FORGE:CARD \(.*\) -->.*/\1/p'`
30
+ regex extraction, which only worked for the deprecated inline-JSON form:
27
31
 
28
32
  ```bash
29
33
  CARDS=""
30
34
  for NUM in {all_completed_issue_numbers}; do
31
- CARD=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
32
- --jq '.[] | select(.body | contains("FORGE:CARD")) | .body' 2>/dev/null \
33
- | sed -n 's/.*<!-- FORGE:CARD \(.*\) -->.*/\1/p' | head -1)
34
- [ -n "$CARD" ] && CARDS="${CARDS}${CARD}"$'\n'
35
+ # Fetch the TRAJECTORY comment body for this issue
36
+ TRAJ_BODY=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
37
+ --jq '[.[] | select(.body | contains("FORGE:CARD:"))] | last | .body // ""' 2>/dev/null || true)
38
+ if [ -n "$TRAJ_BODY" ]; then
39
+ # Decode via protocol CLI — handles both Base64url form (forge#1727) and gracefully
40
+ # exits 1 (skips) for pre-migration inline-JSON entries.
41
+ CARD=$(echo "$TRAJ_BODY" | node packages/protocol/src/cli.js parse --type CARD 2>/dev/null || true)
42
+ [ -n "$CARD" ] && CARDS="${CARDS}${CARD}"$'\n'
43
+ fi
44
+ done
45
+ # CARDS is now a newline-delimited list of per-issue JSON objects decoded from FORGE:CARD payloads.
46
+ # Skip any issue whose card is absent — pre-card runs or non-merged terminal states degrade gracefully.
47
+ ```
48
+
49
+ ### Step 6A.5: Collect merge-ready PRs (`workflow:awaiting-merge`) <!-- Added: forge#1811 -->
50
+
51
+ **Why a separate collection pass**: `workflow:awaiting-merge` issues are open (their PR hasn't
52
+ merged yet), so they are excluded from `{all_completed_issue_numbers}` above — that set only
53
+ contains issues that reached a closed/merged terminal state. Iterate the full batch issue set
54
+ (`{all_batch_issue_numbers}` — same prose-variable used by the batch-wide loops in
55
+ `phase-4-execution.md`) instead, and filter to the awaiting-merge label.
56
+
57
+ For each awaiting-merge issue, resolve its open PR using the `"Closes #N"`-anchored search
58
+ established by forge#1634/#1646 — **do not** use a bare-number `search "${NUM} in:body"`, which
59
+ can false-positive on changelogs or cross-references and resolve the wrong PR:
60
+
61
+ ```bash
62
+ MERGE_READY_PRS=() # each entry: "NUM|PR_NUMBER|TITLE"
63
+
64
+ for NUM in {all_batch_issue_numbers}; do
65
+ IS_AWAITING=$(gh issue view "$NUM" -R {GH_REPO} --json labels \
66
+ --jq '[.labels[].name | select(. == "workflow:awaiting-merge")] | length' 2>/dev/null || echo "0")
67
+ [ "$IS_AWAITING" -gt 0 ] || continue
68
+
69
+ TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
70
+
71
+ # Anchor on "Closes #N" first (forge#1634/#1646 precedent).
72
+ PR_NUM=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${NUM}\" in:body" \
73
+ --json number,updatedAt --jq 'sort_by(.updatedAt) | last | .number' 2>/dev/null || echo "")
74
+ if [ -z "$PR_NUM" ]; then
75
+ # Fallback: a PR may close the issue via "Fixes #N"/"Resolves #N" rather than
76
+ # "Closes #N". Use the bare number only to generate candidates, then RE-VALIDATE
77
+ # each candidate's body against an anchored (Closes|Fixes|Resolves) #N word-boundary
78
+ # regex before accepting it — never accept a bare mention (changelog / cross-reference),
79
+ # which could resolve the wrong PR into the one-shot batch-merge command. This mirrors
80
+ # the recover-orphans.md re-validation precedent. <!-- forge#1822 -->
81
+ PR_NUM=$(gh pr list -R {GH_REPO} --state open --search "${NUM} in:body" \
82
+ --json number,updatedAt,body \
83
+ --jq --arg n "${NUM}" '[ .[] | select(.body | test("(?i)(close[sd]?|fix(e[sd])?|resolve[sd]?)\\s+#" + $n + "\\b")) ] | sort_by(.updatedAt) | last | .number // empty' 2>/dev/null || echo "")
84
+ fi
85
+
86
+ [ -n "$PR_NUM" ] && MERGE_READY_PRS+=("${NUM}|${PR_NUM}|${TITLE}")
35
87
  done
36
- # CARDS is now a newline-delimited list of per-issue JSON objects (skip any issue whose
37
- # card is absent — pre-card runs or non-merged terminal states degrade gracefully).
88
+ # MERGE_READY_PRS now holds one entry per merge-ready PR in the batch (empty array if none).
89
+ # Feeds the "### Merge-Ready" report block and the Implementation Results table row (Step 6B).
90
+
91
+ # Pre-build the batch-merge command (rendered verbatim as {MERGE_READY_CMD} in Step 6B —
92
+ # computed here, not inside the report template, so Step 6B stays plain template text with no
93
+ # nested code fences of its own). `gh pr merge` accepts only ONE PR selector per invocation,
94
+ # so chain one `gh pr merge` per PR with && rather than passing a space-separated list (which
95
+ # `gh` rejects). <!-- forge#1838 review: BUG-3 -->
96
+ MERGE_READY_CMD=""
97
+ if [ "${#MERGE_READY_PRS[@]}" -gt 0 ]; then
98
+ for _entry in "${MERGE_READY_PRS[@]}"; do
99
+ _pr=$(printf '%s' "$_entry" | cut -d'|' -f2)
100
+ [ -z "$_pr" ] && continue
101
+ [ -n "$MERGE_READY_CMD" ] && MERGE_READY_CMD="${MERGE_READY_CMD} && "
102
+ MERGE_READY_CMD="${MERGE_READY_CMD}gh pr merge ${_pr} --merge -R {GH_REPO}"
103
+ done
104
+ fi
105
+ ```
106
+
107
+ ### Step 6A.6: Collect blocked-on-human-merge dependents (`blocked-on-human-merge`) <!-- Added: forge#1812 -->
108
+
109
+ **Why a separate collection pass**: A `blocked-on-human-merge` issue is a *dependent* of a `GATED`
110
+ predecessor (`needs-human` or `workflow:awaiting-merge` — see `phase-4-execution.md` Step 4B's
111
+ Predecessor Classification), not itself blocked or failed. It has no PR of its own yet — it hasn't
112
+ even been dispatched. It is excluded from both `{all_completed_issue_numbers}` (never ran) and
113
+ `MERGE_READY_PRS` (Step 6A.5, which only covers issues that themselves reached `workflow:awaiting-merge`).
114
+ Reporting it as `⏭ Skipped (dep)` would be wrong — that label means the predecessor `FAILED` and the
115
+ work is abandoned; this state means the work is queued and will auto-dispatch on merge, no manual
116
+ re-run required.
117
+
118
+ ```bash
119
+ BLOCKED_ON_MERGE=() # each entry: "NUM|GATING_PRED|GATING_PR|TITLE"
120
+
121
+ for NUM in {all_batch_issue_numbers}; do
122
+ IS_BLOCKED=$(gh issue view "$NUM" -R {GH_REPO} --json labels \
123
+ --jq '[.labels[].name | select(. == "blocked-on-human-merge")] | length' 2>/dev/null || echo "0")
124
+ [ "$IS_BLOCKED" -gt 0 ] || continue
125
+
126
+ TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json title --jq '.title' 2>/dev/null || echo "")
127
+ GATING_PRED=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
128
+ --jq '[.[] | select(.body | contains("FORGE:BLOCKED_ON_HUMAN_MERGE"))] | last | (.body | capture("Gating predecessor\\*\\*: #(?<p>[0-9]+)").p) // ""' 2>/dev/null || echo "")
129
+ GATING_PR=""
130
+ if [ -n "$GATING_PRED" ]; then
131
+ GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${GATING_PRED}\" in:body" \
132
+ --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
133
+ fi
134
+
135
+ BLOCKED_ON_MERGE+=("${NUM}|${GATING_PRED}|${GATING_PR}|${TITLE}")
136
+ done
137
+ # BLOCKED_ON_MERGE now holds one entry per dependent queued behind a human-gated predecessor
138
+ # (empty array if none). Feeds the "Blocked-on-Merge" report block and the Implementation
139
+ # Results table row (Step 6B).
38
140
  ```
39
141
 
40
142
  ### Step 6B: Present consolidated report
@@ -61,7 +163,62 @@ done
61
163
  | #{N1} | {title} | from #{INV1} | ✓ Merged | #{PR} | staging |
62
164
  | #{C} | {title} | original | ✗ Invalid | — | — |
63
165
  | #{D} | {title} | original | ⚠ Blocked | — | — |
166
+ | #{F} | {title} | original | ⏸ Awaiting Merge | #{PR} | staging |
64
167
  | #{E} | {title} | original | ⏭ Skipped (dep) | — | — |
168
+ | #{G} | {title} | original | 🔗 Blocked-on-Merge (gated by #{PRED}) | — | — |
169
+
170
+ `⏸ Awaiting Merge` (`workflow:awaiting-merge`) is structurally distinct from `⚠ Blocked`
171
+ (`needs-human`): a Blocked row means the pipeline hit something it cannot resolve on its own
172
+ and a human must diagnose it; an Awaiting Merge row means the PR was already remediated and
173
+ re-reviewed to a clean APPROVED verdict — a human only needs to click merge. Never render an
174
+ awaiting-merge PR as `⚠ Blocked`.
175
+
176
+ `🔗 Blocked-on-Merge` (`blocked-on-human-merge`, forge#1812) is distinct from all three of the
177
+ above: it is a *dependent* of a GATED predecessor (one currently `⚠ Blocked` or `⏸ Awaiting
178
+ Merge`) — the dependent issue itself was never dispatched and has no problem of its own. It is
179
+ also distinct from `⏭ Skipped (dep)`, which means the predecessor `FAILED` outright and the work
180
+ was abandoned. A Blocked-on-Merge row means the work is queued and will auto-dispatch the instant
181
+ the gating predecessor's PR merges — no manual `/orchestrate` re-run required. Never render a
182
+ blocked-on-human-merge dependent as `⏭ Skipped (dep)`.
183
+
184
+ {IF `MERGE_READY_PRS` (Step 6A.5) is non-empty:}
185
+
186
+ ### Merge-Ready — {N} PR(s) awaiting only a human merge
187
+
188
+ These PRs were remediated and re-reviewed to a clean `APPROVED` verdict after an earlier
189
+ `needs-human` escalation. They do not need further diagnosis — merge them to land the batch.
190
+
191
+ | # | Issue | PR | Title |
192
+ |---|-------|----|----|
193
+ {one row per `MERGE_READY_PRS` entry: | {n} | #{NUM} | #{PR} | {title} |}
194
+
195
+ **One action to land all {N}:** `{MERGE_READY_CMD}`
196
+
197
+ (`{MERGE_READY_CMD}` is pre-built in Step 6A.5 by chaining one `gh pr merge <pr> --merge` per PR
198
+ in `MERGE_READY_PRS` with `&&` — `gh pr merge` takes a single PR selector, so a space-separated
199
+ list would be rejected. Render it verbatim here as plain text, no code fence.)
200
+
201
+ {ELSE: omit this section entirely — do not print an empty "Merge-Ready" heading.}
202
+
203
+ {IF `BLOCKED_ON_MERGE` (Step 6A.6) is non-empty:}
204
+
205
+ ### Blocked-on-Merge — {N} issue(s) queued behind a human-gated predecessor
206
+
207
+ These issues are dependents of a predecessor that is currently `⚠ Blocked` or `⏸ Awaiting Merge`.
208
+ They were never dispatched — no work has started on them — and they need no action right now.
209
+ Each will auto-dispatch the instant its gating predecessor's PR merges (live-session wake via
210
+ `phase-4-execution.md` Step 4B item 6.6, or next-`/orchestrate`-invocation wake via
211
+ `phase-3-dependency.md`'s wake/compaction reconstruction). This batch is a **paused drain**, not
212
+ a complete one — re-running `/orchestrate` after merging the gating PR(s) below will pick these up.
213
+
214
+ | # | Issue | Gated By | Gating PR | Title |
215
+ |---|-------|----------|-----------|-------|
216
+ {one row per `BLOCKED_ON_MERGE` entry: | {n} | #{NUM} | #{GATING_PRED} | #{GATING_PR} | {title} |}
217
+
218
+ **To unblock**: merge the gating PR(s) referenced above, then re-run `/orchestrate` (or let the
219
+ current session's live wake pick them up automatically if it is still running).
220
+
221
+ {ELSE: omit this section entirely — do not print an empty "Blocked-on-Merge" heading.}
65
222
 
66
223
  ### Review-Spawned Issues
67
224
 
@@ -91,6 +248,8 @@ done
91
248
  - **Review findings**: {N} spawned, {M} resolved in-batch, {K} swept, {J} deferred (cosmetic), {L} deferred (gen2)
92
249
  - **Succeeded**: {N} issues resolved (implementation + sweep)
93
250
  - **Failed**: {N} issues need attention
251
+ - **Merge-ready**: {#MERGE_READY_PRS[@]:-0} PRs awaiting only a human merge (see "Merge-Ready" section above)
252
+ - **Blocked-on-merge**: {#BLOCKED_ON_MERGE[@]:-0} issues queued behind a human-gated predecessor, will auto-dispatch on merge (see "Blocked-on-Merge" section above)
94
253
  - **Skipped**: {N} issues (dependency failures)
95
254
 
96
255
  ### Post-Batch Cleanup
@@ -124,6 +283,35 @@ done
124
283
  | Findings validated | {N} |
125
284
  | False positives | {N} ({%}) |
126
285
  | Anomalies flagged | {N} |
286
+ | Budget limit | ${BUDGET_LIMIT:-uncapped} |
287
+ | Projected spend (dispatched issues) | ${PROJECTED_SPEND:-—} |
288
+ | Actual spend (best-effort telemetry) | ${ACTUAL_SPEND:-—} |
289
+ | Issues deferred (budget ceiling) | {#DEFERRED_BUDGET_ISSUES[@]:-0} |
290
+ | ε-reserve issued (no-prior dispatch) | {EPSILON_DISPATCHED:-no} |
291
+ | **Value-weighted throughput** | **{VALUE_THROUGHPUT:-—} value-pts/USD** |
292
+
293
+ `value-weighted throughput` = Σ(priority_weight × danger_zone_weight) for **merged** issues ÷ actual spend (USD). A higher value means more high-priority issues were resolved per dollar. Trend this across runs via `/pipeline-health` to measure the economic scheduling ROI. <!-- Added: forge#1743 -->
294
+
295
+ **Compute value-weighted throughput** (populate before rendering the table):
296
+
297
+ ```bash
298
+ # Collect value scores for merged issues (ISSUE_VALUE[] populated in Step 3E.5)
299
+ MERGED_VALUE_SUM="0"
300
+ for NUM in {all_completed_issue_numbers}; do
301
+ IS_MERGED=$(gh issue view "$NUM" -R {GH_REPO} --json labels \
302
+ --jq '[.labels[].name | select(. == "workflow:merged")] | length' 2>/dev/null || echo "0")
303
+ if [ "$IS_MERGED" -gt 0 ] && [ -n "${ISSUE_VALUE[$NUM]:-}" ]; then
304
+ MERGED_VALUE_SUM=$(echo "scale=4; $MERGED_VALUE_SUM + ${ISSUE_VALUE[$NUM]}" | bc 2>/dev/null \
305
+ || echo "$MERGED_VALUE_SUM")
306
+ fi
307
+ done
308
+
309
+ if [ "${ACTUAL_SPEND:-0}" != "0" ] && echo "${ACTUAL_SPEND:-0}" | grep -qP '^\d+(\.\d+)?$'; then
310
+ VALUE_THROUGHPUT=$(echo "scale=2; $MERGED_VALUE_SUM / $ACTUAL_SPEND" | bc 2>/dev/null || echo "—")
311
+ else
312
+ VALUE_THROUGHPUT="—" # no spend telemetry available
313
+ fi
314
+ ```
127
315
 
128
316
  **Domain breakdown**: {N} scraping, {N} frontend, {N} billing, ...
129
317
  **Routing**: {N} fast-lane, {N} feature-lane
@@ -137,6 +325,7 @@ done
137
325
  - Contract divergences > 20% → investigation quality may need improvement
138
326
  - **Idle% > 50%** → agents stalling at phase boundaries; check work-on routing loop
139
327
  - **Avg resumes > 1** → orchestrator having to compensate for agent stops
328
+ - **Value-weighted throughput trending down** → high-value issues deferred or blocked; check Step 3E.5 score distribution
140
329
 
141
330
  ### Next Steps
142
331
  {If milestone and all issues done: "Milestone ready to ship. Run `/milestone ship {slug}` when ready. The ship command includes a pre-merge hunk-loss audit (Step 2.5) that detects staging-only hunks in milestone-modified files and rebases the milestone branch to absorb them before creating the PR — protecting against squash-merge regressions."}
@@ -45,7 +45,70 @@ gh issue list -R $REPO --state closed --label "review-finding" --json number,cre
45
45
  --jq "[.[] | select(.createdAt > \"$SINCE\")] | length"
46
46
  ```
47
47
 
48
- **False positive rate** = invalid / total closed. Target: < 10%.
48
+ **False positive rate (proxy)** = invalid / total closed. Target: < 10%.
49
+
50
+ **Note**: This proxy uses `workflow:invalid` as a stand-in for false-positive. Phase 2C.5 reports the actual false-positive rate from the `false-positive` label. Both metrics should be reported; they should converge as validation coverage increases.
51
+
52
+ ### 2C.5: Validation coverage (finding lifecycle health) <!-- Added: forge#1730 -->
53
+
54
+ Measures whether the `needs-validation → validated/false-positive` lifecycle is operating. This is the primary health signal for the review system's calibration corpus.
55
+
56
+ ```bash
57
+ # All finding-issue types: review-finding, staging-review, audit-finding
58
+ FINDING_LABELS="review-finding staging-review audit-finding"
59
+
60
+ TOTAL_FINDINGS=0
61
+ TOTAL_VALIDATED=0
62
+ TOTAL_FALSE_POSITIVE=0
63
+ TOTAL_NEEDS_VALIDATION=0
64
+
65
+ for LABEL in $FINDING_LABELS; do
66
+ COUNT=$(gh issue list -R $REPO --state all --label "$LABEL" --limit 1000 --json number \
67
+ --jq 'length' 2>/dev/null || echo 0)
68
+ TOTAL_FINDINGS=$((TOTAL_FINDINGS + COUNT))
69
+
70
+ VAL=$(gh issue list -R $REPO --state all --label "$LABEL,validated" --limit 1000 --json number \
71
+ --jq 'length' 2>/dev/null || echo 0)
72
+ TOTAL_VALIDATED=$((TOTAL_VALIDATED + VAL))
73
+
74
+ FP=$(gh issue list -R $REPO --state all --label "$LABEL,false-positive" --limit 1000 --json number \
75
+ --jq 'length' 2>/dev/null || echo 0)
76
+ TOTAL_FALSE_POSITIVE=$((TOTAL_FALSE_POSITIVE + FP))
77
+
78
+ NV=$(gh issue list -R $REPO --state all --label "$LABEL,needs-validation" --limit 1000 --json number \
79
+ --jq 'length' 2>/dev/null || echo 0)
80
+ TOTAL_NEEDS_VALIDATION=$((TOTAL_NEEDS_VALIDATION + NV))
81
+ done
82
+
83
+ TOTAL_RESOLVED=$((TOTAL_VALIDATED + TOTAL_FALSE_POSITIVE))
84
+ if [ "$TOTAL_FINDINGS" -gt 0 ]; then
85
+ VALIDATION_COVERAGE_PCT=$(echo "scale=1; $TOTAL_RESOLVED * 100 / $TOTAL_FINDINGS" | bc 2>/dev/null || echo "N/A")
86
+ if [ "$TOTAL_RESOLVED" -gt 0 ]; then
87
+ FALSE_POSITIVE_RATE_PCT=$(echo "scale=1; $TOTAL_FALSE_POSITIVE * 100 / $TOTAL_RESOLVED" | bc 2>/dev/null || echo "N/A")
88
+ else
89
+ FALSE_POSITIVE_RATE_PCT="N/A (no resolved findings)"
90
+ fi
91
+ else
92
+ VALIDATION_COVERAGE_PCT="N/A"
93
+ FALSE_POSITIVE_RATE_PCT="N/A"
94
+ fi
95
+
96
+ echo "=== Validation Coverage (2C.5) ==="
97
+ echo "Total finding-issues: $TOTAL_FINDINGS (review-finding + staging-review + audit-finding)"
98
+ echo " validated: $TOTAL_VALIDATED"
99
+ echo " false-positive: $TOTAL_FALSE_POSITIVE"
100
+ echo " resolved (total): $TOTAL_RESOLVED"
101
+ echo " needs-validation (open): $TOTAL_NEEDS_VALIDATION"
102
+ echo "Validation coverage: ${VALIDATION_COVERAGE_PCT}% (target: ≥ 95% for newly closed findings)"
103
+ echo "False-positive rate: ${FALSE_POSITIVE_RATE_PCT}% of resolved findings (actual label-based rate)"
104
+ ```
105
+
106
+ **Metrics to compute**:
107
+ - **Validation coverage**: (validated + false-positive) / total finding-issues. Target: ≥ 95% for newly closed findings.
108
+ - **False-positive rate (actual)**: false-positive / (validated + false-positive). Reports true negative rate, not the `workflow:invalid` proxy from 2C.
109
+ - **Unresolved finding count**: Issues still holding `needs-validation` — actionable target for `/cleanup labels` backfill.
110
+
111
+ **Why this matters**: The `needs-validation` label was previously a dead end — applied at creation, never resolved. With the lifecycle wired (#1730), these metrics become the primary signal for review system calibration quality. Validation coverage < 95% indicates pipeline runs are not completing the verdict transition; false-positive rate is now a reportable number instead of undefined.
49
112
 
50
113
  ### 2D: Build failure rate
51
114
 
@@ -494,6 +557,136 @@ gh issue list -R $FORGE_REPO --state all --label "audit-finding" \
494
557
  - **Failure category distribution**: breakdown by failure point (INVESTIGATION, IMPLEMENTATION, REVIEW, REVIEW_FALSE_NEG, DEPLOY_GATE, etc.). No target — use for diagnosis. High IMPLEMENTATION counts indicate builder gaps; high REVIEW_FALSE_NEG counts indicate review agent gaps.
495
558
  - **Mean time to detect (MTD)**: average days from PR merge to audit-finding creation. Target: < 7 days. Long MTD means defects linger undetected in production.
496
559
 
560
+ ### 2M.5: Review intensity distribution and escaped-defect rate by tier <!-- Added: forge#1745 -->
561
+
562
+ This metric is only meaningful once `scripts/calibration.mjs --provenance` has been publishing to the `forge-knowledge` branch for at least one 30-day shadow-mode window. Before that point, all PRs have `INTENSITY_TIER=SHADOW` in their FORGE:REVIEW annotations and both tier buckets will be empty — this is expected and correct.
563
+
564
+ **Why this matters**: Review spend should track risk. This metric verifies that the trust escalation system is working as intended: proven cells should have lower escaped-defect rates than novel cells, and overall review token cost per PR should trend down while escaped-defect rate stays flat or falls.
565
+
566
+ **Success metric**: Review token cost per PR trending down (fewer agents on proven PRs) while escaped-defect rate (14-day post-merge review-findings) stays flat or falls; zero escapes attributable to a dropped agent that the full panel would have run.
567
+
568
+ ```bash
569
+ # Collect FORGE:REVIEW annotation INTENSITY_TIER fields from merged PRs in the analysis window.
570
+ # Source: the APPROVED:/CHANGES REQUESTED: PR comment from review-pr.md Phase 7B.
571
+ # Field format (set since forge#1745): "**Intensity tier**: PROVEN | **Cell**: `...` | **Shadow mode**: false"
572
+ #
573
+ # This relies on MERGED_PRS (list of PR numbers) being available from Phase 2E.
574
+ # If not available, fall back to gh pr list.
575
+ if [ -z "${MERGED_PRS:-}" ]; then
576
+ MERGED_PRS=$(gh pr list -R "$FORGE_REPO" --state merged --limit 200 \
577
+ --json number,mergedAt \
578
+ --jq "[.[] | select(.mergedAt > \"$SINCE\")] | .[].number" 2>/dev/null || echo "")
579
+ fi
580
+
581
+ # Accumulators per intensity tier
582
+ TIER_NOVEL_COUNT=0
583
+ TIER_PROVEN_COUNT=0
584
+ TIER_SHADOW_COUNT=0
585
+ TIER_NOVEL_NEEDS_HUMAN_COUNT=0
586
+ TIER_NOVEL_ESCAPES=0
587
+ TIER_PROVEN_ESCAPES=0
588
+
589
+ # Also track escaped defect counts per tier using the audit-finding ledger from 2L.
590
+ # AUDIT_FINDINGS must be set from Phase 2L (contains audit-finding issues in window).
591
+ AUDIT_FINDINGS_AVAILABLE="${AUDIT_FINDINGS:+true}"
592
+
593
+ while IFS= read -r PR_NUM; do
594
+ [ -z "$PR_NUM" ] && continue
595
+
596
+ # Read the FORGE:REVIEW comment from this PR to extract INTENSITY_TIER
597
+ REVIEW_COMMENT=$(gh api "repos/${FORGE_REPO}/issues/${PR_NUM}/comments" \
598
+ --jq '[.[] | select(
599
+ (.body | test("APPROVED:|CHANGES REQUESTED:"; "i")) and
600
+ (.body | contains("Intensity tier"))
601
+ )] | last | .body // ""' 2>/dev/null || echo "")
602
+
603
+ if [ -z "$REVIEW_COMMENT" ]; then
604
+ TIER_SHADOW_COUNT=$((TIER_SHADOW_COUNT + 1))
605
+ continue
606
+ fi
607
+
608
+ PR_TIER=$(echo "$REVIEW_COMMENT" | grep -oP '(?<=\*\*Intensity tier\*\*: )[A-Z_]+' | head -1 || echo "SHADOW")
609
+ case "$PR_TIER" in
610
+ PROVEN) TIER_PROVEN_COUNT=$((TIER_PROVEN_COUNT + 1)) ;;
611
+ NOVEL_NEEDS_HUMAN) TIER_NOVEL_NEEDS_HUMAN_COUNT=$((TIER_NOVEL_NEEDS_HUMAN_COUNT + 1))
612
+ TIER_NOVEL_COUNT=$((TIER_NOVEL_COUNT + 1)) ;;
613
+ NOVEL) TIER_NOVEL_COUNT=$((TIER_NOVEL_COUNT + 1)) ;;
614
+ *) TIER_SHADOW_COUNT=$((TIER_SHADOW_COUNT + 1)) ;;
615
+ esac
616
+
617
+ # Cross-reference escaped defects for this PR's files (from 2L AUDIT_FINDINGS)
618
+ # A REVIEW_FALSE_NEG audit-finding referencing PR #{PR_NUM} counts as an escape for this tier.
619
+ if [ "$AUDIT_FINDINGS_AVAILABLE" = "true" ]; then
620
+ ESCAPE_COUNT=$(echo "$AUDIT_FINDINGS" | \
621
+ jq --argjson pr "$PR_NUM" '[.[] | select(
622
+ (.body | test("\\*\\*Failure point\\*\\*:.*REVIEW_FALSE_NEG")) and
623
+ (.body | test("#" + ($pr | tostring)))
624
+ )] | length' 2>/dev/null || echo "0")
625
+
626
+ if [ "${ESCAPE_COUNT:-0}" -gt 0 ]; then
627
+ case "$PR_TIER" in
628
+ PROVEN) TIER_PROVEN_ESCAPES=$((TIER_PROVEN_ESCAPES + ESCAPE_COUNT)) ;;
629
+ NOVEL|NOVEL_NEEDS_HUMAN) TIER_NOVEL_ESCAPES=$((TIER_NOVEL_ESCAPES + ESCAPE_COUNT)) ;;
630
+ esac
631
+ fi
632
+ fi
633
+ done <<< "$MERGED_PRS"
634
+
635
+ TIER_TOTAL=$((TIER_NOVEL_COUNT + TIER_PROVEN_COUNT + TIER_SHADOW_COUNT + TIER_NOVEL_NEEDS_HUMAN_COUNT))
636
+
637
+ echo ""
638
+ echo "=== Review Intensity Distribution (forge#1745 — provenance trust) ==="
639
+ echo "Window: ${SINCE} → now | Total PRs: ${TIER_TOTAL}"
640
+ echo ""
641
+ echo "Tier distribution:"
642
+ printf " %-22s %4d (%s%%)\n" "PROVEN (de-escalated):" "$TIER_PROVEN_COUNT" \
643
+ "$([ "$TIER_TOTAL" -gt 0 ] && echo "scale=0; $TIER_PROVEN_COUNT * 100 / $TIER_TOTAL" | bc || echo "0")"
644
+ printf " %-22s %4d (%s%%)\n" "NOVEL (full panel):" "$TIER_NOVEL_COUNT" \
645
+ "$([ "$TIER_TOTAL" -gt 0 ] && echo "scale=0; $TIER_NOVEL_COUNT * 100 / $TIER_TOTAL" | bc || echo "0")"
646
+ printf " %-22s %4d (%s%%)\n" "SHADOW (no action yet):" "$TIER_SHADOW_COUNT" \
647
+ "$([ "$TIER_TOTAL" -gt 0 ] && echo "scale=0; $TIER_SHADOW_COUNT * 100 / $TIER_TOTAL" | bc || echo "0")"
648
+
649
+ echo ""
650
+ echo "Escaped-defect rate by tier (REVIEW_FALSE_NEG audit-findings):"
651
+ if [ "$TIER_PROVEN_COUNT" -gt 0 ]; then
652
+ PROVEN_ESCAPE_RATE=$(echo "scale=1; $TIER_PROVEN_ESCAPES * 100 / $TIER_PROVEN_COUNT" | bc 2>/dev/null || echo "N/A")
653
+ echo " PROVEN: ${TIER_PROVEN_ESCAPES} escapes / ${TIER_PROVEN_COUNT} PRs = ${PROVEN_ESCAPE_RATE}% (target: ≤ review-wide escape rate)"
654
+ else
655
+ echo " PROVEN: no PRs in this tier yet"
656
+ fi
657
+ if [ "$TIER_NOVEL_COUNT" -gt 0 ]; then
658
+ NOVEL_ESCAPE_RATE=$(echo "scale=1; $TIER_NOVEL_ESCAPES * 100 / $TIER_NOVEL_COUNT" | bc 2>/dev/null || echo "N/A")
659
+ echo " NOVEL: ${TIER_NOVEL_ESCAPES} escapes / ${TIER_NOVEL_COUNT} PRs = ${NOVEL_ESCAPE_RATE}%"
660
+ else
661
+ echo " NOVEL: no PRs in this tier yet"
662
+ fi
663
+
664
+ # Health signal: if PROVEN escape rate > NOVEL escape rate, the de-escalation is leaking.
665
+ # This should never happen in steady state — flag it if it does.
666
+ if [ "$TIER_PROVEN_COUNT" -gt 0 ] && [ "$TIER_NOVEL_COUNT" -gt 0 ]; then
667
+ PROVEN_ESC_CMP=$(echo "scale=3; $TIER_PROVEN_ESCAPES / $TIER_PROVEN_COUNT" | bc 2>/dev/null || echo "0")
668
+ NOVEL_ESC_CMP=$(echo "scale=3; $TIER_NOVEL_ESCAPES / $TIER_NOVEL_COUNT" | bc 2>/dev/null || echo "0")
669
+ if awk "BEGIN{exit !($PROVEN_ESC_CMP > $NOVEL_ESC_CMP)}" 2>/dev/null; then
670
+ echo ""
671
+ echo "⚠ WARNING: PROVEN tier escape rate (${PROVEN_ESC_CMP}) exceeds NOVEL tier (${NOVEL_ESC_CMP})."
672
+ echo " This indicates that de-escalation is suppressing agents that would have caught defects."
673
+ echo " Investigate: run /pipeline-health --detail and check which cells are PROVEN with recent escapes."
674
+ echo " Consider resetting affected provenance cells or raising the PROVEN threshold."
675
+ fi
676
+ fi
677
+
678
+ # Export for downstream phases (phase-3-analyze.md health score)
679
+ export TIER_NOVEL_COUNT TIER_PROVEN_COUNT TIER_SHADOW_COUNT
680
+ export TIER_NOVEL_ESCAPES TIER_PROVEN_ESCAPES
681
+ ```
682
+
683
+ **Metrics to report in Phase 5 health summary**:
684
+ - **Intensity distribution**: PROVEN / NOVEL / SHADOW counts and percentages over the analysis window
685
+ - **Escaped-defect rate by tier**: REVIEW_FALSE_NEG audit-finding rate per tier (target: PROVEN ≤ overall rate, NOVEL ≥ overall rate before de-escalation takes effect, PROVEN never exceeds NOVEL)
686
+ - **Shadow mode PRs**: count of PRs where intensity tier was computed but not acted on (should decrease after 30-day window)
687
+
688
+ **Note**: If `TRUST_SHADOW_MODE=true` is still set in forge.yaml, all PRs will appear as SHADOW tier in this metric. This is correct — shadow mode has not yet been enforced. After setting `trust_shadow_mode: false`, tiers will populate.
689
+
497
690
  ### 2M: Session discovery and date-gated filtering (conversation transcript analytics)
498
691
 
499
692
  This phase locates conversation JSONL files for pipeline command sessions (work-on, orchestrate, review-pr, quality-gate, review-pr-staging) that ran within the analysis window. It produces `SESSION_JSONL_FILES` — consumed by downstream Conversation Transcript Analytics phases to extract flow metrics (phase durations, tool patterns, subagent efficiency, stalls, rate-limits).
@@ -1727,3 +1920,114 @@ fi
1727
1920
 
1728
1921
  ---
1729
1922
 
1923
+ ### 2R: Confidence Calibration Table (from run outcomes)
1924
+
1925
+ **Purpose**: Surfaces whether the pipeline's stated confidence levels predict actual outcomes. A HIGH-confidence issue that frequently triggers review-findings or reverts within 14 days indicates systematic overconfidence — the investigator's confidence is not tracking reality. Conversely, needs-human routing on issues that almost always survive indicates overcaution. This section reads the calibration table produced by `scripts/calibration.mjs` and published to the `forge-knowledge` branch. <!-- Added: forge#1741 -->
1926
+
1927
+ **Skip if**: forge-knowledge branch is absent or `calibration/table.json` does not exist on it — emit the unavailability note below and continue.
1928
+
1929
+ **Read calibration table**:
1930
+ ```bash
1931
+ # Read calibration table from forge-knowledge branch (fail-safe: any error → skip)
1932
+ CALIBRATION_TABLE=""
1933
+ CALIBRATION_AVAILABLE=false
1934
+
1935
+ if git show-ref --quiet "refs/remotes/origin/${FORGE_KNOWLEDGE_BRANCH:-forge-knowledge}" 2>/dev/null || \
1936
+ git ls-remote --exit-code origin "${FORGE_KNOWLEDGE_BRANCH:-forge-knowledge}" >/dev/null 2>&1; then
1937
+ RAW_TABLE=$(git show "origin/${FORGE_KNOWLEDGE_BRANCH:-forge-knowledge}:calibration/table.json" 2>/dev/null || echo '')
1938
+ if [ -n "$RAW_TABLE" ]; then
1939
+ CALIBRATION_TABLE="$RAW_TABLE"
1940
+ CALIBRATION_AVAILABLE=true
1941
+ CALIB_COMPUTED_AT=$(echo "$CALIBRATION_TABLE" | jq -r '.computedAt // "unknown"' 2>/dev/null || echo 'unknown')
1942
+ CALIB_TOTAL_RUNS=$(echo "$CALIBRATION_TABLE" | jq -r '.totalRuns // 0' 2>/dev/null || echo '0')
1943
+ CALIB_WINDOW_DAYS=$(echo "$CALIBRATION_TABLE" | jq -r '.windowDays // 14' 2>/dev/null || echo '14')
1944
+ CALIB_MIN_SAMPLES=$(echo "$CALIBRATION_TABLE" | jq -r '.minSamples // 10' 2>/dev/null || echo '10')
1945
+ echo "Calibration table loaded: ${CALIB_TOTAL_RUNS} runs, computed ${CALIB_COMPUTED_AT}"
1946
+ fi
1947
+ fi
1948
+
1949
+ if [ "$CALIBRATION_AVAILABLE" = false ]; then
1950
+ echo "Phase 2R: Calibration table unavailable (forge-knowledge:calibration/table.json not found)"
1951
+ echo " To compute: node scripts/calibration.mjs --repo ${REPO} --publish"
1952
+ # Store unavailability for Phase 5A report
1953
+ CALIBRATION_AVAILABILITY_NOTE="⚠ Calibration table unavailable — run: node scripts/calibration.mjs --repo ${REPO} --publish"
1954
+ fi
1955
+ ```
1956
+
1957
+ **Render calibration table** (when available):
1958
+
1959
+ ```bash
1960
+ if [ "$CALIBRATION_AVAILABLE" = true ]; then
1961
+ echo "## Confidence Calibration Table"
1962
+ echo "**Source**: forge-knowledge:calibration/table.json"
1963
+ echo "**Computed**: ${CALIB_COMPUTED_AT}"
1964
+ echo "**Outcome window**: ${CALIB_WINDOW_DAYS} days | **Min samples for trust**: ${CALIB_MIN_SAMPLES}"
1965
+ echo "**Total runs in corpus**: ${CALIB_TOTAL_RUNS}"
1966
+ echo ""
1967
+
1968
+ # Extract and render per-cell rows; flag overconfidence and overcaution cells
1969
+ OVERCONFIDENCE_CELLS=""
1970
+ OVERCAUTION_CELLS=""
1971
+
1972
+ echo "$CALIBRATION_TABLE" | jq -r '
1973
+ .rows[] |
1974
+ [
1975
+ .taskType,
1976
+ .confidence,
1977
+ (if .trusted then (.survivalRate * 100 | floor | tostring) + "%" else "—(N<" + (.sampleCount | tostring) + ")" end),
1978
+ (.sampleCount | tostring),
1979
+ (if .trusted then
1980
+ (if .flag == "overconfidence" then "⚠ OVERCONFIDENCE" elif .flag == "overcaution-candidate" then "ℹ OVERCAUTION?" else "✅ OK" end)
1981
+ else "ℹ untrusted" end)
1982
+ ] | @tsv
1983
+ ' 2>/dev/null | while IFS=$'\t' read -r taskType confidence survivalRate sampleCount status; do
1984
+ printf "| %-20s | %-8s | %-18s | %-7s | %s\n" \
1985
+ "$taskType" "$confidence" "$survivalRate" "$sampleCount" "$status"
1986
+
1987
+ # Collect flagged cells for summary
1988
+ if [[ "$status" == *"OVERCONFIDENCE"* ]]; then
1989
+ OVERCONFIDENCE_CELLS="${OVERCONFIDENCE_CELLS}\n - ${taskType} × ${confidence}: ${survivalRate} survival (${sampleCount} runs)"
1990
+ elif [[ "$status" == *"OVERCAUTION"* ]]; then
1991
+ OVERCAUTION_CELLS="${OVERCAUTION_CELLS}\n - ${taskType} × ${confidence}: ${survivalRate} survival (${sampleCount} runs)"
1992
+ fi
1993
+ done
1994
+
1995
+ # Emit flag summaries
1996
+ if [ -n "$OVERCONFIDENCE_CELLS" ]; then
1997
+ echo ""
1998
+ echo "### ⚠ Overconfidence Cells (HIGH confidence, survival < 80%)"
1999
+ echo "These task-type × confidence combinations claim HIGH confidence but have poor real-world outcomes."
2000
+ echo "The review threshold for these cells should be tightened (consider needs-human routing)."
2001
+ echo -e "$OVERCONFIDENCE_CELLS"
2002
+ fi
2003
+
2004
+ if [ -n "$OVERCAUTION_CELLS" ]; then
2005
+ echo ""
2006
+ echo "### ℹ Overcaution Candidates (survival > 95% — needs-human may be unnecessary)"
2007
+ echo "These cells have very high survival rates. Needs-human routing on them may be unnecessary,"
2008
+ echo "but any loosening of behavior requires the eval-gate to pass on the affected task-type corpus."
2009
+ echo "Do NOT loosen automatically — flag for human review."
2010
+ echo -e "$OVERCAUTION_CELLS"
2011
+ fi
2012
+
2013
+ # 30-day trend (compare against prior report's PRIOR_CALIBRATION_COMPUTED_AT if available)
2014
+ if [ -n "${PRIOR_CALIBRATION_COMPUTED_AT:-}" ]; then
2015
+ echo ""
2016
+ echo "### Calibration Trend"
2017
+ echo "Prior table computed: ${PRIOR_CALIBRATION_COMPUTED_AT}"
2018
+ echo "Current table computed: ${CALIB_COMPUTED_AT}"
2019
+ echo "(Trend comparison requires prior report data — compare manually if prior available)"
2020
+ fi
2021
+ fi
2022
+ ```
2023
+
2024
+ **Metrics to surface in Phase 5A report**:
2025
+ - **Calibration data coverage**: `${CALIB_TOTAL_RUNS}` runs in corpus (target: > 20 for meaningful cells)
2026
+ - **Overconfidence cells**: count of cells where HIGH-confidence survival < 80%
2027
+ - **Overcaution candidates**: count of cells where survival > 95% (informational — loosening requires eval-gate)
2028
+ - **Table freshness**: `computedAt` timestamp — flag as stale if > 7 days old
2029
+
2030
+ **Why this matters**: Static confidence thresholds in review-pr.md (Phase 7B blocking criteria) are prose rules with no feedback from outcomes. The calibration table is the mechanism by which the pipeline learns whether its stated confidence is tracking reality. A task-type with persistent overconfidence should have its review intensity increased regardless of the agent's stated confidence. <!-- Added: forge#1741 -->
2031
+
2032
+ ---
2033
+