forgedock 1.8.2 → 1.8.4

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 (49) hide show
  1. package/commands/design-bench.md +1 -0
  2. package/commands/design-render-critique-loop.md +1 -0
  3. package/commands/design.md +1 -0
  4. package/commands/forge-stats.md +1 -0
  5. package/commands/orchestrate/config.md +1 -0
  6. package/commands/orchestrate/phase-1-resolve.md +1 -0
  7. package/commands/orchestrate/phase-2-triage.md +1 -0
  8. package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
  9. package/commands/orchestrate/phase-3-dependency.md +2 -0
  10. package/commands/orchestrate/phase-4-execution.md +65 -31
  11. package/commands/orchestrate/phase-5-cleanup.md +1 -0
  12. package/commands/orchestrate/phase-6-report.md +1 -0
  13. package/commands/orchestrate/safety.md +1 -0
  14. package/commands/pipeline-health/config.md +1 -0
  15. package/commands/pipeline-health/phase-1-context.md +1 -0
  16. package/commands/pipeline-health/phase-2-metrics.md +1 -0
  17. package/commands/pipeline-health/phase-3-analyze.md +1 -0
  18. package/commands/pipeline-health/phase-4-proposals.md +1 -0
  19. package/commands/pipeline-health/phase-5-report.md +1 -0
  20. package/commands/pipeline-health/phase-6-summary.md +1 -0
  21. package/commands/recover-orphans.md +56 -9
  22. package/commands/review-pr-agents/api.md +1 -0
  23. package/commands/review-pr-agents/auth.md +1 -0
  24. package/commands/review-pr-agents/billing.md +1 -0
  25. package/commands/review-pr-agents/concurrency.md +1 -0
  26. package/commands/review-pr-agents/database.md +1 -0
  27. package/commands/review-pr-agents/frontend.md +1 -0
  28. package/commands/review-pr-agents/infra.md +1 -0
  29. package/commands/review-pr-agents/protocols.md +1 -0
  30. package/commands/review-pr-agents/scraper.md +1 -0
  31. package/commands/review-pr-agents/security.md +1 -0
  32. package/commands/review-pr-agents/spec-cli.md +1 -0
  33. package/commands/review-pr-agents.md +4 -0
  34. package/commands/review-pr-staging.md +1 -0
  35. package/commands/review-pr.md +5 -3
  36. package/commands/work-on/build/architect.md +1 -0
  37. package/commands/work-on/build/context.md +1 -0
  38. package/commands/work-on/build/implement.md +1 -0
  39. package/commands/work-on/build/validate.md +1 -0
  40. package/commands/work-on/build.md +4 -0
  41. package/commands/work-on/close.md +4 -0
  42. package/commands/work-on/decompose.md +4 -0
  43. package/commands/work-on/investigate.md +4 -0
  44. package/commands/work-on/remediate.md +4 -0
  45. package/commands/work-on/review.md +4 -0
  46. package/commands/work-on.md +6 -0
  47. package/package.json +1 -1
  48. package/scripts/check-spec-markers.sh +10 -0
  49. package/commands/work-on-monolithic.md +0 -367
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Run the ABC benchmark — reference URL → design-blind brief → render arms A/B/C → blind judge → scorecard with win-rates over n runs
3
4
  argument-hint: "[<brief-name>|all] [--n <runs>] [--corpus-version <ver>]"
4
5
  install: internal
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Render a design from its FORGE:DESIGN_SPEC, critique the rendered pixels against the perceptual negatives, and iterate until it passes or the budget is spent — the missing reward signal.
3
4
  argument-hint: "[<issue-number>|<spec-path>] [--max-iters <n>] [--viewports desktop,mobile]"
4
5
  install: internal
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Route a design GitHub issue through the UI Taste Harness — investigate → architect → implement → critique loop → user-feedback loop → close — accumulating FORGE:DESIGN_* annotations and driving the design:* label state machine to design:shipped.
3
4
  argument-hint: <issue-number> | <brief-name>
4
5
  install: internal
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Track command sizes, detect bloat, compare against baselines and thresholds
3
4
  argument-hint: "[diff | baseline | full | cost]"
4
5
  install: internal
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
 
3
4
  install: core
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1393,6 +1394,7 @@ fi
1393
1394
  # hold_merged_trail, resolve_orch_login, trail_escalation_state, release_merged_trail (reverify_merged_trail is
1394
1395
  # nested inside the classifier). It reports a missing helper on stderr and classifies GATED. The same trusted-author
1395
1396
  # filter (OWNER/MEMBER/COLLABORATOR or the orchestrator login) applies to the release/fail markers on wake.
1397
+ # forge#3169: merged + needs-human with no escalation record classifies GATED on wake (never keyed on the verifier).
1396
1398
  declare -A ISSUE_CLASS
1397
1399
  declare -A ISSUE_FILES
