forgedock 1.13.1 → 1.14.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 (34) hide show
  1. package/bin/engine/phases.mjs +160 -18
  2. package/bin/engine-cli.mjs +18 -5
  3. package/bin/engine.mjs +109 -6
  4. package/bin/forgedock.mjs +4 -1
  5. package/commands/orchestrate/phase-3-dependency.md +5 -2
  6. package/commands/orchestrate/phase-4-execution.md +144 -24
  7. package/commands/pipeline-health/phase-2-metrics.md +5 -3
  8. package/commands/review-pr-staging.md +289 -39
  9. package/commands/review-pr.md +604 -42
  10. package/commands/validate.md +11 -3
  11. package/commands/work-on/build/architect.md +11 -8
  12. package/commands/work-on/build.md +27 -6
  13. package/commands/work-on/close.md +5 -4
  14. package/commands/work-on/decompose.md +7 -2
  15. package/commands/work-on/investigate.md +11 -0
  16. package/commands/work-on/remediate.md +135 -74
  17. package/commands/work-on/review.md +3 -3
  18. package/docs/CONFIG.md +14 -1
  19. package/docs/RELEASE-NOTES.md +8 -0
  20. package/package.json +1 -1
  21. package/scripts/architect-resume-match.test.sh +53 -0
  22. package/scripts/check-spec-bash.sh +73 -3
  23. package/scripts/check-spec-bash.test.sh +50 -0
  24. package/scripts/checkbox-sections.test.sh +19 -0
  25. package/scripts/classify-finding.sh +96 -1
  26. package/scripts/classify-finding.test.sh +47 -0
  27. package/scripts/forge-root.test.sh +48 -1
  28. package/scripts/orchestrate-soft-edge.test.sh +58 -0
  29. package/scripts/remediate-basesync.test.sh +62 -3
  30. package/scripts/review-delta.sh +3 -2
  31. package/scripts/review-delta.test.sh +1 -1
  32. package/scripts/review-pr-delta-wiring.test.sh +266 -0
  33. package/scripts/review-pr-staging-scope.test.sh +60 -0
  34. package/scripts/trusted-comments.test.sh +11 -0
