forgedock 1.8.0 → 1.8.2

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 (67) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/geo-audit.md +3 -1
  11. package/commands/incident-response.md +3 -1
  12. package/commands/issue.md +4 -2
  13. package/commands/milestone.md +8 -6
  14. package/commands/orchestrate/config.md +2 -2
  15. package/commands/orchestrate/phase-1-resolve.md +56 -12
  16. package/commands/orchestrate/phase-3-dependency.md +15 -5
  17. package/commands/orchestrate/phase-4-execution.md +593 -78
  18. package/commands/orchestrate/phase-5-cleanup.md +4 -2
  19. package/commands/orchestrate/phase-6-report.md +7 -3
  20. package/commands/orchestrate.md +13 -9
  21. package/commands/pipeline-health/phase-4-proposals.md +6 -4
  22. package/commands/pipeline-health/phase-5-report.md +4 -2
  23. package/commands/pipeline-health.md +9 -7
  24. package/commands/pipeline-resume.md +4 -2
  25. package/commands/qa-sweep.md +3 -1
  26. package/commands/quality-gate.md +4 -3
  27. package/commands/recover-orphans.md +172 -13
  28. package/commands/review-pr-agents.md +6 -4
  29. package/commands/review-pr-staging.md +13 -5
  30. package/commands/review-pr.md +187 -19
  31. package/commands/rollback.md +3 -1
  32. package/commands/security-audit.md +3 -1
  33. package/commands/signal-planner.md +6 -4
  34. package/commands/sync-ecosystem.md +4 -2
  35. package/commands/test-gate.md +4 -2
  36. package/commands/upgrade-deps.md +4 -2
  37. package/commands/validate.md +3 -1
  38. package/commands/work-on/build/architect.md +81 -19
  39. package/commands/work-on/build/context.md +61 -18
  40. package/commands/work-on/build/implement.md +62 -18
  41. package/commands/work-on/build/validate.md +240 -60
  42. package/commands/work-on/build.md +344 -146
  43. package/commands/work-on/close.md +617 -246
  44. package/commands/work-on/decompose.md +239 -41
  45. package/commands/work-on/investigate.md +377 -61
  46. package/commands/work-on/remediate.md +42 -9
  47. package/commands/work-on/review.md +240 -29
  48. package/commands/work-on-monolithic.md +48 -7
  49. package/commands/work-on.md +180 -2014
  50. package/docs/CODEX.md +2 -0
  51. package/docs/CONFIG.md +11 -0
  52. package/package.json +1 -1
  53. package/scripts/build-spec-graph.mjs +11 -4
  54. package/scripts/check-spec-markers.sh +11 -1
  55. package/scripts/checkbox-sections.test.sh +8 -23
  56. package/scripts/extract-affected-files.sh +5 -2
  57. package/scripts/extract-affected-files.test.sh +27 -0
  58. package/scripts/forge-root.test.sh +176 -0
  59. package/scripts/is-docs-only.sh +31 -0
  60. package/scripts/is-docs-only.test.sh +64 -0
  61. package/scripts/lint-dispatch-prompt.sh +217 -0
  62. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  63. package/scripts/validate-spec-graph.sh +3 -2
  64. package/scripts/verify-phase-trail.sh +282 -0
  65. package/scripts/verify-phase-trail.test.sh +298 -0
  66. package/scripts/wait-ci-green.sh +98 -0
  67. package/scripts/wait-ci-green.test.sh +99 -0
@@ -6,6 +6,8 @@ install: core
6
6
 
7
7
  # /orchestrate — Phase 5: Post-Batch Cleanup
8
8
 
9
+ > **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.
10
+
9
11
  ## Phase 5: Post-Batch Cleanup
10
12
 
11
13
  **This phase is MANDATORY after every orchestration batch.** It prevents the rot that accumulates when multiple agents merge PRs in parallel — stale labels, orphaned worktrees, unclosed issues.
@@ -15,7 +17,7 @@ install: core
15
17
  Invoke the `/cleanup` skill with `all` to sweep everything:
16
18
 
17
19
  ```
18
- Skill(skill="cleanup", args="all")
20
+ Skill(skill="{FORGE_SKILL_PREFIX}cleanup", args="all")
19
21
  ```
20
22
 
21
23
  This will:
@@ -31,7 +33,7 @@ This will:
31
33
  Invoke `/audit-agents` on this session to measure pipeline efficiency:
32
34
 
33
35
  ```
34
- Skill(skill="audit-agents", args="latest")
36
+ Skill(skill="{FORGE_SKILL_PREFIX}audit-agents", args="latest")
35
37
  ```
36
38
 
37
39
  Include the audit summary in the final report (Phase 6). Key metrics to surface:
@@ -39,7 +39,10 @@ for NUM in {all_completed_issue_numbers}; do
39
39
  if [ -n "$TRAJ_BODY" ]; then
40
40
  # Decode via protocol CLI — handles both Base64url form (forge#1727) and gracefully
41
41
  # exits 1 (skips) for pre-migration inline-JSON entries.
