forgedock 1.8.1 → 1.8.3

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 (90) 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/design-bench.md +1 -0
  11. package/commands/design-render-critique-loop.md +1 -0
  12. package/commands/design.md +1 -0
  13. package/commands/forge-stats.md +1 -0
  14. package/commands/geo-audit.md +3 -1
  15. package/commands/incident-response.md +3 -1
  16. package/commands/issue.md +4 -2
  17. package/commands/milestone.md +8 -6
  18. package/commands/orchestrate/config.md +3 -2
  19. package/commands/orchestrate/phase-1-resolve.md +57 -12
  20. package/commands/orchestrate/phase-2-triage.md +1 -0
  21. package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
  22. package/commands/orchestrate/phase-3-dependency.md +17 -5
  23. package/commands/orchestrate/phase-4-execution.md +625 -78
  24. package/commands/orchestrate/phase-5-cleanup.md +5 -2
  25. package/commands/orchestrate/phase-6-report.md +8 -3
  26. package/commands/orchestrate/safety.md +1 -0
  27. package/commands/orchestrate.md +13 -9
  28. package/commands/pipeline-health/config.md +1 -0
  29. package/commands/pipeline-health/phase-1-context.md +1 -0
  30. package/commands/pipeline-health/phase-2-metrics.md +1 -0
  31. package/commands/pipeline-health/phase-3-analyze.md +1 -0
  32. package/commands/pipeline-health/phase-4-proposals.md +7 -4
  33. package/commands/pipeline-health/phase-5-report.md +5 -2
  34. package/commands/pipeline-health/phase-6-summary.md +1 -0
  35. package/commands/pipeline-health.md +9 -7
  36. package/commands/pipeline-resume.md +4 -2
  37. package/commands/qa-sweep.md +3 -1
  38. package/commands/quality-gate.md +4 -3
  39. package/commands/recover-orphans.md +219 -13
  40. package/commands/review-pr-agents/api.md +1 -0
  41. package/commands/review-pr-agents/auth.md +1 -0
  42. package/commands/review-pr-agents/billing.md +1 -0
  43. package/commands/review-pr-agents/concurrency.md +1 -0
  44. package/commands/review-pr-agents/database.md +1 -0
  45. package/commands/review-pr-agents/frontend.md +1 -0
  46. package/commands/review-pr-agents/infra.md +1 -0
  47. package/commands/review-pr-agents/protocols.md +1 -0
  48. package/commands/review-pr-agents/scraper.md +1 -0
  49. package/commands/review-pr-agents/security.md +1 -0
  50. package/commands/review-pr-agents/spec-cli.md +1 -0
  51. package/commands/review-pr-agents.md +10 -4
  52. package/commands/review-pr-staging.md +14 -5
  53. package/commands/review-pr.md +187 -19
  54. package/commands/rollback.md +3 -1
  55. package/commands/security-audit.md +3 -1
  56. package/commands/signal-planner.md +6 -4
  57. package/commands/sync-ecosystem.md +4 -2
  58. package/commands/test-gate.md +4 -2
  59. package/commands/upgrade-deps.md +3 -1
  60. package/commands/validate.md +3 -1
  61. package/commands/work-on/build/architect.md +82 -19
  62. package/commands/work-on/build/context.md +62 -18
  63. package/commands/work-on/build/implement.md +63 -18
  64. package/commands/work-on/build/validate.md +241 -60
  65. package/commands/work-on/build.md +345 -146
  66. package/commands/work-on/close.md +618 -246
  67. package/commands/work-on/decompose.md +240 -41
  68. package/commands/work-on/investigate.md +378 -61
  69. package/commands/work-on/remediate.md +43 -9
  70. package/commands/work-on/review.md +241 -29
  71. package/commands/work-on.md +180 -2014
  72. package/docs/CODEX.md +2 -0
  73. package/docs/CONFIG.md +11 -0
  74. package/package.json +1 -1
  75. package/scripts/build-spec-graph.mjs +11 -4
  76. package/scripts/check-spec-markers.sh +21 -1
  77. package/scripts/checkbox-sections.test.sh +8 -23
  78. package/scripts/extract-affected-files.sh +5 -2
  79. package/scripts/extract-affected-files.test.sh +27 -0
  80. package/scripts/forge-root.test.sh +176 -0
  81. package/scripts/is-docs-only.sh +31 -0
  82. package/scripts/is-docs-only.test.sh +64 -0
  83. package/scripts/lint-dispatch-prompt.sh +217 -0
  84. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  85. package/scripts/validate-spec-graph.sh +3 -2
  86. package/scripts/verify-phase-trail.sh +282 -0
  87. package/scripts/verify-phase-trail.test.sh +298 -0
  88. package/scripts/wait-ci-green.sh +98 -0
  89. package/scripts/wait-ci-green.test.sh +99 -0
  90. package/commands/work-on-monolithic.md +0 -326