@@ -387,7 +387,8 @@ Before building agent prompts, run `classify-lane.sh` for every issue in the cur
387
387
  ```bash
388
388
  # Resolve the helper once and reuse it for the initial dispatch and all finding
389
389
  # classification loops. Claude keeps its installed path as the default, while
390
- # OpenCode and Codex can use ForgeDock's repository-local scripts.
390
+ # OpenCode and Codex use FORGE_HOME / FORGEDOCK_HOME / FORGE_ROOT. Never the target repo
391
+ # ($REPO_PATH): it holds consumer-controlled code (#3400/#3483, #3596).
391
392
  resolve_classify_lane() {
392
393
  local candidates=()
393
394
  if [ "${FORGE_RUNTIME:-}" = "opencode" ] ||
@@ -395,14 +396,22 @@ resolve_classify_lane() {
395
396
  [ -n "${OPENCODE_PID:-}" ] ||
396
397
  [ -n "${OPENCODE:-}" ]; then
397
398
  case '${CLAUDE_PLUGIN_ROOT}' in /*) candidates+=('${CLAUDE_PLUGIN_ROOT}/scripts/classify-lane.sh') ;; esac # running plugin first
399
+ [ -n "${FORGE_ROOT:-}" ] && candidates+=("$FORGE_ROOT/scripts/classify-lane.sh")
400
+ [ -n "${FORGEDOCK_HOME:-}" ] && candidates+=("$FORGEDOCK_HOME/scripts/classify-lane.sh")
398
401
  [ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
399
- [ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
400
402
  candidates+=("$HOME/.opencode/scripts/classify-lane.sh")
401
403
  else
402
404
  case '${CLAUDE_PLUGIN_ROOT}' in /*) candidates+=('${CLAUDE_PLUGIN_ROOT}/scripts/classify-lane.sh') ;; esac # running plugin first
405
+ [ -n "${FORGE_ROOT:-}" ] && candidates+=("$FORGE_ROOT/scripts/classify-lane.sh")
406
+ [ -n "${FORGEDOCK_HOME:-}" ] && candidates+=("$FORGEDOCK_HOME/scripts/classify-lane.sh")
403
407
  [ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/classify-lane.sh")
404
408
  candidates+=("$HOME/.claude/scripts/classify-lane.sh")
405
- [ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/classify-lane.sh")
409
+ local _cfg _d
410
+ for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do # newest pinned plugin cache
411
+ [ -n "$_cfg" ] || continue
412
+ _d=$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1)
413
+ [ -n "$_d" ] && candidates+=("$_d/scripts/classify-lane.sh")
414
+ done
406
415
  fi
407
416
 
408
417
  local candidate
@@ -549,6 +558,19 @@ BATCH_FULLY_GATED=false
549
558
  # Initialise the per-entry keep flag to "keep" at the top of each iteration (unknown = keep).
550
559
  PENDING_DECISIONS=()
551
560
  DECISIONS_PRESENTED=false
561
+ # forge#3621: the breaker decision is enqueued on the false->true trip transition, not at one call site.
562
+ # Every `AMPLIFICATION_BREAKER_TRIPPED=true` assignment (deterministic check, live pre-trip, settle) runs
563
+ # this idempotent helper right after it; it mirrors the release dequeue (`breaker\|*`) and queues at most
564
+ # one breaker entry. Trip sites live in separate bash fences (fresh shell each): they paste this block behind a `declare -f` guard, and the orchestrator records the printed `PENDING_DECISION_QUEUED=` line.
565
+ enqueue_breaker_decision() {
566
+ [ "${AMPLIFICATION_BREAKER:-on}" = "on" ] || return 0
567
+ for _d in ${PENDING_DECISIONS[@]+"${PENDING_DECISIONS[@]}"}; do
568
+ case "$_d" in breaker\|*) return 0 ;; esac
569
+ done
570
+ _BD="breaker|amplification breaker tripped, P3 cascade admission paused|Batch the paused P3 findings or defer them?"
571
+ PENDING_DECISIONS+=("$_BD")
572
+ echo "PENDING_DECISION_QUEUED=$_BD" # forge#3690: a fresh shell drops the array, so the orchestrator records this line
573
+ }
552
574
 
553
575
  # Per-batch token budget for Step 4C's review-finding cascade control (forge#1858).
554
576
  # Distinct from the $-denominated `--budget N` flag (economic scheduling, forge#1743,
@@ -983,7 +1005,7 @@ if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
983
1005
  # were already ready but held back purely by the concurrency cap. DEFERRED_CONCURRENCY_ISSUES
984
1006
  # would become a write-only sink on the engine-first path, silently starving deferred work —
985
1007
  # the same class of bug this PR exists to fix, just relocated.
986
- HEADROOM=$(dispatch_headroom)
1008
+ HEADROOM=$(dispatch_headroom) # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
987
1009
  CANDIDATES=("${DEFERRED_CONCURRENCY_ISSUES[@]}" "${DISPATCH_QUEUE[@]}")
988
1010
  DEFERRED_CONCURRENCY_ISSUES=()
989
1011
  DISPATCH_NOW=()
@@ -1002,11 +1024,11 @@ if [ "$FORGEDOCK_AVAILABLE" = "true" ]; then
1002
1024
  # instruction below re-checks immediately before each launch to close the remaining TOCTOU gap.
1003
1025
  CLAIM_SAFE_DISPATCH=()
1004
1026
  for NUM in "${DISPATCH_NOW[@]}"; do
1005
- if recovery_claim_blocks "$NUM"; then # forge#3172: live FORGE:RECOVERY_CLAIM (fails closed)
1027
+ if recovery_claim_blocks "$NUM"; then # forge#3172: live FORGE:RECOVERY_CLAIM (fails closed) # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
1006
1028
  DEFERRED_CONCURRENCY_ISSUES+=("$NUM")
1007
1029
  continue
1008
1030
  fi
1009
- if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && claim_conflicts_with_live_holder "$NUM"; then
1031
+ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && claim_conflicts_with_live_holder "$NUM"; then # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
1010
1032
  DEFERRED_CLAIM_ISSUES+=("$NUM")
1011
1033
  continue
1012
1034
  fi
@@ -1380,7 +1402,7 @@ AGENT_ISSUE_MAP[{NUMBER}] = <agent_id returned by Agent()>
1380
1402
  # Compute this dispatch batch (forge#1912): merge any previously-deferred concurrency
1381
1403
  # issues (oldest first, for fairness) with the current ready set, then take only as many
1382
1404
  # as headroom allows. The remainder is re-queued, not dropped.
1383
- HEADROOM=$(dispatch_headroom)
1405
+ HEADROOM=$(dispatch_headroom) # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
1384
1406
  CANDIDATES=("${DEFERRED_CONCURRENCY_ISSUES[@]}" {ready_issue_numbers})
1385
1407
  DEFERRED_CONCURRENCY_ISSUES=()
1386
1408
 
@@ -1405,7 +1427,7 @@ If `DISPATCH_NOW` is empty (headroom is 0), do not spawn any agents this cycle
1405
1427
 
1406
1428
  ### Step 4B: Monitor completions and dispatch newly ready issues
1407
1429
 
1408
- **Hard Rule (root orchestrator) — never ask the operator mid-batch; no mid-batch operator question while any issue is dispatchable or in flight** <!-- Added: forge#3497 -->: The worker prompts' "NEVER ask the user questions" rule binds the root session too. A question blocks the root turn, so no completion is processed and no dispatch happens until it is answered, which lets one issue's decision stall every independent issue. When an issue reaches `needs-human`, classify it `GATED` (items 6.4/6.5 below, unchanged), append its decision to `PENDING_DECISIONS` (item 6.5), print the one-line status (item 8), and keep dispatching every issue not gated by it. All queued decisions are presented together, one question per gated issue, at the drain point (see Termination condition), on an operator interrupt (Step 4A-pre.-0.5), or at the Step 4F exit, whichever comes first. Each of those presentation points first runs the `PENDING_DECISIONS` present-time liveness filter (declared with the array in Step 4A; drops entries whose issue is no longer `needs-human`/open, or whose breaker has released) <!-- Added: forge#3508 -->.
1430
+ **Hard Rule (root orchestrator) — never ask the operator mid-batch; no mid-batch operator question while any issue is dispatchable or in flight** <!-- Added: forge#3497 -->: The worker prompts' "NEVER ask the user questions" rule binds the root session too. A question blocks the root turn, so no completion is processed and no dispatch happens until it is answered, which lets one issue's decision stall every independent issue. When an issue reaches `needs-human`, classify it `GATED` (items 6.4/6.5 below, unchanged), append its decision to `PENDING_DECISIONS` (item 6.5), print the one-line status (item 8), and keep dispatching every issue not gated by it. The breaker's own entry arrives as a printed `PENDING_DECISION_QUEUED=breaker|...` line (each trip-site fence is a fresh shell): record it into `PENDING_DECISIONS` unless a `breaker|` entry is already held. All queued decisions are presented together, one question per gated issue, at the drain point (see Termination condition), on an operator interrupt (Step 4A-pre.-0.5), or at the Step 4F exit, whichever comes first. Each of those presentation points first runs the `PENDING_DECISIONS` present-time liveness filter (declared with the array in Step 4A; drops entries whose issue is no longer `needs-human`/open, or whose breaker has released) <!-- Added: forge#3508 -->.
1409
1431
 
1410
1432
  **Exceptions — batch-wide blockers that may still be raised immediately** (each blocks every remaining issue, so there is no independent work left to stall): an orchestrator lease conflict (Step 4A-pre.-1), `SECONDARY_RATE_LIMITED` (Step 4A-pre.0.3, rate limit pause awaiting operator resume), and a fully gated batch with nothing runnable (`BATCH_FULLY_GATED`, item 6.7, which is the drain itself). No other condition is a reason to ask.
1411
1433
 
@@ -1837,7 +1859,54 @@ verify_file_overlap_edge() {
1837
1859
 
1838
1860
  **Recursive cascade note (item 6 only)**: when a dependent `DEP` is itself marked "skipped — dependency failed" (edge confirmed `KEEP`, not dropped), `DEP` becomes the new FAILED anchor for its own direct dependents — re-run this same `verify_file_overlap_edge` check at that next hop too, rather than assuming every transitive descendant is unconditionally skipped. A grandchild dependent whose only path to the failure runs through a spurious (droppable) edge one hop down must not be skipped just because its parent was.
1839
1861
 
1840
- **Never called from**: the `IN_PROGRESS` branch (nothing to re-verify yet — the predecessor hasn't concluded).
1862
+ **Not called from the `IN_PROGRESS` branch**: this helper's `DROP`-on-no-PR rule is only safe for a predecessor that has concluded. The `IN_PROGRESS` branch uses the stricter `verify_soft_edge_early` below instead, which can only `RELEASE` or `HOLD`.
1863
+
1864
+ **Soft-edge early release** <!-- Added: forge#3697 -->: a Layer 1 edge is *soft* (`EDGE_SOFT["${PRED}:${DEP}"]=true`, tagged in `phase-3-dependency.md` Step 3C) when either endpoint's file set came from `body-fallback` provenance, i.e. a pre-investigation guess. Waiting for such a predecessor to merge can serialize independent work for its whole life, so on each readiness pass (every predecessor build -> in-review transition and the periodic poll) an `IN_PROGRESS` predecessor on a soft edge is checked against its live PR diff. `verify_soft_edge_early` returns `RELEASE` only on positive evidence: a non-CLOSED PR exists, `gh pr diff --name-only` succeeded, and no `EDGE_FILES` entry appears in it. Every other path returns `HOLD`: hard edge, no `EDGE_KIND`, lookup error, **no PR yet (HOLD, never DROP)**, diff error, or overlap. Residual risk: a predecessor's PR may still add a shared file in a later commit; the claims-board dispatch gate (forge#2844) re-checks live claims before dispatch, and only soft edges are ever released. A `RELEASE` is recorded in `EDGE_RELEASED` so the edge is not re-queried.
1865
+
1866
+ ```bash
1867
+ verify_soft_edge_early() {
1868
+ local PRED="$1"
1869
+ local DEP="$2"
1870
+
1871
+ # Hard edges (investigator/contract provenance), explicit dependencies, DATABASE chain and
1872
+ # Layer 2-5 edges are never released early.
1873
+ if [ "${EDGE_SOFT["${PRED}:${DEP}"]:-}" != "true" ] || [ -z "${EDGE_KIND["${PRED}:${DEP}"]:-}" ]; then
1874
+ echo "HOLD"
1875
+ return
1876
+ fi
1877
+
1878
+ local PRED_PR PRED_PR_EXIT
1879
+ PRED_PR=$(gh pr list -R {GH_REPO} --state all --search "\"Closes #${PRED}\" in:body" \
1880
+ --json number,state --jq '[.[] | select(.state != "CLOSED")][0].number // empty' 2>/dev/null)
1881
+ PRED_PR_EXIT=$?
1882
+ if [ "$PRED_PR_EXIT" -ne 0 ] || [ -z "$PRED_PR" ]; then
1883
+ echo "HOLD" # lookup failed or no live PR yet: no evidence, keep waiting
1884
+ return
1885
+ fi
1886
+
1887
+ local ACTUAL_FILES DIFF_EXIT
1888
+ ACTUAL_FILES=$(gh pr diff "$PRED_PR" -R {GH_REPO} --name-only 2>/dev/null)
1889
+ DIFF_EXIT=$?
1890
+ if [ "$DIFF_EXIT" -ne 0 ]; then
1891
+ echo "HOLD" # a failed fetch is not "confirmed no overlap"
1892
+ return
1893
+ fi
1894
+
1895
+ local EF EF_CLEAN EF_ESCAPED
1896
+ for EF in ${EDGE_FILES["${PRED}:${DEP}"]:-}; do
1897
+ [ -z "$EF" ] && continue
1898
+ EF_CLEAN="${EF%/}"
1899
+ EF_ESCAPED=$(printf '%s' "$EF_CLEAN" | sed 's/[.[\*^$()+?{|]/\\&/g')
1900
+ if echo "$ACTUAL_FILES" | grep -qE "(^|/)${EF_ESCAPED}(/|$)"; then
1901
+ echo "HOLD" # real overlap: stay serialized
1902
+ return
1903
+ fi
1904
+ done
1905
+ echo "RELEASE"
1906
+ }
1907
+ ```
1908
+
1909
+ **Call sites of `verify_soft_edge_early`**: only the `IN_PROGRESS` arm of the Step 4B readiness loop (below). `verify_file_overlap_edge` keeps its three terminal-state call sites unchanged.
1841
1910
 
1842
1911
  **DONE-path edge re-verification** <!-- Added: forge#2848 --> — the DONE branch *does* call this helper, and it is the only call site that fires on the happy path. Be precise about what it buys, because the obvious reading is wrong:
1843
1912
 
@@ -1860,7 +1929,7 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
1860
1929
  ANY_PRED_GATED=false
1861
1930
  GATING_PREDS=()
1862
1931
  for PRED in {predecessors_of_BLOCKED_NUM}; do
1863
- PRED_STATE=$(classify_predecessor_state "$PRED")
1932
+ PRED_STATE=$(classify_predecessor_state "$PRED") # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
1864
1933
  case "$PRED_STATE" in
1865
1934
  DONE)
1866
1935
  # Same-file current-state brief forwarding (forge#1860): only fires when the
@@ -1881,7 +1950,7 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
1881
1950
  # Fail-safe direction is unchanged: verify_file_overlap_edge returns KEEP on any
1882
1951
  # fetch failure, so a transient API error can only ever preserve the brief, never
1883
1952
  # silently suppress a real one.
1884
- EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$BLOCKED_NUM")
1953
+ EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$BLOCKED_NUM") # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
1885
1954
  if [ "$EDGE_VERDICT" = "DROP" ]; then
1886
1955
  echo "DONE-path re-verification: edge #${PRED} → #${BLOCKED_NUM} (${EDGE_TYPE}, files: ${EDGE_FILES["${PRED}:${BLOCKED_NUM}"]:-?}) DROPPED — #${PRED}'s merged diff never touched the guessed file. Skipping same-file brief."
1887
1956
 
@@ -2006,7 +2075,18 @@ for BLOCKED_NUM in {all_blocked_issue_numbers}; do
2006
2075
  ANY_PRED_GATED=true
2007
2076
  GATING_PREDS+=("$PRED")
2008
2077
  ;; # handled by item 6.5 (blocked-on-human-merge)
2009
- IN_PROGRESS|*) ALL_PREDS_DONE=false ;; # just keep waiting
2078
+ IN_PROGRESS)
2079
+ # forge#3697: a soft (body-fallback) edge is released early when the predecessor's live
2080
+ # PR diff shows no overlap; anything else keeps waiting.
2081
+ if [ "${EDGE_SOFT["${PRED}:${BLOCKED_NUM}"]:-}" = "true" ] && [ "${EDGE_RELEASED["${PRED}:${BLOCKED_NUM}"]:-}" != "true" ]; then
2082
+ if [ "$(verify_soft_edge_early "$PRED" "$BLOCKED_NUM")" = "RELEASE" ]; then # allowlist:fence-funcs (forge#3697: helper defined in the Step 4B fence above)
2083
+ EDGE_RELEASED["${PRED}:${BLOCKED_NUM}"]=true
2084
+ echo "soft edge #${PRED} -> #${BLOCKED_NUM} released early (predecessor PR diff has no overlap)"
2085
+ fi
2086
+ fi
2087
+ [ "${EDGE_RELEASED["${PRED}:${BLOCKED_NUM}"]:-}" = "true" ] || ALL_PREDS_DONE=false
2088
+ ;;
2089
+ *) ALL_PREDS_DONE=false ;; # just keep waiting
2010
2090
  esac
2011
2091
  done
2012
2092
  if [ "$ALL_PREDS_DONE" = "true" ]; then
@@ -2108,7 +2188,7 @@ done
2108
2188
  # retried), then posts the escalation comment (re-read before posting, verified). If the label is not verified no
2109
2189
  # comment is posted. A merged + needs-human issue with no comment classifies GATED regardless of the verifier. A
2110
2190
  # failed half is reported on stderr and the operator must hold dependents manually when the label failed.
2111
- hold_merged_trail {NUMBER} "${TRAIL}" || echo "PHASE TRAIL ESCALATION FAILED: #{NUMBER} — comment or needs-human not verified." >&2
2191
+ hold_merged_trail {NUMBER} "${TRAIL}" || echo "PHASE TRAIL ESCALATION FAILED: #{NUMBER} — comment or needs-human not verified." >&2 # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2112
2192
  fi
2113
2193
  fi
2114
2194
  ```
