forgedock 1.8.1 → 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 +3 -1
  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
@@ -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 -->
@@ -10,11 +10,11 @@ Read this file at the start of every `/orchestrate` invocation.
10
10
 
11
11
  ## HARD RULES — READ BEFORE ANYTHING ELSE
12
12
 
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.
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. 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
14
 
15
15
  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
16
 
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.
17
+ 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
18
 
19
19
  ---
20
20
 
@@ -7,6 +7,8 @@ install: core
7
7
 
8
8
  # /orchestrate — Phase 1: Resolve the Issue Set
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
  ## Phase 1: Resolve the Issue Set
11
13
 
12
14
  ### Batch-Start Timestamp (T0) — capture FIRST, before any resolution
@@ -201,11 +203,32 @@ Phase 1 resolves `$ARGUMENTS` to a concrete issue-number list exactly once, at T
201
203
  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
204
 
203
205
  ```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"
206
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
207
+ FORGE_ROOT=""
208
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
209
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
210
+ _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")")"
211
+ # 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)
212
+ _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-)"
213
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
214
+ # '${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.
215
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
216
+ while IFS= read -r _c; do
217
+ 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
218
+ done <<< "$_k"
219
+ fi
220
+ # Import ForgeDock's OWN resolve.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}
221
+ # (inert on non-ForgeDock repos, and would execute repo-controlled JS). Unresolved/missing => no output,
222
+ # which Phase 4 treats as "literal" (never re-resolve) — fail closed, loudly.
223
+ if [ -n "$FORGE_ROOT" ] && [ -r "$FORGE_ROOT/bin/engine/resolve.mjs" ]; then
224
+ node -e '
225
+ import(require("node:url").pathToFileURL(process.argv[1]).href).then(({ classifyInputPattern }) => {
226
+ console.log(JSON.stringify(classifyInputPattern(process.argv[2])));
227
+ }, () => process.exit(0));
228
+ ' "$FORGE_ROOT/bin/engine/resolve.mjs" "$ARGUMENTS"
229
+ else
230
+ echo "WARNING: ForgeDock install root unresolved or resolve.mjs missing (set FORGEDOCK_HOME) — input pattern left unclassified (treated as literal; no re-resolution)" >&2
231
+ fi
209
232
  ```
210
233
 
211
234
  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 +386,33 @@ After executing the plan, retain every singleton in the registry for the next ad
363
386
  # createdAt, and isBatch. OPEN_BATCHES_JSON contains existing batch issue metadata.
364
387
  # Run this after initial resolution and every later admission event; execute returned
365
388
  # 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")