package/commands/issue.md CHANGED
@@ -7,13 +7,15 @@ argument-hint: "[description of the problem or feature] [--dry-run] | --title \"
7
7
 
8
8
  # /issue — Deterministic Issue Creator
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
  You create GitHub issues with the exact structure the `/work-on` pipeline expects. Every issue you create must give the investigation agent enough context to find the right files on the first pass — no vague descriptions, no missing domains, no ambiguous scope.
13
15
 
14
16
  `/issue` is the structured create-hook for issue creation across the pipeline — it enforces the canonical template, reads code before drafting, runs dedup, and validates mandatory sections. Because those checks are deterministic, `/issue` **creates the issue by default once they pass** — it does not wait for a human to approve the draft. Pass `--dry-run` to review the draft without creating anything (see Argument Parsing below).
15
17
 
16
- `/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="issue", args="--title \"fix: ...\" --body-file \"$BODY_FILE\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — every `--body-file` caller must use an entity- and agent-scoped scratch path and include a unique integrity marker which `/issue` verifies after creation. This is required because a collision can substitute plausible but unrelated content, not just produce an obvious file error (forge#2843).
18
+ `/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" --body-file \"$BODY_FILE\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — every `--body-file` caller must use an entity- and agent-scoped scratch path and include a unique integrity marker which `/issue` verifies after creation. This is required because a collision can substitute plausible but unrelated content, not just produce an obvious file error (forge#2843).
17
19
 
18
20
  **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.
19
21
  **NEVER use plan mode (EnterPlanMode).**
@@ -43,7 +45,7 @@ PROGRAMMATIC_MILESTONE=""
43
45
  PROGRAMMATIC_EXCLUDE=""
44
46
 
45
47
  # Positional/flag parsing. $ARGUMENTS is a single opaque string (the invoking
46
- # agent's raw args, e.g. from `Skill(skill="issue", args="--title \"fix: ...\" ...")`)
48
+ # agent's raw args, e.g. from `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" ...")`)
47
49
  # — it is NOT pre-tokenized by any harness, and a naive `ARGS=($ARGUMENTS)` would
48
50
  # word-split on whitespace without honoring embedded quote characters (a literal
49
51
  # `"` inside the string is not shell syntax at this point), fragmenting any
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /milestone — Milestone Lifecycle Manager
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
  Milestones are the top-level planning unit. They group related issues into a shippable feature set. Each milestone gets its own long-lived Git branch (`milestone/{slug}`) where feature PRs accumulate. When the milestone is ready, it ships to `main` as a single reviewed merge.
@@ -15,7 +17,7 @@ Milestones are the top-level planning unit. They group related issues into a shi
15
17
  **Hierarchy**: Milestone → Issues → Sub-issues (optional decomposition)
16
18
 
17
19
  **NEVER use plan mode (EnterPlanMode).**
18
- **NEVER use the Agent tool for issue work** — milestone dispatches issue work via `Skill(skill="work-on", ...)` only. The Agent tool bypasses the Skill pipeline's label state machine, FORGE annotations, and structured review. The Agent tool may be used only for coordination tasks within milestone planning itself (e.g., parallel issue list queries) — never for /work-on sub-dispatch.
20
+ **NEVER use the Agent tool for issue work** — milestone dispatches issue work via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` only. The Agent tool bypasses the Skill pipeline's label state machine, FORGE annotations, and structured review. The Agent tool may be used only for coordination tasks within milestone planning itself (e.g., parallel issue list queries) — never for /work-on sub-dispatch.
19
21
 
20
22
  <!-- FORGE:SPEC_LOADED — milestone.md loaded and active. Agent is bound by this spec. -->
21
23
 
@@ -211,7 +213,7 @@ BODY_EOF
211
213
  ```