@@ -2268,7 +2348,7 @@ done
2268
2348
  # After CLAIM_RELEASED for issue NUM:
2269
2349
  # Read all active FORGE:CLAIM annotations from coordination issue
2270
2350
  if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_NUM:-}" ]; then
2271
- ACTIVE_CLAIMS=$(read_active_claims "$COORD_NUM" 2>/dev/null || echo '[]')
2351
+ ACTIVE_CLAIMS=$(read_active_claims "$COORD_NUM" 2>/dev/null || echo '[]') # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2272
2352
  # For each still-blocked issue in a Layer-2/4 pair: check if its claim's file set
2273
2353
  # is disjoint from all remaining active claims. If so, mark it ready.
2274
2354
  # (Layer-1 and Layer-3 edges are never relaxed — this check is Layer-2/4 only.)
@@ -2370,7 +2450,7 @@ Do not ask the user questions — you are running autonomously in the background
2370
2450
  # stdin: rows of {id, body, login, assoc, at}; stdout: the trusted rows.
2371
2451
  rereview_trusted_rows() {
2372
2452
  local ORCH PERMS ROW RA RL RP
2373
- ORCH=$(resolve_orch_login); PERMS=""
2453
+ ORCH=$(resolve_orch_login); PERMS="" # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2374
2454
  while IFS= read -r ROW; do
2375
2455
  [ -n "$ROW" ] || continue
2376
2456
  RA=$(printf '%s' "$ROW" | jq -r '.assoc'); RL=$(printf '%s' "$ROW" | jq -r '.login')
@@ -2405,7 +2485,7 @@ Do not ask the user questions — you are running autonomously in the background
2405
2485
  REREVIEW_STATE="UNREADABLE"
2406
2486
  elif [ "${ISSUE_META%% *}" != "OPEN" ] || [ "${ISSUE_META#* }" != "workflow:in-review" ]; then
2407
2487
  REREVIEW_STATE="NONE" # closed/merged, already at needs-human, or not at workflow:in-review: nothing to re-review
2408
- elif [ -z "$(resolve_orch_login)" ]; then
2488
+ elif [ -z "$(resolve_orch_login)" ]; then # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2409
2489
  # SEC-8: with no own login the orchestrator's own marker is dropped as untrusted, so a dispatch could never be
2410
2490
  # recorded or deduped. Fail closed BEFORE any dispatch.
2411
2491
  REREVIEW_STATE="UNREADABLE"
@@ -2500,7 +2580,7 @@ Do not ask the user questions — you are running autonomously in the background
2500
2580
  local RB_RAW RB_TRUSTED RB_FIRST
2501
2581
  RB_RAW=$(gh api --paginate "repos/{GH_REPO}/issues/${REREVIEW_PR}/comments" \
2502
2582
  --jq '.[] | {id: (.id // 0), body: (.body // ""), login: (.user.login // ""), assoc: (.author_association // "")}' 2>/dev/null) || return 1
2503
- RB_TRUSTED=$(printf '%s\n' "$RB_RAW" | rereview_trusted_rows) || return 1
2583
+ RB_TRUSTED=$(printf '%s\n' "$RB_RAW" | rereview_trusted_rows) || return 1 # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2504
2584
  RB_FIRST=$(printf '%s' "$RB_TRUSTED" | jq -s -r --arg sha "$REREVIEW_SHA" "${REREVIEW_CLAIMS} | .[0].body // \"\"" 2>/dev/null) || return 1
2505
2585
  [ -n "$RB_FIRST" ] || return 1
2506
2586
  printf '%s' "$RB_FIRST"
@@ -2567,7 +2647,7 @@ Do not ask the user questions — you are running autonomously in the background
2567
2647
 
2568
2648
  **Edge re-verification gate (run FIRST, before any tracking)** <!-- Added: forge#1904 -->:
2569
2649
  ```bash
2570
- EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$DEP")
2650
+ EDGE_VERDICT=$(verify_file_overlap_edge "$PRED" "$DEP") # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2571
2651
  if [ "$EDGE_VERDICT" = "DROP" ]; then
2572
2652
  echo "#{DEP} — predecessor #{PRED} is GATED but its EDGE_KIND file-overlap edge does not hold (no PR ever opened, or its actual diff never touched the guessed shared file(s)). Dropping the edge — not tracking #{DEP} as blocked-on-human-merge for this predecessor."
2573
2653
  # Remove PRED from DEP's predecessor set. If DEP has other unresolved predecessors it
@@ -2620,7 +2700,7 @@ Do not ask the user questions — you are running autonomously in the background
2620
2700
  [ -n "$GATING_PREDS" ] || { echo "WARNING: #${DEP} has blocked-on-human-merge but no parseable Gating predecessor line - leaving blocked" >&2; continue; }
2621
2701
  STILL_GATED=false
2622
2702
  for GPRED in $GATING_PREDS; do
2623
- [ "$(classify_predecessor_state "$GPRED")" = "DONE" ] || STILL_GATED=true
2703
+ [ "$(classify_predecessor_state "$GPRED")" = "DONE" ] || STILL_GATED=true # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2624
2704
  done
2625
2705
  [ "$STILL_GATED" = "false" ] || continue
2626
2706
  # Idempotency: only dispatch if DEP hasn't already been dispatched by another path.
@@ -2650,7 +2730,7 @@ Do not ask the user questions — you are running autonomously in the background
2650
2730
  --jq '[.labels[].name | select(startswith("workflow:"))] | length' 2>/dev/null || echo "0")
2651
2731
  [ "$DEP_ALREADY_DISPATCHED" -eq 0 ] || continue
2652
2732
  # Record a human release of a trail-held merged predecessor so the escalation stops gating (decay).
2653
- for GPRED in $GATING_PREDS; do release_merged_trail "$GPRED"; done
2733
+ for GPRED in $GATING_PREDS; do release_merged_trail "$GPRED"; done # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2654
2734
  echo "#{DEP} unblocked (gating predecessors resolved) — dispatching immediately (Steps 4A.pre.0 → 4A.pre → 4A)."
2655
2735
  # Add DEP to the same-response dispatch batch
2656
2736
  fi
@@ -2670,7 +2750,7 @@ Do not ask the user questions — you are running autonomously in the background
2670
2750
  ANY_ORIGINAL_IN_PROGRESS=false
2671
2751
  ANY_ORIGINAL_GATED=false
2672
2752
  for ORIG_NUM in {all_batch_issue_numbers}; do
2673
- ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
2753
+ ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM") # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
2674
2754
  case "$ORIG_STATE" in
2675
2755
  IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
2676
2756
  GATED) ANY_ORIGINAL_GATED=true ;;
@@ -3082,6 +3162,18 @@ When `CASCADE_MAX_AMPLIFICATION` is not `off`, and the current ratio is greater
3082
3162
  **Deterministic breaker check (MANDATORY on every dispatch path — engine CLI, Agent-spawn fallback, or a hand-driven orchestrator):** the in-spec bookkeeping above only runs when the Step 4B/4C bash runs. A batch driven through the Agent-spawn fallback never evaluated it and reached about 2 findings per merge (2026-10-08 audit). Before dispatching ANY `review-finding` issue at `priority:P3` or below, and after every completed merge, run `scripts/amplification-breaker.sh` against GitHub state. Its exit code is authoritative, and it overrides an in-memory `AMPLIFICATION_BREAKER_TRIPPED=false`:
3083
3163
 
3084
3164
  ```bash
3165
+ # forge#3690: every Bash call is a fresh shell, so the helper from Step 4A is not in scope here. Re-declare it (guarded),
3166
+ # and record every printed PENDING_DECISION_QUEUED= line into the orchestrator-held PENDING_DECISIONS (dedup on `breaker|`).
3167
+ declare -p PENDING_DECISIONS >/dev/null 2>&1 || PENDING_DECISIONS=()
3168
+ declare -f enqueue_breaker_decision >/dev/null 2>&1 || enqueue_breaker_decision() {
3169
+ [ "${AMPLIFICATION_BREAKER:-on}" = "on" ] || return 0
3170
+ for _d in ${PENDING_DECISIONS[@]+"${PENDING_DECISIONS[@]}"}; do
3171
+ case "$_d" in breaker\|*) return 0 ;; esac
3172
+ done
3173
+ _BD="breaker|amplification breaker tripped, P3 cascade admission paused|Batch the paused P3 findings or defer them?"
3174
+ PENDING_DECISIONS+=("$_BD")
3175
+ echo "PENDING_DECISION_QUEUED=$_BD" # forge#3690: a fresh shell drops the array, so the orchestrator records this line
3176
+ }
3085
3177
  # HELPER_SCRIPT resolver (AMP_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
