@entelligentsia/forgecli 1.0.10 → 1.0.14

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 (167) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/CHANGELOG-forge-plugin.md +150 -0
  3. package/dist/bin/forge.js +0 -0
  4. package/dist/extensions/forgecli/config-layer.d.ts +16 -0
  5. package/dist/extensions/forgecli/config-layer.js +5 -0
  6. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  7. package/dist/extensions/forgecli/dashboard/component.d.ts +102 -0
  8. package/dist/extensions/forgecli/dashboard/component.js +882 -0
  9. package/dist/extensions/forgecli/dashboard/component.js.map +1 -0
  10. package/dist/extensions/forgecli/dashboard/register.d.ts +2 -0
  11. package/dist/extensions/forgecli/dashboard/register.js +45 -0
  12. package/dist/extensions/forgecli/dashboard/register.js.map +1 -0
  13. package/dist/extensions/forgecli/dashboard/view-model.d.ts +35 -0
  14. package/dist/extensions/forgecli/dashboard/view-model.js +54 -0
  15. package/dist/extensions/forgecli/dashboard/view-model.js.map +1 -0
  16. package/dist/extensions/forgecli/fix-bug.js +72 -7
  17. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  18. package/dist/extensions/forgecli/forge-cli-schema.json +4 -0
  19. package/dist/extensions/forgecli/forge-commands.js +1 -0
  20. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  21. package/dist/extensions/forgecli/forge-init/phase4-register.js +53 -0
  22. package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
  23. package/dist/extensions/forgecli/forge-subagent.js +6 -4
  24. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  25. package/dist/extensions/forgecli/index.js +5 -0
  26. package/dist/extensions/forgecli/index.js.map +1 -1
  27. package/dist/extensions/forgecli/lib/halt-advisor.d.ts +54 -0
  28. package/dist/extensions/forgecli/lib/halt-advisor.js +90 -0
  29. package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -0
  30. package/dist/extensions/forgecli/migration-engine.js +25 -12
  31. package/dist/extensions/forgecli/migration-engine.js.map +1 -1
  32. package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +25 -0
  33. package/dist/extensions/forgecli/orchestrator-status-bar.js +183 -0
  34. package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -0
  35. package/dist/extensions/forgecli/orchestrator-tree.d.ts +96 -0
  36. package/dist/extensions/forgecli/orchestrator-tree.js +390 -0
  37. package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -0
  38. package/dist/extensions/forgecli/project-orientation.js +12 -8
  39. package/dist/extensions/forgecli/project-orientation.js.map +1 -1
  40. package/dist/extensions/forgecli/regenerate.d.ts +16 -0
  41. package/dist/extensions/forgecli/regenerate.js +110 -0
  42. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  43. package/dist/extensions/forgecli/run-sprint.js +33 -3
  44. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  45. package/dist/extensions/forgecli/run-task.d.ts +32 -0
  46. package/dist/extensions/forgecli/run-task.js +185 -12
  47. package/dist/extensions/forgecli/run-task.js.map +1 -1
  48. package/dist/extensions/forgecli/thread-switcher.js +105 -764
  49. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  50. package/dist/extensions/forgecli/viewport-events.js +32 -0
  51. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  52. package/dist/forge-payload/.base-pack/commands/fix-bug.md +1 -1
  53. package/dist/forge-payload/.base-pack/commands/run-sprint.md +1 -1
  54. package/dist/forge-payload/.base-pack/commands/run-task.md +1 -1
  55. package/dist/forge-payload/.base-pack/personas/architect.md +1 -1
  56. package/dist/forge-payload/.base-pack/personas/bug-fixer.md +1 -1
  57. package/dist/forge-payload/.base-pack/personas/collator.md +3 -3
  58. package/dist/forge-payload/.base-pack/personas/engineer.md +1 -1
  59. package/dist/forge-payload/.base-pack/personas/librarian.md +1 -1
  60. package/dist/forge-payload/.base-pack/personas/orchestrator.md +1 -1
  61. package/dist/forge-payload/.base-pack/personas/product-manager.md +1 -1
  62. package/dist/forge-payload/.base-pack/personas/qa-engineer.md +1 -1
  63. package/dist/forge-payload/.base-pack/personas/supervisor.md +1 -1
  64. package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +1 -1
  65. package/dist/forge-payload/.base-pack/workflows/_fragments/friction-emit.md +1 -1
  66. package/dist/forge-payload/.base-pack/workflows/_fragments/iron-laws.md +1 -1
  67. package/dist/forge-payload/.base-pack/workflows/_fragments/progress-reporting.md +2 -2
  68. package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +11 -2
  69. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +6 -7
  70. package/dist/forge-payload/.base-pack/workflows/architect_review_sprint_completion.md +2 -2
  71. package/dist/forge-payload/.base-pack/workflows/architect_sprint_intake.md +2 -2
  72. package/dist/forge-payload/.base-pack/workflows/architect_sprint_plan.md +5 -5
  73. package/dist/forge-payload/.base-pack/workflows/collator_agent.md +4 -6
  74. package/dist/forge-payload/.base-pack/workflows/commit_task.md +5 -6
  75. package/dist/forge-payload/.base-pack/workflows/enhance.md +5 -5
  76. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +6 -7
  77. package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +12 -13
  78. package/dist/forge-payload/.base-pack/workflows/plan_task.md +5 -6
  79. package/dist/forge-payload/.base-pack/workflows/review_code.md +8 -8
  80. package/dist/forge-payload/.base-pack/workflows/review_plan.md +8 -8
  81. package/dist/forge-payload/.base-pack/workflows/sprint_retrospective.md +3 -3
  82. package/dist/forge-payload/.base-pack/workflows/triage.md +12 -9
  83. package/dist/forge-payload/.base-pack/workflows/update_implementation.md +2 -2
  84. package/dist/forge-payload/.base-pack/workflows/update_plan.md +2 -2
  85. package/dist/forge-payload/.base-pack/workflows/validate_task.md +5 -6
  86. package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +490 -0
  87. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-sprint.js +416 -0
  88. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +608 -0
  89. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  90. package/dist/forge-payload/.schemas/config.schema.json +2 -3
  91. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  92. package/dist/forge-payload/.schemas/event.schema.json +16 -0
  93. package/dist/forge-payload/.schemas/migrations.json +236 -0
  94. package/dist/forge-payload/commands/health.md +29 -0
  95. package/dist/forge-payload/commands/rebuild.md +143 -15
  96. package/dist/forge-payload/commands/update.md +28 -27
  97. package/dist/forge-payload/hooks/preflight-session.cjs +99 -0
  98. package/dist/forge-payload/init/phases/phase-3-materialize.md +18 -5
  99. package/dist/forge-payload/integrity.json +7 -6
  100. package/dist/forge-payload/meta/fragments/tool-discipline.md +1 -1
  101. package/dist/forge-payload/meta/personas/meta-architect.md +1 -1
  102. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +1 -1
  103. package/dist/forge-payload/meta/personas/meta-collator.md +7 -7
  104. package/dist/forge-payload/meta/personas/meta-engineer.md +1 -1
  105. package/dist/forge-payload/meta/personas/meta-orchestrator.md +1 -1
  106. package/dist/forge-payload/meta/personas/meta-supervisor.md +1 -1
  107. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +1 -1
  108. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +1 -1
  109. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +1 -1
  110. package/dist/forge-payload/meta/workflows/_fragments/iron-laws.md +1 -1
  111. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +2 -2
  112. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +11 -2
  113. package/dist/forge-payload/meta/workflows/meta-approve.md +6 -7
  114. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +12 -9
  115. package/dist/forge-payload/meta/workflows/meta-collate.md +5 -7
  116. package/dist/forge-payload/meta/workflows/meta-commit.md +5 -6
  117. package/dist/forge-payload/meta/workflows/meta-enhance.md +5 -5
  118. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +35 -11
  119. package/dist/forge-payload/meta/workflows/meta-implement.md +15 -7
  120. package/dist/forge-payload/meta/workflows/meta-migrate.md +13 -14
  121. package/dist/forge-payload/meta/workflows/meta-new-sprint.md +3 -3
  122. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +138 -39
  123. package/dist/forge-payload/meta/workflows/meta-plan-sprint.md +6 -6
  124. package/dist/forge-payload/meta/workflows/meta-plan-task.md +12 -6
  125. package/dist/forge-payload/meta/workflows/meta-retro.md +4 -4
  126. package/dist/forge-payload/meta/workflows/meta-retrospective.md +4 -4
  127. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +8 -8
  128. package/dist/forge-payload/meta/workflows/meta-review-plan.md +8 -8
  129. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +3 -3
  130. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +3 -3
  131. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +6 -6
  132. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +2 -2
  133. package/dist/forge-payload/meta/workflows/meta-update-plan.md +2 -2
  134. package/dist/forge-payload/meta/workflows/meta-validate.md +5 -6
  135. package/dist/forge-payload/schemas/config.schema.json +2 -3
  136. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  137. package/dist/forge-payload/schemas/event.schema.json +16 -0
  138. package/dist/forge-payload/schemas/structure-manifest.json +75 -73
  139. package/dist/forge-payload/skills/refresh-kb-links/SKILL.md +14 -7
  140. package/dist/forge-payload/tools/banners.cjs +29 -10
  141. package/dist/forge-payload/tools/check-structure.cjs +88 -7
  142. package/dist/forge-payload/tools/collate.cjs +16 -2
  143. package/dist/forge-payload/tools/manage-config.cjs +5 -7
  144. package/dist/forge-payload/tools/parse-gates.cjs +73 -1
  145. package/dist/forge-payload/tools/postflight-gate.cjs +252 -0
  146. package/dist/forge-payload/tools/preflight-gate.cjs +47 -0
  147. package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -4
  148. package/dist/forge-payload/tools/verify-phase.cjs +17 -0
  149. package/package.json +1 -1
  150. package/dist/bin/forgecli.d.ts +0 -2
  151. package/dist/bin/forgecli.js +0 -6
  152. package/dist/bin/forgecli.js.map +0 -1
  153. package/dist/extensions/forgecli/config-tui/index.d.ts +0 -5
  154. package/dist/extensions/forgecli/config-tui/index.js +0 -5
  155. package/dist/extensions/forgecli/config-tui/index.js.map +0 -1
  156. package/dist/extensions/forgecli/loaders/persona-skill-loader.d.ts +0 -45
  157. package/dist/extensions/forgecli/loaders/persona-skill-loader.js +0 -227
  158. package/dist/extensions/forgecli/loaders/persona-skill-loader.js.map +0 -1
  159. package/dist/extensions/forgecli/loaders/template-render.d.ts +0 -20
  160. package/dist/extensions/forgecli/loaders/template-render.js +0 -85
  161. package/dist/extensions/forgecli/loaders/template-render.js.map +0 -1
  162. package/dist/extensions/forgecli/loaders/workflow-loader.d.ts +0 -41
  163. package/dist/extensions/forgecli/loaders/workflow-loader.js +0 -164
  164. package/dist/extensions/forgecli/loaders/workflow-loader.js.map +0 -1
  165. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +0 -446
  166. package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +0 -928
  167. package/dist/forge-payload/.base-pack/workflows/run_sprint.md +0 -225