212
214
 
213
215
  ```
214
- ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"{fix|feat|refactor}: {issue_title}\" --body-file \"${MILESTONE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\" --milestone \"{TITLE}\""))
216
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{fix|feat|refactor}: {issue_title}\" --body-file \"${MILESTONE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\" --milestone \"{TITLE}\""))
215
217
  ```
216
218
 
217
219
  ```bash
@@ -255,7 +257,7 @@ BODY_EOF
255
257
  ```
256
258
 
257
259
  ```
258
- ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"{SATELLITE_PREFIX}: {fix|feat|refactor}: {issue_title}\" --body-file \"${SATELLITE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\""))
260
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{SATELLITE_PREFIX}: {fix|feat|refactor}: {issue_title}\" --body-file \"${SATELLITE_ISSUE_BODY_FILE}\" --label \"{type}\" --label \"{priority}\""))
259
261
  ```
260
262
 
261
263
  **Extract each created issue number from the Skill output** (see `commands/issue.md` Phase 4C/4E — it echoes `Created: {url}` and reports `**#{NUMBER}**: {title}`) — repeat for both the default-repo and satellite-repo calls above:
@@ -514,7 +516,7 @@ ISSUE_EOF
514
516
  ```
515
517
 
516
518
  ```
517
- ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"fix(milestone): rebase conflict during hunk-loss audit for milestone/{slug}\" --body-file \"${REBASE_ISSUE_BODY_FILE}\" --label \"bug\" --label \"priority:P0\" --label \"needs-human\""))
519
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix(milestone): rebase conflict during hunk-loss audit for milestone/{slug}\" --body-file \"${REBASE_ISSUE_BODY_FILE}\" --label \"bug\" --label \"priority:P0\" --label \"needs-human\""))
518
520
  ```
519
521
 
520
522
  ```bash
@@ -617,7 +619,7 @@ fi
617
619
  Use the `/review-pr` skill to review the shipping PR. This is a comprehensive review of the entire feature set:
618
620
 
619
621
  ```
620
- Skill(skill="review-pr", args="{PR_NUMBER}")
622
+ Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER}")
621
623
  ```
622
624
 
623
625
  **If the review returns blocking findings (FINDINGS, not PASSED)**:
@@ -625,7 +627,7 @@ Skill(skill="review-pr", args="{PR_NUMBER}")
625
627
  - Do NOT restart from Step 1.
626
628
  - Fix the blockers on `milestone/{slug}`, push the commits, then re-invoke review on the same PR:
627
629
  ```
628
- Skill(skill="review-pr", args="{PR_NUMBER}")
630
+ Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="{PR_NUMBER}")
629
631
  ```
630
632
  - Step 3 will detect the open PR on the next `/milestone ship {slug}` call and update it in place — the same PR number is preserved, review history is retained, and the reviewer is re-notified.
631
633
  - This eliminates the close-and-recreate churn that inflates the failed-PR count and lowers the staging→main pass rate. <!-- Added: forge#1328 -->
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
  install: core
3
4
  ---
4
5
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
@@ -10,11 +11,11 @@ Read this file at the start of every `/orchestrate` invocation.
10
11
 
11
12
  ## HARD RULES — READ BEFORE ANYTHING ELSE
12
13
 
