forgedock 1.13.0 → 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.
- package/bin/engine/phases.mjs +160 -18
- package/bin/engine-cli.mjs +18 -5
- package/bin/engine.mjs +109 -6
- package/bin/forgedock.mjs +4 -1
- package/commands/orchestrate/phase-3-dependency.md +5 -2
- package/commands/orchestrate/phase-4-execution.md +144 -24
- package/commands/pipeline-health/phase-2-metrics.md +5 -3
- package/commands/review-pr-agents/auth.md +1 -1
- package/commands/review-pr-agents/infra.md +6 -0
- package/commands/review-pr-agents/protocols.md +6 -1
- package/commands/review-pr-agents/scraper.md +3 -0
- package/commands/review-pr-agents/security.md +12 -0
- package/commands/review-pr-staging.md +370 -44
- package/commands/review-pr.md +620 -48
- package/commands/validate.md +11 -3
- package/commands/work-on/build/architect.md +11 -8
- package/commands/work-on/build.md +27 -6
- package/commands/work-on/close.md +5 -4
- package/commands/work-on/decompose.md +7 -2
- package/commands/work-on/investigate.md +11 -0
- package/commands/work-on/remediate.md +135 -74
- package/commands/work-on/review.md +3 -3
- package/docs/CONFIG.md +14 -1
- package/docs/RELEASE-NOTES.md +8 -0
- package/docs/spec/review-protocol.md +6 -1
- package/package.json +1 -1
- package/scripts/architect-resume-match.test.sh +53 -0
- package/scripts/check-spec-bash.sh +73 -3
- package/scripts/check-spec-bash.test.sh +50 -0
- package/scripts/checkbox-sections.test.sh +19 -0
- package/scripts/classify-finding.sh +96 -1
- package/scripts/classify-finding.test.sh +47 -0
- package/scripts/forge-root.test.sh +48 -1
- package/scripts/orchestrate-soft-edge.test.sh +58 -0
- package/scripts/remediate-basesync.test.sh +62 -3
- package/scripts/review-delta.sh +3 -2
- package/scripts/review-delta.test.sh +1 -1
- package/scripts/review-pr-delta-wiring.test.sh +266 -0
- package/scripts/review-pr-gate2.test.sh +13 -0
- package/scripts/review-pr-staging-scope.test.sh +60 -0
- 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
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
|
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 ]
|
|
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
|
-
|
|
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
|
|
@@ -73,7 +73,7 @@ grep -rn "@router\.(get\|post\|put\|delete)\|app\.(get\|post\|put\|delete)" [CHA
|
|
|
73
73
|
- PR description: [PR_BODY]
|
|
74
74
|
|
|
75
75
|
## Steps
|
|
76
|
-
1. Read the pre-supplied diff slice (`$SLICE_FILE`, materialized in Evidence-Based Review Protocol §1) — do NOT re-fetch `gh pr diff [PR_NUMBER]`; use `--name-only` for the file list
|
|
76
|
+
1. Read the pre-supplied diff slice (`$SLICE_FILE`, the deterministic `forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff` path materialized in Evidence-Based Review Protocol §1; re-declare it in any fence that reads it) — do NOT re-fetch `gh pr diff [PR_NUMBER]`; use `--name-only` for the file list
|
|
77
77
|
2. For each endpoint, verify auth dependency matches route type
|
|
78
78
|
3. Check ownership queries for multi-tenancy
|
|
79
79
|
4. Search for auth dependency patterns: `grep -rn "get_current\|require_auth\|authenticated_user\|current_user" $(git ls-files | grep -E "router|route|endpoint" | head -20)`
|
|
@@ -59,6 +59,9 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
59
59
|
|
|
60
60
|
**Scan all consumer locations for references to the old response format:**
|
|
61
61
|
```bash
|
|
62
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
63
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
64
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
62
65
|
# Identify old response field values from the diff
|
|
63
66
|
OLD_VALUES=$(grep "^\-" "$SLICE_FILE" | grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+' | sed -E 's/.*"//' | sort -u)
|
|
64
67
|
|
|
@@ -158,6 +161,9 @@ If no infra context is configured above, derive the deployment model from the ch
|
|
|
158
161
|
11. **Config field type/doc-comment consistency** (when the diff introduces new pydantic-settings fields):
|
|
159
162
|
When a new env var is introduced as a pydantic-settings field with a collection type (`list[str]`, `List[str]`, `set[str]`, `Set[str]`), cross-reference the field's type annotation against the format hint documented in `.env.example`. **pydantic-settings v2 parses collection-type fields via `json.loads()` — they require JSON array format like `["a","b"]`, NOT comma-separated format like `a,b`**. A doc comment saying "comma-separated" on a `list[str]` field is a CONFIRMED HIGH finding — it guarantees a startup crash for anyone following the documented format.
|
|
160
163
|
```bash
|
|
164
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
165
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
166
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
161
167
|
# Find new pydantic-settings fields with collection types in the diff
|
|
162
168
|
grep "^\+" "$SLICE_FILE" | grep -E ":\s*(list|List|set|Set)\[str\]" | grep -oE "[A-Z_]{3,}[[:space:]]*:" | sed -E 's/[[:space:]]*:$//' | sort -u
|
|
163
169
|
# For each field, find its .env.example entry and read the format documentation
|
|
@@ -64,12 +64,17 @@ gh pr diff [PR_NUMBER] --name-only
|
|
|
64
64
|
|
|
65
65
|
# Materialize the pre-computed domain diff slice supplied by the orchestrator.
|
|
66
66
|
# Persona greps below read "$SLICE_FILE" -- they never re-fetch the full diff.
|
|
67
|
-
|
|
67
|
+
# Deterministic path (PR + reviewed SHA + domain): every Bash call is a fresh shell, so later fences
|
|
68
|
+
# re-declare this exact line instead of relying on a variable set here.
|
|
69
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
70
|
+
rm -f "$SLICE_FILE"
|
|
68
71
|
cat > "$SLICE_FILE" <<'FORGE_SLICE_EOF'
|
|
69
72
|
[DOMAIN_DIFF_SLICE]
|
|
70
73
|
FORGE_SLICE_EOF
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
**Later fences**: persona fences that read `$SLICE_FILE` re-declare the identical `SLICE_FILE=` line above and guard it with `[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing ..." >&2; exit 1; }`. The guard tests existence, not size, so a legitimately empty slice still takes the fallback below while a lost path fails loudly instead of reporting a false clean scan.
|
|
77
|
+
|
|
73
78
|
**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.
|
|
74
79
|
|
|
75
80
|
**Tool-result truncation**: When reading individual files or running commands for deeper investigation, always cap output: `cat file.py | head -200`, `grep ... | head -50`. Never pipe unbounded output into context.
|
|
@@ -56,6 +56,9 @@ If no domain context is configured above, derive the architecture from the chang
|
|
|
56
56
|
- If different fields in the condition reach different execution paths (one resource-dependent, one not), the condition must be split — gate only the fields that require the resource.
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
60
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
61
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
59
62
|
# Identify gate conditions in changed router files
|
|
60
63
|
ROUTER_FILES=$(gh pr diff [PR_NUMBER] --name-only | grep -E "(router|route|endpoint|handler)" | head -10)
|
|
61
64
|
awk -v files="$ROUTER_FILES" 'BEGIN{n=split(files,a,"\n");for(i=1;i<=n;i++)w[a[i]]=1} /^diff --git /{p=((substr($4,3)) in w)} p' "$SLICE_FILE" | grep "^\+" | grep -E "if request\.|if.*or.*request\." | head -20
|
|
@@ -212,6 +212,9 @@ For each hit: read the fallback value.
|
|
|
212
212
|
|
|
213
213
|
**H. Runtime UID × volume ownership** (when any Dockerfile is in diff AND diff contains `USER`, `su-exec`, `gosu`, or `setuid` — trigger: `$DOCKERFILE_FILES` non-empty AND UID change detected)
|
|
214
214
|
```bash
|
|
215
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
216
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
217
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
215
218
|
UID_CHANGE=$(while IFS= read -r f; do
|
|
216
219
|
[ -z "$f" ] && continue
|
|
217
220
|
awk -v f="$f" '/^diff --git /{p=(substr($4,3)==f)} p' "$SLICE_FILE" | grep -E '^\+.*(USER\s+[^0]|su-exec|gosu|setuid)' | grep -v '^+++' && break
|
|
@@ -248,6 +251,9 @@ For each named volume found: determine its container mount point. Named volumes
|
|
|
248
251
|
7. **Callback/callable signature mismatch**: When the diff passes a `lambda`, function reference, or callable as a keyword argument to a library/framework function (e.g., `prepared_statement_name_func=lambda _: ""`, `key=lambda x, y: ...`, `default=lambda: ...`), verify the callback's parameter count matches what the library expects. Check the library's default value for that parameter or its documentation. A lambda with wrong arity is a guaranteed `TypeError` at runtime — this is CONFIRMED CRITICAL. Wrong arity cannot be caught by static analysis or `py_compile` — it only fails at the call site. The review agent must verify signature, not just quote the lambda and approve.
|
|
249
252
|
8. **Fix PR input-format coverage** (when PR title contains "fix" and the diff patches a specific input format failure): When a fix PR addresses one documented failure mode (e.g., blank string → `[]`), search `.env.example` for ALL documented input format examples for the affected env var or config field. For each documented format that the fix does NOT handle, flag as CONFIRMED HIGH.
|
|
250
253
|
```bash
|
|
254
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
255
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
256
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
251
257
|
# Find the env var(s) touched by the fix
|
|
252
258
|
FIXED_VARS=$(grep -oE "ENABLED_[A-Z_]+|[A-Z_]{3,}(?=.*decode_complex_value|.*list\[str\]|.*List\[str\])" "$SLICE_FILE" | sort -u)
|
|
253
259
|
# For each var, read its .env.example comment for documented format examples
|
|
@@ -260,6 +266,9 @@ For each named volume found: determine its container mount point. Named volumes
|
|
|
260
266
|
Compare documented formats against handled formats. Any documented format not handled by the fix is a gap. A fix that addresses one documented failure mode (e.g., blank string) while leaving another documented format (e.g., CSV) unhandled is an incomplete fix. <!-- Added: forge#190 -->
|
|
261
267
|
9. **Scope creep detection**: Compare the PR title/description scope against the actual diff size. When a fix PR's diff contains significantly more logic than its stated scope implies, there is a high risk that the extra code introduces bugs the reviewer is not primed to look for — and that the builder agent added context from a different branch or a different issue.
|
|
262
268
|
```bash
|
|
269
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
270
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
271
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
263
272
|
# Count lines added in the diff (excluding whitespace-only lines and file headers)
|
|
264
273
|
DIFF_LINES_ADDED=$(grep -c '^+[^+]' "$SLICE_FILE" 2>/dev/null || echo 0)
|
|
265
274
|
PR_TITLE=$(gh pr view [PR_NUMBER] --json title --jq '.title')
|
|
@@ -304,6 +313,9 @@ For each named volume found: determine its container mount point. Named volumes
|
|
|
304
313
|
|
|
305
314
|
11. **Pattern-completion check** *(conditional — when PR title contains "fix" AND the diff removes or narrows a condition or field gate in a router file)*: When a fix PR changes a condition that guards a function call or restricts a set of fields, grep for the same original (unfixed) condition in sibling files within the same router directory. A fix that corrects one router but leaves identical gates in sibling routers is incomplete — users of those endpoints still receive the original error.
|
|
306
315
|
```bash
|
|
316
|
+
# Fresh shell per Bash call: re-declare the slice path (same value as protocols.md §1) and fail loudly if it is gone.
|
|
317
|
+
SLICE_FILE="${TMPDIR:-/tmp}/forge-slice-[PR_NUMBER]-[REVIEW_SHA_SHORT]-[AGENT_DOMAIN].diff"
|
|
318
|
+
[ -f "$SLICE_FILE" ] || { echo "SLICE_FILE missing: $SLICE_FILE (materialize it per Evidence-Based Review Protocol §1)" >&2; exit 1; }
|
|
307
319
|
# Identify the router directory of the changed file
|
|
308
320
|
ROUTER_DIR=$(gh pr diff [PR_NUMBER] --name-only | grep -E "routers/.*\.py$" | head -1 | xargs dirname 2>/dev/null)
|
|
309
321
|
|