@@ -0,0 +1,608 @@
1
+ export const meta = {
2
+ name: 'wfl:run-task',
3
+ description: 'Code-orchestrated port of /forge:run-task — resolve the task pipeline, drive each phase (plan→review→implement→review→validate→approve→commit) through a subagent on its ROLE_TIER model (review/validate/approve→opus, plan/implement→sonnet, commit→haiku), hold the revision loop + verdict routing + escalation in JS.',
4
+ whenToUse: 'Run a single Forge task through its full plan→implement→review→approve→commit pipeline via a deterministic JS driver instead of the LLM orchestrator. Pass the task id as args, e.g. args: "FORGE-S27-T01".',
5
+ phases: [
6
+ { title: 'Resolve', detail: 'one agent reads the task manifest + config, returns the resolved pipeline phases and pre-task status' },
7
+ { title: 'Pipeline', detail: 'per phase: one subagent runs the gate + phase workflow + emits its own event; JS owns the phase index, revision counters, verdict routing, and escalation decision' },
8
+ { title: 'Report', detail: 'summarise the terminal outcome — committed / escalated / blocked' },
9
+ ],
10
+ }
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // wfl:run-task — a code-orchestrated port of .forge/workflows/orchestrate_task.md
14
+ //
15
+ // Why a script: orchestrate_task.md is a deterministic phase FSM — a linear
16
+ // pipeline with review→revision back-edges, per-phase iteration caps, declarative
17
+ // pre-flight gates, and escalate-don't-continue on any failure. In the LLM
18
+ // orchestrator that loop is hand-run turn-by-turn. Here the JS holds the phase
19
+ // index, the revision counters, the verdict routing, and the escalation decision;
20
+ // subagents only run a single phase's workflow and write artifacts/events to disk.
21
+ //
22
+ // HOW THIS DIFFERS FROM wfl:run-sprint:
23
+ // wfl:run-sprint ported the OUTER wave-sort FSM but delegated each whole task
24
+ // to ONE orchestrate_task agent — it never decomposed the per-phase loop.
25
+ // wfl:run-task decomposes that loop: one subagent PER PHASE. That is the only
26
+ // version with a reason to exist (a single orchestrate_task agent == the
27
+ // existing /forge:run-task, and adds nothing).
28
+ //
29
+ // SIDE-EFFECT OWNERSHIP — READ BEFORE EDITING:
30
+ // The vanished orchestrate_task agent used to do a stack of shell-dependent
31
+ // jobs for free. This script has NO filesystem/shell access, so each per-phase
32
+ // subagent now owns them: preflight-gate, the phase workflow (which writes its
33
+ // own artifacts + {PHASE}-SUMMARY.json + status), read-verdict (review phases),
34
+ // token sidecar, friction drain, AND its own canonical phase event.
35
+ //
36
+ // *** DELIBERATE DEVIATION from orchestrate_task.md's "the orchestrator is the
37
+ // sole actor that calls store-cli emit" rule: here each phase subagent emits
38
+ // its OWN phase event. This is defensible — the subagent is the only actor that
39
+ // holds its own runtime attribution (model, provider, token usage). The JS
40
+ // driver cannot run store-cli. This is a control-flow-authoritative port with
41
+ // delegated telemetry, NOT a byte-for-byte reproduction of the emit contract.
42
+ //
43
+ // Split start/complete emit contract: per orchestrate_task.md §Event Emission,
44
+ // each phase subagent emits a start event (action="start") BEFORE executing its
45
+ // phase workflow, then a complete event (action="complete") AFTER. The JS driver
46
+ // delegates wall-time bracketing to the subagent: subagent notes startTimestamp,
47
+ // runs the workflow, then computes durationMinutes = (endMs - startMs) / 60000
48
+ // and includes it in the complete event. The start event carries a 0-duration
49
+ // placeholder (startTimestamp == endTimestamp); the complete event carries the
50
+ // real bracket. This mirrors the orchestrate_task.md start+complete pattern. ***
51
+ //
52
+ // Honest fallback if per-phase emission ever proves too lossy: collapse to the
53
+ // thin port (one agent reading orchestrate_task.md, == wfl:run-sprint.dispatchTask),
54
+ // which inherits every side-effect for free. Do NOT ship a silently-lossy deep port.
55
+ //
56
+ // #21 STRUCTURAL LIMITATION — Progress-Monitor IPC:
57
+ // The JS driver has no shell access (Workflow tool sandbox) and therefore CANNOT
58
+ // write progress lines to a named pipe / Unix socket for the Progress-Monitor.
59
+ // Wiring real-time progress telemetry to the Forge UI requires the forge-cli TS
60
+ // layer to open the pipe before spawning the Workflow tool and inject the fd via
61
+ // the Pi runtime's stdio bridging API — this is a host-layer concern, not a
62
+ // JS workflow concern. Documenting here so future sprints know where the
63
+ // architectural seam is. No implementation in this file is possible or correct.
64
+ //
65
+ // MODEL CLUSTER RESOLUTION (Gap #12 — FORGE-S28-T05):
66
+ // Replaces the old hard-tier dispatch with three-cluster logic matching the prose:
67
+ // • single cluster (ANTHROPIC_DEFAULT_OPUS_MODEL == ANTHROPIC_DEFAULT_SONNET_MODEL
68
+ // or both absent): pass model=undefined so subagent inherits parent session model.
69
+ // • tiered cluster (vars differ): pass the tier NAME ('opus'|'sonnet'|'haiku').
70
+ // • unknown cluster (no ANTHROPIC_DEFAULT_*_MODEL vars set): pass the canonical
71
+ // model ID from ROLE_TIER_DEFAULTS.
72
+ // • per-phase override (phase.model field from resolve): highest precedence.
73
+ // ROLE_TIER still exists for the resolve agent to return dispatchModel per phase,
74
+ // but the JS loop now calls resolveModel(role, phase) instead of tierFor(role).
75
+ // DELIBERATE DEVIATION: Pi workflow scripts do not expose a reliable `env` or
76
+ // `process.env` global for reading ANTHROPIC_DEFAULT_*_MODEL vars at the JS
77
+ // driver level. The three-cluster logic is structurally wired (env-var guards
78
+ // use `typeof env !== 'undefined'` which will be false in the sandbox, causing
79
+ // the function to always take the unknown-cluster path → ROLE_TIER_DEFAULTS[tier]).
80
+ // This is the safe, predictable fallback: explicit canonical IDs per role tier.
81
+ // True single-cluster / tiered-cluster behavior requires the caller (forge-cli
82
+ // TS layer) to inject dispatchModel into the phase via RESOLVE_SCHEMA phase.model,
83
+ // which is the per-phase override path (highest precedence, always respected).
84
+ //
85
+ // SESSION PREFLIGHT (Gap #6 — FORGE-S28-T05):
86
+ // Instructed in phase-0 subagent prompt only (firstPhase=true). Reads
87
+ // .forge/cache/preflight-status.json; blob.ok===false halts before gate step.
88
+ // Port limitation: subsequent phases skip preflight to avoid redundant re-checks.
89
+ //
90
+ // FRICTION EMISSION (Gap #5 — FORGE-S28-T05):
91
+ // Orchestrator-experienced friction (spawn failure, FSM rejection) cannot be
92
+ // emitted by the JS driver because it cannot shell out to store-cli. Documented
93
+ // as a port limitation. Per-phase subagents are instructed to drain
94
+ // .forge/cache/FRICTION-*.jsonl and emit type:friction events after failures.
95
+ //
96
+ // ON_REVISION ROUTING (Gap #13 — FORGE-S28-T05):
97
+ // RESOLVE_SCHEMA phase items gain optional on_revision field. revisionTarget()
98
+ // prefers phase.on_revision (command-name lookup) over nearest-preceding-non-review.
99
+ //
100
+ // TOKEN SIDECAR MERGE (Gap #14 — FORGE-S28-T05):
101
+ // After each phase subagent returns, an escalate-agent pattern calls
102
+ // store-cli merge-sidecar with the eventId agreed before spawn.
103
+ // The eventId uses the _complete suffix (token usage lands on the COMPLETE event,
104
+ // not the start event). The driver passes eventId into runPhase() so the subagent
105
+ // prompt references the same eventId when writing the sidecar via --sidecar.
106
+ //
107
+ // PERSONA/SKILL INJECTION (Gap #8 — FORGE-S28-T05):
108
+ // ROLE_TO_NOUN maps each role to its persona noun. Subagent prompt instructs
109
+ // reading persona-pack.json and composing role block (reference mode, inline fallback).
110
+ //
111
+ // BUILD-OVERLAY CONTEXT (Gap #9 — FORGE-S28-T05):
112
+ // Raw MASTER_INDEX.md read replaced with build-overlay.cjs --task --format md.
113
+ // Stale direct read is a documented degradation fallback.
114
+ //
115
+ // REVIEW LOOP CONTEXT (Gap #10 — FORGE-S28-T05):
116
+ // REVIEW_ROLES phases receive a "Review Loop Context" block with iteration/maxIter.
117
+ //
118
+ // GATE EXIT-CODE DISTINCTION (Gap #7 — FORGE-S28-T05):
119
+ // Subagent prompt distinguishes exit_code==1 (gate_failed) vs exit_code==2
120
+ // (gate_misconfigured) in the note field.
121
+ //
122
+ // SIMPLIFIED RETRY PROMPT (Gap #11 — FORGE-S28-T05):
123
+ // Empty/whitespace/timeout result triggers subagent_retry event then retries
124
+ // with a simplified prompt (no arch block, no summary block, adds YOU MUST produce a result).
125
+ //
126
+ // Invocation (Workflow tool): { name: 'wfl:run-task', args: 'FORGE-S27-T01' }
127
+ // args may also be an object: { taskId: 'FORGE-S27-T01' }
128
+ // ---------------------------------------------------------------------------
129
+
130
+ // Task statuses that mean "do not run any phase" — orchestrate_task pre-task guard.
131
+ const SKIP_STATUS = ['blocked', 'escalated', 'committed', 'abandoned']
132
+ // Phase roles whose artifact carries a **Verdict:** that routes the FSM.
133
+ // NOTE: `approve` is NOT here — orchestrate_task advances it on completion like a
134
+ // non-review phase (the approve workflow self-escalates if it rejects).
135
+ const REVIEW_ROLES = ['review-plan', 'review-code', 'validate']
136
+ // Per-phase model tier — verbatim port of orchestrate_task.md § Role-to-Tier Mapping.
137
+ // The resolve agent uses this as a reference; JS loop calls resolveModel() not tierFor().
138
+ const ROLE_TIER = {
139
+ 'plan': 'sonnet',
140
+ 'implement': 'sonnet',
141
+ 'review-plan': 'opus',
142
+ 'review-code': 'opus',
143
+ 'validate': 'opus',
144
+ 'approve': 'opus',
145
+ 'commit': 'haiku',
146
+ 'writeback': 'haiku',
147
+ }
148
+ const tierFor = (role) => ROLE_TIER[role] || 'sonnet' // orchestrate_task's ROLE_TIER.get(role, "sonnet")
149
+
150
+ // Canonical model IDs for unknown-cluster fallback (Gap #12).
151
+ const ROLE_TIER_DEFAULTS = {
152
+ opus: 'claude-opus-4-5',
153
+ sonnet: 'claude-sonnet-4-6',
154
+ haiku: 'claude-haiku-4-5',
155
+ }
156
+
157
+ // Resolve the dispatch model per the three-cluster + per-phase-override logic (Gap #12).
158
+ // • phase.model (from resolve) — highest precedence (per-phase override).
159
+ // • ANTHROPIC_DEFAULT_OPUS_MODEL === ANTHROPIC_DEFAULT_SONNET_MODEL or both absent → undefined (inherit).
160
+ // • vars differ → tier name (tiered cluster).
161
+ // • no vars set → canonical ID from ROLE_TIER_DEFAULTS (unknown cluster).
162
+ function resolveModel(role, phase) {
163
+ if (phase && phase.model) return phase.model // per-phase override wins
164
+ const tier = tierFor(role)
165
+ const opusVar = (typeof env !== 'undefined' && env.ANTHROPIC_DEFAULT_OPUS_MODEL) || undefined
166
+ const sonnetVar = (typeof env !== 'undefined' && env.ANTHROPIC_DEFAULT_SONNET_MODEL) || undefined
167
+ const haikiVar = (typeof env !== 'undefined' && env.ANTHROPIC_DEFAULT_HAIKU_MODEL) || undefined
168
+ const anySet = opusVar || sonnetVar || haikiVar
169
+ if (!anySet) return ROLE_TIER_DEFAULTS[tier] // unknown cluster: canonical ID
170
+ // If all three are equal (or only one is set and it matches), treat as single cluster.
171
+ const uniqueVals = new Set([opusVar, sonnetVar, haikiVar].filter(Boolean))
172
+ if (uniqueVals.size <= 1) return undefined // single cluster: inherit parent
173
+ return tier // tiered cluster: pass tier name
174
+ }
175
+
176
+ // Phase banner map — visual phase identity for log lines (LOW #22).
177
+ // Parallel to ROLE_TO_NOUN: maps each role to the persona banner label shown at
178
+ // phase-announcement time so the transcript log identifies which Forge persona is active.
179
+ // The subagent already gets the full persona-block via ROLE_TO_NOUN; this is display-only.
180
+ const BANNER_MAP = {
181
+ 'plan': 'forge-architect',
182
+ 'review-plan': 'forge-architect',
183
+ 'implement': 'forge-engineer',
184
+ 'review-code': 'forge-engineer',
185
+ 'validate': 'forge-validator',
186
+ 'approve': 'forge-architect',
187
+ 'commit': 'forge-engineer',
188
+ 'writeback': 'forge-engineer',
189
+ }
190
+
191
+ // Role → persona noun mapping for role-block injection (Gap #8).
192
+ const ROLE_TO_NOUN = {
193
+ 'plan': 'architect',
194
+ 'review-plan': 'architect',
195
+ 'implement': 'engineer',
196
+ 'review-code': 'engineer',
197
+ 'validate': 'validator',
198
+ 'approve': 'architect',
199
+ 'commit': 'engineer',
200
+ 'writeback': 'engineer',
201
+ }
202
+
203
+ const RESOLVE_SCHEMA = {
204
+ type: 'object',
205
+ additionalProperties: false,
206
+ required: ['taskId', 'sprintId', 'taskStatus', 'phases'],
207
+ properties: {
208
+ taskId: { type: 'string' },
209
+ sprintId: { type: 'string' },
210
+ taskStatus: { type: 'string' }, // status read from .forge/store/tasks/{id}.json
211
+ phases: {
212
+ type: 'array',
213
+ items: {
214
+ type: 'object',
215
+ additionalProperties: false,
216
+ required: ['command', 'role', 'workflow', 'maxIterations'],
217
+ properties: {
218
+ command: { type: 'string' }, // slash-command name, e.g. "review-plan"
219
+ role: { type: 'string' }, // semantic role, e.g. "review-plan"
220
+ workflow: { type: 'string' }, // workflow file under .forge/workflows/, e.g. "review_plan.md"
221
+ maxIterations: { type: 'integer' }, // revision cap for review roles (default 3)
222
+ on_revision: { type: 'string' }, // optional: command name to route to on revision (Gap #13)
223
+ model: { type: 'string' }, // optional: per-phase model override (Gap #12)
224
+ },
225
+ },
226
+ },
227
+ },
228
+ }
229
+
230
+ const PHASE_RESULT_SCHEMA = {
231
+ type: 'object',
232
+ additionalProperties: false,
233
+ required: ['phase', 'role', 'gatePassed', 'verdict', 'escalated', 'taskStatus'],
234
+ properties: {
235
+ phase: { type: 'string' }, // the command name dispatched
236
+ role: { type: 'string' },
237
+ gatePassed: { type: 'boolean' }, // preflight-gate.cjs exit 0
238
+ verdict: { type: 'string', enum: ['approved', 'revision', 'malformed', 'none'] }, // 'none' for non-review phases
239
+ escalated: { type: 'boolean' }, // subagent set status=escalated (gate fail / malformed / self-escalation)
240
+ taskStatus: { type: 'string' }, // status read back after the phase
241
+ note: { type: 'string' },
242
+ },
243
+ }
244
+
245
+ // --- nearest preceding non-review phase (revision target) -------------------
246
+ // Port of orchestrate_task.md: a "Revision Required" verdict routes back to the
247
+ // nearest earlier phase whose role is NOT a review role (i.e. the producer).
248
+ // Gap #13: if the current review phase specifies on_revision (a command name), look
249
+ // it up by command name in phases and return that index. Fallback to nearest-preceding.
250
+ function revisionTarget(phases, reviewIdx) {
251
+ const reviewPhase = phases[reviewIdx]
252
+ if (reviewPhase && reviewPhase.on_revision) {
253
+ const targetIdx = phases.findIndex((p) => p.command === reviewPhase.on_revision)
254
+ if (targetIdx !== -1) return targetIdx
255
+ }
256
+ for (let j = reviewIdx - 1; j >= 0; j--) {
257
+ if (!REVIEW_ROLES.includes(phases[j].role)) return j
258
+ }
259
+ return 0 // degenerate pipeline with no producer before the review — loop to start
260
+ }
261
+
262
+ // --- dispatch one phase as a subagent ---------------------------------------
263
+ // The subagent owns ALL shell-dependent side-effects for this phase (see header).
264
+ // Gap #6: firstPhase=true triggers session preflight check (phase-index-0 only).
265
+ // Gap #11: simplified=true uses a shorter prompt (retry path, strips arch+summary block).
266
+ // Gap #14: eventId is the COMPLETE-event id pre-computed by the JS loop; the subagent
267
+ // must use this exact id when writing its token sidecar (--sidecar form) so mergeSidecar()
268
+ // can find the file. Use the _complete suffix because token usage lands on the COMPLETE event.
269
+ function runPhase(taskId, sprintId, phase, iteration, { firstPhase = false, simplified = false, eventId = null } = {}) {
270
+ const personaNoun = ROLE_TO_NOUN[phase.role] || 'engineer'
271
+ const reviewLoopCtx = REVIEW_ROLES.includes(phase.role)
272
+ ? [
273
+ '',
274
+ '### Review Loop Context',
275
+ `Iteration: ${iteration} of ${phase.maxIterations}`,
276
+ `Is final iteration: ${iteration >= phase.maxIterations}`,
277
+ ].join('\n')
278
+ : ''
279
+
280
+ // Build the prompt lines list.
281
+ const lines = [
282
+ `You are running a SINGLE pipeline phase for Forge task ${taskId} (sprint ${sprintId}).`,
283
+ `Phase: role="${phase.role}", command="${phase.command}", workflow="${phase.workflow}", iteration=${iteration}.`,
284
+ ]
285
+
286
+ // Gap #6: Session Preflight — first phase only.
287
+ if (firstPhase) {
288
+ lines.push(
289
+ '',
290
+ '0. SESSION PREFLIGHT (first phase only). Read `.forge/cache/preflight-status.json`.',
291
+ ' If the file is absent, run `node .forge/tools/forge-preflight.cjs` and read the JSON it writes.',
292
+ ' If blob.ok === false in the result, HALT immediately — do NOT proceed to the gate or phase.',
293
+ ' Set status escalated, and return gatePassed=false, escalated=true, verdict="none",',
294
+ ' with the preflight warnings in note.',
295
+ )
296
+ }
297
+
298
+ // Gap #7: Gate exit-code distinction.
299
+ lines.push(
300
+ '',
301
+ '1. PRE-FLIGHT GATE. Run `node .forge/tools/preflight-gate.cjs --phase ' + phase.role + ' --task ' + taskId + '`.',
302
+ ' Capture the exit code:',
303
+ ' • exit_code == 0 → gate passed, continue.',
304
+ ' • exit_code == 1 → gate failed (prerequisite missing). Set status escalated.',
305
+ ' Return gatePassed=false, escalated=true, verdict="none", note: "gate_failed: <stderr>".',
306
+ ' • exit_code == 2 → gate misconfigured (unknown phase or malformed block). Set status escalated.',
307
+ ' Return gatePassed=false, escalated=true, verdict="none", note: "gate_misconfigured: <stderr>".',
308
+ )
309
+
310
+ // Gap #8: Persona/skill role-block injection.
311
+ lines.push(
312
+ '',
313
+ '1b. ROLE BLOCK INJECTION. Read `.forge/cache/persona-pack.json` and look up the entry for',
314
+ ` noun="${personaNoun}" (role="${phase.role}" maps to this noun via ROLE_TO_NOUN).`,
315
+ ' Prepend the compact persona+skill summary to your working context (reference mode).',
316
+ ' If persona-pack.json is unavailable, read `.forge/personas/' + personaNoun + '.md` and',
317
+ ' `.forge/skills/' + personaNoun + '-skills.md` directly (inline fallback).',
318
+ )
319
+
320
+ // Gap #9: build-overlay replaces raw MASTER_INDEX read.
321
+ lines.push(
322
+ '',
323
+ '2. PROJECT CONTEXT + RUN THE PHASE.',
324
+ ' Run `node .forge/tools/build-overlay.cjs --task ' + taskId + ' --format md`',
325
+ ' and inject its stdout as the Project Context block for this phase.',
326
+ ' If build-overlay.cjs exits non-zero, fall back to reading `engineering/MASTER_INDEX.md`',
327
+ ' (documented degradation path — not silent swallow).',
328
+ ' Then read `.forge/workflows/' + phase.workflow + '` and follow it for task ' + taskId + '.',
329
+ ' The workflow writes its own artifacts, {PHASE}-SUMMARY.json, and any task-status changes.',
330
+ )
331
+
332
+ // Gap #10: Review Loop Context — injected for review phases.
333
+ if (reviewLoopCtx) lines.push(reviewLoopCtx)
334
+
335
+ // Gap #3/emit: PHASE EVENTS.
336
+ // Gap #14: if an eventId was threaded in from the JS loop, instruct the subagent to use it
337
+ // for the COMPLETE event's eventId and for the --sidecar token file so mergeSidecar() matches.
338
+ const eventIdLine = eventId
339
+ ? ' Use eventId="' + eventId + '" for the COMPLETE event (the driver will call merge-sidecar with this id).'
340
+ : ' Use a fresh crypto.randomUUID() for both start and complete event ids.'
341
+ lines.push(
342
+ '',
343
+ '3. EMIT YOUR PHASE EVENTS. You are the only actor that knows your runtime attribution.',
344
+ ' 3a. BEFORE running the phase workflow: note the start timestamp (startTimestamp = new Date().toISOString()).',
345
+ ' Emit a start event via `node .forge/tools/store-cli.cjs emit ' + sprintId + " '{event-json}'\`",
346
+ ' with action="start", role="' + phase.role + '", iteration=' + iteration + ', startTimestamp and endTimestamp both equal to startTimestamp (0-duration placeholder).',
347
+ ' 3b. AFTER the phase workflow completes: note the end timestamp (endTimestamp = new Date().toISOString()).',
348
+ ' Compute durationMinutes = (new Date(endTimestamp) - new Date(startTimestamp)) / 60000.',
349
+ ' Emit a complete event via `node .forge/tools/store-cli.cjs emit ' + sprintId + " '{event-json}'\`",
350
+ ' conforming to `.forge/schemas/event.schema.json` (role, action="complete", phase, iteration=' + iteration + ',',
351
+ ' startTimestamp, endTimestamp, durationMinutes, plus your own model/provider/token usage — do NOT invent placeholder model strings).',
352
+ ' ' + eventIdLine,
353
+ ' If `/cost` data is available, also write the token sidecar via the `--sidecar` form with the COMPLETE eventId. Best-effort; skip silently if unavailable.',
354
+ '',
355
+ ' Gap #5 FRICTION DRAIN: After any failure event (malformed verdict, null dispatch, max-iter exhaustion),',
356
+ ' drain any `.forge/cache/FRICTION-*.jsonl` files and emit each record as type "friction" with',
357
+ ' `persona:"orchestrator"` and the appropriate issue token.',
358
+ ' Also emit a type:friction event with persona="orchestrator" for any orchestrator-experienced failures.',
359
+ ' Then drain any FRICTION-*.jsonl records you produced as phase subagent and emit them as type "friction".',
360
+ )
361
+
362
+ // Gap #4: Verdict or non-review.
363
+ lines.push(
364
+ '',
365
+ REVIEW_ROLES.includes(phase.role)
366
+ ? '4. READ VERDICT. This is a REVIEW phase. The phase workflow records its verdict into the store '
367
+ + 'summary (`summaries.' + phase.role + '.verdict`) via set-summary — make sure that write happened. '
368
+ + 'Then resolve it with the canonical tool `node .forge/tools/read-verdict.cjs --phase ' + phase.role + ' --task ' + taskId + '` '
369
+ + '(reads the structured summary, NOT a markdown artifact path). '
370
+ + 'Route on the STDOUT token the tool prints (approved | revision | n/a | unknown), NOT on the exit code. '
371
+ + 'Map STDOUT token → verdict: "approved"→"approved", "revision"→"revision", "n/a"→"malformed", "unknown"→"malformed". '
372
+ + 'The exit code is unreliable (exits 1 for both revision AND missing/n/a). NEVER guess.'
373
+ : '4. NON-REVIEW phase: return verdict="none".',
374
+ )
375
+
376
+ lines.push(
377
+ '',
378
+ '5. Read `.forge/store/tasks/' + taskId + '.json` and return its final status as taskStatus, plus a one-line note.',
379
+ )
380
+
381
+ // Gap #11: simplified retry strips arch+summary block and adds strong directive.
382
+ if (simplified) {
383
+ lines.push(
384
+ '',
385
+ 'IMPORTANT: You MUST produce a result. This is a retry after a failed dispatch.',
386
+ 'Skip the architecture context block and the summary block. Proceed directly to the phase workflow.',
387
+ )
388
+ }
389
+
390
+ return agent(
391
+ lines.join('\n'),
392
+ { label: `${taskId}:${phase.role}:${iteration}`, phase: 'Pipeline', schema: PHASE_RESULT_SCHEMA, model: resolveModel(phase.role, phase) }
393
+ )
394
+ }
395
+
396
+ // --- emit task_skipped event (LOW #19) --------------------------------------
397
+ // When the pre-task status guard finds a SKIP_STATUS, the task is silently
398
+ // skipped. To give the event log a complete picture, emit a task-dispatch event
399
+ // with action:"skip" so downstream collators can account for every task.
400
+ // Pattern: mirrors escalateTask / mergeSidecar agent delegation (JS cannot shell out).
401
+ function emitSkip(taskId, sprintId, taskStatus) {
402
+ return agent(
403
+ [
404
+ `Emit a task_skipped event for Forge task ${taskId} (sprint ${sprintId}).`,
405
+ `node .forge/tools/store-cli.cjs emit ${sprintId}`,
406
+ `'{"type":"task-dispatch","action":"skip","taskId":"${taskId}","sprintId":"${sprintId}",`,
407
+ `"role":"orchestrator","phase":"pre-task","iteration":0,`,
408
+ `"notes":"pre-task SKIP_STATUS guard: task status is ${taskStatus}",`,
409
+ `"startTimestamp":"<ISO-now>","endTimestamp":"<ISO-now>","durationMinutes":0,`,
410
+ `"model":"<your-model-id>","provider":"anthropic"}'`,
411
+ 'Replace <ISO-now> with the current UTC ISO 8601 timestamp and <your-model-id> with your actual model id.',
412
+ 'Best-effort — if the emit fails, log and continue. Return "ok".',
413
+ ].join(' '),
414
+ { label: `skip-event:${taskId}`, phase: 'Resolve', model: resolveModel('commit', {}) }
415
+ )
416
+ }
417
+
418
+ // --- escalate from the JS driver (maxIterations exhaustion / null dispatch) --
419
+ // The script can't write the store, so a tiny agent performs the status write + event.
420
+ function escalateTask(taskId, sprintId, reason) {
421
+ return agent(
422
+ [
423
+ `Escalate Forge task ${taskId} to a human.`,
424
+ `run \`node .forge/tools/store-cli.cjs update-status task ${taskId} status escalated\``,
425
+ `and emit one event (sprint ${sprintId}) with verdict="escalated" and notes="${reason}".`,
426
+ `Return the task's final status as taskStatus, gatePassed=true, verdict="none", escalated=true, phase="escalate", role="escalate".`,
427
+ ].join(' '),
428
+ { label: `${taskId}:escalate`, phase: 'Pipeline', schema: PHASE_RESULT_SCHEMA, model: resolveModel('commit', {}) }
429
+ )
430
+ }
431
+
432
+ // --- emit subagent_retry event (Gap #11) ------------------------------------
433
+ // The JS driver cannot shell out; a tiny agent writes the event.
434
+ function emitRetryEvent(taskId, sprintId, role, iteration, reason) {
435
+ return agent(
436
+ [
437
+ `Emit a subagent_retry event for Forge task ${taskId} (sprint ${sprintId}).`,
438
+ `node .forge/tools/store-cli.cjs emit ${sprintId} '{"type":"task-implemented","action":"subagent_retry","role":"${role}","taskId":"${taskId}","phase":"${role}","iteration":${iteration},"notes":"${reason}"}'`,
439
+ `(fill in eventId, sprintId, startTimestamp, endTimestamp, durationMinutes=0, model, provider from runtime.)`,
440
+ `Return "ok".`,
441
+ ].join(' '),
442
+ { label: `${taskId}:retry-event:${iteration}`, phase: 'Pipeline', model: resolveModel('commit', {}) }
443
+ )
444
+ }
445
+
446
+ // --- merge token sidecar (Gap #14) ------------------------------------------
447
+ // After each phase, call merge-sidecar to merge the phase subagent's token sidecar.
448
+ function mergeSidecar(sprintId, eventId) {
449
+ return agent(
450
+ [
451
+ `Merge the token sidecar for sprint ${sprintId}, eventId ${eventId}.`,
452
+ `node .forge/tools/store-cli.cjs merge-sidecar ${sprintId} ${eventId}`,
453
+ `Best-effort — if the sidecar file does not exist, skip silently. Return "ok".`,
454
+ ].join(' '),
455
+ { label: `merge-sidecar:${eventId}`, phase: 'Pipeline', model: resolveModel('commit', {}) }
456
+ )
457
+ }
458
+
459
+ // --- Main -------------------------------------------------------------------
460
+ const taskId = (typeof args === 'string' ? args : args?.taskId)
461
+ if (!taskId) throw new Error('wfl:run-task requires a task id — pass args: "FORGE-S27-T01"')
462
+
463
+ // Phase 1 — Resolve the pipeline + pre-task status (agent does the store/config I/O).
464
+ phase('Resolve')
465
+ const resolved = await agent(
466
+ [
467
+ `Resolve the run-task pipeline for Forge task ${taskId}..`,
468
+ `Read \`node .forge/tools/store-cli.cjs read task ${taskId} --json\` for its current status and sprintId.`,
469
+ 'Then resolve the phase pipeline EXACTLY as `.forge/workflows/orchestrate_task.md` § Pipeline Resolution prescribes:',
470
+ 'if task.pipeline names a key in `.forge/config.json` pipelines, use those phases; otherwise use the default pipeline.',
471
+ // LOW #20: writeback added to hardcoded default pipeline (orchestrate_task.md §3 full default).
472
+ 'The hardcoded default is: plan → review-plan → implement → review-code → validate → approve → writeback → commit,',
473
+ 'mapping roles to workflow files: plan→plan_task.md, review-plan→review_plan.md, implement→implement_plan.md,',
474
+ 'review-code→review_code.md, validate→validate_task.md, approve→architect_approve.md,',
475
+ 'writeback→update_implementation.md, commit→commit_task.md.',
476
+ 'maxIterations defaults to 3 for review roles (review-plan, review-code, validate) and 1 otherwise.',
477
+ 'Return taskId, sprintId, taskStatus, and the ordered phases[]. Read-only — do NOT modify anything.',
478
+ ].join(' '),
479
+ { label: `resolve:${taskId}`, phase: 'Resolve', schema: RESOLVE_SCHEMA }
480
+ )
481
+ if (!resolved) throw new Error(`Could not resolve pipeline for task ${taskId}`)
482
+
483
+ const { sprintId, phases } = resolved
484
+ // Pre-task status guard — orchestrate_task skips already-terminal/blocked tasks.
485
+ // LOW #19: emit task_skipped event so the event log accounts for every task.
486
+ if (SKIP_STATUS.includes(resolved.taskStatus)) {
487
+ log(`⚠ ${taskId} — status is ${resolved.taskStatus}, nothing to run.`)
488
+ await emitSkip(taskId, sprintId, resolved.taskStatus)
489
+ return { taskId, sprintId, skipped: true, taskStatus: resolved.taskStatus, results: [] }
490
+ }
491
+ log(`Task ${taskId} (sprint ${sprintId}) — ${phases.length} phases: ${phases.map(p => p.role).join(' → ')}`)
492
+
493
+ // Phase 2 — drive the phase FSM. JS owns sequencing, counters, routing, escalation.
494
+ phase('Pipeline')
495
+ const iterationCounts = {} // keyed by phase command
496
+ const results = []
497
+ let i = 0
498
+ let escalated = false
499
+ let escalationReason = null
500
+
501
+ while (i < phases.length) {
502
+ const p = phases[i]
503
+ const iteration = (iterationCounts[p.command] || 0) + 1
504
+ const isFirstPhase = i === 0 && iteration === 1
505
+ // LOW #22: use BANNER_MAP for phase-announcement log identity.
506
+ const banner = BANNER_MAP[p.role] || p.role
507
+ log(`→ ${taskId} [${banner}] ${p.role} [${resolveModel(p.role, p) || 'inherit'}] (iteration ${iteration})`)
508
+
509
+ // Gap #11: Simplified-retry-prompt — detect empty/whitespace/timeout result.
510
+ // Emit subagent_retry event, then retry with simplified prompt.
511
+ // Gap #14: Compute eventId using _complete suffix (token usage lands on COMPLETE event,
512
+ // not start). Pass eventId into runPhase so subagent uses it for sidecar agreement.
513
+ // Determinism: eventId must NOT read the wall clock — new Date()/Date.now() throw in the
514
+ // workflow sandbox (breaks resume) and surface as a runtime throw because nested-by-name
515
+ // workflows aren't statically pre-scanned. A deterministic key unique per
516
+ // (sprint, task, role, iteration) is sufficient: controller and subagent only need to AGREE
517
+ // on the same string; event time-ordering comes from payload timestamps the subagent emits.
518
+ const eventId = `${sprintId}_${taskId}_${p.role}_iter${iteration}_complete`
519
+ let r = await runPhase(taskId, sprintId, p, iteration, { firstPhase: isFirstPhase, eventId })
520
+ const isEmpty = !r || (typeof r === 'string' && !r.trim())
521
+ if (isEmpty) {
522
+ // Emit subagent_retry event (best-effort, non-blocking).
523
+ await emitRetryEvent(taskId, sprintId, p.role, iteration, 'empty_or_null_dispatch')
524
+ log(`↺ ${taskId} ${p.role} — empty/null dispatch, retrying with simplified prompt`)
525
+ r = await runPhase(taskId, sprintId, p, iteration, { firstPhase: false, simplified: true, eventId })
526
+ }
527
+ if (!r) {
528
+ escalated = true
529
+ escalationReason = `phase ${p.role} dispatch returned null after retry`
530
+ log(`✗ ${taskId} ${p.role} — dispatch failed twice, escalating`)
531
+ break
532
+ }
533
+ // Gap #14: merge token sidecar after each phase.
534
+ await mergeSidecar(sprintId, eventId)
535
+ results.push(r)
536
+
537
+ // Gate failure or subagent self-escalation (already wrote status=escalated).
538
+ if (!r.gatePassed || r.escalated) {
539
+ escalated = true
540
+ escalationReason = r.note || `${p.role} gate failed / self-escalated`
541
+ log(`⚠ ${taskId} ${p.role} — escalated (${escalationReason})`)
542
+ break
543
+ }
544
+
545
+ // Review phases route on verdict; non-review phases advance on completion.
546
+ if (REVIEW_ROLES.includes(p.role)) {
547
+ if (r.verdict === 'approved') {
548
+ log(`✓ ${taskId} ${p.role} — Approved`)
549
+ i += 1
550
+ } else if (r.verdict === 'revision') {
551
+ iterationCounts[p.command] = (iterationCounts[p.command] || 0) + 1
552
+ log(`↻ ${taskId} ${p.role} — Revision Required (iteration ${iterationCounts[p.command]})`)
553
+ if (iterationCounts[p.command] >= p.maxIterations) {
554
+ escalated = true
555
+ escalationReason = `max iterations (${p.maxIterations}) reached at ${p.role}`
556
+ break
557
+ }
558
+ i = revisionTarget(phases, i) // loop back to the producing phase
559
+ } else {
560
+ // 'malformed' (or unexpected 'none' from a review phase) — never guess.
561
+ escalated = true
562
+ escalationReason = `verdict malformed at ${p.role}`
563
+ break
564
+ }
565
+ } else {
566
+ log(`✓ ${taskId} ${p.role} — completed`)
567
+ i += 1
568
+ }
569
+ // #22 PARITY SEAM — Post-phase exit guard (FORGE-S26-T19):
570
+ // forge-cli/run-task.ts owns hard enforcement: runPostflightGate() is called
571
+ // after runForgeSubagent returns and before currentPhaseIndex++ — if the
572
+ // outputs block is unsatisfied the FSM does not advance and runHaltAdvisor
573
+ // is invoked. This JS driver delegates post-phase output verification to
574
+ // each per-phase subagent (which receives postflight-gate.cjs via its phase
575
+ // prompt and is instructed to satisfy the outputs block before returning
576
+ // gatePassed=true in its StructuredOutput). The JS driver owns advance/halt
577
+ // on the returned `gatePassed` field already present in the StructuredOutput
578
+ // schema (see wfl-run-task.js lines above). No shell execution of
579
+ // postflight-gate.cjs in this JS driver (matches no-shell constraint, #21).
580
+ }
581
+
582
+ // If the JS driver decided to escalate (not the subagent), perform the status write.
583
+ const lastWroteEscalation = results.length && results[results.length - 1].escalated
584
+ if (escalated && !lastWroteEscalation) {
585
+ await escalateTask(taskId, sprintId, escalationReason)
586
+ }
587
+
588
+ // Phase 3 — Report terminal outcome.
589
+ phase('Report')
590
+ const reachedEnd = !escalated && i >= phases.length
591
+ const finalStatus = reachedEnd ? 'committed' : 'escalated'
592
+ if (reachedEnd) {
593
+ log(`🌱 Task ${taskId} complete — pipeline reached terminal (committed).`)
594
+ } else {
595
+ log(`⚠ Task ${taskId} escalated: ${escalationReason}`)
596
+ log(` Resume with the failing phase command after addressing the issue, or re-run wfl:run-task.`)
597
+ }
598
+
599
+ return {
600
+ taskId,
601
+ sprintId,
602
+ finalStatus,
603
+ escalated,
604
+ escalationReason,
605
+ phasesRun: results.length,
606
+ iterationCounts,
607
+ results,
608
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge",
3
- "version": "1.0.10",
3
+ "version": "1.2.14",
4
4
  "description": "Self-enhancing AI software development lifecycle — generates project-specific SDLC instances from meta-definitions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "paths": {
63
63
  "type": "object",
64
- "required": ["engineering", "store", "forgeRoot"],
64
+ "required": ["engineering", "store"],
65
65
  "properties": {
66
66
  "engineering": { "type": "string", "default": "engineering" },
67
67
  "store": { "type": "string", "default": ".forge/store" },
@@ -69,8 +69,7 @@
69
69
  "commands": { "type": "string", "default": ".claude/commands" },
70
70
  "templates": { "type": "string", "default": ".forge/templates" },
71
71
  "customCommands": { "type": "string", "default": "engineering/commands", "description": "Directory for project-specific custom pipeline phase commands. Files here are workflow scripts invoked directly by the orchestrator via the phase workflow field." },
72
- "forgeRoot": { "type": "string", "description": "Absolute path to the installed Forge plugin root. Set at init time and refreshed by /forge:update. Used by generated workflows to invoke Forge tools without requiring $CLAUDE_PLUGIN_ROOT in subagent contexts." },
73
- "forgeRef": { "type": "string", "description": "Pinned Forge plugin reference (tag or commit) the project was generated against." }
72
+ "forgeRef": { "type": "string", "description": "Pinned Forge plugin reference (tag or commit) the project was generated against. Used to resolve the plugin root via cache lookup." }
74
73
  }
75
74
  },
76
75
  "pipeline": {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.0.10",
3
- "generated": "2026-05-31",
2
+ "version": "1.2.13",
3
+ "generated": "2026-06-03",
4
4
  "note": "Authoritative enum catalog. Source: build-enum-catalog.cjs. Regenerate via node forge/tools/build-manifest.cjs.",
5
5
  "enums": {
6
6
  "task.status": [