forgedock 1.12.0 → 1.12.1

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/labels.json CHANGED
@@ -39,6 +39,11 @@
39
39
  "color": "5319E7",
40
40
  "description": "Pipeline: PR created, under review. Managed by ForgeDock."
41
41
  },
42
+ {
43
+ "name": "workflow:remediating",
44
+ "color": "C5DEF5",
45
+ "description": "Pipeline: autonomous remediation pending (no human action needed). Managed by ForgeDock."
46
+ },
42
47
  {
43
48
  "name": "workflow:awaiting-merge",
44
49
  "color": "FF8C00",
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  import { classifyInputPattern } from "./engine/resolve.mjs";
17
17
 
18
18
  const VERSION = "opencode-preflight-v1";
19
- const IN_FLIGHT_LABELS = new Set(["workflow:building", "workflow:in-review"]);
19
+ const IN_FLIGHT_LABELS = new Set(["workflow:building", "workflow:in-review", "workflow:remediating"]);
20
20
  const EXCLUDED_LABELS = new Set([
21
21
  "needs-human",
22
22
  "workflow:decomposed",
package/bin/runner.mjs CHANGED
@@ -942,6 +942,24 @@ const USAGE_LIMIT_RESET_PAREN_RE = new RegExp(
942
942
  // Zone-less fallback: capture to end of line, but never across a quote.
943
943
  const USAGE_LIMIT_RESET_LINE_RE = new RegExp(
944
944
  `${USAGE_LIMIT_PHRASE}[^\\n"]*?resets?\\s+([^\\n"]+?)\\s*$`, "im");
945
+ // The two regexes above are polynomial on long no-match lines, so they only
946
+ // ever run on a short window per phrase hit (untrusted input can be tens of MB).
947
+ const USAGE_LIMIT_SCAN_WINDOW_CHARS = 512;
948
+ const USAGE_LIMIT_MAX_PHRASE_HITS = 32;
949
+
950
+ /** Linear scan for phrase hits; returns at most MAX_PHRASE_HITS newline-cut windows. */
951
+ function collectUsageLimitWindows(source) {
952
+ const windows = [];
953
+ const phraseRe = new RegExp(USAGE_LIMIT_PHRASE, "gi");
954
+ let hit;
955
+ while (windows.length < USAGE_LIMIT_MAX_PHRASE_HITS && (hit = phraseRe.exec(source)) !== null) {
956
+ let window = source.slice(hit.index, hit.index + USAGE_LIMIT_SCAN_WINDOW_CHARS);
957
+ const nl = window.indexOf("\n");
958
+ if (nl !== -1) window = window.slice(0, nl);
959
+ windows.push(window);
960
+ }
961
+ return windows;
962
+ }
945
963
 
946
964
  /**
947
965
  * Parses a `claude --output-format json` single-result envelope from `text`.
@@ -1006,8 +1024,12 @@ export function extractSessionLimitResetTime(output) {
1006
1024
  // JSON-wrapped message cannot swallow the trailing `","type":...` fields.
1007
1025
  const text = parseCliEnvelope(output)?.result ?? output;
1008
1026
  const source = typeof text === "string" ? text : output;
1009
- const match =
1010
- USAGE_LIMIT_RESET_PAREN_RE.exec(source) ?? USAGE_LIMIT_RESET_LINE_RE.exec(source);
1027
+ const windows = collectUsageLimitWindows(source);
1028
+ let match = null;
1029
+ for (const w of windows) { if ((match = USAGE_LIMIT_RESET_PAREN_RE.exec(w))) break; }
1030
+ if (!match) {
1031
+ for (const w of windows) { if ((match = USAGE_LIMIT_RESET_LINE_RE.exec(w))) break; }
1032
+ }
1011
1033
  if (!match) return undefined;
1012
1034
  const resetAt = match[1].trim();
1013
1035
  if (resetAt.length === 0) return undefined;
@@ -25,6 +25,7 @@ export const FALLBACK_WORKFLOW_LABELS = [
25
25
  "workflow:ready-to-build",
26
26
  "workflow:building",
27
27
  "workflow:in-review",
28
+ "workflow:remediating",
28
29
  "needs-human",
29
30
  ];
30
31
 
@@ -226,7 +226,7 @@ For each orphaned issue found:
226
226
  ```bash
227
227
  gh issue close $NUM {GH_FLAG} --comment "Closed by cleanup — PR #$MERGED_PR was already merged."
228
228
  gh issue edit $NUM {GH_FLAG} --add-label "workflow:merged"
229
- gh issue edit $NUM {GH_FLAG} --remove-label "workflow:in-review,workflow:awaiting-merge" 2>/dev/null || true
229
+ gh issue edit $NUM {GH_FLAG} --remove-label "workflow:in-review,workflow:remediating,workflow:awaiting-merge" 2>/dev/null || true # allowlist:check-command-side-effects
230
230
  ```
231
231
 
232
232
  Also check open issues with `workflow:building` — same pattern (search for merged PRs referencing them).
@@ -1691,6 +1691,11 @@ classify_predecessor_state() {
1691
1691
  esac
1692
1692
  elif echo "$PRED_LABELS" | grep -qxE "needs-human|workflow:awaiting-merge"; then
1693
1693
  echo "GATED"
1694
+ elif echo "$PRED_LABELS" | grep -qx "workflow:remediating"; then
1695
+ # forge#3541: autonomous remediation (ci-gate / in-pr-fix / base-sync) is pending or running. No human decision is
1696
+ # needed, so this is NOT GATED: dependents keep waiting exactly as for any mid-pipeline predecessor, and item 6.4
1697
+ # (or the engine handoff) drives the fix. A concurrent `needs-human` is matched by the GATED arm above first.
1698
+ echo "IN_PROGRESS"
1694
1699
  elif echo "$PRED_LABELS" | grep -qx "workflow:engine-error"; then
1695
1700
  # forge#2261: the engine/tool itself broke on this run (e.g. a fail-fast
1696
1701
  # CLI_BACKEND_FAILED, or an exhausted retry loop where the runner never
@@ -1721,7 +1726,7 @@ classify_predecessor_state() {
1721
1726
  - **DONE** — predecessor's code is in the base branch (`workflow:merged`, unless gated by a Step 4B item 0 trail escalation), or the predecessor is closed with no pending code. Safe for dependents to dispatch.
1722
1727
  - **GATED** — predecessor is paused pending a human decision (`needs-human`) or pending only a human merge click (`workflow:awaiting-merge`). Its code is NOT yet in the base branch — except for a `workflow:merged` issue that also carries `needs-human` and the Step 4B item 0 trail-escalation comment (merged with an incomplete phase trail), whose code is merged but whose trail needs human review. Dependents are neither dispatched nor skipped — they move to the `blocked-on-human-merge` tracked state (item 6.5 below).
1723
1728
  - **FAILED** — predecessor was closed as `workflow:invalid`, or the agent explicitly reported a build/test error. Dependents are marked "skipped — dependency failed" (item 6 below) — unchanged from prior behavior.
1724
- - **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`), or terminated `workflow:engine-error` (forge#2261 — an engine/tool failure, not a human-judgment block or a genuine content failure; treated as still-in-flight since stall-detection auto-resumes it). Dependent simply continues waiting; no special tracking needed.
1729
+ - **IN_PROGRESS** — predecessor is still mid-pipeline (`investigating`/`ready-to-build`/`building`/`in-review`), or autonomous remediation is pending (`workflow:remediating`, forge#3541 — the pipeline is fixing a base conflict, red CI or an in-PR finding itself; no human gate, so dependents are not held as GATED), or terminated `workflow:engine-error` (forge#2261 — an engine/tool failure, not a human-judgment block or a genuine content failure; treated as still-in-flight since stall-detection auto-resumes it). Dependent simply continues waiting; no special tracking needed.
1725
1730
 
1726
1731
  A GATED predecessor whose PR later merges reclassifies to DONE the next time `classify_predecessor_state` runs (its label flips to `workflow:merged`; a merged predecessor gated on its phase trail reclassifies to DONE once a human removes `needs-human`) — this is exactly what the merge-triggered wake check (item 6.6 below) relies on.
1727
1732
 
@@ -2284,7 +2289,7 @@ done
2284
2289
 
2285
2290
  Report every dependent whose skip was avoided this way — e.g. "#{DEP} — predecessor #{PRED} failed but never touched the shared file(s); edge dropped, #{DEP} not skipped." For all remaining dependents (real `EDGE_KIND` overlap confirmed, or a non-`EDGE_KIND` edge type), mark them "skipped — dependency #{X} failed" and report them. Do NOT dispatch them.
2286
2291
 
2287
- 6.4. **Auto-dispatch remediation against a `needs-human`-gated issue's own PR — runs unconditionally per completion, with or without dependents** <!-- Added: forge#1813, fixed: forge#2243 --> — item 6.5 below tracks the *dependents* of a `GATED` predecessor; this item handles the gated issue's own PR, which item 6.5/6.6 never re-drive on their own. **This check is bound directly to the issue that just completed — `PRED="$NUM"` (the same issue number carried from items 1-4 of this Step 4B sequence) — NOT to any `$PRED` produced by walking a dependent's predecessor list (item 5's readiness loop) or by item 6's FAILED-specific dependent-walk.** Run it for every completed agent, every cycle, regardless of whether that issue has any dependents in the DAG at all — a leaf issue (no dependents) is just as eligible as an issue that blocks others. Trigger condition: the completed issue classifies `GATED` **specifically via `needs-human`** — NOT `workflow:awaiting-merge`. That second state already means "remediated and re-reviewed to a clean verdict" (see forge#1810's guard) — dispatching remediation again would be redundant, not just wasteful, since there is nothing left to fix. A PR that reaches `workflow:awaiting-merge` now does so only when it targets `main` / the deploy gate (where `staging → main` is the genuine human gate and a human's merge click is required); a remediated-clean PR targeting a non-`main` base (`staging`, `milestone/*`) auto-lands to `workflow:merged` via remediate.md Phase M7's base-scoped auto-land bar (forge#2570) and never parks here. Either way this item's skip of `workflow:awaiting-merge` is unchanged.
2292
+ 6.4. **Auto-dispatch remediation against a `needs-human`-gated or `workflow:remediating` issue's own PR — runs unconditionally per completion, with or without dependents** <!-- Added: forge#1813, fixed: forge#2243 --> — item 6.5 below tracks the *dependents* of a `GATED` predecessor; this item handles the gated issue's own PR, which item 6.5/6.6 never re-drive on their own. **This check is bound directly to the issue that just completed — `PRED="$NUM"` (the same issue number carried from items 1-4 of this Step 4B sequence) — NOT to any `$PRED` produced by walking a dependent's predecessor list (item 5's readiness loop) or by item 6's FAILED-specific dependent-walk.** Run it for every completed agent, every cycle, regardless of whether that issue has any dependents in the DAG at all — a leaf issue (no dependents) is just as eligible as an issue that blocks others. Trigger condition: the completed issue carries `needs-human` (classifies `GATED`) **or `workflow:remediating`** (classifies `IN_PROGRESS`, forge#3541 — the non-human state review sets for autonomous ci-gate / in-pr-fix / base-sync handoffs) — NOT `workflow:awaiting-merge`. That second state already means "remediated and re-reviewed to a clean verdict" (see forge#1810's guard) — dispatching remediation again would be redundant, not just wasteful, since there is nothing left to fix. A PR that reaches `workflow:awaiting-merge` now does so only when it targets `main` / the deploy gate (where `staging → main` is the genuine human gate and a human's merge click is required); a remediated-clean PR targeting a non-`main` base (`staging`, `milestone/*`) auto-lands to `workflow:merged` via remediate.md Phase M7's base-scoped auto-land bar (forge#2570) and never parks here. Either way this item's skip of `workflow:awaiting-merge` is unchanged.
2288
2293
 
2289
2294
  ```bash
2290
2295
  # Bind PRED to the issue that just completed — independent of item 5's dependent-walk loop
@@ -2294,9 +2299,9 @@ done
2294
2299
  PRED="$NUM"
2295
2300
 
2296
2301
  PRED_CURRENT_LABEL=$(gh issue view "$PRED" -R {GH_REPO} --json labels \
2297
- --jq '[.labels[].name | select(. == "needs-human" or . == "workflow:awaiting-merge")] | .[0] // empty' 2>/dev/null)
2302
+ --jq '[.labels[].name | select(. == "needs-human" or . == "workflow:remediating" or . == "workflow:awaiting-merge")] | .[0] // empty' 2>/dev/null)
2298
2303
 
2299
- if [ "$PRED_CURRENT_LABEL" = "needs-human" ]; then
2304
+ if [ "$PRED_CURRENT_LABEL" = "needs-human" ] || [ "$PRED_CURRENT_LABEL" = "workflow:remediating" ]; then
2300
2305
  # Resolve PRED's open PR using the anchored search (forge#1634/#1646 precedent —
2301
2306
  # never a bare-number search, which would misattribute an unrelated PR).
2302
2307
  GATING_PR=$(gh pr list -R {GH_REPO} --state open --search "\"Closes #${PRED}\" in:body" \
@@ -2308,18 +2313,23 @@ done
2308
2313
  # avoids spawning a redundant agent that would immediately no-op on entry).
2309
2314
  # Leading-marker + trusted-author count (forge#3412): a comment that merely quotes FORGE:REMEDIATION
2310
2315
  # (review findings, INPR_FIX work order) must not suppress dispatch. Unreadable => skip dispatch (fail closed).
2316
+ # TRUSTED_SCRIPT resolver (canonical; byte-identical across specs, guarded by scripts/forge-root.test.sh): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. Never the working directory (author-controlled, #3400). The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
2311
2317
  _l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
2318
+ _tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
2319
+ for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
2320
+ [ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
2321
+ done
2312
2322
  TRUSTED_SCRIPT=""
2313
- for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
2323
+ while IFS= read -r _c; do
2314
2324
  case "$_c" in /*) [ -z "$TRUSTED_SCRIPT" ] && [ -f "$_c/scripts/trusted-comments.sh" ] && TRUSTED_SCRIPT="$_c/scripts/trusted-comments.sh" ;; esac
2315
- done
2325
+ done <<< "$_tc"
2316
2326
  ALREADY_REMEDIATED=1
2317
2327
  if [ -n "$TRUSTED_SCRIPT" ] && GATING_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/${GATING_PR}/comments 2>/dev/null); then
2318
2328
  ALREADY_REMEDIATED=$(printf '%s' "$GATING_COMMENTS" | bash "$TRUSTED_SCRIPT" count '^<!-- FORGE:REMEDIATION -->' 2>/dev/null || echo "1")
2319
2329
  fi
2320
2330
 
2321
2331
  if [ "$ALREADY_REMEDIATED" -eq 0 ]; then
2322
- echo "Dispatching remediation for #{PRED}'s gating PR #{GATING_PR} (needs-human)"
2332
+ echo "Dispatching remediation for #{PRED}'s gating PR #{GATING_PR} (${PRED_CURRENT_LABEL})"
2323
2333
  # Same Agent-spawn-fallback style as Step 4A's template — one background agent,
2324
2334
  # whose sole job is to invoke /work-on in remediation mode and let it run to
2325
2335
  # completion (AUTO-LANDED, HELD-AWAITING-MERGE, RE-ESCALATED, or UNFIXABLE — all
@@ -2329,9 +2339,9 @@ done
2329
2339
  model="{SUBAGENT_MODEL}",
2330
2340
  description="Remediate PR #{GATING_PR} (needs-human, blocks #{PRED})",
2331
2341
  run_in_background=true,
2332
- prompt="You are remediating GitHub PR #{GATING_PR} for the {PROJECT_NAME} project (repo: {GH_REPO}), which is currently held at `needs-human` on its linked issue #{PRED}.
2342
+ prompt="You are remediating GitHub PR #{GATING_PR} for the {PROJECT_NAME} project (repo: {GH_REPO}), which is currently held at `needs-human` or `workflow:remediating` (autonomous remediation pending) on its linked issue #{PRED}.
2333
2343
 
2334
- **YOUR MISSION**: Invoke `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{GATING_PR} --remediate --issue {PRED} --repo {GH_REPO} --gh-flag {GH_FLAG} --under-orchestration')` and let it run to completion. This is a self-contained flow: it classifies the gate first, replaces `needs-human` with `workflow:in-review` only for fixable remediation, checks out the PR branch, fixes any fixable review findings, re-reviews, and either auto-lands the PR, holds it at `workflow:awaiting-merge` for a human, re-escalates back to `needs-human`, or reports the block as policy-level and unfixable. Do NOT intervene manually — do not run raw git/gh commands yourself.
2344
+ **YOUR MISSION**: Invoke `Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='{GATING_PR} --remediate --issue {PRED} --repo {GH_REPO} --gh-flag {GH_FLAG} --under-orchestration')` and let it run to completion. This is a self-contained flow: it classifies the gate first, replaces `needs-human` / `workflow:remediating` with `workflow:in-review` only for fixable remediation, checks out the PR branch, fixes any fixable review findings, re-reviews, and either auto-lands the PR, holds it at `workflow:awaiting-merge` for a human, re-escalates back to `needs-human`, or reports the block as policy-level and unfixable. Do NOT intervene manually — do not run raw git/gh commands yourself.
2335
2345
 
2336
2346
  **DO NOT STOP EARLY**: if the Skill call returns without a terminal `REMEDIATE_RESULT.status`, invoke it again — it re-reads GitHub state and resumes. Terminal statuses are: `COMPLETE`, `ALREADY_DONE`, `UNFIXABLE`, `BLOCKED`, `REREVIEW_REQUIRED`. `REREVIEW_REQUIRED` is terminal for the worker only: the orchestrator then runs the re-review handoff (item 6.4) or the terminal fallback, so it is never a silent end state.
2337
2347
 
@@ -2378,8 +2388,9 @@ Do not ask the user questions — you are running autonomously in the background
2378
2388
  # Lease age reads the NEWEST row carrying the same `Claim:` id as the earliest claim (.[0]); ownership/LOST stays on .[0].
2379
2389
  REREVIEW_LEASE_FILTER='(.[0].body | capture("Claim: (?<c>[^\\n]+)").c) as $c | [.[] | select(.body | split("\n") | any(. == ("Claim: " + $c)))] | .[-1].body | capture("Lease: (?<t>[0-9T:Z-]+)").t'
2380
2390
  REREVIEW_FIRST_LEASE_FILTER='.[0].body | capture("Lease: (?<t>[0-9T:Z-]+)").t'
2381
- # Explicit release: a trusted leading `<!-- FORGE:REREVIEW_RELEASED -->` row for this head (posted by the claimant after review-pr returned).
2382
- REREVIEW_RELEASED_FILTER='[.[] | select((.body | startswith("<!-- FORGE:REREVIEW_RELEASED -->")) and (.body | contains("Head: " + $sha)))] | length'
2391
+ # Explicit release: a trusted leading `<!-- FORGE:REREVIEW_RELEASED -->` row for this head (posted by the claimant only after the outcome is settled, forge#3486).
2392
+ # Only a row carrying `Outcome: SETTLED` counts; a legacy or unsettled release (no Outcome line) is ignored so it can never short-circuit staleness detection.
2393
+ REREVIEW_RELEASED_FILTER='[.[] | select((.body | startswith("<!-- FORGE:REREVIEW_RELEASED -->")) and (.body | contains("Head: " + $sha)) and (.body | split("\n") | any(. == "Outcome: SETTLED")))] | length'
2383
2394
 
2384
2395
  PRED="$NUM"
2385
2396
  REREVIEW_STATE="NONE"; REREVIEW_PR=""; REREVIEW_SHA=""; REREVIEW_BASE=""
@@ -2414,7 +2425,8 @@ Do not ask the user questions — you are running autonomously in the background
2414
2425
  REREVIEW_STATE="PENDING" # DRY_RUN is handled in Step 2 (no `DRY_RUN != true` term here, so its branch is reachable)
2415
2426
  elif [ "$NEEDS_REREVIEW" -ge 1 ]; then
2416
2427
  # A claim for this head already exists (orchestrator or router). forge#3428: age alone never makes it stale.
2417
- # released -> NONE (claimant finished; nothing to dispatch or escalate)
2428
+ # released -> STALE (forge#3486: Step 1 only reaches here with the issue still at workflow:in-review, so a SETTLED release
2429
+ # is not terminal; the outcome never landed (crash or label reset) and the fallback must re-apply)
2418
2430
  # lease still fresh -> CLAIMED (the lease is the newest row of the owner's claim; a renewal extends it)
2419
2431
  # lease expired -> LIVENESS probe: PR not open -> NONE; recent trusted FORGE:REVIEW* activity inside the lease
2420
2432
  # window or running checks -> CLAIMED; no sign of life -> STALE. A lookup error is UNREADABLE.
@@ -2426,7 +2438,7 @@ Do not ask the user questions — you are running autonomously in the background
2426
2438
  FIRST_EPOCH=$(printf '%s' "$FIRST_T" | jq -R 'fromdateiso8601' 2>/dev/null || true)
2427
2439
  NOW=$(date -u +%s)
2428
2440
  if [ -z "$RELEASED" ]; then REREVIEW_STATE="UNREADABLE"
2429
- elif [ "$RELEASED" -ge 1 ]; then REREVIEW_STATE="NONE"
2441
+ elif [ "$RELEASED" -ge 1 ]; then REREVIEW_STATE="STALE" # released but the issue is still workflow:in-review with an open PR: not terminal, re-apply the fallback
2430
2442
  elif [ -z "$CLAIM_EPOCH" ] || [ -z "$FIRST_EPOCH" ]; then REREVIEW_STATE="STALE" # malformed claim (no parseable Lease)
2431
2443
  elif [ $(( NOW - CLAIM_EPOCH )) -le "$REREVIEW_LEASE_SECS" ]; then REREVIEW_STATE="CLAIMED"
2432
2444
  elif ! LIVE_META=$(gh pr view "$REREVIEW_PR" -R {GH_REPO} --json state,statusCheckRollup \
@@ -2454,7 +2466,7 @@ Do not ask the user questions — you are running autonomously in the background
2454
2466
  echo "REREVIEW_STATE=$REREVIEW_STATE PR=$REREVIEW_PR SHA=$REREVIEW_SHA BASE=$REREVIEW_BASE FIRST_CLAIM_ID=$FIRST_CLAIM_ID"
2455
2467
  ```
2456
2468
 
2457
- `REREVIEW_STATE=NONE` means nothing to do (including an issue already at `needs-human`, closed, or not at `workflow:in-review`). `CLAIMED` means another actor holds a live claim for this head (an unexpired or renewed lease, or an expired lease with recent liveness such as a trusted `FORGE:REVIEW*` comment inside the lease window or running checks): do nothing, re-run Step 1 next cycle. A `FORGE:REREVIEW_RELEASED` marker for the head (posted by the claimant after `review-pr` returned) or a merged/closed PR is `NONE`: the claimant finished, so there is nothing to dispatch or escalate. `UNREADABLE` fails closed: classify the issue `GATED` this cycle without writing anything, and re-run Step 1 next cycle. `STALE` (an expired lease with no liveness and the PR still open, a lease past the `REREVIEW_MAX_SECS` ceiling, an unparseable claim, or a trail that does not match the current head) goes straight to the Step 3 terminal fallback with the reason `stale re-review claim or trail`, without dispatching. Only `PENDING` continues.
2469
+ `REREVIEW_STATE=NONE` means nothing to do (including an issue already at `needs-human`, closed, or not at `workflow:in-review`). `CLAIMED` means another actor holds a live claim for this head (an unexpired or renewed lease, or an expired lease with recent liveness such as a trusted `FORGE:REVIEW*` comment inside the lease window or running checks): do nothing, re-run Step 1 next cycle. A merged/closed PR is `NONE`. A `FORGE:REREVIEW_RELEASED` marker (`Outcome: SETTLED`) for the head whose issue is still at `workflow:in-review` is `STALE`, not `NONE` (forge#3486): the release was posted but the terminal outcome never landed (a crash between the release and the fallback write, or a human label reset on the same head), so the release is not terminal and the fallback re-applies. `UNREADABLE` fails closed: classify the issue `GATED` this cycle without writing anything, and re-run Step 1 next cycle. `STALE` (an expired lease with no liveness and the PR still open, a lease past the `REREVIEW_MAX_SECS` ceiling, an unparseable claim, or a trail that does not match the current head) goes straight to the Step 3 terminal fallback with the reason `stale re-review claim or trail`, without dispatching. Only `PENDING` continues.
2458
2470
 
2459
2471
  **Step 2 — dispatch (claim first, forge#3413).** The claim is written and read back BEFORE `review-pr` is invoked. A failed post or read-back means NO dispatch (fail closed, terminal fallback), so there is never a wasted duplicate dispatch, and a second actor sees the claim and stands down. Bash blocks are independent executions: run the claim block with the **SHARED HELPERS** (`rereview_trusted_rows`, `REREVIEW_CLAIMS`, `REREVIEW_DONE_FILTER`, the lease/released filters and `REREVIEW_LEASE_SECS` / `REREVIEW_MAX_SECS`, plus its transitive deps `resolve_orch_login` and `trail_cache_dir`, pasted if not already defined in the block) from Step 1 pasted verbatim at its top, and `REREVIEW_PR` / `REREVIEW_SHA` set to the values Step 1 printed. The read-back lists the PR's comments by number and applies the same trusted predicate. The EARLIEST trusted claim for the head wins, defined as the LOWEST comment id (`.[0]` of the id-sorted `REREVIEW_CLAIMS`, unchanged by renewals; forge#3429): if the first matching row does not carry this actor's `Claim:` id, another actor claimed first (`LOST`) and this one stands down. The read-back waits a short settle delay before the first read and re-reads once after `VERIFIED` (a lower-id claim that appears late demotes it to `LOST`). Any API failure, zero matching rows, or a failed confirming re-read is `UNVERIFIED` (fail closed, never `VERIFIED`). The block has no `exit`, so it is safe in a shared or sourced shell:
2460
2472
 
@@ -2519,7 +2531,7 @@ Do not ask the user questions — you are running autonomously in the background
2519
2531
 
2520
2532
  If the call returns no `REVIEW_RESULT` (interrupted or no result), invoke it once more; this retry is the only one (bounded, independent of the Phase 4R `github-unavailable` waits) and does not post a second claim: the claim for this head is already recorded.
2521
2533
 
2522
- **Renew and release (forge#3428).** After `review-pr` returns (any result, including none), the claimant posts a checked, DRY_RUN-guarded release so the next Step 1 reads `NONE` instead of aging the claim into `STALE`. Run it with the SHARED HELPERS (the same set, including `resolve_orch_login` and `trail_cache_dir`) pasted at the top and `REREVIEW_PR` / `REREVIEW_SHA` / `CLAIM_ID` from Step 2. A failed post is reported (never ignored) but is not fatal: liveness in Step 1 still protects a live claimant. Renewal is best-effort: between review phases the claimant may re-post `<!-- FORGE:REREVIEW_DISPATCHED -->` with the SAME `Claim:` id and a fresh `Lease:` (Step 1 reads the newest row of the earliest claim's id for lease age; the earliest-claim-wins read-back stays on `.[0]`):
2534
+ **Renew and release (forge#3428).** After `review-pr` returns (any result, including none), the claimant posts a checked, DRY_RUN-guarded release, but only AFTER the outcome is settled (forge#3486): `review-pr` produced a labelled terminal state (merged, or `needs-human`), or the Step 3 terminal fallback's label write was verified. Never post it before Step 3 has run: a release that precedes an unfinished outcome would hide a crash from Step 1. Because Step 1 only classifies issues still at `workflow:in-review`, a release found there means the outcome never landed and reads `STALE`. Run it with the SHARED HELPERS (the same set, including `resolve_orch_login` and `trail_cache_dir`) pasted at the top and `REREVIEW_PR` / `REREVIEW_SHA` / `CLAIM_ID` from Step 2. A failed post is reported (never ignored) but is not fatal: liveness in Step 1 still protects a live claimant. Renewal is best-effort: between review phases the claimant may re-post `<!-- FORGE:REREVIEW_DISPATCHED -->` with the SAME `Claim:` id and a fresh `Lease:` (Step 1 reads the newest row of the earliest claim's id for lease age; the earliest-claim-wins read-back stays on `.[0]`):
2523
2535
 
2524
2536
  ```bash
2525
2537
  if [ "${DRY_RUN:-false}" = "true" ]; then
@@ -2529,13 +2541,16 @@ Do not ask the user questions — you are running autonomously in the background
2529
2541
  Head: ${REREVIEW_SHA}
2530
2542
  Actor: orchestrator
2531
2543
  Claim: ${CLAIM_ID}
2544
+ Outcome: SETTLED
2532
2545
 
2533
- Re-review finished by the orchestrator: claim released."; }
2546
+ Re-review finished by the orchestrator and the outcome is settled: claim released."; }
2534
2547
  post_release || { sleep 2; post_release || echo "REREVIEW release post failed twice (liveness still protects the claim)" >&2; }
2535
2548
  fi
2536
2549
  ```
2537
2550
 
2538
- **Step 3 — outcome.** On `REVIEW_RESULT: status: COMPLETE` (merged), drive `work-on:close` and let item 6.6 wake dependents as normal. If `review-pr` returns another result with its own labelled terminal state (CHANGES REQUESTED, or `needs-human` for genuine blockers), that verdict applies and the issue is `GATED` or failed per the existing rules. `REREVIEW_MARKER=LOST` (another actor claimed this head first) and `REREVIEW_STATE=CLAIMED` take no action: leave the issue as it is and re-check next cycle. Before applying the terminal fallback for `STALE`, re-read the PR state and the issue labels once more: a merged or closed PR, or an issue that already advanced, means skip the fallback and report the state (forge#3428). If dispatch failed, the skill did not resolve, the issue is still at `workflow:in-review` with no `needs-human` after the retry, Step 1 printed `STALE`, or Step 2 printed `REREVIEW_MARKER=UNVERIFIED` and the re-check below found no live claim, apply the **terminal fallback** defined in `commands/work-on.md` Phase 0A.1 (labels re-read first, comment with the reason, add `needs-human` and remove `workflow:in-review` in one edit, verify the write), classify the issue `GATED`, and surface it to the operator in the run summary as `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): <reason>`. For `REREVIEW_MARKER=UNVERIFIED`, first re-check for another actor's fresh claim (forge#3429): re-run the Step 1 CLAIMED/STALE/DONE classification for this head (with the SHARED HELPERS pasted at the top of that block). Map the result, reading the printed `FIRST_CLAIM_ID` (the lowest-id, winning claim, the same `.[0]` row that decides ownership everywhere else): if `FIRST_CLAIM_ID` equals this actor's own `CLAIM_ID`, the winning claim is our own unverified one, so go to the fallback (do not stand down on it). If `FIRST_CLAIM_ID` is another actor's id, the winning claim is not ours: classify it with the mapping below, exactly as the router's Claim pre-check step 4 does for `LOST` (forge#3431), so a `STALE` foreign winner (a crashed earlier claimant) goes to the fallback and never strands the issue, even though our own row still exists. Otherwise (and for a foreign winner) `CLAIMED` (another actor's live claim) and `NONE` (released or done) stand down like `LOST` (no dispatch, no fallback, re-check next cycle); `UNREADABLE` writes nothing (fail closed, classify `GATED` this cycle without the fallback, as in Step 1); `STALE` and `PENDING` go to the fallback. The reason is `re-review claim could not be recorded` and the report line is `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): marker write failed`; because the claim is written first, `UNVERIFIED` now means `review-pr` was NOT dispatched. Do not leave the issue `PENDING`. A failed label write is reported as such, not as done. The verified per-head claim bounds this to one re-review per PR head across the orchestrator and the router (no loop). Never review inline.
2551
+ Run the release block only at the end of Step 3 below, once the outcome is settled (merged or labelled terminal state, or the fallback's label write verified), not right after `review-pr` returns.
2552
+
2553
+ **Step 3 — outcome.** On `REVIEW_RESULT: status: COMPLETE` (merged), drive `work-on:close` and let item 6.6 wake dependents as normal. If `review-pr` returns another result with its own labelled terminal state (CHANGES REQUESTED, or `needs-human` for genuine blockers), that verdict applies and the issue is `GATED` or failed per the existing rules. `REREVIEW_MARKER=LOST` (another actor claimed this head first) and `REREVIEW_STATE=CLAIMED` take no action: leave the issue as it is and re-check next cycle. Before applying the terminal fallback for `STALE`, re-read the PR state and the issue labels once more: a merged or closed PR, or an issue that already advanced, means skip the fallback and report the state (forge#3428). If dispatch failed, the skill did not resolve, the issue is still at `workflow:in-review` with no `needs-human` after the retry, Step 1 printed `STALE`, or Step 2 printed `REREVIEW_MARKER=UNVERIFIED` and the re-check below found no live claim, apply the **terminal fallback** defined in `commands/work-on.md` Phase 0A.1 (labels re-read first, comment with the reason, add `needs-human` and remove `workflow:in-review` in one edit, verify the write), classify the issue `GATED`, and surface it to the operator in the run summary as `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): <reason>`. For `REREVIEW_MARKER=UNVERIFIED`, first re-check for another actor's fresh claim (forge#3429): re-run the Step 1 CLAIMED/STALE/DONE classification for this head (with the SHARED HELPERS pasted at the top of that block). Map the result, reading the printed `FIRST_CLAIM_ID` (the lowest-id, winning claim, the same `.[0]` row that decides ownership everywhere else): if `FIRST_CLAIM_ID` equals this actor's own `CLAIM_ID`, the winning claim is our own unverified one, so go to the fallback (do not stand down on it). If `FIRST_CLAIM_ID` is another actor's id, the winning claim is not ours: classify it with the mapping below, exactly as the router's Claim pre-check step 4 does for `LOST` (forge#3431), so a `STALE` foreign winner (a crashed earlier claimant) goes to the fallback and never strands the issue, even though our own row still exists. Otherwise (and for a foreign winner) `CLAIMED` (another actor's live claim) and `NONE` (merged, closed, or advanced past in-review) stand down like `LOST` (no dispatch, no fallback, re-check next cycle); `UNREADABLE` writes nothing (fail closed, classify `GATED` this cycle without the fallback, as in Step 1); `STALE` and `PENDING` go to the fallback. The reason is `re-review claim could not be recorded` and the report line is `GATED: re-review could not run (#{PRED}, PR #{REREVIEW_PR}): marker write failed`; because the claim is written first, `UNVERIFIED` now means `review-pr` was NOT dispatched. Do not leave the issue `PENDING`. A failed label write is reported as such, not as done. The verified per-head claim bounds this to one re-review per PR head across the orchestrator and the router (no loop). Never review inline.
2539
2554
 
2540
2555
  This satisfies #1809 Q2 (the orchestrator auto-dispatches remediation against the gated issue itself — the exact gap forge#1812's item 6.5/6.6 left open, since those items only ever track and wake *dependents*, never the gated PR's own remediation) — and closes forge#2243 (the gap that #1812's fix left open for *leaf* issues with no dependents: because this item's `$PRED` binding was never made explicit and self-contained, it was only ever reached while walking a dependent's predecessor list, so a `needs-human` issue that has no dependents never got remediation dispatched and its CHANGES-REQUESTED PR re-reviewed the same unchanged commit forever). The remediation agent's outcome is picked up on the **next** completion-monitoring cycle of this same Step 4B loop: if it lands (`workflow:merged`), item 6.6 below fires normally and wakes any `blocked-on-human-merge` dependents (if any exist — a leaf issue simply has none to wake); if it holds/re-escalates, the issue simply remains `GATED` and item 6.5 continues tracking its dependents (if any) unchanged.
2541
2556
 
@@ -100,7 +100,7 @@ if [ -n "$TARGET_ISSUE" ]; then
100
100
  else
101
101
  # Fleet scan: query each workflow label separately, merge, deduplicate
102
102
  ORPHAN_JSON=$(
103
- for LABEL in "workflow:investigating" "workflow:ready-to-build" "workflow:building" "workflow:in-review"; do
103
+ for LABEL in "workflow:investigating" "workflow:ready-to-build" "workflow:building" "workflow:in-review" "workflow:remediating"; do
104
104
  gh issue list ${GH_FLAG} \
105
105
  --state open \
106
106
  --label "$LABEL" \
@@ -457,7 +457,7 @@ for NUM in $ORPHAN_LIST; do
457
457
  else
458
458
  gh issue edit "$NUM" ${GH_FLAG} \
459
459
  --add-label "workflow:merged" \
460
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" \
460
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" \
461
461
  2>/dev/null || true
462
462
  gh issue close "$NUM" ${GH_FLAG} \
463
463
  --comment "Closed by /recover-orphans: PR #${PR_NUM} was already merged. Labels corrected." \
@@ -506,7 +506,7 @@ ${CI_GATE_OUT}
506
506
  2>/dev/null || true
507
507
  gh issue edit "$NUM" ${GH_FLAG} \
508
508
  --add-label "workflow:merged" \
509
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" \
509
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge,workflow:invalid,workflow:decomposed" \
510
510
  2>/dev/null || true
511
511
  fi
512
512
  fi
@@ -579,7 +579,7 @@ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run e
579
579
  else
580
580
  # After review: update label
581
581
  gh issue edit "$NUM" ${GH_FLAG} --add-label "workflow:in-review" \
582
- --remove-label "workflow:building,workflow:awaiting-merge" 2>/dev/null || true
582
+ --remove-label "workflow:building,workflow:remediating,workflow:awaiting-merge" 2>/dev/null || true
583
583
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} submitted for review |\n"
584
584
  fi
585
585
  fi
@@ -607,7 +607,7 @@ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run e
607
607
  echo " [DRY-RUN] Would: remove workflow:investigating, workflow:ready-to-build, workflow:building, workflow:in-review"
608
608
  else
609
609
  gh issue edit "$NUM" ${GH_FLAG} \
610
- --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge" \
610
+ --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:awaiting-merge" \
611
611
  2>/dev/null || true
612
612
  gh issue comment "$NUM" ${GH_FLAG} \
613
613
  --body "<!-- FORGE:ORPHAN_RECOVERED -->
@@ -176,7 +176,7 @@ Append this block at the very end of your comment (after the `---` footer line,
176
176
  6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
177
177
  7. **Empty block**: If no findings at all, include just the START/END markers
178
178
  8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
179
- 9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the lowercase dispatched domain.
179
+ 9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
180
180
  10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
181
181
 
182
182
  ### Domain Prefixes
@@ -46,17 +46,17 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
46
46
 
47
47
  2. **Then read the persona file for each selected agent**:
48
48
 
49
- | Agent | Trigger | File |
50
- |-------|---------|------|
51
- | General Security & Quality Scan | ALWAYS RUNS | `review-pr-agents/security.md` |
52
- | Auth Conventions Auditor | AUTH domain | `review-pr-agents/auth.md` |
53
- | Billing Integrity Auditor | BILLING domain | `review-pr-agents/billing.md` |
54
- | Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `review-pr-agents/concurrency.md` |
55
- | Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `review-pr-agents/scraper.md` |
56
- | Frontend Quality Auditor | WEB service touched | `review-pr-agents/frontend.md` |
57
- | API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `review-pr-agents/api.md` |
58
- | Database & Migration Auditor | DATABASE domain | `review-pr-agents/database.md` |
59
- | Infrastructure & Deploy Safety Auditor | INFRA service touched | `review-pr-agents/infra.md` |
49
+ | Agent | Trigger | Marker domain | File |
50
+ |-------|---------|---------------|------|
51
+ | General Security & Quality Scan | ALWAYS RUNS | `security` | `review-pr-agents/security.md` |
52
+ | Auth Conventions Auditor | AUTH domain | `auth` | `review-pr-agents/auth.md` |
53
+ | Billing Integrity Auditor | BILLING domain | `billing` | `review-pr-agents/billing.md` |
54
+ | Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `concurrency` | `review-pr-agents/concurrency.md` |
55
+ | Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `scraper` | `review-pr-agents/scraper.md` |
56
+ | Frontend Quality Auditor | WEB service touched | `frontend` | `review-pr-agents/frontend.md` |
57
+ | API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `api` | `review-pr-agents/api.md` |
58
+ | Database & Migration Auditor | DATABASE domain | `database` | `review-pr-agents/database.md` |
59
+ | Infrastructure & Deploy Safety Auditor | INFRA service touched | `infra` | `review-pr-agents/infra.md` |
60
60
 
61
61
  ## Domain Prefixes (for Structured Findings)
62
62