13
- 1. **Every agent MUST invoke `/work-on` via the Skill tool.** You do NOT write implementation prompts. You copy the Phase 4A template verbatim and fill in the `{VARIABLES}`. Nothing else. No custom prompts. No "just read and edit" shortcuts. The Skill tool invocation is what triggers labels, investigation comments, structured review, and trajectory tracking. Without it, the agent's work has no paper trail and is worthless.
14
+ 1. **Every agent MUST invoke `/work-on` via the Skill tool.** You do NOT write implementation prompts. You copy the Phase 4A template verbatim and fill in the `{VARIABLES}`. Nothing else. No custom prompts. No "just read and edit" shortcuts. No pre-written diagnoses, verdicts ("likely already resolved"), or fix designs, and no free-text resume prompts — relaunches use the same unmodified template. This is guarded by a best-effort, self-run lint, not a tamper-proof gate: every rendered prompt MUST pass `scripts/lint-dispatch-prompt.sh` before dispatch (Phase 4, Step 4A), and a failing prompt is refused. No hook gates `Agent(...)`, so the orchestrator itself polices this; template slots are exact-matched, but the free-text slots (issue title, `DISPATCH_CONTEXT` block) are only phrase-scanned and can be bypassed by rephrasing, so the verbatim-template rule above still binds even when the lint passes. The Skill tool invocation is what triggers labels, investigation comments, structured review, and trajectory tracking. Without it, the agent's work has no paper trail and is worthless.
14
15
 
15
16
  2. **You are a dispatcher, not a builder.** You resolve issues, build the dependency DAG, spawn agents, and report results. You NEVER read code, edit files, or implement fixes yourself.
16
17
 
17
- 3. **After each agent completes, verify it used `/work-on`.** Check that completed issues have `workflow:*` labels and structured comments. If an agent bypassed the pipeline, report it as a failure.
18
+ 3. **After each agent completes, verify it used `/work-on`.** Check that completed issues have `workflow:*` labels and structured comments. Labels alone are not verification: run `scripts/verify-phase-trail.sh` on every completion (Phase 4, Step 4B). A failing trail (`PHASE_TRAIL: FAIL`, exit 1) is a bypass. Never classify from a worker's own narrative (what it says it skipped or how it dispatched) — only from the trail verdict and labels. An unreadable or unresolvable trail (`PHASE_TRAIL: ERROR`, exit ≥2 / 127) is an infrastructure BLOCKED: add `needs-human` and do not re-dispatch. For a bypass, classify it FAILED (never DONE), report it to the operator, and re-dispatch with the unmodified template so the missing phases run via `Skill(...)`.
18
19
 
19
20
  ---
20
21
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ user-invocable: false
2
3
 
3
4
  install: core
4
5
  ---
@@ -7,6 +8,8 @@ install: core
7
8
 
8
9
  # /orchestrate — Phase 1: Resolve the Issue Set
9
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
+
10
13
  ## Phase 1: Resolve the Issue Set
11
14
 
12
15
  ### Batch-Start Timestamp (T0) — capture FIRST, before any resolution
@@ -201,11 +204,32 @@ Phase 1 resolves `$ARGUMENTS` to a concrete issue-number list exactly once, at T
201
204
  Classify `$ARGUMENTS` and persist that classification alongside the resolved issue-number list, so Phase 4 (`phase-4-execution.md` Step 4B) can decide whether to re-run the query later in the batch:
202
205
 
203
206
  ```bash
204
- node -e '
205
- import("{REPO_PATH}/bin/engine/resolve.mjs").then(({ classifyInputPattern }) => {
206
- console.log(JSON.stringify(classifyInputPattern(process.argv[1])));
207
- }, () => process.exit(0));
208
- ' "$ARGUMENTS"
207
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
208
+ FORGE_ROOT=""
209
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
210
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
211
+ _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")")"
212
+ # 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)
213
+ _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-)"
214
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
215
+ # '${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.
216
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
217
+ while IFS= read -r _c; do
218
+ 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
219
+ done <<< "$_k"
220
+ fi
221
+ # Import ForgeDock's OWN resolve.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}
222
+ # (inert on non-ForgeDock repos, and would execute repo-controlled JS). Unresolved/missing => no output,
223
+ # which Phase 4 treats as "literal" (never re-resolve) — fail closed, loudly.
224
+ if [ -n "$FORGE_ROOT" ] && [ -r "$FORGE_ROOT/bin/engine/resolve.mjs" ]; then
225
+ node -e '
226
+ import(require("node:url").pathToFileURL(process.argv[1]).href).then(({ classifyInputPattern }) => {
227
+ console.log(JSON.stringify(classifyInputPattern(process.argv[2])));
228
+ }, () => process.exit(0));
229
+ ' "$FORGE_ROOT/bin/engine/resolve.mjs" "$ARGUMENTS"
230
+ else
231
+ echo "WARNING: ForgeDock install root unresolved or resolve.mjs missing (set FORGEDOCK_HOME) — input pattern left unclassified (treated as literal; no re-resolution)" >&2
232
+ fi
209
233
  ```