1398
1400
  DONE_ISSUES=()
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1342,21 +1343,19 @@ For an OpenCode completion, retain `OPENCODE_DISPATCH_MAP[{NUMBER}]` until the t
1342
1343
  ```bash
1343
1344
  # hold_merged_trail <issue> <trail-text> (forge#3156): the single escalation path for a merged issue whose phase
1344
1345
  # trail failed or could not be verified. Used by Step 4B item 0 and by the classifier's wake re-verification.
1345
- # Order matters: the `needs-human` label is written FIRST (read-back verified, 3 tries) because the classifier
1346
- # holds on merged + needs-human even before the comment exists; the comment follows (verified, 2 tries) and is
1347
- # the audit record that lets a human release the hold. Returns 0 when the label hold is verified, 1 otherwise.
1346
+ # Order matters (forge#3169): the escalation COMMENT is posted FIRST (re-read immediately before each post, skipped when
1347
+ # an ACTIVE escalation already exists, verified ACTIVE by re-read after), then the `needs-human` label is written
1348
+ # (read-back verified, 3 tries). The comment is the durable audit record, independent of the verifier, so a label-only
1349
+ # hold is not the normal state and a human release always has an anchor to be recorded against. Returns 0 only when both
1350
+ # halves are verified; otherwise returns 1 and stderr names which half failed (the label is still attempted after a
1351
+ # comment failure, because holding dependents is the safe side).
1348
1352
  hold_merged_trail() {
1349
- local N="$1" TXT="$2" TRY HELD=1
1350
- for TRY in 1 2 3; do
1351
- gh issue edit "$N" -R {GH_REPO} --add-label "needs-human" >/dev/null 2>&1 || true # <!-- allowlist:check-command-side-effects -->
1352
- if gh issue view "$N" -R {GH_REPO} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null \
1353
- | grep -qE '(^|,)needs-human(,|$)'; then HELD=0; break; fi
1354
- done
1355
- [ "$HELD" -ne 0 ] && echo "PHASE TRAIL HOLD FAILED: #${N} — needs-human label could not be verified; dependents are NOT held. Hold them manually." >&2
1353
+ local N="$1" TXT="$2" TRY COMMENTED=1 HELD=1
1356
1354
  for TRY in 1 2; do
1357
1355
  # forge#3157: dedupe only against an ACTIVE escalation, so a re-escalation after a recorded release posts a new one.
1356
+ # forge#3169: re-read right before each post (cache entry removed) to narrow the concurrent-classifier duplicate window.
1358
1357
  [ -n "${TRAIL_CACHE_DIR:-}" ] && rm -f "$TRAIL_CACHE_DIR/trail-$N"
1359
- [ "$(trail_escalation_state "$N")" = "ACTIVE" ] && break
1358
+ if [ "$(trail_escalation_state "$N")" = "ACTIVE" ]; then COMMENTED=0; break; fi
1360
1359
  gh issue comment "$N" -R {GH_REPO} --body "<!-- FORGE:PHASE_TRAIL_FAILED -->
1361
1360
  #${N} merged with an incomplete phase trail (or one that could not be verified), detected by /orchestrate.
1362
1361
 
@@ -1364,10 +1363,19 @@ hold_merged_trail() {
1364
1363
  ${TXT}
1365
1364
  \`\`\`
1366
1365
 
1367
- The PR is already merged, so \`/work-on\` cannot re-run the missing phases. Review the merged change, then remove \`needs-human\` to release dependents held as \`blocked-on-human-merge\`." >/dev/null 2>&1 || true
1366
+ The PR is already merged, so \`/work-on\` cannot re-run the missing phases. Review the merged change, then remove \`needs-human\` to release dependents held as \`blocked-on-human-merge\`." >/dev/null 2>&1 || true # <!-- allowlist:check-command-side-effects -->
1367
+ [ -n "${TRAIL_CACHE_DIR:-}" ] && rm -f "$TRAIL_CACHE_DIR/trail-$N"
1368
+ if [ "$(trail_escalation_state "$N")" = "ACTIVE" ]; then COMMENTED=0; break; fi
1369
+ done
1370
+ for TRY in 1 2 3; do
1371
+ gh issue edit "$N" -R {GH_REPO} --add-label "needs-human" >/dev/null 2>&1 || true # <!-- allowlist:check-command-side-effects -->
1372
+ if gh issue view "$N" -R {GH_REPO} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null \
1373
+ | grep -qE '(^|,)needs-human(,|$)'; then HELD=0; break; fi
1368
1374
  done
1369
1375
  [ -n "${TRAIL_CACHE_DIR:-}" ] && rm -f "$TRAIL_CACHE_DIR/trail-$N"
1370
- return "$HELD"
1376
+ [ "$COMMENTED" -ne 0 ] && echo "PHASE TRAIL HOLD FAILED: #${N} — escalation comment could not be verified (audit record missing); hold dependents manually." >&2
1377
+ [ "$HELD" -ne 0 ] && echo "PHASE TRAIL HOLD FAILED: #${N} — needs-human label could not be verified; dependents are NOT held. Hold them manually." >&2
1378
+ [ "$COMMENTED" -eq 0 ] && [ "$HELD" -eq 0 ]
1371
1379
  }
1372
1380
 
1373
1381
  # resolve_orch_login (forge#3168): prints the login of the identity that authors the orchestrator's own FAILED/RELEASED
@@ -1410,6 +1418,8 @@ trail_escalation_state() {
1410
1418
 
1411
1419
  # release_merged_trail <issue> (forge#3157): record that a human cleared a merged-trail hold, so the escalation
1412
1420
  # stops gating. Call only after classify_predecessor_state reported DONE for a merged issue whose escalation is ACTIVE.
1421
+ # Only an ACTIVE escalation is released here: hold_merged_trail posts the comment before the label (forge#3169), so a
1422
+ # human release of a label-only (ABSENT) hold is not expected and is not recorded (avoids a stray RELEASED marker).
1413
1423
  # Idempotent (no-op unless ACTIVE) and never run in dry-run. The comment must not contain the escalation marker text.
1414
1424
  release_merged_trail() {
1415
1425
  local N="$1"
@@ -1452,13 +1462,29 @@ classify_predecessor_state() {
1452
1462
 
1453
1463
  # reverify_merged_trail: re-run the phase-trail verifier for a merged predecessor that has NO escalation
1454
1464
  # record (forge#3156), so wake reconstruction does not trust the `workflow:merged` label alone. Prints OK
1455
- # when the trail verifies, UNVERIFIED otherwise. FORGE_ROOT comes from the Step 4A-pre/item 0 bootstrap; if
1456
- # it is unresolved the verifier cannot run, so a predecessor already holding `needs-human` fails closed
1457
- # (UNVERIFIED), while a plain merged one is trusted (OK) because item 0 verified it on its completion.
1465
+ # when the trail verifies, UNVERIFIED otherwise. Fails closed: a wake runs in a fresh shell where FORGE_ROOT is
1466
+ # unset, so the helper resolves it itself (canonical bootstrap, only when unset); if it is still unresolved or
1467
+ # the verifier is missing, the result is UNVERIFIED for every predecessor, never OK.
1458
1468
  reverify_merged_trail() {
1459
- local P="$1" HAS_HUMAN="$2" RC
1469
+ local P="$1" RC
1470
+ if [ -z "${FORGE_ROOT:-}" ]; then
1471
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
1472
+ FORGE_ROOT=""
1473
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
1474
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
1475
+ _l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
1476
+ # newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
1477
+ _v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
1478
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
1479
+ # '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
1480
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
1481
+ while IFS= read -r _c; do
1482
+ case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
1483
+ done <<< "$_k"
1484
+ fi
1485
+ fi
1460
1486
  if [ -z "${FORGE_ROOT:-}" ] || [ ! -f "$FORGE_ROOT/scripts/verify-phase-trail.sh" ]; then
1461
- [ "$HAS_HUMAN" = "yes" ] && echo "UNVERIFIED" || echo "OK"; return
1487
+ echo "UNVERIFIED"; return
1462
1488
  fi
1463
1489
  bash "$FORGE_ROOT/scripts/verify-phase-trail.sh" "$P" -R {GH_REPO} --code-diff >/dev/null 2>&1; RC=$?
1464
1490
  [ "$RC" -eq 0 ] && echo "OK" || echo "UNVERIFIED"
@@ -1469,13 +1495,14 @@ classify_predecessor_state() {
1469
1495
  elif echo "$PRED_LABELS" | grep -qx "workflow:merged" && echo "$PRED_LABELS" | grep -qx "needs-human"; then
1470
1496
  # forge#3148: GATED when Step 4B item 0 escalated a merged issue with an incomplete phase trail
1471
1497
  # (the code is in the base branch, but /work-on cannot repair a merged issue, so a human must review it;
1472
- # dependents wait until a human clears needs-human). Item 0 adds `needs-human` BEFORE posting the comment
1473
- # (forge#3156), so merged + needs-human with no comment yet is either the in-flight window or a stale label:
1474
- # re-verify the trail to tell them apart (clean trail => stale label => DONE). Unreadable comments fail closed.
1498
+ # dependents wait until a human clears needs-human). forge#3169: the hold is NOT keyed on the verifier. A passing
1499
+ # verifier does not mean "not held" (an agent-reported bypass verifies clean), and hold_merged_trail posts the
1500
+ # comment before the label, so merged + needs-human with no record is a failed/partial hold: fail closed (GATED).
1501
+ # A human release is the removal of needs-human, recorded by the release sweep via release_merged_trail.
1475
1502
  local TRAIL_ESC
1476
1503
  TRAIL_ESC=$(trail_escalation_state "$PRED")
1477
1504
  case "$TRAIL_ESC" in
1478
- ABSENT) [ "$(reverify_merged_trail "$PRED" yes)" = "OK" ] && echo "DONE" || echo "GATED" ;;
1505
+ ABSENT) echo "GATED" ;; # forge#3169: no record yet; never trust the verifier to release a labelled hold
1479
1506
  RELEASED) echo "DONE" ;; # forge#3157: a human release was recorded after the last escalation; this needs-human is unrelated
1480
1507
  *) echo "GATED" ;; # ACTIVE or UNREADABLE: fail closed
1481
1508
  esac
@@ -1483,16 +1510,20 @@ classify_predecessor_state() {
1483
1510
  # forge#3156: no needs-human. A recorded escalation (ACTIVE or RELEASED) means a human already released the hold
1484
1511
  # (DONE, forge#3157: the sweep records it via release_merged_trail); otherwise re-verify the trail so a merged
1485
1512
  # predecessor that item 0 never got to escalate (orchestrator died mid-hold, wake reconstruction) is not trusted
1486
- # on the label alone.
1513
+ # on the label alone. forge#3169: hold only when the record re-reads exactly ABSENT immediately before (an
1514
+ # UNREADABLE read classifies GATED without writing), so a concurrent classifier or a recorded release is not
1515
+ # overridden and at most one escalation comment is posted in the common case.
1487
1516
  case "$(trail_escalation_state "$PRED")" in
1488
1517
  ACTIVE|RELEASED) echo "DONE" ;;
1518
+ UNREADABLE) echo "GATED" ;;
1489
1519
  *)
1490
- if [ "$(reverify_merged_trail "$PRED" no)" = "OK" ]; then
1520
+ if [ "$(reverify_merged_trail "$PRED")" = "OK" ]; then
1491
1521
  echo "DONE"
1492
1522
  else
1493
1523
  # Unverified trail with no hold on record: establish it now so a human can release it.
1494
1524
  # Output is discarded (stdout is the classification), but a failed hold is reported on stderr, not swallowed.
1495
- [ "${DRY_RUN:-false}" = "true" ] || hold_merged_trail "$PRED" "PHASE_TRAIL: FAIL (re-verified at wake classification)" >/dev/null || echo "classify_predecessor_state: hold on #${PRED} could not be verified" >&2
1525
+ [ -n "${TRAIL_CACHE_DIR:-}" ] && rm -f "$TRAIL_CACHE_DIR/trail-$PRED"
1526
+ [ "${DRY_RUN:-false}" = "true" ] || [ "$(trail_escalation_state "$PRED")" != "ABSENT" ] || hold_merged_trail "$PRED" "PHASE_TRAIL: FAIL (re-verified at wake classification)" >/dev/null || echo "classify_predecessor_state: hold on #${PRED} could not be verified" >&2
1496
1527
  echo "GATED"
1497
1528
  fi ;;
1498
1529
  esac
@@ -1879,13 +1910,15 @@ done
1879
1910
  - `TRAIL_RC -ge 2` (`PHASE_TRAIL: ERROR`: unreadable trail, unreadable labels, or 127 when the verifier is unresolvable) → classify as **BLOCKED (infrastructure)**, NOT a bypass. A re-dispatched `/work-on` would hit the same unreadable or unresolvable verifier in its resume preflight, so do NOT re-dispatch. Surface `PHASE TRAIL ERROR: #{NUMBER} — <TRAIL line>` in the operator status update, add `needs-human`, and continue with the rest of the batch.