42
- CARD=$(echo "$TRAJ_BODY" | node packages/protocol/src/cli.js parse --type CARD 2>/dev/null || true)
42
+ # The codec ships with ForgeDock, not with the consumer repo: resolve it from the install root.
43
+ _l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"; CODEC_CLI=""
44
+ for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do case "$_c" in /*) [ -z "$CODEC_CLI" ] && [ -f "$_c/packages/protocol/src/cli.js" ] && CODEC_CLI="$_c/packages/protocol/src/cli.js" ;; esac; done
45
+ CARD=$( [ -n "$CODEC_CLI" ] && printf '%s\n' "$TRAJ_BODY" | node "$CODEC_CLI" parse --type CARD 2>/dev/null | jq -c '.payload // .' 2>/dev/null || true)
43
46
  [ -n "$CARD" ] && CARDS="${CARDS}${CARD}"$'\n'
44
47
  fi
45
48
  done
@@ -163,7 +166,7 @@ ORCHESTRATION_ENDED_IDLE="${BATCH_FULLY_GATED:-false}"
163
166
  IDLE_POLICY_DEFERRED_COUNT=0
164
167
  for FINDING_NUM in "${DEFERRED_FINDINGS[@]:-}"; do
165
168
  [ -z "$FINDING_NUM" ] && continue
166
- echo "${DEFERRED_REASONS[$FINDING_NUM]:-}" | grep -qi "batch fully human-gated" && \
169
+ echo "${DEFERRED_REASONS[$FINDING_NUM]:-}" | grep -qi "batch fully human-gated" && ! echo "${DEFERRED_REASONS[$FINDING_NUM]:-}" | grep -qi "amplification breaker" && \
167
170
  IDLE_POLICY_DEFERRED_COUNT=$((IDLE_POLICY_DEFERRED_COUNT + 1))
168
171
  done
169
172
  ```
@@ -345,6 +348,7 @@ current session's live wake pick them up automatically if it is still running).
345
348
  | **Cascade amplification** | **{FINDINGS_SPAWNED:-0}/{MERGED_UNITS:-0} = {AMPLIFICATION_RATIO:-0} findings per merged unit** |
346
349
  | Amplification composition | {#REFINEMENT_FINDINGS[@]:-0} same-lineage refinements; {#NEW_SURFACE_FINDINGS[@]:-0} new-surface findings |
347
350
  | Amplification bound | {CASCADE_MAX_AMPLIFICATION:-off}; {#AMPLIFICATION_DEFERRED[@]:-0} same-lineage refinements deferred to completion sweep |
351
+ | Amplification breaker | {AMPLIFICATION_BREAKER:-on}; {tripped|clear}; {#AMPLIFICATION_BREAKER_DEFERRED[@]:-0} P3 findings paused ({#BREAKER_HELD[@]:-0} still held at sweep) for bounded P3 batches (never dispatched individually) |
348
352
  | Competing recommendations reconciled (Phase 2.5) | {RECONCILED_COUNT} (investigation plans arbitrated in place + serialized) |
349
353
  | Findings validated | {N} |
350
354
  | False positives | {N} ({%}) |
@@ -363,7 +367,7 @@ current session's live wake pick them up automatically if it is still running).
363
367
 
364
368
  `P3 findings clubbed` and `Findings deferred — token budget` are a DIFFERENT mechanism from the `$`-denominated `Budget limit`/`Projected spend`/`Actual spend`/`Issues deferred (budget ceiling)` rows above (forge#1743, opt-in `--budget N` flag, gates the *original* issue dispatch order). The token-budget row reads the always-on, token-denominated ceiling that scopes ONLY to Step 4C's review-finding cascade dispatch (forge#1858); `SURFACE_BATCH_COUNT` and `SURFACE_BATCHED_FINDINGS` are populated by the same Step 4C surface-area-batching block (forge#1818) that clubs P3 findings sharing a file/leaf-directory into one dispatched pipeline. Both degrade gracefully to `0`/`uncapped` when the batch had no review findings or ran with defaults. <!-- Added: forge#1858 -->
365
369
 
366
- **Cascade amplification detail**: render one row per `FINDINGS_BY_SOURCE_PR` entry: `| PR #{source} | {finding count} | {refinement count} | {new-surface count} |`. A ratio at or above 1.0 means the batch is not reducing its open-work count through merges alone; it does **not** mean the findings are low-value. Include any convergence warning and state that the default `max_amplification: off` never defers findings. When an opt-in bound deferred a refinement, list it under Completion Sweep as re-evaluable rather than silently omitting it.
370
+ **Cascade amplification detail**: render one row per `FINDINGS_BY_SOURCE_PR` entry: `| PR #{source} | {finding count} | {refinement count} | {new-surface count} |`. A ratio at or above 1.0 means the batch is not reducing its open-work count through merges alone; it does **not** mean the findings are low-value. Include any convergence warning and state that the default `max_amplification: off` never defers findings. State the amplification breaker (on by default, forge#3060) separately: when it tripped, report the ratio, the number of paused P3 findings, and the operator decision (batch or defer); when opted out via `amplification_breaker: off`, say so. When an opt-in bound deferred a refinement, list it under Completion Sweep as re-evaluable rather than silently omitting it.
367
371
 
368
372
  For `policy: all`, include the full-repo intake counts and list each automated duplicate with its canonical issue. A sweep measures all new open issues, including CI alerts without `review-finding`; it must not claim that the cascade ratio alone measures total queue growth.
369
373
 
@@ -51,7 +51,7 @@ dispatch a compact plan.
51
51
 
52
52
  ## Execution Order
53
53
 
54
- Read and execute phases in sequence. Each phase file is self-contained.
54
+ Read and execute phases in sequence. Each phase file is self-contained. Resolve every path from the spec root defined in the Quick Reference below.
55
55
 
56
56
  | Step | File | Description |
57
57
  |------|------|-------------|
@@ -67,15 +67,19 @@ Read and execute phases in sequence. Each phase file is self-contained.
67
67
 
68
68
  ## Quick Reference
69
69
 
70
+ **Spec root (MANDATORY)**: read every file below from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
71
+
72
+ **Raw reads are not substituted (MANDATORY)**: Claude Code fills in the plugin root only in the spec it loads (this file), never in files you open with Read. The phase files contain the placeholder written as `$` immediately followed by `{CLAUDE_PLUGIN_ROOT}`. Before running any bash block taken from a phase file, replace every occurrence of that placeholder with `${CLAUDE_PLUGIN_ROOT}` (the resolved root shown here), exactly as Claude Code would have. Left unreplaced it is rejected as a non-absolute path and resolution falls back to `$FORGE_HOME`, which may be stale.
73
+
70
74
  ```
71
- Read: $FORGE_HOME/commands/orchestrate/config.md # ALWAYS READ FIRST
72
- Read: $FORGE_HOME/commands/orchestrate/phase-1-resolve.md
73
- Read: $FORGE_HOME/commands/orchestrate/phase-2-triage.md
74
- Read: $FORGE_HOME/commands/orchestrate/phase-2.5-synthesis.md
75
- Read: $FORGE_HOME/commands/orchestrate/phase-3-dependency.md
76
- Read: $FORGE_HOME/commands/orchestrate/phase-4-execution.md
77
- Read: $FORGE_HOME/commands/orchestrate/phase-5-cleanup.md
78
- Read: $FORGE_HOME/commands/orchestrate/phase-6-report.md
75
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/config.md # ALWAYS READ FIRST
76
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-1-resolve.md
77
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2-triage.md
78
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-2.5-synthesis.md
79
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-3-dependency.md
80
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-4-execution.md
81
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-5-cleanup.md
82
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/orchestrate/phase-6-report.md
79
83
  ```
80
84
 
81
85
  The orchestrator reads only the phase file(s) relevant to the current step rather than
@@ -6,6 +6,8 @@ install: extras
6
6
 
7
7
  # /pipeline-health — Phase 4: Generate Improvement Proposals
8
8
 
9
+ > **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.
10
+
9
11
  ## Phase 4: Generate Improvement Proposals
10
12
 
11
13
  ### 4A: Pattern recurrence analysis — check-promotion threshold <!-- Added: forge#1331 -->
@@ -40,7 +42,7 @@ while IFS= read -r line; do
40
42
 
41
43
  # SECURITY: $SLUG is sourced from a <!-- FORGE:PATTERN: ([^\s>]+) --> tag inside
42
44
  # a review-finding issue body — untrusted, externally-influenced text. It is
43
- # interpolated below into a Skill(skill="issue", args="...") string, and
45
+ # interpolated below into a Skill(skill="{FORGE_SKILL_PREFIX}issue", args="...") string, and
44
46
  # commands/issue.md's Phase 1A resolves that string via `eval "set -- $ARGUMENTS"`.
45
47
  # Any shell metacharacter (quote, backtick, $, ;) in $SLUG would break out of the
46
48
  # intended --title token and be eval'd as shell. Whitelist-validate at the
@@ -108,7 +110,7 @@ ${SOURCE_FINDINGS}
108
110
  <!-- AUTO-CREATED: pipeline-health Phase 4A — pattern recurrence threshold exceeded -->
109
111
  CHECK_EOF
110
112
 
111
- Skill(skill="issue", args="--title \"feat(quality-gate): promote pattern '$SLUG' to deterministic registry check (recurred ${COUNT}x)\" --body-file $CHECK_BODY_FILE --label check-promotion --label priority:P2")
113
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"feat(quality-gate): promote pattern '$SLUG' to deterministic registry check (recurred ${COUNT}x)\" --body-file $CHECK_BODY_FILE --label check-promotion --label priority:P2")
112
114
  rm -f "$CHECK_BODY_FILE"
113
115
  echo "Created check-promotion issue for pattern '$SLUG' (recurred ${COUNT}x from: $SOURCE_FINDINGS)"
114
116
  done <<< "$PATTERN_LIST"
@@ -170,7 +172,7 @@ echo "$SPEC_CONCENTRATION" | while IFS=' ' read -r count spec_file finding_nums;
170
172
 
171
173
  # SECURITY (defense-in-depth, consistency with $SLUG above): $spec_file is
172
174
  # already constrained by the extraction regex (commands/[a-z0-9_/-]+\.md), but
173
- # it is interpolated below into a Skill(skill="issue", args="...") string that
175
+ # it is interpolated below into a Skill(skill="{FORGE_SKILL_PREFIX}issue", args="...") string that
174
176
  # /issue's Phase 1A resolves via eval. Whitelist-validate again at point of use
175
177
  # so this call site does not depend solely on the extraction regex staying safe.
176
178
  if ! echo "$spec_file" | grep -qE '^commands/[A-Za-z0-9_/-]+\.md$'; then
@@ -235,7 +237,7 @@ This issue was auto-created by \`pipeline-health\` Phase 4A.5.
235
237
  <!-- AUTO-CREATED: pipeline-health Phase 4A.5 — spec concentration threshold exceeded -->
236
238
  TRIGGER_EOF
237
239
 
238
- Skill(skill="issue", args="--title \"feat(spec-doctor): queue spec evolution pass for $(basename $spec_file .md) (${count} concentrated findings)\" --body-file $TRIGGER_BODY_FILE --label spec-doctor-trigger --label priority:P2")
240
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"feat(spec-doctor): queue spec evolution pass for $(basename $spec_file .md) (${count} concentrated findings)\" --body-file $TRIGGER_BODY_FILE --label spec-doctor-trigger --label priority:P2")
239
241
  rm -f "$TRIGGER_BODY_FILE"
240
242
  echo "Created spec-doctor trigger for '$spec_file' (${count} findings: $finding_nums)"
241
243
  done
@@ -6,6 +6,8 @@ install: extras
6
6
 
7
7
  # /pipeline-health — Phase 5: Report & Track
8
8
 
9
+ > **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.
10
+
9
11
  ## Phase 5: Report & Track
10
12
 
11
13
  ### 5A: Post health report
@@ -279,7 +281,7 @@ restore_forge_yaml() {
279
281
  trap restore_forge_yaml EXIT
280
282
  cp "$FORGE_ISSUE_HOOK_CONFIG" forge.yaml
281
283
 
282
- ISSUE_RESULT=$(Skill(skill="issue", args="--title \"Pipeline Health: $REPO — $(date +%Y-%m-%d) — Score: [SCORE]/100\" --body-file $HEALTH_BODY_FILE --label health-report"))
284
+ ISSUE_RESULT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"Pipeline Health: $REPO — $(date +%Y-%m-%d) — Score: [SCORE]/100\" --body-file $HEALTH_BODY_FILE --label health-report"))
283
285
  echo "$ISSUE_RESULT"
284
286
 
285
287
  # Restore the original forge.yaml immediately — so a subsequent Bash call in this same
@@ -390,7 +392,7 @@ restore_forge_yaml() {
390
392
  trap restore_forge_yaml EXIT
391
393
  cp "$FORGE_ISSUE_HOOK_CONFIG" forge.yaml
392
394
 
393
- ISSUE_RESULT=$(Skill(skill="issue", args="--title \"fix([command]): [description]\" --body-file $PROPOSAL_BODY_FILE --label \"[bug|enhancement|feature]\" --label \"[P1|P2|P3]\""))
395
+ ISSUE_RESULT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix([command]): [description]\" --body-file $PROPOSAL_BODY_FILE --label \"[bug|enhancement|feature]\" --label \"[P1|P2|P3]\""))
394
396
  echo "$ISSUE_RESULT"
395
397
 
396
398
  # Restore the original forge.yaml immediately — same rationale as 5A. Clears the EXIT trap
@@ -31,14 +31,16 @@ Read and execute phases in sequence. Each phase file is self-contained.
31
31
 
32
32
  ## Quick Reference
33
33
 
34
+ **Spec root (MANDATORY)**: read every file below from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
35
+
34
36
  ```
35
- Read: $FORGE_HOME/commands/pipeline-health/config.md # ALWAYS READ FIRST
36
- Read: $FORGE_HOME/commands/pipeline-health/phase-1-context.md
37
- Read: $FORGE_HOME/commands/pipeline-health/phase-2-metrics.md
38
- Read: $FORGE_HOME/commands/pipeline-health/phase-3-analyze.md
39
- Read: $FORGE_HOME/commands/pipeline-health/phase-4-proposals.md
40
- Read: $FORGE_HOME/commands/pipeline-health/phase-5-report.md
41
- Read: $FORGE_HOME/commands/pipeline-health/phase-6-summary.md
37
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/config.md # ALWAYS READ FIRST
38
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-1-context.md
39
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-2-metrics.md
40
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-3-analyze.md
41
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-4-proposals.md
42
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-5-report.md
43
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/pipeline-health/phase-6-summary.md
42
44
  ```
43
45
 
44
46
  The command reads only the phase file(s) relevant to the current step rather than loading
@@ -6,13 +6,15 @@ install: extras
6
6
 
7
7
  # /pipeline-resume — Context Recovery After Compaction
8
8
 
9
+ > **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.
10
+
9
11
  **Input**: $ARGUMENTS
10
12
 
11
13
  You are the context-recovery agent. When a session ends or context is compressed, all working state is lost. Your job is to restore it instantly — find the most recently active issue, read all FORGE annotations, reconstruct the current pipeline phase, and re-enter the pipeline exactly where it left off.
12
14
 
13
15
  **Agent model policy**: `model: "haiku"`, `effort: low` (mechanical tier — context recovery, annotation reading, state reconstruction). Fallback: `model: "sonnet"` if rate-limited. Feature gate: pass `effort` only on Claude Code >= 2.1.154.
14
16
  **NEVER use plan mode (EnterPlanMode).**
15
- **NEVER use the Agent tool** — pipeline-resume re-enters the pipeline via `Skill(skill="work-on", ...)` only. Using the Agent tool would restart the pipeline in an untracked subprocess rather than resuming the existing tracked session.
17
+ **NEVER use the Agent tool** — pipeline-resume re-enters the pipeline via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` only. Using the Agent tool would restart the pipeline in an untracked subprocess rather than resuming the existing tracked session.
16
18
 
17
19
  <!-- FORGE:SPEC_LOADED — pipeline-resume.md loaded and active. Agent is bound by this spec. -->
18
20
 
@@ -267,7 +269,7 @@ Emit a structured context block before resuming — this is what anchors the age
267
269
  Invoke `/work-on` to re-enter the pipeline. Work-on reads GitHub state independently and routes to the correct phase automatically.
268
270
 
269
271
  ```
270
- Skill("work-on", args="{TARGET_NUMBER}")
272
+ Skill("{FORGE_SKILL_PREFIX}work-on", args="{TARGET_NUMBER}")
271
273
  ```
272
274
 
273
275
  **Why delegation works**: `/work-on` Phase 0B already implements "Determine resume point" — it reads issue labels and FORGE annotations to route to the correct phase. `/pipeline-resume` provides the human-readable context summary, then hands off to `/work-on` for execution.
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /qa-sweep — Platform-Wide QA Testing
10
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
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  You are the QA orchestrator. Auto-discover every page across the platform (dashboard, marketing, blog, auth, docs, pricing), then systematically test every UI element, interaction, workflow, and state transition using browser automation. Create GitHub issues for every finding.
@@ -339,7 +341,7 @@ BODY_EOF
339
341
  ```
340
342
 
341
343
  ```
342
- Skill(skill="issue", args="--title \"{fix|feat}: {title} on {page}\" --body-file \"$QA_BODY_FILE\" --label \"qa\" --label \"{bug_or_enhancement}\" --label \"{priority}\"")
344
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{fix|feat}: {title} on {page}\" --body-file \"$QA_BODY_FILE\" --label \"qa\" --label \"{bug_or_enhancement}\" --label \"{priority}\"")
343
345
  ```
344
346
 
345
347
  `qa`, `{bug_or_enhancement}`, and `{priority}` are each passed as their own `--label` flag — `/issue`'s programmatic mode does not comma-split a single `--label` value.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Pre-commit quality check — catches defects the review would flag, so the builder can fix them before committing
3
3
  argument-hint: (invoked by /work-on, not directly)
4
+ context: fork
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -1712,13 +1713,13 @@ OR:
1712
1713
 
1713
1714
  Return the findings list directly. Do NOT post to GitHub. Do NOT create issues. The builder will fix the findings and re-run the gate if needed.
1714
1715
 
1715
- **CRITICAL — continuation directive**: Your output is consumed by a calling pipeline agent. You MUST append the continuation block below to your output in ALL cases (both PASS and FINDINGS). Without it, the calling agent treats your output as a terminal signal and stops the pipeline.
1716
+ **Result delivery**: This skill runs in an isolated forked context, so your final reply is the only thing the caller sees. The PASS or FINDINGS block below MUST be your final reply, in ALL cases, with nothing after it. The caller (the validate quality-gate loop) decides what happens next.
1716
1717
 
1717
1718
  If 0 findings:
1718
1719
  ```
1719
1720
  QUALITY GATE: PASS — no issues detected.
1720
1721
 
1721
- **YOU MUST NOW continue to sub-phase 3H (Format and verify) in work-on.md — this PASS is intermediate, NOT terminal. Do NOT stop.**
1722
+ Return this result to the caller.
1722
1723
  ```
1723
1724
 
1724
1725
  If findings exist:
@@ -1726,5 +1727,5 @@ If findings exist:
1726
1727
  QUALITY GATE FINDINGS: N issues detected.
1727
1728
  {findings list}
1728
1729
 
1729
- **YOU MUST NOW fix each HIGH and MEDIUM finding, then re-run the quality gate — this result is intermediate, NOT terminal. Do NOT stop.**
1730
+ Return this result to the caller; the caller fixes each HIGH and MEDIUM finding and re-runs the gate.
1730
1731
  ```
@@ -7,13 +7,15 @@ argument-hint: "[--dry-run | --since <hours> | --issue <number>]"
7
7
 
8
8
  # /recover-orphans — Pipeline Orphan Recovery
9
9
 
10
+ > **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.
11
+
10
12
  **Input**: $ARGUMENTS
11
13
 
12
14
  Scan ALL open issues with intermediate workflow labels for orphaned state — issues where the agent died mid-pipeline (context expired, rate-limited, crashed) and no active agent is continuing. Diagnose each orphan's actual GitHub state and apply the appropriate recovery action.
13
15
 
14
16
  **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet". Fallback: `model: "opus"` if rate-limited.
15
17
  **NEVER use plan mode (EnterPlanMode).**
16
- **NEVER use the Agent tool** — recover-orphans re-enters the pipeline via `Skill(skill="work-on", ...)` and `Skill(skill="review-pr", ...)` only.
18
+ **NEVER use the Agent tool** — recover-orphans re-enters the pipeline via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` and `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", ...)` only.
17
19
 
18
20
  <!-- FORGE:SPEC_LOADED — recover-orphans.md loaded and active. Agent is bound by this spec. -->
19
21
 
@@ -171,7 +173,6 @@ for NUM in $ORPHAN_LIST; do
171
173
  DIAG_REASON[$NUM]="Already in terminal state: $ISSUE_LABELS"
172
174
  continue
173
175
  fi
174
-
175
176
  # Check for merged PR referencing this issue
176
177
  MERGED_PR=$(gh pr list ${GH_FLAG} \
177
178
  --state merged \
@@ -198,6 +199,15 @@ for NUM in $ORPHAN_LIST; do
198
199
  continue
199
200
  fi
200
201
 
202
+ # forge#3148: checked after the merged-PR label cleanup above, so a merged orphan is still closed out.
203
+ # An escalated orphan is waiting on a human — re-running a recovery action every sweep
204
+ # (e.g. re-invoking /review-pr against an unrepaired phase trail) only repeats the same refusal.
205
+ if echo ", $ISSUE_LABELS," | grep -q ", needs-human,"; then
206
+ DIAG_ACTION[$NUM]="skip"
207
+ DIAG_REASON[$NUM]="Escalated (needs-human) — waiting on a human"
208
+ continue
209
+ fi
210
+
201
211
  # Check for an open PR on any branch associated with this issue
202
212
  # Strategy: look for branches with the issue number in the name
203
213
  ISSUE_BRANCH=$(git ls-remote --heads origin 2>/dev/null | grep "/$NUM" | sed 's|.*refs/heads/||' | head -1)
@@ -307,6 +317,68 @@ done
307
317
 
308
318
  For each diagnosed issue, apply the recovery action. All mutating actions are skipped when `DRY_RUN=true`.
309
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).
321
+
322
+ ```bash
323
+ RECOVERY_CLAIM_TTL_MIN="${RECOVERY_CLAIM_TTL_MIN:-30}" # a claim or heartbeat older than this is treated as dead
324
+ SWEEP_ID="sweep-$(date -u +%Y%m%dT%H%M%SZ)-$$"
325
+
326
+ # claim_orphan <issue> — returns 0 if this sweep now holds the claim, 1 if it must skip (reason in CLAIM_SKIP_REASON).
327
+ # Fails closed: an unreadable comment list (initial read or race-check re-read) is treated as "held by someone else".
328
+ # Both reads run under pipefail in a subshell: the pipeline status would otherwise be jq's, and a gh failure
329
+ # (rate limit, 403) would yield "[]" with exit 0, indistinguishable from "no live holder".
330
+ claim_orphan() {
331
+ local num="$1" now cutoff comments live
332
+ [ "$DRY_RUN" = "true" ] && return 0 # dry-run: no claim comment is posted (callers already skip dry-run resumes)
333
+ now=$(date -u +%s); cutoff=$(( now - RECOVERY_CLAIM_TTL_MIN * 60 ))
334
+ comments=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${num}/comments" 2>/dev/null \
335
+ | jq -s 'add // []') || { CLAIM_SKIP_REASON="could not read comments to check for a live holder"; return 1; }
336
+
337
+ # Live orchestrator signal: a recent heartbeat from /work-on --under-orchestration, or a recent claim of any sweep.
338
+ # A claim is "released" only by a later RELEASED marker naming its sweep id.
339
+ live=$(echo "$comments" | jq --argjson cutoff "$cutoff" '
340
+ ([.[] | select(.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | .body | capture("Sweep: (?<id>[^ \n]+)").id]) as $rel
341
+ | [ .[] | select(
342
+ ((.body | contains("FORGE:HEARTBEAT")) or (.body | contains("<!-- FORGE:RECOVERY_CLAIM -->")))
343
+ and ((.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | not)
344
+ and ((.updated_at | fromdateiso8601) >= $cutoff)
345
+ and ((.body | capture("Sweep: (?<id>[^ \n]+)")? // {id:""}).id as $sid | ($rel | index($sid)) == null)
346
+ ) ] | length') || live=1
347
+ if [ "${live:-1}" -gt 0 ]; then
348
+ CLAIM_SKIP_REASON="live holder (heartbeat or unreleased recovery claim within ${RECOVERY_CLAIM_TTL_MIN}m)"
349
+ return 1
350
+ fi
351
+
352
+ gh issue comment "$num" ${GH_FLAG} --body "<!-- FORGE:RECOVERY_CLAIM -->
353
+ **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; }
356
+
357
+ # Race check: re-read; the earliest unreleased claim wins. Losing means another sweep claimed first.
358
+ local first
359
+ first=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${num}/comments" 2>/dev/null | jq -rs --argjson cutoff "$cutoff" '
360
+ add // [] | ([.[] | select(.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | .body | capture("Sweep: (?<id>[^ \n]+)").id]) as $rel
361
+ | [.[] | select(.body | contains("<!-- FORGE:RECOVERY_CLAIM -->"))
362
+ | select((.updated_at | fromdateiso8601) >= $cutoff)
363
+ | select((.body | capture("Sweep: (?<id>[^ \n]+)").id) as $sid | ($rel | index($sid)) == null)]
364
+ | first | .body // ""' | sed -n 's/^\*\*Sweep: \(.*\)\*\*$/\1/p') \
365
+ || { release_orphan "$num"; CLAIM_SKIP_REASON="could not re-read comments to confirm the claim race"; return 1; }
366
+ if [ "$first" != "$SWEEP_ID" ]; then
367
+ release_orphan "$num"
368
+ CLAIM_SKIP_REASON="lost claim race to ${first:-unknown}"
369
+ return 1
370
+ fi
371
+ return 0
372
+ }
373
+
374
+ release_orphan() {
375
+ [ "$DRY_RUN" = "true" ] && return 0
376
+ gh issue comment "$1" ${GH_FLAG} --body "<!-- FORGE:RECOVERY_CLAIM_RELEASED -->
377
+ **Sweep: ${SWEEP_ID}**
378
+ Released by /recover-orphans." >/dev/null 2>&1 || true
379
+ }
380
+ ```
381
+
310
382
  ```bash
311
383
  RECOVERY_RESULTS=""
312
384
 
@@ -352,8 +424,31 @@ for NUM in $ORPHAN_LIST; do
352
424
  if [ "$DRY_RUN" = "true" ]; then
353
425
  echo " [DRY-RUN] Would: gh pr merge $PR_NUM --merge --auto"
354
426
  else
355
- MERGE_RESULT=$(gh pr merge "$PR_NUM" ${GH_FLAG} --merge --auto 2>&1)
356
- MERGE_EXIT=$?
427
+ # CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
428
+ # green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
429
+ # protection required none and an auto-merge waits only for *required* checks.
430
+ CI_GATE_SCRIPT=""
431
+ _l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
432
+ for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
433
+ case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
434
+ done
435
+ if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" "$PR_NUM" ${GH_FLAG}); CI_GATE_RC=$?
436
+ else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
437
+ echo "$CI_GATE_OUT"
438
+ GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
439
+ # rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
440
+ if [ "$CI_GATE_RC" -eq 0 ]; then
441
+ MERGE_RESULT=$(gh pr merge "$PR_NUM" ${GH_FLAG} --merge --auto --match-head-commit "$GATED_HEAD" 2>&1)
442
+ MERGE_EXIT=$?
443
+ else
444
+ MERGE_RESULT="not merged: CI gate rc=${CI_GATE_RC}"; MERGE_EXIT=1
445
+ CI_MSG="⛔ /recover-orphans did not merge PR #${PR_NUM}: CI is not green.
446
+ \`\`\`
447
+ ${CI_GATE_OUT}
448
+ \`\`\`"
449
+ gh issue comment "$NUM" ${GH_FLAG} --body "$CI_MSG" 2>/dev/null || true # allowlist:check-command-side-effects
450
+ gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
451
+ fi
357
452
  echo " Merge result (exit $MERGE_EXIT): $MERGE_RESULT"
358
453
  if [ $MERGE_EXIT -eq 0 ]; then
359
454
  # Close the issue explicitly (Closes # only auto-closes on default branch)
@@ -373,25 +468,89 @@ for NUM in $ORPHAN_LIST; do
373
468
  # Open PR awaiting review — invoke /review-pr
374
469
  echo " Applying review-pr: invoking /review-pr on PR #$PR_NUM"
375
470
  if [ "$DRY_RUN" = "true" ]; then
376
- echo " [DRY-RUN] Would: Skill(skill='review-pr', args='$PR_NUM --auto-merge --issue $NUM --gh-flag $GH_FLAG')"
471
+ echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}review-pr', args='$PR_NUM --auto-merge --issue $NUM --gh-flag $GH_FLAG')"
377
472
  else
378
- Skill(skill="review-pr", args="${PR_NUM} --auto-merge --issue ${NUM} --gh-flag ${GH_FLAG}")
379
- # After review: update label
380
- gh issue edit "$NUM" ${GH_FLAG} --add-label "workflow:in-review" \
381
- --remove-label "workflow:building,workflow:awaiting-merge" 2>/dev/null || true
473
+ # forge#3148: count trail refusals that predate this sweep, so a refusal from an earlier sweep escalates
474
+ # instead of re-reviewing the same unrepaired trail every run.
475
+ # forge#3159: count only TRUSTED comments (pipeline bot, or OWNER/MEMBER/COLLABORATOR) newer than the latest
476
+ # repair (the most recent needs-human unlabel event), so a stranger's comment or a refusal a human already
477
+ # repaired never forces an escalation. Epoch fallback on a failed lookup counts more, i.e. fails toward escalation.
478
+ # --paginate: the comments endpoint returns 30 per page, oldest first. An unreadable count fails toward escalation.
479
+ PRIOR_TRAIL_CUTOFF=$(gh api --paginate "repos/${GH_REPO}/issues/${NUM}/events" \
480
+ --jq '.[] | select(.event == "unlabeled" and .label.name == "needs-human") | .created_at' 2>/dev/null \
481
+ | sort | tail -1)
482
+ PRIOR_TRAIL_CUTOFF="${PRIOR_TRAIL_CUTOFF:-1970-01-01T00:00:00Z}"
483
+ # gh api --jq takes one expression and no --arg, so the cutoff goes through a real jq pipe.
484
+ if PRIOR_TRAIL_IDS=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${NUM}/comments" 2>/dev/null \
485
+ | jq -r --arg cutoff "$PRIOR_TRAIL_CUTOFF" '.[] | select(.body | contains("FORGE:PHASE_TRAIL_FAILED"))
486
+ | select(.created_at > $cutoff)
487
+ | select(.user.type == "Bot" or (.author_association | IN("OWNER","MEMBER","COLLABORATOR"))) | .id'); then
488
+ PRIOR_TRAIL_FAILS=$(printf '%s\n' "$PRIOR_TRAIL_IDS" | grep -c '[0-9]')
489
+ else
490
+ PRIOR_TRAIL_FAILS=1
491
+ fi
492
+ REVIEW_STATUS=""; REVIEW_BLOCKER="" # reset per orphan so a previous orphan's result never leaks into this one
493
+ Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${PR_NUM} --auto-merge --issue ${NUM} --gh-flag ${GH_FLAG}")
494
+ # REVIEW_STATUS = the `status:` field of the REVIEW_RESULT block the Skill call returned.
495
+ if [ "$REVIEW_STATUS" = "PHASE_TRAIL_FAILED" ]; then
496
+ # Phase 8 refused the merge: the issue's phase trail is incomplete (review-pr posted the
497
+ # MISSING lines as FORGE:PHASE_TRAIL_FAILED). Do NOT set workflow:in-review — that only re-queues
498
+ # the same refusal for the next sweep. Never hand-post the missing markers.
499
+ if [ "${PRIOR_TRAIL_FAILS:-0}" -gt 0 ]; then
500
+ gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
501
+ gh issue comment "$NUM" ${GH_FLAG} --body "<!-- FORGE:ORPHAN_RECOVERED -->
502
+ ## Orphan Recovery — Escalated
503
+
504
+ **Action**: \`/review-pr\` refused to merge PR #${PR_NUM} again because the phase trail is incomplete, and an earlier refusal was not repaired.
505
+ **Recovered by**: /recover-orphans
506
+
507
+ See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run each one via its Skill (or \`/work-on ${NUM}\`), then remove \`needs-human\`." 2>/dev/null || true
508
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) again — needs-human added |\n"
509
+ else
510
+ # First refusal: resume /work-on, whose resume preflight re-runs each missing phase via Skill(...)
511
+ # and then re-enters review (work-on.md Phase 0B resume → work-on/review.md).
512
+ if claim_orphan "$NUM"; then
513
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${NUM}")
514
+ release_orphan "$NUM"
515
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — resumed /work-on to re-run missing phases |\n"
516
+ else
517
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
518
+ fi
519
+ fi
520
+ elif [ "$REVIEW_STATUS" = "BLOCKED" ] && echo "$REVIEW_BLOCKER" | grep -qE 'phase trail|auto-merge requires --issue'; then
521
+ # REVIEW_BLOCKER = the `blocker:` field of the same REVIEW_RESULT block. The gate could not run (verifier
522
+ # rc>=2 / unresolvable) or had no issue to verify (forge#3147). review-pr already added needs-human for the
523
+ # rc>=2 case; add it here too so the diagnosis skip above stops re-sweeping this orphan.
524
+ gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
525
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} blocked by the merge gate — needs-human added |\n"
526
+ elif case "$REVIEW_STATUS" in COMPLETE|ALREADY_MERGED|BLOCKED) false ;; *) true ;; esac; then
527
+ # forge#3159: an empty or unrecognized status means the review result could not be read. Do not report
528
+ # "submitted for review" or re-queue it; escalate to a human.
529
+ gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
530
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} unrecognized review status '${REVIEW_STATUS}' — needs-human added |\n"
531
+ else
532
+ # After review: update label
533
+ gh issue edit "$NUM" ${GH_FLAG} --add-label "workflow:in-review" \
534
+ --remove-label "workflow:building,workflow:awaiting-merge" 2>/dev/null || true
535
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} submitted for review |\n"
536
+ fi
382
537
  fi
383
- RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} submitted for review |\n"
538
+ [ "$DRY_RUN" = "true" ] && RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} would be submitted for review |\n"
384
539
  ;;
385
540
 
386
541
  create-pr)
387
542
  # Branch has commits but no PR — resume /work-on to create PR
388
543
  echo " Applying create-pr: resuming /work-on to advance from build to PR creation"
389
544
  if [ "$DRY_RUN" = "true" ]; then
390
- echo " [DRY-RUN] Would: Skill(skill='work-on', args='$NUM')"
545
+ echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='$NUM')"
546
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Would resume /work-on — branch $BRANCH has commits, no PR |\n"
547
+ elif claim_orphan "$NUM"; then
548
+ Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${NUM}")
549
+ release_orphan "$NUM"
550
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Resumed /work-on — branch $BRANCH has commits, no PR |\n"
391
551
  else
392
- Skill(skill="work-on", args="${NUM}")
552
+ RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
393
553
  fi
394
- RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Resumed /work-on — branch $BRANCH has commits, no PR |\n"
395
554
  ;;
396
555
 
397
556
  reset-labels)
@@ -37,7 +37,7 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
37
37
 
38
38
  1. **Always read first** (shared protocols, all agents require this):
39
39
  ```
40
- Read: $FORGE_HOME/commands/review-pr-agents/protocols.md
40
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/protocols.md
41
41
  ```
42
42
 
43
43
  2. **Then read the persona file for each selected agent**:
@@ -70,11 +70,11 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
70
70
 
71
71
  ## Loading Instructions for Phase 3C
72
72
 
73
- Replace the single `Read: $FORGE_HOME/commands/review-pr-agents.md` call with:
73
+ Replace the single `Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents.md` call with:
74
74
 
75
75
  ```
76
- Read: $FORGE_HOME/commands/review-pr-agents/protocols.md
77
- Read: $FORGE_HOME/commands/review-pr-agents/<persona>.md (one per selected agent)
76
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/protocols.md
77
+ Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/<persona>.md (one per selected agent)
78
78
  ```
79
79
 
80
80
  Each persona file contains:
@@ -87,3 +87,5 @@ The `protocols.md` file contains:
87
87
  - Tool-Result Truncation Discipline
88
88
  - Evidence-Based Review Protocol (all agents follow)
89
89
  - Structured Findings Protocol (machine-readable findings block format)
90
+
91
+ **Spec root (MANDATORY)**: read every file named in this catalog from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
@@ -9,7 +9,9 @@ install: core
9
9
 
10
10
  # Staging Review
11
11
 
12
- **Trigger**: Invoked by the orchestrator via `Skill("review-pr-staging", $ARGUMENTS)`, or directly with `$ARGUMENTS` = "staging", a PR number targeting main, or "staging:feature".
12
+ > **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.
13
+
14
+ **Trigger**: Invoked by the orchestrator via `Skill("{FORGE_SKILL_PREFIX}review-pr-staging", $ARGUMENTS)`, or directly with `$ARGUMENTS` = "staging", a PR number targeting main, or "staging:feature".
13
15
 
14
16
  Performs comprehensive review of `staging` before merging to `main`. Handles large diffs (1,000-10,000+ lines), diverse changes, deep analysis, and business impact assessment.
15
17
 
@@ -502,7 +504,13 @@ Missing persona templates are a fatal setup error, not permission to skip multi-
502
504
 
503
505
  ```bash
504
506
  TEMPLATE_BASE=""
505
- if [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
507
+ # Tier 0: the running plugin's own root (Claude Code substitutes this exact spelling; elsewhere it stays a
508
+ # literal that the /* check rejects). Wins over an exported FORGE_HOME, which may name an older checkout.
509
+ _PLUGIN_ROOT='${CLAUDE_PLUGIN_ROOT}'; case "$_PLUGIN_ROOT" in /*) ;; *) _PLUGIN_ROOT="" ;; esac
510
+ if [[ -n "$_PLUGIN_ROOT" && -f "$_PLUGIN_ROOT/commands/review-pr-agents/protocols.md" ]]; then
511
+ TEMPLATE_BASE="$_PLUGIN_ROOT/commands/review-pr-agents"
512
+ TEMPLATE_SOURCE="plugin_root"
513
+ elif [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
506
514
  TEMPLATE_BASE="$FORGE_HOME/commands/review-pr-agents"
507
515
  TEMPLATE_SOURCE="forge_home"
508
516
  else
@@ -530,7 +538,7 @@ fi
530
538
 
531
539
  **If `TEMPLATE_SOURCE` is `none`**: HARD STOP. Post a PR comment explaining the setup is broken, instructing the user to run `npx forgedock update` to repair the install, add `needs-human`, and exit without posting any findings or a verdict. **NEVER perform the review inline in the main agent context as a substitute.**
532
540
 
533
- **If `TEMPLATE_SOURCE` is `forge_home` or `repo_path`** (normal cases — behavior unchanged): `Read: $TEMPLATE_BASE/protocols.md` and `Read: $TEMPLATE_BASE/<persona>.md` per selected agent.
541
+ **If `TEMPLATE_SOURCE` is `plugin_root`, `forge_home` or `repo_path`** (normal cases — behavior unchanged): `Read: $TEMPLATE_BASE/protocols.md` and `Read: $TEMPLATE_BASE/<persona>.md` per selected agent.
534
542
 
535
543
  **If `TEMPLATE_SOURCE` is `monolithic_catalog`** (last resort): `Read: $MONOLITHIC_CATALOG` and extract the shared protocols section plus each selected persona's section from within that single file.
536
544
 
@@ -593,7 +601,7 @@ TEST_GATE_REASON="Phase 6.5 not yet run"
593
601
 
594
602
  # Invoke /test-gate with the bundle PRs already computed in Phase 0A
595
603
  # ALL_PR_NUMBERS is the de-duplicated union of both scan methods (commit log + merge subjects)
596
- GATE_OUTPUT=$(Skill("test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
604
+ GATE_OUTPUT=$(Skill("{FORGE_SKILL_PREFIX}test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
597
605
 
598
606
  # Extract machine-readable verdict from Skill output
599
607
  TEST_GATE_VERDICT=$(echo "$GATE_OUTPUT" | grep -oP '(?<=FORGE:TEST_GATE:RESULT=)(BLOCK|PASS|SKIP)' | tail -1 || echo "SKIP")
@@ -872,7 +880,7 @@ else
872
880
  # --label is repeatable (not comma-joined) per the /issue programmatic contract.
873
881
  # ${MILESTONE_FLAG} carries the Phase 7D derivation through — empty string is
874
882
  # a no-op arg when the reviewed branch has no milestone (plain staging→main).
875
- ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
883
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
876
884
  # /issue re-reads the created issue and hard-fails unless this exact marker is present.
877
885
  rm -f "$STAGING_FINDING_BODY_FILE"
878
886