3086
3178
  _l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
3087
3179
  _tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
@@ -3101,7 +3193,10 @@ if [ "$AMPLIFICATION_BREAKER" != "off" ]; then
3101
3193
  echo "$AMP_LINE" # include this line verbatim in the Step 4B/4C status update
3102
3194
  # rc 3 (tripped) or 4 (unreadable / unresolved): defer every P3-and-below finding under rule 6
3103
3195
  # into planP3BatchGroups() bounded batches. P1/P2 findings are admitted as before.
3104
- [ "$AMP_RC" -eq 3 ] || [ "$AMP_RC" -eq 4 ] && AMPLIFICATION_BREAKER_TRIPPED=true
3196
+ if [ "$AMP_RC" -eq 3 ] || [ "$AMP_RC" -eq 4 ]; then
3197
+ AMPLIFICATION_BREAKER_TRIPPED=true
3198
+ enqueue_breaker_decision # forge#3621: idempotent; re-trip after a release re-queues the decision
3199
+ fi
3105
3200
  fi
3106
3201
  ```
3107
3202
 
@@ -3156,6 +3251,18 @@ ALL_BATCH_FILES=$(echo "$ALL_BATCH_FILES" | sort -u | grep -v '^$')
3156
3251
  ```
3157
3252
 
3158
3253
  ```bash
3254
+ # forge#3690: every Bash call is a fresh shell, so the helper from Step 4A is not in scope here. Re-declare it (guarded),
3255
+ # and record every printed PENDING_DECISION_QUEUED= line into the orchestrator-held PENDING_DECISIONS (dedup on `breaker|`).
3256
+ declare -p PENDING_DECISIONS >/dev/null 2>&1 || PENDING_DECISIONS=()
3257
+ declare -f enqueue_breaker_decision >/dev/null 2>&1 || enqueue_breaker_decision() {
3258
+ [ "${AMPLIFICATION_BREAKER:-on}" = "on" ] || return 0
3259
+ for _d in ${PENDING_DECISIONS[@]+"${PENDING_DECISIONS[@]}"}; do
3260
+ case "$_d" in breaker\|*) return 0 ;; esac
3261
+ done
3262
+ _BD="breaker|amplification breaker tripped, P3 cascade admission paused|Batch the paused P3 findings or defer them?"
3263
+ PENDING_DECISIONS+=("$_BD")
3264
+ echo "PENDING_DECISION_QUEUED=$_BD" # forge#3690: a fresh shell drops the array, so the orchestrator records this line
3265
+ }
3159
3266
  # Use the same bounded ancestor walk as Phase 1 so batching records actual depth,
3160
3267
  # not just whether the immediate source is a review finding.
3161
3268
  compute_finding_generation() {
@@ -3469,6 +3576,7 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
3469
3576
  printf '%s\n' "${AMPLIFICATION_RATIO_HISTORY[@]: -$((CONVERGENCE_WINDOW - 1))}" | awk '$1 < 1 { exit 1 }'; }; then
3470
3577
  BREAKER_LIVE_TRIPPED=true
3471
3578
  AMPLIFICATION_BREAKER_TRIPPED=true
3579
+ enqueue_breaker_decision # forge#3621: the settle's TRIPPED=false guard will skip its own enqueue
3472
3580
  fi
3473
3581
  fi
3474
3582
  if [ "$BREAKER_LIVE_TRIPPED" = "true" ] && [ "$BREAKER_SAFETY" = "routine" ]; then
@@ -3496,6 +3604,18 @@ done
3496
3604
  **Amplification settle (MANDATORY — after the rule loop above, before batching/dispatch; forge#3060):** now that this unit's findings are counted, record its ratio observation (unrounded; the printed value is rounded for display only) and evaluate the breaker over the latest `CONVERGENCE_WINDOW` observations. Release as soon as the newest observation is below 1.0.
3497
3605
 
3498
3606
  ```bash