1880
1911
  - `TRAIL_RC=1` (`PHASE_TRAIL: FAIL`) → classify as a **bypass** (handling below).
1881
1912
 
1913
+ **Transient GitHub outage** — if a worker returns (or the issue's latest blocker comment shows) a blocker starting with `github-unavailable:`, the issue is not human-gated: do not count it as `needs-human`/GATED. Re-dispatch it once after the other ready issues (the `/work-on` router already retried each phase twice). <!-- field test: GitHub HTTP 500 window -->
1914
+
1882
1915
  On `PHASE_TRAIL: FAIL`, classify the issue as a **bypass**, never `DONE`. Classify ONLY on objective evidence — the trail verdict above and the issue's labels. Do NOT reclassify an issue from a worker's own narrative ("ran inline", "dispatched reviewers directly", "skipped an optional step"): every phase runs forked and the merge itself is gated (CI green on the exact merged commit, reviewed head or docs-only delta, phase trail), so a merged issue whose trail passes is DONE. Field evidence: narrative-based holds flagged spec-compliant behaviour and put 5 of 6 merged issues in a stress batch on needs-human. Surface it in the operator status update (`PHASE BYPASS: #{NUMBER} — missing: ...`) and do not cascade it as a satisfied predecessor.
1883
1916
 
1884
- **Do NOT re-dispatch a merged issue** (forge#3148): `/work-on` Phase 0B stops on a closed or `workflow:merged` issue before its resume preflight, so a fresh agent exits without repairing anything and the predecessor would stay unresolved forever. The PR is already merged, so the gap can only be resolved by a human. Escalate it instead, which classifies it **GATED** (`classify_predecessor_state` holds `workflow:merged` + `needs-human` unless the trail re-verifies clean; the label is written first, then the comment), so item 6.5 tracks its dependents as `blocked-on-human-merge` instead of dispatching them. A merged issue whose trail could not be read (`TRAIL_RC -ge 2`, handled above) is escalated by the same block, so it is gated the same way. A bypass reported by the agent on a `workflow:merged` issue whose trail verifies is handled the same way (set `TRAIL_RC=1` before this block):
1917
+ **Do NOT re-dispatch a merged issue** (forge#3148): `/work-on` Phase 0B stops on a closed or `workflow:merged` issue before its resume preflight, so a fresh agent exits without repairing anything and the predecessor would stay unresolved forever. The PR is already merged, so the gap can only be resolved by a human. Escalate it instead, which classifies it **GATED** (`classify_predecessor_state` holds `workflow:merged` + `needs-human` with no recorded release, regardless of the verifier; the comment is posted first, then the label, both verified), so item 6.5 tracks its dependents as `blocked-on-human-merge` instead of dispatching them. A merged issue whose trail could not be read (`TRAIL_RC -ge 2`, handled above) is escalated by the same block, so it is gated the same way. A bypass reported by the agent on a `workflow:merged` issue whose trail verifies is handled the same way (set `TRAIL_RC=1` before this block):
1885
1918
  ```bash
1886
1919
  # Only a merged issue is escalated here; an unmerged bypass is resumed by item 2 (see below).
1887
1920
  # Any non-zero rc on a merged issue escalates: rc 1 (missing phases) and rc >= 2 (unverifiable trail) alike,
1888
- # because classify_predecessor_state() gates a merged predecessor only when this escalation comment exists.
1921
+ # because classify_predecessor_state() gates a merged predecessor on the escalation record plus the needs-human label.
1889
1922
  # forge#3156: also escalate when the label read itself failed: an unreadable issue cannot be proven unmerged, so
1890
1923
  # fail closed instead of posting nothing. forge#3168: key on TRAIL_LABELS_READ_OK, not an empty TRAIL_LABELS, so a
1891
1924
  # successfully read, label-less, non-merged issue is never escalated (read OK + empty labels => no escalation).
@@ -1893,10 +1926,11 @@ done
1893
1926
  if [ "${DRY_RUN:-false}" = "true" ]; then
1894
1927
  echo "[DRY-RUN] Would add needs-human and post FORGE:PHASE_TRAIL_FAILED on merged #{NUMBER}"
1895
1928
  else
1896
- # hold_merged_trail (defined with classify_predecessor_state) writes `needs-human` FIRST, read-back verified
1897
- # with retries, then the escalation comment (verified, retried). The label alone holds dependents, so there
1898
- # is no window where a comment without the label reads as DONE. A failed verification is reported on stderr.
1899
- hold_merged_trail {NUMBER} "${TRAIL}" || echo "PHASE TRAIL ESCALATION FAILED: #{NUMBER} — needs-human not verified." >&2
1929
+ # hold_merged_trail (defined with classify_predecessor_state) posts the escalation comment FIRST (re-read before
1930
+ # posting, verified), then writes `needs-human` (read-back verified, retried). A merged + needs-human issue with no
1931
+ # comment classifies GATED regardless of the verifier. A failed half is reported on stderr and the operator must
1932
+ # hold dependents manually (residual: a human clearing the label after both comment attempts failed).
1933
+ hold_merged_trail {NUMBER} "${TRAIL}" || echo "PHASE TRAIL ESCALATION FAILED: #{NUMBER} — comment or needs-human not verified." >&2
1900
1934
  fi
1901
1935
  fi
1902
1936
  ```
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
 
3
4
  install: core
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: extras
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -317,7 +317,9 @@ done
317
317
 
318
318
  For each diagnosed issue, apply the recovery action. All mutating actions are skipped when `DRY_RUN=true`.
319
319
 
320
- **Claim before re-entering the pipeline** (forge#3158): the `review-pr` first-refusal resume and `create-pr` re-enter `/work-on` inline. A repeated or concurrent sweep, or a live `/orchestrate` agent, could otherwise run the pipeline twice on one issue. Every inline `/work-on` resume MUST go through `claim_orphan` first and call `release_orphan` afterwards. A sweep is not an orchestrator, so it takes an issue-scoped `FORGE:RECOVERY_CLAIM` marker (visible to other sweeps) and defers to any live orchestrator signal (a fresh `FORGE:HEARTBEAT`, which `/work-on --under-orchestration` posts at every phase entry).
320
+ **Claim before re-entering the pipeline** (forge#3158): the `review-pr` first-refusal resume and `create-pr` re-enter `/work-on` inline. A repeated or concurrent sweep, or a live `/orchestrate` agent, could otherwise run the pipeline twice on one issue. Every inline `/work-on` resume MUST go through `claim_orphan` first and run inside `resume_orphan_inline`, which releases the claim on every exit. A sweep is not an orchestrator, so it takes an issue-scoped `FORGE:RECOVERY_CLAIM` marker (visible to other sweeps) and defers to any live orchestrator signal (a fresh `FORGE:HEARTBEAT`, which `/work-on --under-orchestration` posts at every phase entry).
321
+
322
+ **Claim refresh and guaranteed release** (forge#3171): liveness is judged from the claim comment's `updated_at`, and an inline resume posts no `FORGE:HEARTBEAT` (that is gated on `--under-orchestration`, which sweeps do not pass). So the claim is kept alive by editing it: `claim_orphan` records the claim comment id, and `start_claim_keepalive` runs `refresh_orphan` in a background loop every `RECOVERY_CLAIM_TTL_MIN / 3` minutes while the resume runs. `refresh_orphan` PATCHes the claim body, keeping the `**Sweep: id**` line and appending a changing `Refreshed: <UTC>` line, so `updated_at` always advances. Refresh is best-effort; if the keepalive is lost the claim degrades to the plain TTL behavior. `resume_orphan_inline` registers `trap ... release_orphan ... EXIT` in a subshell, so the claim is released always, on success and when the `Skill(...)` call fails, and `release_orphan` stops the keepalive first and is idempotent (it never posts the release marker twice). The keepalive and the `Skill(...)` call belong to the same sweep run. DRY_RUN makes every one of these helpers a no-op.
321
323
 
322
324
  ```bash
323
325
  RECOVERY_CLAIM_TTL_MIN="${RECOVERY_CLAIM_TTL_MIN:-30}" # a claim or heartbeat older than this is treated as dead
@@ -328,7 +330,7 @@ SWEEP_ID="sweep-$(date -u +%Y%m%dT%H%M%SZ)-$$"
328
330
  # Both reads run under pipefail in a subshell: the pipeline status would otherwise be jq's, and a gh failure
329
331
  # (rate limit, 403) would yield "[]" with exit 0, indistinguishable from "no live holder".
330
332
  claim_orphan() {
331
- local num="$1" now cutoff comments live
333
+ local num="$1" now cutoff comments live resp
332
334
  [ "$DRY_RUN" = "true" ] && return 0 # dry-run: no claim comment is posted (callers already skip dry-run resumes)
333
335
  now=$(date -u +%s); cutoff=$(( now - RECOVERY_CLAIM_TTL_MIN * 60 ))
334
336
  comments=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${num}/comments" 2>/dev/null \
@@ -349,10 +351,13 @@ claim_orphan() {
349
351
  return 1
350
352
  fi
351
353
 
352
- gh issue comment "$num" ${GH_FLAG} --body "<!-- FORGE:RECOVERY_CLAIM -->
354
+ # Post via the API so the comment id is known: refresh_orphan needs it. The Sweep line must stay line 2.
355
+ CLAIM_BODY="<!-- FORGE:RECOVERY_CLAIM -->
353
356
  **Sweep: ${SWEEP_ID}**
354
- Holder: /recover-orphans — resuming /work-on #${num}. Other sweeps and dispatchers: do not re-enter this issue until the matching release marker is posted." >/dev/null 2>&1 \
355
- || { CLAIM_SKIP_REASON="could not post FORGE:RECOVERY_CLAIM"; return 1; }
357
+ Holder: /recover-orphans — resuming /work-on #${num}. Other sweeps and dispatchers: do not re-enter this issue until the matching release marker is posted."
358
+ resp=$(gh api -X POST "repos/${GH_REPO}/issues/${num}/comments" -f body="$CLAIM_BODY" --jq '.id' 2>/dev/null) \
359
+ && [ -n "$resp" ] || { CLAIM_SKIP_REASON="could not post FORGE:RECOVERY_CLAIM"; return 1; }
360
+ CLAIM_COMMENT_ID="$resp"; CLAIM_HELD_NUM="$num" # set before the race check so release_orphan covers the race-lost paths
356
361
 
357
362
  # Race check: re-read; the earliest unreleased claim wins. Losing means another sweep claimed first.
358
363
  local first
@@ -371,12 +376,56 @@ Holder: /recover-orphans — resuming /work-on #${num}. Other sweeps and dispatc
371
376
  return 0
372
377
  }
373
378
 
379
+ # refresh_orphan <issue> — best-effort: edit the claim comment so its updated_at advances. The body must change
380
+ # every time (an identical-body edit may not bump updated_at). Never carries FORGE:HEARTBEAT or the RELEASED marker.
381
+ refresh_orphan() {
382
+ [ "$DRY_RUN" = "true" ] && return 0
383
+ [ -n "${CLAIM_COMMENT_ID:-}" ] && [ "${CLAIM_HELD_NUM:-}" = "$1" ] || return 0
384
+ gh api -X PATCH "repos/${GH_REPO}/issues/comments/${CLAIM_COMMENT_ID}" \
385
+ -f body="${CLAIM_BODY}
386
+ Refreshed: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >/dev/null 2>&1 || true
387
+ }
388
+
389
+ # start_claim_keepalive <issue> — non-blocking background refresh every TTL/3 (strictly under the TTL).
390
+ start_claim_keepalive() {
391
+ [ "$DRY_RUN" = "true" ] && return 0
392
+ stop_claim_keepalive
393
+ local num="$1" interval=$(( RECOVERY_CLAIM_TTL_MIN * 60 / 3 ))
394
+ [ "$interval" -ge 1 ] || interval=1
395
+ ( while sleep "$interval"; do refresh_orphan "$num"; done ) >/dev/null 2>&1 &
396
+ KEEPALIVE_PID=$!
397
+ }
398
+
399
+ stop_claim_keepalive() {
400
+ [ -n "${KEEPALIVE_PID:-}" ] || return 0
401
+ pkill -P "$KEEPALIVE_PID" 2>/dev/null || true # the pending sleep
402
+ kill "$KEEPALIVE_PID" 2>/dev/null || true
403
+ wait "$KEEPALIVE_PID" 2>/dev/null || true
404
+ unset KEEPALIVE_PID
405
+ }
406
+
407
+ # release_orphan <issue> — stops the keepalive first; idempotent (the release marker is posted at most once per claim).
374
408
  release_orphan() {
375
409
  [ "$DRY_RUN" = "true" ] && return 0
410
+ stop_claim_keepalive
411
+ [ "${CLAIM_HELD_NUM:-}" = "$1" ] || return 0
412
+ unset CLAIM_HELD_NUM CLAIM_COMMENT_ID
376
413
  gh issue comment "$1" ${GH_FLAG} --body "<!-- FORGE:RECOVERY_CLAIM_RELEASED -->
377
414
  **Sweep: ${SWEEP_ID}**
378
415
  Released by /recover-orphans." >/dev/null 2>&1 || true
379
416
  }
417
+
418
+ # resume_orphan_inline <issue> — run after claim_orphan succeeded. The subshell EXIT trap releases the claim always:
419
+ # on success and when the Skill call fails. The keepalive runs alongside the Skill call and is stopped by the release.
420
+ resume_orphan_inline() {
421
+ local num="$1"
422
+ (
423
+ trap 'release_orphan "$num"' EXIT
424
+ start_claim_keepalive "$num"
425
+ refresh_orphan "$num"
426
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${num}")
427
+ )
428
+ }
380
429
  ```
381
430
 
382
431
  ```bash
@@ -510,8 +559,7 @@ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run e
510
559
  # First refusal: resume /work-on, whose resume preflight re-runs each missing phase via Skill(...)
511
560
  # and then re-enters review (work-on.md Phase 0B resume → work-on/review.md).
512
561
  if claim_orphan "$NUM"; then
513
- Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${NUM}")
514
- release_orphan "$NUM"
562
+ resume_orphan_inline "$NUM" # releases the claim on every exit
515
563
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — resumed /work-on to re-run missing phases |\n"
516
564
  else
517
565
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
@@ -545,8 +593,7 @@ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run e
545
593
  echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='$NUM')"
546
594
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Would resume /work-on — branch $BRANCH has commits, no PR |\n"
547
595
  elif claim_orphan "$NUM"; then
548
- Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${NUM}")
549
- release_orphan "$NUM"
596
+ resume_orphan_inline "$NUM" # releases the claim on every exit
550
597
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Resumed /work-on — branch $BRANCH has commits, no PR |\n"
551
598
  else
552
599
  RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: internal
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Review agent catalog (read by /review-pr; not a user entrypoint)
3
+ user-invocable: false
4
+ ---
1
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
6
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
7
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Staging review mode — comprehensive review of staging branch before deploy to main
3
4
  argument-hint: "[PR number or \"staging\"]"
4
5
  allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch
@@ -1751,7 +1751,9 @@ Run the deterministic dedup script first, then fall through to the line-range ch
1751
1751
  # Step 0: Deterministic title dedup — catches near-duplicates before line-range check
1752
1752
  # See scripts/issue-dedup.sh for the token-overlap algorithm. <!-- Added: forge#1335 -->
1753
1753
  FINDING_TITLE_DEDUP="fix: brief description of finding (review finding — PR #${PR_NUMBER})"
1754
- DEDUP_RESULT=$(scripts/issue-dedup.sh "$FINDING_TITLE_DEDUP" "$GH_FLAG" 2>&1)
1754
+ # Never dedup against the issue this PR fixes (MERGE_ISSUE): it is about to be closed, so a
1755
+ # finding "matched" to it is tracked nowhere. Field test #3169: SEC-2 matched #3169 itself and was dropped.
1756
+ DEDUP_RESULT=$(scripts/issue-dedup.sh "$FINDING_TITLE_DEDUP" "$GH_FLAG" ${MERGE_ISSUE:+--exclude "$MERGE_ISSUE"} 2>&1)
1755
1757
  DEDUP_EXIT=$?
1756
1758
  if [ "$DEDUP_EXIT" -eq 1 ]; then
1757
1759
  echo "DEDUP: Skipping — $DEDUP_RESULT"
@@ -1775,7 +1777,7 @@ LINE_MAX=$((FINDING_LINE + 5))
1775
1777
 
1776
1778
  # Check open issues for line-range overlap OR title similarity on the same file
1777
1779
  CANDIDATES=$(gh issue list --state open --label "review-finding" --limit 100 --json number,title,body \
1778
- --jq "[.[] | select(.body | test(\"${FINDING_FILE}\"))]" 2>/dev/null)
1780
+ --jq "[.[] | select(.number != ${MERGE_ISSUE:-0}) | select(.body | test(\"${FINDING_FILE}\"))]" 2>/dev/null)
1779
1781
 
1780
1782
  EXISTING=$(echo "$CANDIDATES" | jq -r --arg file "$FINDING_FILE" --argjson min "$LINE_MIN" --argjson max "$LINE_MAX" --arg title "$FINDING_TITLE" '
1781
1783
  .[] |
@@ -1926,7 +1928,7 @@ if [ "$FINDING_PRIORITY_EXIT" -ne 0 ]; then
1926
1928
  else
1927
1929
 
1928
1930
  # --label is repeatable (not comma-joined) per the /issue programmatic contract.
1929
- ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
1931
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG} ${MERGE_ISSUE:+--exclude \"$MERGE_ISSUE\"}"))
1930
1932
  # /issue re-reads the created issue and hard-fails unless this exact marker is present.
1931
1933
  rm -f "$FINDING_ISSUE_BODY_FILE"
1932
1934
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Pre-implementation architecture planning — traces ALL affected code paths and produces an ordered implementation plan before any code is written
3
4
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE} --files \"<space-separated>\""
4
5
  context: fork
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Pre-implementation context gathering — surfaces historical findings, bug patterns, and related code paths before the builder writes any code
3
4
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag {GH_FLAG} --repo-path {WORKTREE} --files \"<space-separated>\" [--functions \"<names>\"]"
4
5
  context: fork
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Implementation agent — writes code, makes commits, posts builder comment
3
4
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE} [--fix-acceptance \"<failed checks>\"]"
4
5
  context: fork
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Validation agent — quality gate loop, format/verify, proxy check, deploy check
3
4
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE} --files \"<changed files>\""
4
5
  context: fork
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Build subcommand — create worktree, post contract, sequence context/architect/implement/validate
3
4
  context: fork
4
5
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BASE}"
@@ -12,6 +13,9 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
12
13
 
13
14
  **Input**: $ARGUMENTS
14
15
 
16
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
17
+
18
+
15
19
  **Invoked by**: the work-on router, when the issue carries label `workflow:ready-to-build` or `workflow:building`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub and git.
16
20
  **Output**: Create worktree, classify complexity, post contract, run the child phases (context, architect, implement, validate) through `Skill()`, run the acceptance gate, and print exactly one `BUILD_RESULT:` block as the final reply.
17
21
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Close subcommand — update project board, final issue body, parent tracker, summary report, trajectory log
3
4
  argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--pr PR_NUMBER] [--base PR_BASE] [--branch BRANCH] [--worktree WORKTREE_PATH] --terminal-state merged|investigation|decomposed|invalid"
4
5
  context: fork
@@ -12,6 +13,9 @@ context: fork
12
13
 
13
14
  **Input**: $ARGUMENTS
14
15
 
16
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
17
+
18
+
15
19
  **Invoked by**: the `work-on` router, as its final step, via `Skill(skill="{FORGE_SKILL_PREFIX}work-on:close", args="...")`. This file declares `context: fork`, so it runs in an isolated sub-agent context that sees ONLY this text and its args — there is no "Phase 0 state" to rely on. Everything not passed as an arg is re-derived from GitHub/git in Phase C0. The router sees ONLY the final `CLOSE_RESULT:` block (see Output), so this file is the single source of truth for the close report, the summary card, the trajectory comment and the decision record.
16
20
  **Output**: Update project board, close issue, update parent tracker, post trajectory log, post decision record, clean up the worktree. Final reply is the `CLOSE_RESULT:` block.
17
21
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Decompose subcommand — break a complex issue into ordered sub-issues, post FORGE:DECOMPOSED, stop
3
4
  context: fork
4
5
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
@@ -12,6 +13,9 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
12
13
 
13
14
  **Input**: $ARGUMENTS
14
15
 
16
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
17
+
18
+
15
19
  **Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
16
20
  **Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`. STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
17
21
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Investigate a GitHub issue — validate it's real, determine root cause, post findings
3
4
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
4
5
  context: fork
@@ -10,6 +11,9 @@ context: fork
10
11
 
11
12
  **Input**: $ARGUMENTS
12
13
 
14
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
15
+
16
+
13
17
  Standalone investigation phase for the work-on pipeline. Validates whether an issue is real, determines root cause, posts a structured FORGE:INVESTIGATOR comment to GitHub, and updates workflow labels.
14
18
 
15
19
  This skill runs in an **isolated forked context**: it sees only this file and its args. It has no variables from the caller — every other piece of state is re-read from GitHub/git here. It ends by printing exactly one `INVESTIGATE_RESULT:` block (see `## Output`) as its final reply, on **every** exit path (success, ALREADY_DONE, INVALID, BLOCKED).
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Remediate subcommand — checkout a needs-human PR, fix review findings, re-review, and re-gate with a FORGE:REMEDIATION paper trail
3
4
  argument-hint: "[PR number] [--issue N] [--repo GH_REPO] [--gh-flag GH_FLAG] [--base PR_BASE]"
4
5
  context: fork
@@ -12,6 +13,9 @@ context: fork
12
13
 
13
14
  **Input**: $ARGUMENTS
14
15
 
16
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
17
+
18
+
15
19
  **Invoked by**:
16
20
  - `work-on.md` Phase 0A.1 (router), standalone: `/work-on <pr> --remediate` (see forge#1813).
17
21
  - `commands/orchestrate/phase-4-execution.md` item 6.4, auto-dispatched against a `needs-human`-gated predecessor's own open PR.
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  description: Review subcommand — push branch, create PR, invoke /review-pr with --auto-merge
3
4
  context: fork
4
5
  argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE} --branch {BRANCH} --base {PR_BASE}"
@@ -12,6 +13,9 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WO
12
13
 
13
14
  **Input**: $ARGUMENTS
14
15
 
16
+ > **Transient GitHub failures** (field test: a 12-minute GitHub HTTP 500 window parked an issue at needs-human): retry any `gh` call that fails with HTTP 5xx, a timeout or "Something went wrong" up to 3 times with 10s/30s/60s backoff. If it still fails, do NOT add `needs-human` — print this phase's RESULT block with `status: BLOCKED` and a blocker that starts with `github-unavailable:`. The router retries the phase; every phase resumes from GitHub state, so a retry is safe.
17
+
18
+
15
19
  **Invoked by**: the `work-on` router, after `build/validate.md` returns `GATE_PASSED: true`.
16
20
  **Output**: Push branch, create PR, invoke /review-pr --auto-merge, verify the merge, and print exactly one `REVIEW_RESULT:` block as the final reply (all paths, including every guard/failure).
17
21
 
@@ -507,6 +507,12 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:HEARTBEAT -->
507
507
 
508
508
  ---
509
509
 
510
+ ## Transient GitHub failures (router-owned retry)
511
+
512
+ Any phase result whose blocker starts with `github-unavailable:` is an infrastructure outage, not a decision: do not add `needs-human`. Wait 2 minutes, then re-invoke the **same phase with the same args**; repeat at most twice (5 minutes before the second retry). Phases are idempotent and resume from GitHub state. If the third attempt still returns `github-unavailable:`, post one comment with the blocker, add `needs-human`, and STOP — at that point GitHub has been unavailable for 10+ minutes.
513
+
514
+ ---
515
+
510
516
  ## Phase 1: Investigate
511
517
 
512
518
  **Skip if**: a `<!-- FORGE:INVESTIGATOR -->` comment contains `<!-- INVESTIGATION:COMPLETE -->` or `<!-- INVESTIGATION:INVALID -->` (route on the existing verdict as below — the skill's own resume check returns it if invoked).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -284,6 +284,16 @@ if [ -d "$WORKFLOWS_DIR" ]; then
284
284
  done < <(find "$WORKFLOWS_DIR" -name '*.yml' | sort)
285
285
  fi
286
286
 
287
+ # ---------------------------------------------------------------------------
288
+ # Merged-trail hold must not be keyed on the verifier (forge#3169)
289
+ # ---------------------------------------------------------------------------
290
+
291
+ P4_SPEC="$COMMANDS_DIR/orchestrate/phase-4-execution.md"
292
+ if [ -f "$P4_SPEC" ] && grep -qE 'ABSENT\) +\[ "\$\(reverify_merged_trail "\$PRED"[^)]*\)" = "OK" \] *&& *echo "DONE"' "$P4_SPEC"; then
293
+ echo "HIGH | $P4_SPEC | merged + needs-human + ABSENT classifies DONE via the verifier (must fail closed to GATED)" >&2
294
+ VIOLATIONS=$((VIOLATIONS + 1))
295
+ fi
296
+
287
297
  # ---------------------------------------------------------------------------
288
298
  # Summary
289
299
  # ---------------------------------------------------------------------------
@@ -1,367 +0,0 @@
1
- ---
2
- description: "[BENCHMARK] Monolithic work-on — single-prompt pipeline with no Skill sub-phase boundaries"
3
- argument-hint: "[issue number]"
4
- install: internal
5
- ---
6
- <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
7
- <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
8
-
9
- # /work-on-monolithic — Full Issue Pipeline (Single Prompt)
10
-
11
- > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
-
13
- **Input**: $ARGUMENTS
14
-
15
- Orchestrator for the full issue lifecycle: investigate → build → review → merge → close. GitHub issues are the persistent context layer — read existing comments before starting, write structured reports back, use `workflow:*` labels to track state.
16
-
17
- **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
18
- **NEVER use plan mode (EnterPlanMode).**
19
-
20
- > **Relationship to `/work-on` (canonical path)**: This file is a `[BENCHMARK]` reference variant. It documents the same inline-execution model that `/work-on` Phase 3 and `work-on/build.md` use as their **default** for STANDARD/fast-lane issues — all build sub-phases (context → architect → implement → validate) run inline in the current context window with no `Skill()` sub-agent spawns. The difference is that this file omits the worktree lifecycle, `FORGE:CONTRACT` posting, and the Spawn-Decision exception path for large builds. Use `/work-on` for production runs; use this file for baseline benchmarking or when comparing single-prompt token cost against the modular path. See `work-on/build.md` (Canonical Build Path section) for the reconciled build topology. <!-- Added: forge#1276 -->
21
- **NEVER use the Agent tool** — this spec executes all phases inline as a single monolithic prompt. The Agent tool would spawn a subprocess outside this context, breaking the single-prompt contract and losing all accumulated pipeline state.
22
-
23
- <!-- FORGE:SPEC_LOADED — work-on-monolithic.md loaded and active. Agent is bound by this spec. -->
24
-
25
- ### Compaction Resilience
26
-
27
- 1. Write state to GitHub after EVERY significant step
28
- 2. Re-read GitHub state at the START of each phase (don't rely on in-memory context)
29
- 3. After compaction: re-read issue (body + comments + labels) to reconstruct state
30
- 4. Key principle: A NEW session running this command should pick up where the last left off by reading GitHub state alone
31
-
32
- ---
33
-
34
- ## Pipeline Rules
35
-
36
- - **NEVER merge to main.** PRs target `staging` (fast lane) or `milestone/{slug}` (feature lane).
37
- - **`Closes #N` does not auto-close for non-default-branch PRs.** You MUST explicitly `gh issue close`.
38
- - **Review findings are NOT merge blockers.** They become separate issues.
39
-
40
- ---
41
-
42
- ## Project Configuration
43
-
44
- Read `forge.yaml` from the repository root. If missing, tell the user to run `npx forgedock init` and stop.
45
-
46
- Resolve the following variables from `forge.yaml` before running any pipeline phase:
47
-
48
- | Variable | Source in forge.yaml | Description |
49
- |----------|----------------------|-------------|
50
- | `GH_REPO` | `project.owner` + `project.repo` | Full GitHub repo identifier (`owner/repo`) |
51
- | `REPO_PATH` | `paths.root` | Absolute path to the local repository |
52
- | `STAGING_BRANCH` | `branches.staging` | Branch for fast-lane PRs (no milestone) |
53
- | `WORKTREE_BASE` | `paths.worktree_base` | Directory where git worktrees are created |
54
- | `GH_FLAG` | _(empty for default repo; `-R owner/repo` for satellites)_ | Extra flag for `gh` CLI satellite repo calls |
55
-
56
- **Multi-repo routing** (optional): If `forge.yaml` contains a `repos.satellites` list, build the prefix routing table from it. Each satellite entry has `prefix`, `repo`, `staging_branch`, and `local_path`. Issues without a prefix route to the default `GH_REPO`. Satellite repos have no staging — their fast-lane PRs go to their configured `staging_branch` (typically `main`).
57
-
58
- If no `repos.satellites` section exists, only the primary repo is available — all issues route to `GH_REPO`.
59
-
60
- ---
61
-
62
- ## Phase 0: Resolve Issue & Load Context
63
-
64
- ### 0A: Parse input
65
- Extract project prefix and issue number.
66
-
67
- ### 0B: Load issue + existing context
68
- ```bash
69
- gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state,comments,milestone
70
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | {id: .id, author: .user.login, body: .body}'
71
- ```
72
-
73
- **Check**: state (closed → STOP), terminal labels (`workflow:merged`/`workflow:invalid` → STOP), existing agent comments (`FORGE:INVESTIGATOR`, `FORGE:CONTRACT`, `FORGE:BUILDER`, `FORGE:TRAJECTORY`), parent tracker status.
74
-
75
- **Determine resume point**: No comments → Phase 1. Investigation exists + ready-to-build → Phase 3. Builder:COMPLETE + no PR → Phase 4. Builder without :COMPLETE (partial/interrupted build) + no PR → Phase 3 (partial-build cleanup). Builder + PR open → Phase 5. PR merged + issue open → close issue.
76
-
77
- **Classify lane**: Milestone → feature lane (`milestone/{slug}`). No milestone → fast lane (`staging`).
78
-
79
- **Source branch for review-findings**: Parse `**Code branch**: \`{branch}\`` from body. Branch from there, not main.
80
-
81
- ### 0C: Sync to Project board
82
- Add issue to project, set Status=In Progress, Lane, Component, Priority, Workflow=Investigating.
83
-
84
- ---
85
-
86
- ## Phase 1: Investigation
87
-
88
- **Skip if**: `<!-- FORGE:INVESTIGATOR -->` exists with `<!-- INVESTIGATION:COMPLETE -->`.
89
-
90
- ### 1A: Set label
91
- ```bash
92
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating"
93
- gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review" 2>/dev/null || true
94
- ```
95
-
96
- ### 1B: Investigate the issue
97
-
98
- Mission: Validate whether the issue is real. Assume description is wrong until proven otherwise.
99
-
100
- **Domain file hints** (start search here):
101
-
102
- Configure domain-to-file mappings for your project in `forge.yaml → review.key_paths`. The table below shows the standard layout — override any entry that doesn't match your codebase:
103
-
104
- | Domain | Default key files (override via forge.yaml) |
105
- |--------|---------------------------------------------|
106
- | BILLING | `routers/billing.py`, `core/pricing.py`, `services/payment_service.py` |
107
- | WORKER | `worker/main.py`, `worker/queues.py`, `worker/tasks.py` |
108
- | AUTH | `core/auth.py`, `routers/auth.py`, `dependencies.py` |
109
- | DATABASE | `infra/migrations/`, `models/`, `db/` |
110
- | FRONTEND | `web/src/app/`, `web/src/components/`, `web/src/lib/` |
111
- | AI | `services/ai_client.py`, `routers/ai.py` |
112
- | INFRA | `.github/workflows/`, `docker-compose.yml`, `infra/traefik/` |
113
-
114
- If `forge.yaml → review.key_paths` is present, use those mappings instead of the defaults above.
115
-
116
- **Steps**: Check right branch → read domain files → verify claims → git blame → determine root cause → identify affected files → validate proposed fixes against full system stack.
117
-
118
- **Fix-Approach Validation**: If issue proposes a fix, don't adopt it as spec. Trace through target system's middleware, auth, routing.
119
-
120
- **Domain Context Discovery** (narrow scope, max 5 files):
121
- ```bash
122
- git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u
123
- ```
124
-
125
- **Output format**: Verdict (CONFIRMED/PARTIAL/INVALID), Confidence, Severity, Task Type, What Was Claimed, What We Found, Root Cause, Affected Files, Evidence, Recommendation.
126
-
127
- ### 1C: Post investigation comment
128
- ```bash
129
- gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
130
- <!-- INVESTIGATION:COMPLETE -->
131
- ## Investigation Report
132
- {report}"
133
- ```
134
-
135
- ### 1D: Update labels
136
- CONFIRMED/PARTIAL → add `workflow:ready-to-build`, continue to Phase 3.
137
- INVALID → close issue, add `workflow:invalid`, STOP.
138
-
139
- ---
140
-
141
- ## Phase 3: Build
142
-
143
- **Skip if**: `<!-- FORGE:BUILDER -->` exists.
144
-
145
- ### 3A: Re-read state from GitHub (MANDATORY)
146
- ```bash
147
- gh issue view {NUMBER} {GH_FLAG} --json body,labels
148
- gh api repos/{GH_REPO}/issues/{NUMBER}/comments --jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
149
- ```
150
-
151
- ### 3B: Classify task type
152
-
153
- | Signal | Type | Approach |
154
- |--------|------|----------|
155
- | UI/UX, feature + web/ files | UI/UX | `frontend-design` skill |
156
- | Feature + services/ | Backend Feature | Implement directly |
157
- | Feature + both | Full-Stack | Backend first, then frontend-design |
158
- | Bug + web/ | Frontend Fix | Direct |
159
- | Bug + services/ | Backend Fix | Direct |
160
- | Refactor/docs | Maintenance | Direct |
161
-
162
- Then classify COMPLEXITY_BAND with the same table as `work-on/build.md` B0.5 (TRIVIAL: single doc/config file, no logic; STANDARD: 1–5 files, existing patterns; COMPLEX: 6+ files, new abstractions, cross-service, schema changes) and post the `FORGE:FAST_PATH` marker. `scripts/verify-phase-trail.sh` reads the band from it and fails the review-pr merge gate when it is absent (forge#3148):
163
-
164
- ```bash
165
- FP_BODY="<!-- FORGE:FAST_PATH -->
166
- ## Fast-Path Classification
167
-
168
- **COMPLEXITY_BAND**: {TRIVIAL|STANDARD|COMPLEX}
169
- **Task type**: {TASK_TYPE}
170
- **Affected file count**: {N}
171
- **Rationale**: {one-sentence explanation of classification decision}
172
- **Phases skipped**: {3C.5, 3C.6 for TRIVIAL | none — full pipeline}"
173
- gh issue comment {NUMBER} {GH_FLAG} --body "$FP_BODY" # <!-- allowlist:check-command-side-effects -->
174
- ```
175
-
176
- ### 3C: Builder Contract (MANDATORY)
177
- Post `<!-- FORGE:CONTRACT -->` comment with: task type, proposed approach, deliverables table (file/change/why), acceptance criteria, quality considerations.
178
-
179
- ### 3C.5: Context Gathering (max 2 minutes — MANDATORY for STANDARD/COMPLEX, skip for TRIVIAL)
180
-
181
- Surface institutional memory before writing code. For each affected file:
182
- ```bash
183
- # Recent agent work on these files
184
- git log --oneline --all -30 -- {affected_files} | grep -oP '#\d+' | sort -u | head -5
185
- # Check for review findings on same files
186
- for ISSUE_NUM in {related_issue_numbers}; do
187
- gh api repos/{GH_REPO}/issues/${ISSUE_NUM}/comments --jq '.[] | select(.body | contains("FORGE:")) | .body' 2>/dev/null | head -20
188
- done
189
- ```
190
-
191
- Post `<!-- FORGE:CONTEXT -->` comment with findings. For STANDARD/COMPLEX it is required even when no relevant history is found (say so in the comment): the merge gate checks for it.
192
-
193
- ### 3C.6: Architecture Plan (MANDATORY for STANDARD/COMPLEX — skip for TRIVIAL)
194
-
195
- For multi-file changes, trace ALL affected code paths before writing code:
196
- - Map the call chain: entry point → middleware → handler → service → model → response
197
- - Identify every file that needs to change
198
- - Order implementation steps to avoid broken intermediate states
199
-
200
- Post `<!-- FORGE:ARCHITECT -->` comment with the implementation plan.
201
-
202
- Skip this step only when COMPLEXITY_BAND is TRIVIAL. For STANDARD/COMPLEX, the merge gate checks for the `FORGE:ARCHITECT` comment.
203
-
204
- ### 3D: Set building label
205
- ```bash
206
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:building" --remove-label "workflow:ready-to-build,workflow:investigating"
207
- ```
208
-
209
- ### 3E: Create worktree
210
- Branch slug from title (lowercase, hyphenated, max 40 chars). Prefix: `fix/` (bugs) or `feat/` (features).
211
- - Review-finding → branch from `origin/{SOURCE_BRANCH}`
212
- - Fast lane → branch from `origin/{STAGING_BRANCH}`
213
- - Feature lane → branch from `origin/milestone/{slug}`
214
-
215
- ```bash
216
- cd {REPO_PATH}
217
- git fetch origin
218
- BRANCH="fix/{slug}-{NUMBER}"
219
- git worktree add {WORKTREE_BASE}/{BRANCH} -b {BRANCH} origin/{PR_BASE}
220
- ```
221
-
222
- ### 3F: Implement
223
- Route by task type. Read the investigation report and contract. Implement the fix/feature.
224
-
225
- ### 3F.5: Quality Gate
226
- Invoke quality-gate on changed files:
227
- ```
228
- Skill(skill="{FORGE_SKILL_PREFIX}quality-gate", args="{changed_files} --worktree {WORKTREE_PATH}")
229
- ```
230
- Fix HIGH/MEDIUM findings. Max 2 iterations. Skip for 1-file config/docs edits.
231
-
232
- Then post the `FORGE:QUALITY_GATE` marker with the real gate result (same contract as `work-on/build/validate.md` V1/V5). Post it even when the gate was skipped (`**Result**: PASS (skipped — single config/docs file)`). The merge gate requires it unless the whole diff is docs-only (forge#3148):
233
-
234
- ```bash
235
- QG_BODY="<!-- FORGE:QUALITY_GATE -->
236
- ## Quality Gate Result
237
-
238
- **Result**: {PASS|FAIL}
239
- **Iterations**: {N}
240
- **Commands run**: {quality-gate invocation actually executed}
241
- **Findings remaining**: {none | summary}"
242
- gh issue comment {NUMBER} {GH_FLAG} --body "$QG_BODY" # <!-- allowlist:check-command-side-effects -->
243
- ```
244
-
245
- If the gate still reports HIGH/MEDIUM findings after 2 iterations, post `**Result**: FAIL`, add `needs-human`, and STOP.
246
-
247
- ### 3G: Format and verify
248
- - Python: `black` + `isort` + `py_compile`
249
- - TypeScript (primary repo): `prettier --write` + `tsc --noEmit` (BLOCKING if fails)
250
- - TypeScript (satellite): `npm run build` + `prettier --write`
251
-
252
- ### 3H: Frontend proxy wiring check (MANDATORY)
253
- All client-side fetch/useSWR/apiFetch must use `/api/...` proxy routes, NEVER `/api/v1/...` directly.
254
-
255
- ### 3I: Deployment completeness check (MANDATORY)
256
- Skip if no new env vars introduced.
257
-
258
- For each new env var, verify present in ALL required locations:
259
-
260
- | Location | Required for |
261
- |----------|-------------|
262
- | `.env.example` | All new vars |
263
- | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
264
- | `app/env_validation.py` | API service vars (if project has one) |
265
-
266
- **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
267
-
268
- If the project uses SOPS, verify the new var is present in all SOPS chain locations:
269
- - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
270
- - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
271
-
272
- If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
273
- > `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
274
-
275
- ### 3J: Commit
276
- Conventional prefix (fix/feat/refactor/docs). Reference #{NUMBER} in the commit message.
277
-
278
- ### 3K: Post implementation comment
279
- Post `<!-- FORGE:BUILDER -->` with: branch, commit, files changed, approach, changes list, testing checklist.
280
-
281
- ---
282
-
283
- ## Phase 4: PR Creation
284
-
285
- ### 4A: Push branch
286
- ```bash
287
- cd {WORKTREE_PATH} && git push -u origin {BRANCH}
288
- ```
289
- If fails: try `--force-with-lease`. If still fails: post comment, add `needs-human`, STOP.
290
-
291
- ### 4B: Determine PR target
292
- No milestone → `staging`. Has milestone → `milestone/{slug}`. NEVER `main`.
293
-
294
- ### 4C: Create PR
295
- ```bash
296
- gh pr create {GH_FLAG} --base {PR_BASE} --title "{Fix|Feat}: {description} (#{NUMBER})" --body "..."
297
- ```
298
- Include `Closes #{NUMBER}`.
299
-
300
- ### 4D: Update labels
301
- ```bash
302
- gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" --remove-label "workflow:building"
303
- ```
304
-
305
- ---
306
-
307
- ## Phase 5: Auto-Review
308
-
309
- ### 5A: Re-read state from GitHub (MANDATORY)
310
- ### 5B: Invoke /review-pr with --auto-merge
311
- ```
312
- if DRY_RUN=true:
313
- record "Would invoke review-pr --auto-merge for PR #{PR_NUMBER}; skipped (dry-run)."
314
- else:
315
- Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER} --auto-merge --issue {NUMBER} --base {PR_BASE} --gh-flag {GH_FLAG}")
316
- ```
317
-
318
- ### 5C: Verify merge and close (recovery)
319
-
320
- **`REVIEW_RESULT: status: PHASE_TRAIL_FAILED`** (forge#3148): review-pr's Phase 8 refused the merge because a phase marker is missing. The PR is intentionally unmerged — do NOT run the `gh pr merge` recovery below, which would merge around the gate. Re-run each phase named on the `MISSING: <marker> -> <action>` lines (never hand-post a marker), then re-run 5B once. If it returns `PHASE_TRAIL_FAILED` again, add `needs-human` and STOP; review-pr has already posted the `FORGE:PHASE_TRAIL_FAILED` comment.
321
-
322
- **`REVIEW_RESULT: status: BLOCKED` with a phase-trail or auto-merge-gate blocker** (any blocker containing "phase trail", or "auto-merge requires --issue"; forge#3147): the gate refused or could not run. Do NOT re-run phases and do NOT run the manual merge below. Add `needs-human` and STOP.
323
-
324
- Otherwise:
325
- - PR MERGED + issue CLOSED → proceed to Phase 6/7
326
- - PR MERGED + issue OPEN → close issue manually
327
- - PR NOT MERGED + review returned `REVIEW_RESULT: status: COMPLETE` → run `scripts/wait-ci-green.sh {PR_NUMBER} {GH_FLAG}` (resolved from the plugin root); only on exit 0 merge with `--merge --match-head-commit <the CI_GATE_HEAD it printed>` and close the issue. A non-zero gate (or a "ci gate" / "stale review" blocker) → do NOT merge; post the gate output, add `needs-human`, STOP. If the merge fails → post comment, add `needs-human`, STOP.
328
- - PR NOT MERGED + any other, missing, or unparseable `REVIEW_RESULT` → do NOT merge (an unrecognized refusal must never fall through to a manual merge). Add `needs-human` and STOP.
329
-
330
- ### 5D: Project board update (Workflow=Merged, Status=Done)
331
-
332
- ### 5E: Final issue body update
333
- Check off remaining items.
334
-
335
- ---
336
-
337
- ## Phase 6: Parent Tracker Update (Sub-Issues Only)
338
-
339
- **Skip if**: Not a sub-issue.
340
-
341
- Check off this sub-issue in parent body. If ALL sub-issues closed → close parent with `workflow:merged`.
342
-
343
- ---
344
-
345
- ## Phase 7: Summary & Trajectory
346
-
347
- ### 7A: Report
348
- ```
349
- ## Done: #{NUMBER} — {TITLE}
350
- - Investigation: {verdict} ({confidence})
351
- - Lane: {FAST/FEATURE}
352
- - Fix: {branch} → PR #{PR_NUMBER} → merged to `{PR_BASE}`
353
- - Files changed: {count}
354
- ```
355
-
356
- ### 7B: Trajectory Log (MANDATORY)
357
- Post `<!-- FORGE:TRAJECTORY -->` comment with phase-by-phase results table, decisions, anomalies.
358
-
359
- ---
360
-
361
- ## Error Handling
362
-
363
- - Worktree exists: reuse or clean up
364
- - PR creation fails: check if branch pushed, if PR already exists
365
- - Merge conflicts: report to user, do NOT auto-resolve
366
- - gh CLI fails: check `gh auth status`
367
- - Label missing: create it