389
+ # FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
390
+ FORGE_ROOT=""
391
+ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
392
+ # Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
393
+ _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")")"
394
+ # 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)
395
+ _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-)"
396
+ _m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
397
+ # '${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.
398
+ _k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
399
+ while IFS= read -r _c; do
400
+ 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
401
+ done <<< "$_k"
402
+ fi
403
+ # Import ForgeDock's OWN admission.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}.
404
+ # Unresolved/missing => no plan: every candidate stays an ungrouped singleton (fail closed, loudly).
405
+ BATCH_PLAN=""
406
+ if [ -n "$FORGE_ROOT" ] && [ -r "$FORGE_ROOT/bin/engine/admission.mjs" ]; then
407
+ BATCH_PLAN=$(node -e '
408
+ import(require("node:url").pathToFileURL(process.argv[1]).href).then(({ planP3Batches, summarizeP3BatchPlan }) => {
409
+ const plan = planP3Batches({ candidates: JSON.parse(process.argv[2]), openBatches: JSON.parse(process.argv[3]) });
410
+ console.log(JSON.stringify({ plan, summary: summarizeP3BatchPlan(plan) }));
411
+ });
412
+ ' "$FORGE_ROOT/bin/engine/admission.mjs" "$BATCH_CANDIDATE_REGISTRY_JSON" "$OPEN_BATCHES_JSON")
413
+ else
414
+ echo "WARNING: ForgeDock install root unresolved or admission.mjs missing (set FORGEDOCK_HOME) — no P3 batch plan; candidates retained as ungrouped singletons" >&2
415
+ fi
372
416
  ```
373
417
 
374
418
  **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 +553,7 @@ BATCH_EOF
509
553
  ```
510
554
 
511
555
  ```
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}\""))
556
+ 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
557
  ```
514
558
 
515
559
  **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:
@@ -6,6 +6,8 @@ install: core
6
6
 
7
7
  # /orchestrate — Phase 3: Dependency Analysis & Execution Plan
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 3: Dependency Analysis & Execution Plan
10
12
 
11
13
  ### Step 3A: Analyze explicit dependencies
@@ -88,6 +90,7 @@ declare -A ISSUE_FILES # {NUM} → newline-separated declared file set (forge#2
88
90
  # phase-4-execution.md's classify-lane resolver.
89
91
  resolve_extract_affected_files() {
90
92
  local candidates=()
93
+ case '${CLAUDE_PLUGIN_ROOT}' in /*) candidates+=('${CLAUDE_PLUGIN_ROOT}/scripts/extract-affected-files.sh') ;; esac # running plugin first
91
94
  [ -n "${FORGE_HOME:-}" ] && candidates+=("$FORGE_HOME/scripts/extract-affected-files.sh")
92
95
  [ -n "${REPO_PATH:-}" ] && candidates+=("$REPO_PATH/scripts/extract-affected-files.sh")
93
96
  candidates+=("$PWD/scripts/extract-affected-files.sh")
@@ -617,7 +620,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
617
620
 
618
621
  case "$LEASE_STATE" in
619
622
  free|self)
620
- HOSTNAME_ID=$(hostname 2>/dev/null || echo "unknown-host")
623
+ HOSTNAME_ID=$(hostname 2>/dev/null || uname -n 2>/dev/null || true); HOSTNAME_ID="${HOSTNAME_ID:-unknown-host}"
621
624
  # 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
625
  gh issue comment "$COORD_ISSUE_NUMBER" -R {GH_REPO} --body "<!-- FORGE:LEASE -->
623
626
  **Holder Batch ID**: ${BATCH_ID}
@@ -1140,7 +1143,7 @@ The engine drives every phase transition deterministically, mirrors state to the
1140
1143
 
1141
1144
  **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
1145
 
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.
1146
+ 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
1147
  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
1148
 
1146
1149
  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 +1368,7 @@ if [ -n "${FORGE_COORD_ISSUE:-}" ] && [ -n "${COORD_ISSUE_NUMBER:-}" ] && [ -n "
1365
1368
  free|self)
1366
1369
  # Free (no live holder) or self (this exact batch already holds it) — safe to
1367
1370
  # refresh and continue reconstruction below.
1368
- HOSTNAME_ID=$(hostname 2>/dev/null || echo "unknown-host")
1371
+ HOSTNAME_ID=$(hostname 2>/dev/null || uname -n 2>/dev/null || true); HOSTNAME_ID="${HOSTNAME_ID:-unknown-host}"
1369
1372
  # 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
1373
  gh issue comment "$COORD_ISSUE_NUMBER" -R {GH_REPO} --body "<!-- FORGE:LEASE -->
1371
1374
  **Holder Batch ID**: ${BATCH_ID}
@@ -1386,6 +1389,10 @@ fi
1386
1389
  # 1. Re-fetch all issue labels and classify each into DONE / GATED / FAILED / IN_PROGRESS
1387
1390
  # (same classify_predecessor_state() function defined in phase-4-execution.md Step 4B —
1388
1391
  # re-declare it here if this block runs in a fresh context that hasn't sourced Step 4B yet).
1392
+ # forge#3168: classify_predecessor_state() also needs these Step 4B helpers declared in the SAME shell:
1393
+ # hold_merged_trail, resolve_orch_login, trail_escalation_state, release_merged_trail (reverify_merged_trail is
1394
+ # nested inside the classifier). It reports a missing helper on stderr and classifies GATED. The same trusted-author
1395
+ # filter (OWNER/MEMBER/COLLABORATOR or the orchestrator login) applies to the release/fail markers on wake.
1389
1396
  declare -A ISSUE_CLASS
1390
1397
  declare -A ISSUE_FILES
1391
1398
  DONE_ISSUES=()
@@ -1486,7 +1493,7 @@ for ENTRY in "${NEWLY_BLOCKED[@]:-}"; do
1486
1493
  --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
1487
1494
  gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:BLOCKED_ON_HUMAN_MERGE -->
1488
1495
  **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\`."
1496
+ **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
1497
  gh issue edit "$DEP" -R {GH_REPO} --add-label "blocked-on-human-merge" 2>/dev/null || true
1491
1498
  fi
1492
1499
  done
@@ -1507,9 +1514,12 @@ for DEP in $BLOCKED_NOW; do
1507
1514
  [ "$GPRED_CLASS" != "DONE" ] && STILL_GATED=true
1508
1515
  done
1509
1516
  if [ "$STILL_GATED" = "false" ]; then
1517
+ # forge#3157: record a human release (FORGE:PHASE_TRAIL_RELEASED) of a trail-held merged predecessor (decay of the escalation marker;
1518
+ # release_merged_trail is defined in phase-4-execution.md Step 4B and is a no-op unless the escalation is ACTIVE).
1519
+ for GPRED in $(echo "$GATING_PREDS_RAW" | jq -r '.[]' 2>/dev/null); do release_merged_trail "$GPRED"; done
1510
1520
  gh issue edit "$DEP" -R {GH_REPO} --remove-label "blocked-on-human-merge" 2>/dev/null || true
1511
1521
  gh issue comment "$DEP" -R {GH_REPO} --body "<!-- FORGE:UNBLOCKED -->
1512
- All gating predecessor(s) reached \`workflow:merged\` (detected on orchestrator wake) — dispatching now."
1522
+ 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
1523
  READY_ISSUES+=("$DEP")
1514
1524
  fi
1515
1525
  done