3607
+ # forge#3690: every Bash call is a fresh shell, so the helper from Step 4A is not in scope here. Re-declare it (guarded),
3608
+ # and record every printed PENDING_DECISION_QUEUED= line into the orchestrator-held PENDING_DECISIONS (dedup on `breaker|`).
3609
+ declare -p PENDING_DECISIONS >/dev/null 2>&1 || PENDING_DECISIONS=()
3610
+ declare -f enqueue_breaker_decision >/dev/null 2>&1 || enqueue_breaker_decision() {
3611
+ [ "${AMPLIFICATION_BREAKER:-on}" = "on" ] || return 0
3612
+ for _d in ${PENDING_DECISIONS[@]+"${PENDING_DECISIONS[@]}"}; do
3613
+ case "$_d" in breaker\|*) return 0 ;; esac
3614
+ done
3615
+ _BD="breaker|amplification breaker tripped, P3 cascade admission paused|Batch the paused P3 findings or defer them?"
3616
+ PENDING_DECISIONS+=("$_BD")
3617
+ echo "PENDING_DECISION_QUEUED=$_BD" # forge#3690: a fresh shell drops the array, so the orchestrator records this line
3618
+ }
3499
3619
  if [ "$UNIT_MERGED_THIS_CYCLE" = "true" ]; then