210
234
 
211
235
  This mirrors `bin/engine/resolve.mjs`'s `classifyInputPattern` — a typed, unit-tested reference implementation of the same literal-vs-query rule table shown here (see that file's docstring). As with `admission.mjs` for cascade policy, the two must stay in sync by hand: any change to which patterns count as `literal` vs `query` in this table must be mirrored in `resolve.mjs`, and vice versa.
@@ -363,12 +387,33 @@ After executing the plan, retain every singleton in the registry for the next ad
363
387
  # createdAt, and isBatch. OPEN_BATCHES_JSON contains existing batch issue metadata.
364
388
  # Run this after initial resolution and every later admission event; execute returned
365
389
  # actions using memberIds, never bare issue numbers, so satellite issues cannot collide.
366
- BATCH_PLAN=$(node -e '
367
- import("{REPO_PATH}/bin/engine/admission.mjs").then(({ planP3Batches, summarizeP3BatchPlan }) => {
368
- const plan = planP3Batches({ candidates: JSON.parse(process.argv[1]), openBatches: JSON.parse(process.argv[2]) });
369
- console.log(JSON.stringify({ plan, summary: summarizeP3BatchPlan(plan) }));
370
- });
371
- ' "$BATCH_CANDIDATE_REGISTRY_JSON" "$OPEN_BATCHES_JSON")
390
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
391
+ FORGE_ROOT=""
392
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
393
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
394
+ _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")")"
395
+ # 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)
396
+ _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-)"
397
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
398
+ # '${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.
399
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
400
+ while IFS= read -r _c; do
401
+ 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
402
+ done <<< "$_k"
403
+ fi
404
+ # Import ForgeDock's OWN admission.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}.
405
+ # Unresolved/missing => no plan: every candidate stays an ungrouped singleton (fail closed, loudly).
406
+ BATCH_PLAN=""
407
+ if [ -n "$FORGE_ROOT" ] && [ -r "$FORGE_ROOT/bin/engine/admission.mjs" ]; then
408
+ BATCH_PLAN=$(node -e '
409
+ import(require("node:url").pathToFileURL(process.argv[1]).href).then(({ planP3Batches, summarizeP3BatchPlan }) => {
410
+ const plan = planP3Batches({ candidates: JSON.parse(process.argv[2]), openBatches: JSON.parse(process.argv[3]) });
411
+ console.log(JSON.stringify({ plan, summary: summarizeP3BatchPlan(plan) }));
412
+ });
413
+ ' "$FORGE_ROOT/bin/engine/admission.mjs" "$BATCH_CANDIDATE_REGISTRY_JSON" "$OPEN_BATCHES_JSON")
414
+ else
415
+ echo "WARNING: ForgeDock install root unresolved or admission.mjs missing (set FORGEDOCK_HOME) — no P3 batch plan; candidates retained as ungrouped singletons" >&2
416
+ fi
372
417
  ```
373
418
 
374
419
  **Priority label schema**: ForgeDock's own issue creator (`review-pr.md`) writes only the canonical `priority:P<n>` label form. Some repos this pipeline operates against (issues opened externally, imported, or predating ForgeDock adoption) instead carry a bare `P<n>` label with no `priority:` prefix. Every priority-read check in this section — and its mirrors in `phase-4-execution.md` and `cleanup.md` — MUST accept both forms. `priority:P<n>` wins when (unusually) both are present on the same issue. Issue-*creation* call sites (the batch-issue creation below) are unaffected and keep writing canonical `priority:P<n>` only — there is no case where this pipeline needs to *write* the bare form. <!-- Added: forge#2232 -->
@@ -509,7 +554,7 @@ BATCH_EOF
509
554
  ```
510
555
 
511
556
  ```
512
- ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (batch #{BATCH_N})\" --body-file \"${BATCH_BODY_FILE}\" --label \"review-finding\" --label \"priority:P3\" --label \"batch\" --exclude \"${MEMBER_LIST}\""))
557
+ ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix(batch): P3 review findings — ${SAFE_SURFACE_AREA} (batch #{BATCH_N})\" --body-file \"${BATCH_BODY_FILE}\" --label \"review-finding\" --label \"priority:P3\" --label \"batch\" --exclude \"${MEMBER_LIST}\""))
513
558
  ```
514
559
 
515
560
  **Consume `/issue`'s explicit result contract** (see `commands/issue.md` Phases 2D and 4C). A dedup STOP is an expected, named outcome; any output without either result marker is a hard create failure:
@@ -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 -->
@@ -6,6 +7,8 @@ install: core
6
7
 
7
8
  # /orchestrate — Phase 3: Dependency Analysis & Execution Plan
8
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
+
9
12
  ## Phase 3: Dependency Analysis & Execution Plan
10
13
 
11
14
  ### Step 3A: Analyze explicit dependencies
@@ -88,6 +91,7 @@ declare -A ISSUE_FILES # {NUM} → newline-separated declared file set (forge#2
88
91
  # phase-4-execution.md's classify-lane resolver.
89
92
  resolve_extract_affected_files() {
90
93
  local candidates=()
94
+ case '${CLAUDE_PLUGIN_ROOT}' in /*) candidates+=('${CLAUDE_PLUGIN_ROOT}/scripts/extract-affected-files.sh') ;; esac # running plugin first
91
95
  [ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/extract-affected-files.sh")
92
96
  [ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/extract-affected-files.sh")
93
97
  candidates+=("$PWD/scripts/extract-affected-files.sh")
@@ -617,7 +621,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
617
621
 
618
622
  case "$LEASE_STATE" in
619
623
  free|self)
620
- HOSTNAME_ID=$(hostname 2>/dev/null || echo "unknown-host")
624
+ HOSTNAME_ID=$(hostname 2>/dev/null || uname -n 2>/dev/null || true); HOSTNAME_ID="${HOSTNAME_ID:-unknown-host}"
621
625
  # GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
622
626
  gh issue comment "$COORD_ISSUE_NUMBER" -R {GH_REPO} --body "<!-- FORGE:LEASE -->
623
627
  **Holder Batch ID**: ${BATCH_ID}
@@ -1140,7 +1144,7 @@ The engine drives every phase transition deterministically, mirrors state to the
1140
1144
 
1141
1145
  **Fallback — best-effort, not all-or-nothing (fixed forge#2743)**: Engine-first is guaranteed to have an Agent-spawn fallback under two distinct trigger conditions, not just CLI absence:
1142
1146
 
1143
- 1. **CLI absent at dispatch time**: If `forgedock` is not in PATH, `phase-4-execution.md` Step 4A falls back to spawning Agent sub-agents that run `Skill("work-on", ...)` per issue for the whole batch, before any dispatch happens.
1147
+ 1. **CLI absent at dispatch time**: If `forgedock` is not in PATH, `phase-4-execution.md` Step 4A falls back to spawning Agent sub-agents that run `Skill("{FORGE_SKILL_PREFIX}work-on", ...)` per issue for the whole batch, before any dispatch happens.
1144
1148
  2. **Backend unavailable despite CLI presence, or a runtime engine-error with empty committed state**: `command -v forgedock` only proves the orchestrator CLI is installed — it says nothing about whether the engine's execution backend (the `claude` CLI spawn) can actually run a phase (forge#2741 is a concrete case: `spawnSync claude` ENOENTs even though a shell `command -v` probe reports the binary present). Step 4A now runs a cheap backend preflight canary before committing the whole ready set to engine-first, downgrading the entire run to Agent-spawn on failure. And per-issue, Step 4B's completion handler auto-falls-back any individual engine-dispatched issue that completes at `workflow:engine-error` with an empty committed state (`committed=[] branch=null pr=null` — see `bin/engine-cli.mjs`'s `formatTerminalDiagnostics()`) to the same Agent-spawn template, rather than leaving it stuck waiting on a resume mechanism that only exists for Agent-spawn-dispatched issues. An engine-error with partial committed state (branch/PR non-null) is NOT auto-fallen-back, to avoid double-work — it surfaces via the existing stall-detection alert instead.
1145
1149
 
1146
1150
  In both cases the SubagentStop hook still bridges the fallback Agent-spawn runs to the engine run-log for state persistence. Engine-first is therefore best-effort: it is always backed by a working Agent-spawn path, whether the gap is discovered before dispatch (canary) or after (per-issue fallback) — a whole ready set is never permanently lost to an environmental engine failure.
@@ -1365,7 +1369,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && [ -n "
1365
1369
  free|self)
1366
1370
  # Free (no live holder) or self (this exact batch already holds it) — safe to
1367
1371
  # refresh and continue reconstruction below.
1368
- HOSTNAME_ID=$(hostname 2>/dev/null || echo "unknown-host")
1372
+ HOSTNAME_ID=$(hostname 2>/dev/null || uname -n 2>/dev/null || true); HOSTNAME_ID="${HOSTNAME_ID:-unknown-host}"
1369
1373
  # GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
1370
1374
  gh issue comment "$COORD_ISSUE_NUMBER" -R {GH_REPO} --body "<!-- FORGE:LEASE -->
1371
1375
  **Holder Batch ID**: ${BATCH_ID}
@@ -1386,6 +1390,11 @@ fi
1386
1390
  # 1. Re-fetch all issue labels and classify each into DONE / GATED / FAILED / IN_PROGRESS
1387
1391
  # (same classify_predecessor_state() function defined in phase-4-execution.md Step 4B —
1388
1392
  # re-declare it here if this block runs in a fresh context that hasn't sourced Step 4B yet).
1393
+ # forge#3168: classify_predecessor_state() also needs these Step 4B helpers declared in the SAME shell:
1394
+ # hold_merged_trail, resolve_orch_login, trail_escalation_state, release_merged_trail (reverify_merged_trail is
1395
+ # nested inside the classifier). It reports a missing helper on stderr and classifies GATED. The same trusted-author
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).
1389
1398
  declare -A ISSUE_CLASS
1390
1399
  declare -A ISSUE_FILES
1391
1400
  DONE_ISSUES=()
@@ -1486,7 +1495,7 @@ for ENTRY in "${NEWLY_BLOCKED[@]:-}"; do
1486
1495
  --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
1487
1496
  gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
1488
1497
  **Gating predecessor**: #${PRED} (state: \`${ISSUE_CLASS[$PRED]}\`${GATING_PR:+, open PR #${GATING_PR}})
1489
- **Status**: Detected on orchestrator wake/compaction reconstruction. Ready to dispatch as soon as #${PRED} reaches \`workflow:merged\`."
1498
+ **Status**: Detected on orchestrator wake/compaction reconstruction. Ready to dispatch once #${PRED} is resolved: when its gating PR merges, or — if #${PRED} is already merged and held on its phase trail — when a human clears \`needs-human\` on it."
1490
1499
  gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
1491
1500
  fi
1492
1501
  done
@@ -1507,9 +1516,12 @@ for DEP in $BLOCKED_NOW; do
1507
1516
  [ "$GPRED_CLASS" != "DONE" ] && STILL_GATED=true
1508
1517
  done
1509
1518
  if [ "$STILL_GATED" = "false" ]; then
1519
+ # forge#3157: record a human release (FORGE:PHASE_TRAIL_RELEASED) of a trail-held merged predecessor (decay of the escalation marker;
1520
+ # release_merged_trail is defined in phase-4-execution.md Step 4B and is a no-op unless the escalation is ACTIVE).
1521
+ for GPRED in $(echo "$GATING_PREDS_RAW" | jq -r '.[]' 2>/dev/null); do release_merged_trail "$GPRED"; done
1510
1522
  gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
1511
1523
  gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
1512
- All gating predecessor(s) reached \`workflow:merged\` (detected on orchestrator wake) — dispatching now."
1524
+ All gating predecessor(s) are now resolved — merged, or released by a human after a phase-trail hold (detected on orchestrator wake) — dispatching now."
1513
1525
  READY_ISSUES+=("$DEP")
1514
1526
  fi
1515
1527
  done