3500
3620
  AMPLIFICATION_RATIO=$(awk "BEGIN { printf \"%.2f\", $FINDINGS_SPAWNED / $MERGED_UNITS }")
3501
3621
  AMPLIFICATION_RATIO_HISTORY+=("$(awk "BEGIN { printf \"%.6f\", $FINDINGS_SPAWNED / $MERGED_UNITS }")")
@@ -3506,7 +3626,7 @@ if [ "$UNIT_MERGED_THIS_CYCLE" = "true" ]; then
3506
3626
  if [ "$AMPLIFICATION_BREAKER" = "on" ] && [ "$AMPLIFICATION_BREAKER_TRIPPED" = "false" ]; then
3507
3627
  AMPLIFICATION_BREAKER_TRIPPED=true
3508
3628
  # Queue the decision instead of asking mid-batch (forge#3497); batch-level entry, issue field = breaker.
3509
- PENDING_DECISIONS+=("breaker|amplification breaker tripped, P3 cascade admission paused|Batch the paused P3 findings or defer them?")
3629
+ enqueue_breaker_decision # forge#3621: shared idempotent enqueue (dedups against earlier trip sites)
3510
3630
  # One operator report per trip: ratio and queue size, no per-finding spam.
3511
3631
  echo "AMPLIFICATION BREAKER TRIPPED: ratio ${AMPLIFICATION_RATIO} (${FINDINGS_SPAWNED} findings / ${MERGED_UNITS} merged units) stayed >= 1.0 for ${CONVERGENCE_WINDOW} units. P3 cascade admission is paused; ${#AMPLIFICATION_BREAKER_DEFERRED[@]} finding(s) paused so far. P1/P2 are still admitted. Paused P3s are routed to the bounded P3 batch planner (planP3BatchGroups) and are never dispatched individually. Operator decision queued for drain: batch them or defer. Opt out: orchestration.cascade.amplification_breaker: off"
3512
3632
  fi
@@ -4121,7 +4241,7 @@ if [ "${#IDLE_DEFERRED[@]}" -gt 0 ]; then
4121
4241
  ANY_ORIGINAL_IN_PROGRESS=false
4122
4242
  ANY_ORIGINAL_GATED=false
4123
4243
  for ORIG_NUM in {all_batch_issue_numbers}; do
4124
- ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM")
4244
+ ORIG_STATE=$(classify_predecessor_state "$ORIG_NUM") # allowlist:fence-funcs (forge#3690: pre-existing cross-fence call, tracked separately)
4125
4245
  case "$ORIG_STATE" in
4126
4246
  IN_PROGRESS) ANY_ORIGINAL_IN_PROGRESS=true ;;
4127
4247
  GATED) ANY_ORIGINAL_GATED=true ;;
@@ -148,19 +148,21 @@ Noise share is the fraction of candidate review findings that the `/review-pr`
148
148
  # Records written before forge#3452 lack the keys: skip them (do not count them as zero) and report the sample size.
149
149
  MERGED_PRS=$(gh pr list -R $REPO --state merged --limit 200 --json number,mergedAt \
150
150
  --jq "[.[] | select(.mergedAt > \"$SINCE\")] | .[].number")
151
- DROPPED=0; PREEXISTING=0; DEMOTED=0; FILED=0; NOTES=0; SAMPLE=0
151
+ DROPPED=0; PREEXISTING=0; DEMOTED=0; FILED=0; NOTES=0; REFLAG=0; SAMPLE=0
152
152
  for PR in $MERGED_PRS; do
153
153
  REC=$(gh api --paginate "repos/$REPO/issues/$PR/comments" --jq '.[].body' 2>/dev/null \
154
154
  | grep -m1 'FORGE:NOTE_DISPOSITION' | grep 'findings_dropped_stale=' || true)
155
155
  [ -n "$REC" ] || continue
156
156
  SAMPLE=$((SAMPLE+1))
157
- for K in findings_dropped_stale findings_preexisting notes_demoted_no_scenario findings_filed notes_fixed notes_listed notes_dropped inpr_fix; do
157
+ for K in findings_dropped_stale findings_preexisting notes_demoted_no_scenario findings_filed notes_fixed notes_listed notes_dropped inpr_fix notes_reflag; do
158
158
  V=$(echo "$REC" | tr ' ' '\n' | sed -n "s/^${K}=\([0-9][0-9]*\)$/\1/p" | head -1)
159
159
  case "$K" in
160
160
  findings_dropped_stale) DROPPED=$((DROPPED+${V:-0})) ;;
161
161
  findings_preexisting) PREEXISTING=$((PREEXISTING+${V:-0})) ;;
162
162
  notes_demoted_no_scenario) DEMOTED=$((DEMOTED+${V:-0})) ;;
163
163
  findings_filed|inpr_fix) FILED=$((FILED+${V:-0})) ;;
164
+ # notes_reflag (forge#3641) is a subset of the notes totals: reported only, never added to NOISE or TOTAL. Records lacking it count as 0.
165
+ notes_reflag) REFLAG=$((REFLAG+${V:-0})) ;;
164
166
  *) NOTES=$((NOTES+${V:-0})) ;;
165
167
  esac
166
168
  done
@@ -170,7 +172,7 @@ NOISE=$((DROPPED+PREEXISTING+DEMOTED))
170
172
  # totals, so it is not added to the denominator again.
171
173
  TOTAL=$((DROPPED+PREEXISTING+FILED+NOTES))
172
174
  if [ "$TOTAL" -gt 0 ]; then
173
- echo "Noise share: $((NOISE * 100 / TOTAL))% over $SAMPLE PRs"
175
+ echo "Noise share: $((NOISE * 100 / TOTAL))% over $SAMPLE PRs (cross-head re-flags noted: $REFLAG)"
174
176
  else
175
177
  echo "Noise share: N/A (no post-forge#3452 reviews) over $SAMPLE PRs"
176
178
  fi