@entelligentsia/forgecli 1.0.10 → 1.0.20

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 (183) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/dist/CHANGELOG-forge-plugin.md +211 -0
  3. package/dist/bin/forge.js +0 -0
  4. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  5. package/dist/extensions/forgecli/context-governor-compaction.d.ts +83 -0
  6. package/dist/extensions/forgecli/context-governor-compaction.js +302 -0
  7. package/dist/extensions/forgecli/context-governor-compaction.js.map +1 -0
  8. package/dist/extensions/forgecli/context-governor.d.ts +173 -0
  9. package/dist/extensions/forgecli/context-governor.js +618 -0
  10. package/dist/extensions/forgecli/context-governor.js.map +1 -0
  11. package/dist/extensions/forgecli/dashboard/component.d.ts +105 -0
  12. package/dist/extensions/forgecli/dashboard/component.js +861 -0
  13. package/dist/extensions/forgecli/dashboard/component.js.map +1 -0
  14. package/dist/extensions/forgecli/dashboard/register.d.ts +2 -0
  15. package/dist/extensions/forgecli/dashboard/register.js +31 -0
  16. package/dist/extensions/forgecli/dashboard/register.js.map +1 -0
  17. package/dist/extensions/forgecli/dashboard/theme.d.ts +27 -0
  18. package/dist/extensions/forgecli/dashboard/theme.js +91 -0
  19. package/dist/extensions/forgecli/dashboard/theme.js.map +1 -0
  20. package/dist/extensions/forgecli/dashboard/view-model.d.ts +35 -0
  21. package/dist/extensions/forgecli/dashboard/view-model.js +54 -0
  22. package/dist/extensions/forgecli/dashboard/view-model.js.map +1 -0
  23. package/dist/extensions/forgecli/fix-bug.js +126 -7
  24. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  25. package/dist/extensions/forgecli/forge-artifact-tool.js +2 -1
  26. package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
  27. package/dist/extensions/forgecli/forge-commands.js +1 -0
  28. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  29. package/dist/extensions/forgecli/forge-init/phase4-register.js +53 -0
  30. package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
  31. package/dist/extensions/forgecli/forge-subagent.d.ts +20 -1
  32. package/dist/extensions/forgecli/forge-subagent.js +23 -7
  33. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  34. package/dist/extensions/forgecli/forge-tools.js +3 -1
  35. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  36. package/dist/extensions/forgecli/hook-dispatcher.d.ts +3 -1
  37. package/dist/extensions/forgecli/hook-dispatcher.js +37 -3
  38. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  39. package/dist/extensions/forgecli/index.js +38 -1
  40. package/dist/extensions/forgecli/index.js.map +1 -1
  41. package/dist/extensions/forgecli/lib/halt-advisor.d.ts +59 -0
  42. package/dist/extensions/forgecli/lib/halt-advisor.js +113 -0
  43. package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -0
  44. package/dist/extensions/forgecli/migration-engine.js +25 -12
  45. package/dist/extensions/forgecli/migration-engine.js.map +1 -1
  46. package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +26 -0
  47. package/dist/extensions/forgecli/orchestrator-status-bar.js +213 -0
  48. package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -0
  49. package/dist/extensions/forgecli/orchestrator-tree.d.ts +96 -0
  50. package/dist/extensions/forgecli/orchestrator-tree.js +390 -0
  51. package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -0
  52. package/dist/extensions/forgecli/project-orientation.js +12 -8
  53. package/dist/extensions/forgecli/project-orientation.js.map +1 -1
  54. package/dist/extensions/forgecli/regenerate.d.ts +16 -0
  55. package/dist/extensions/forgecli/regenerate.js +110 -0
  56. package/dist/extensions/forgecli/regenerate.js.map +1 -1
  57. package/dist/extensions/forgecli/run-sprint.d.ts +3 -1
  58. package/dist/extensions/forgecli/run-sprint.js +34 -3
  59. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  60. package/dist/extensions/forgecli/run-task.d.ts +66 -1
  61. package/dist/extensions/forgecli/run-task.js +323 -12
  62. package/dist/extensions/forgecli/run-task.js.map +1 -1
  63. package/dist/extensions/forgecli/thread-switcher.d.ts +4 -1
  64. package/dist/extensions/forgecli/thread-switcher.js +118 -762
  65. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  66. package/dist/extensions/forgecli/viewport-events.js +32 -0
  67. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  68. package/dist/forge-payload/.base-pack/commands/fix-bug.md +1 -1
  69. package/dist/forge-payload/.base-pack/commands/run-sprint.md +1 -1
  70. package/dist/forge-payload/.base-pack/commands/run-task.md +1 -1
  71. package/dist/forge-payload/.base-pack/personas/architect.md +1 -1
  72. package/dist/forge-payload/.base-pack/personas/bug-fixer.md +1 -1
  73. package/dist/forge-payload/.base-pack/personas/collator.md +3 -3
  74. package/dist/forge-payload/.base-pack/personas/engineer.md +1 -1
  75. package/dist/forge-payload/.base-pack/personas/librarian.md +1 -1
  76. package/dist/forge-payload/.base-pack/personas/orchestrator.md +1 -1
  77. package/dist/forge-payload/.base-pack/personas/product-manager.md +1 -1
  78. package/dist/forge-payload/.base-pack/personas/qa-engineer.md +1 -1
  79. package/dist/forge-payload/.base-pack/personas/supervisor.md +1 -1
  80. package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +1 -1
  81. package/dist/forge-payload/.base-pack/workflows/_fragments/friction-emit.md +1 -1
  82. package/dist/forge-payload/.base-pack/workflows/_fragments/iron-laws.md +1 -1
  83. package/dist/forge-payload/.base-pack/workflows/_fragments/progress-reporting.md +2 -2
  84. package/dist/forge-payload/.base-pack/workflows/_fragments/store-cli-verbs.md +11 -2
  85. package/dist/forge-payload/.base-pack/workflows/architect_approve.md +6 -7
  86. package/dist/forge-payload/.base-pack/workflows/architect_review_sprint_completion.md +2 -2
  87. package/dist/forge-payload/.base-pack/workflows/architect_sprint_intake.md +2 -2
  88. package/dist/forge-payload/.base-pack/workflows/architect_sprint_plan.md +5 -5
  89. package/dist/forge-payload/.base-pack/workflows/collator_agent.md +4 -6
  90. package/dist/forge-payload/.base-pack/workflows/commit_task.md +5 -6
  91. package/dist/forge-payload/.base-pack/workflows/enhance.md +5 -5
  92. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +15 -7
  93. package/dist/forge-payload/.base-pack/workflows/migrate_structural.md +12 -13
  94. package/dist/forge-payload/.base-pack/workflows/plan_task.md +12 -6
  95. package/dist/forge-payload/.base-pack/workflows/review_code.md +12 -11
  96. package/dist/forge-payload/.base-pack/workflows/review_plan.md +12 -11
  97. package/dist/forge-payload/.base-pack/workflows/sprint_retrospective.md +3 -3
  98. package/dist/forge-payload/.base-pack/workflows/triage.md +12 -9
  99. package/dist/forge-payload/.base-pack/workflows/update_implementation.md +2 -2
  100. package/dist/forge-payload/.base-pack/workflows/update_plan.md +2 -2
  101. package/dist/forge-payload/.base-pack/workflows/validate_task.md +9 -9
  102. package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +490 -0
  103. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-sprint.js +416 -0
  104. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +608 -0
  105. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  106. package/dist/forge-payload/.schemas/config.schema.json +2 -3
  107. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  108. package/dist/forge-payload/.schemas/event.schema.json +16 -0
  109. package/dist/forge-payload/.schemas/migrations.json +359 -18
  110. package/dist/forge-payload/commands/health.md +29 -0
  111. package/dist/forge-payload/commands/rebuild.md +143 -15
  112. package/dist/forge-payload/commands/update.md +28 -27
  113. package/dist/forge-payload/hooks/preflight-session.cjs +99 -0
  114. package/dist/forge-payload/init/phases/phase-3-materialize.md +18 -5
  115. package/dist/forge-payload/integrity.json +7 -6
  116. package/dist/forge-payload/meta/fragments/tool-discipline.md +1 -1
  117. package/dist/forge-payload/meta/personas/meta-architect.md +1 -1
  118. package/dist/forge-payload/meta/personas/meta-bug-fixer.md +1 -1
  119. package/dist/forge-payload/meta/personas/meta-collator.md +7 -7
  120. package/dist/forge-payload/meta/personas/meta-engineer.md +1 -1
  121. package/dist/forge-payload/meta/personas/meta-orchestrator.md +1 -1
  122. package/dist/forge-payload/meta/personas/meta-supervisor.md +1 -1
  123. package/dist/forge-payload/meta/tool-specs/store-cli.spec.md +1 -1
  124. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +1 -1
  125. package/dist/forge-payload/meta/workflows/_fragments/friction-emit.md +1 -1
  126. package/dist/forge-payload/meta/workflows/_fragments/iron-laws.md +1 -1
  127. package/dist/forge-payload/meta/workflows/_fragments/progress-reporting.md +2 -2
  128. package/dist/forge-payload/meta/workflows/_fragments/store-cli-verbs.md +11 -2
  129. package/dist/forge-payload/meta/workflows/meta-approve.md +6 -7
  130. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +12 -9
  131. package/dist/forge-payload/meta/workflows/meta-collate.md +5 -7
  132. package/dist/forge-payload/meta/workflows/meta-commit.md +5 -6
  133. package/dist/forge-payload/meta/workflows/meta-enhance.md +5 -5
  134. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +35 -11
  135. package/dist/forge-payload/meta/workflows/meta-implement.md +15 -7
  136. package/dist/forge-payload/meta/workflows/meta-migrate.md +13 -14
  137. package/dist/forge-payload/meta/workflows/meta-new-sprint.md +3 -3
  138. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +138 -39
  139. package/dist/forge-payload/meta/workflows/meta-plan-sprint.md +6 -6
  140. package/dist/forge-payload/meta/workflows/meta-plan-task.md +12 -6
  141. package/dist/forge-payload/meta/workflows/meta-retro.md +4 -4
  142. package/dist/forge-payload/meta/workflows/meta-retrospective.md +4 -4
  143. package/dist/forge-payload/meta/workflows/meta-review-implementation.md +12 -11
  144. package/dist/forge-payload/meta/workflows/meta-review-plan.md +12 -11
  145. package/dist/forge-payload/meta/workflows/meta-review-sprint-completion.md +3 -3
  146. package/dist/forge-payload/meta/workflows/meta-sprint-intake.md +3 -3
  147. package/dist/forge-payload/meta/workflows/meta-sprint-plan.md +6 -6
  148. package/dist/forge-payload/meta/workflows/meta-update-implementation.md +2 -2
  149. package/dist/forge-payload/meta/workflows/meta-update-plan.md +2 -2
  150. package/dist/forge-payload/meta/workflows/meta-validate.md +9 -9
  151. package/dist/forge-payload/schemas/config.schema.json +2 -3
  152. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  153. package/dist/forge-payload/schemas/event.schema.json +16 -0
  154. package/dist/forge-payload/schemas/structure-manifest.json +75 -73
  155. package/dist/forge-payload/skills/refresh-kb-links/SKILL.md +14 -7
  156. package/dist/forge-payload/tools/banners.cjs +29 -10
  157. package/dist/forge-payload/tools/check-structure.cjs +88 -7
  158. package/dist/forge-payload/tools/collate.cjs +48 -2
  159. package/dist/forge-payload/tools/manage-config.cjs +5 -7
  160. package/dist/forge-payload/tools/parse-gates.cjs +73 -1
  161. package/dist/forge-payload/tools/postflight-gate.cjs +298 -0
  162. package/dist/forge-payload/tools/preflight-gate.cjs +47 -0
  163. package/dist/forge-payload/tools/substitute-placeholders.cjs +5 -4
  164. package/dist/forge-payload/tools/verify-phase.cjs +17 -0
  165. package/package.json +2 -2
  166. package/dist/bin/forgecli.d.ts +0 -2
  167. package/dist/bin/forgecli.js +0 -6
  168. package/dist/bin/forgecli.js.map +0 -1
  169. package/dist/extensions/forgecli/config-tui/index.d.ts +0 -5
  170. package/dist/extensions/forgecli/config-tui/index.js +0 -5
  171. package/dist/extensions/forgecli/config-tui/index.js.map +0 -1
  172. package/dist/extensions/forgecli/loaders/persona-skill-loader.d.ts +0 -45
  173. package/dist/extensions/forgecli/loaders/persona-skill-loader.js +0 -227
  174. package/dist/extensions/forgecli/loaders/persona-skill-loader.js.map +0 -1
  175. package/dist/extensions/forgecli/loaders/template-render.d.ts +0 -20
  176. package/dist/extensions/forgecli/loaders/template-render.js +0 -85
  177. package/dist/extensions/forgecli/loaders/template-render.js.map +0 -1
  178. package/dist/extensions/forgecli/loaders/workflow-loader.d.ts +0 -41
  179. package/dist/extensions/forgecli/loaders/workflow-loader.js +0 -164
  180. package/dist/extensions/forgecli/loaders/workflow-loader.js.map +0 -1
  181. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +0 -446
  182. package/dist/forge-payload/.base-pack/workflows/orchestrate_task.md +0 -928
  183. package/dist/forge-payload/.base-pack/workflows/run_sprint.md +0 -225
@@ -19,16 +19,20 @@ import * as path from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { assertAudience, CallerContextStore } from "./audience-gate.js";
21
21
  import { loadLayeredConfig } from "./config-layer.js";
22
+ import { buildGovernorFactory } from "./context-governor.js";
23
+ import { buildForgeCompactionFactory } from "./context-governor-compaction.js";
22
24
  import { loadForgePersona, runForgeSubagent } from "./forge-subagent.js";
23
25
  import { getSubagentTools } from "./forge-tools.js";
24
26
  import { readPersonaDir, readPipelineNames } from "./lib/catalog-helpers.js";
25
27
  import { discoverForgeConfigCached } from "./lib/forge-config.js";
28
+ import { resolveAdvisorModel, runHaltAdvisor } from "./lib/halt-advisor.js";
26
29
  import { checkMaterialization } from "./lib/manifest-checker.js";
27
30
  import { runOrchestratorPreflight } from "./lib/orchestrator-preflight.js";
28
31
  import { isStateStale as isJsonStateStale, readJsonState, taskStateFilePath, writeJsonState, } from "./lib/state-helpers.js";
29
32
  import { resolveModelForPhase } from "./model-resolver.js";
30
33
  import { loadWorkflow } from "./parsers/workflow-loader.js";
31
34
  import { getSessionRegistry } from "./session-registry.js";
35
+ import { getOrchestratorTree } from "./orchestrator-tree.js";
32
36
  import { OrchestratorTranscriptWriter } from "./subagent/orchestrator-transcript.js";
33
37
  import { resolveToCanonicalId, resolveToolDir } from "./store-resolver.js";
34
38
  import { attachViewportObserver } from "./viewport-events.js";
@@ -224,6 +228,49 @@ export function emitEvent(storeCli, cwd, sprintId, event) {
224
228
  });
225
229
  return { ok: result.status === 0, stderr: typeof result.stderr === "string" ? result.stderr : "" };
226
230
  }
231
+ /**
232
+ * Emit a phase event for an INCOMPLETE attempt (cancelled / failed) so its
233
+ * provider-billed tokens reach the store. Bug B: the cancel and halt-on-failure
234
+ * branches used to return without emitting, so collate's COST_REPORT
235
+ * under-counted real spend by exactly the aborted passes (CART-S02-T03
236
+ * baseline: 259,950 tokens across two aborted plan attempts, invisible).
237
+ *
238
+ * The event is the canonical phase event (schema-unchanged) with
239
+ * `verdict: "aborted" | "failed"` marking the outcome.
240
+ *
241
+ * Zero-token attempts are skipped — there is no spend to account, and a
242
+ * token-less event would be flagged as a husk by collate's ingestion-quality
243
+ * pass. Never throws: emission must not perturb the cancel/halt return paths.
244
+ *
245
+ * @param opts.decorate Optional event mutation hook applied before emit
246
+ * (fix-bug uses it for the BUG_TYPE_TOKENS `type` field).
247
+ * @returns true when the event was emitted and store-cli accepted it.
248
+ */
249
+ export function emitIncompletePhaseEvent(opts) {
250
+ try {
251
+ const { emitCtx, outcome } = opts;
252
+ const u = emitCtx.usage;
253
+ if (u.input + u.output + u.cacheRead + u.cacheWrite <= 0) {
254
+ opts.onDebug?.({ kind: "incomplete_emit_skipped", reason: "no-tokens", outcome });
255
+ return false;
256
+ }
257
+ const judgement = { verdict: outcome };
258
+ if (opts.notes)
259
+ judgement.notes = opts.notes;
260
+ const event = buildPhaseEvent({ ...emitCtx, judgement });
261
+ opts.decorate?.(event);
262
+ const res = emitEvent(emitCtx.storeCli, emitCtx.cwd, emitCtx.sprintId, event);
263
+ opts.onDebug?.(res.ok
264
+ ? { kind: "incomplete_emit_ok", eventId: event.eventId, outcome }
265
+ : { kind: "incomplete_emit_failed", stderr: res.stderr, outcome });
266
+ return res.ok;
267
+ }
268
+ catch (err) {
269
+ const msg = err instanceof Error ? err.message : String(err);
270
+ opts.onDebug?.({ kind: "incomplete_emit_failed", stderr: msg, outcome: opts.outcome });
271
+ return false;
272
+ }
273
+ }
227
274
  export function judgementFromSummary(record, phaseRole, summaryKeyByRole) {
228
275
  if (!record || !record.summaries)
229
276
  return undefined;
@@ -348,13 +395,71 @@ export function composeTaskBody(subWorkflowMd, taskId, summariesBlock) {
348
395
  return parts.join("\n");
349
396
  }
350
397
  export function runPreflightGate(preflightGate, role, taskId, cwd, entityType) {
398
+ const outcome = runPreflightGateWithData(preflightGate, role, taskId, cwd, entityType);
399
+ return outcome.result;
400
+ }
401
+ /**
402
+ * Run postflight-gate.cjs after a phase subagent returns, before FSM advance.
403
+ * Mirrors runPreflightGateWithData — same argv-array discipline, same structured-JSON
404
+ * parsing from stdout on exit 1.
405
+ *
406
+ * Returns:
407
+ * "ok" — gate passed (or no outputs block for this phase); advance may proceed.
408
+ * "unsatisfied" — gate failed; do NOT advance FSM; halt and call runHaltAdvisor.
409
+ * "error" — gate binary missing or parse error; treat as pass-through (additive).
410
+ */
411
+ export function runPostflightGate(postflightGate, role, taskId, cwd) {
412
+ if (!fs.existsSync(postflightGate)) {
413
+ // postflight-gate.cjs not present in this forgeRoot — pass through (additive).
414
+ return { result: "ok", gateFailure: null };
415
+ }
416
+ const spawnResult = spawnSync("node", [postflightGate, "--phase", role, "--task", taskId], { cwd, encoding: "utf8" });
417
+ if (spawnResult.status === 0)
418
+ return { result: "ok", gateFailure: null };
419
+ if (spawnResult.status === 2)
420
+ return { result: "error", gateFailure: null };
421
+ // Exit 1: parse structured JSON from stdout
422
+ let gateFailure = null;
423
+ try {
424
+ const stdout = typeof spawnResult.stdout === "string" ? spawnResult.stdout.trim() : "";
425
+ if (stdout) {
426
+ const parsed = JSON.parse(stdout);
427
+ if (parsed && typeof parsed.reasonCode === "string") {
428
+ gateFailure = parsed;
429
+ }
430
+ }
431
+ }
432
+ catch {
433
+ // stdout not valid JSON — gate failure but no structured data
434
+ }
435
+ return { result: "unsatisfied", gateFailure };
436
+ }
437
+ /**
438
+ * Upgraded variant that returns structured failure data alongside the status enum.
439
+ * Callers that need the advisory data should use this function directly.
440
+ */
441
+ export function runPreflightGateWithData(preflightGate, role, taskId, cwd, entityType) {
351
442
  const entityFlag = entityType === "bug" ? "--bug" : "--task";
352
- const result = spawnSync("node", [preflightGate, "--phase", role, entityFlag, taskId], { cwd });
353
- if (result.status === 0)
354
- return "proceed";
355
- if (result.status === 2)
356
- return "escalate";
357
- return "halt";
443
+ const spawnResult = spawnSync("node", [preflightGate, "--phase", role, entityFlag, taskId], { cwd, encoding: "utf8" });
444
+ if (spawnResult.status === 0)
445
+ return { result: "proceed", gateFailure: null };
446
+ if (spawnResult.status === 2)
447
+ return { result: "escalate", gateFailure: null };
448
+ // Exit 1: parse structured JSON from stdout
449
+ let gateFailure = null;
450
+ try {
451
+ const stdout = typeof spawnResult.stdout === "string" ? spawnResult.stdout.trim() : "";
452
+ if (stdout) {
453
+ const parsed = JSON.parse(stdout);
454
+ if (parsed && typeof parsed.reasonCode === "string") {
455
+ gateFailure = parsed;
456
+ }
457
+ }
458
+ }
459
+ catch {
460
+ // stdout not valid JSON — gate failure but no structured data
461
+ }
462
+ return { result: "halt", gateFailure };
358
463
  }
359
464
  // ── Per-task orchestrator pipeline (FORGE-S21-T03 extracted) ──────────────
360
465
  // The entire phase loop was inline in registerRunTask. It is now a standalone
@@ -369,6 +474,8 @@ export { extractTurnPreview } from "./viewport-renderer.js";
369
474
  // ── runTaskPipeline ──────────────────────────────────────────────────────
370
475
  export async function runTaskPipeline(opts) {
371
476
  const { taskId, cwd, ctx, forgeRoot, storeCli, preflightGate, registry, resumeFromState, onPhaseEvent } = opts;
477
+ // Bridge: OrchestratorTree for the dashboard overlay.
478
+ const tree = getOrchestratorTree();
372
479
  // Load per-phase model routing config once at task entry (Plan 16 Slice 2).
373
480
  // Empty / absent config produces inherit for every phase — no behaviour change.
374
481
  // N-B-E: surface schema errors to caller (Decision 9 — orchestrators fail-fast).
@@ -513,9 +620,16 @@ export async function runTaskPipeline(opts) {
513
620
  }
514
621
  // ── 6a. Preflight gate ────────────────────────────────────────
515
622
  if (fs.existsSync(preflightGate)) {
516
- const preflightResult = runPreflightGate(preflightGate, phase.role, taskId, cwd);
517
- if (preflightResult === "halt") {
518
- ctx.ui.notify(`× forge:run-task preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
623
+ const preflightOutcome = runPreflightGateWithData(preflightGate, phase.role, taskId, cwd);
624
+ if (preflightOutcome.result === "halt") {
625
+ // Render structured failure reason if available.
626
+ if (preflightOutcome.gateFailure) {
627
+ ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} ` +
628
+ `[${preflightOutcome.gateFailure.reasonCode}]: ${preflightOutcome.gateFailure.detail}`, "error");
629
+ }
630
+ else {
631
+ ctx.ui.notify(`× forge:run-task — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
632
+ }
519
633
  writeState(cwd, {
520
634
  taskId,
521
635
  phaseIndex: currentPhaseIndex,
@@ -524,6 +638,18 @@ export async function runTaskPipeline(opts) {
524
638
  lastError: `preflight gate exit 1 for ${phase.role}`,
525
639
  savedAt: new Date().toISOString(),
526
640
  });
641
+ // Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
642
+ if (preflightOutcome.gateFailure) {
643
+ const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
644
+ void runHaltAdvisor({
645
+ gateFailure: preflightOutcome.gateFailure,
646
+ advisorModel,
647
+ taskId,
648
+ cwd,
649
+ ctx: { ui: ctx.ui },
650
+ forgeRoot,
651
+ });
652
+ }
527
653
  return {
528
654
  status: "halted",
529
655
  lastPhaseIndex: currentPhaseIndex,
@@ -531,7 +657,7 @@ export async function runTaskPipeline(opts) {
531
657
  lastError: `preflight gate exit 1 for ${phase.role}`,
532
658
  };
533
659
  }
534
- if (preflightResult === "escalate") {
660
+ if (preflightOutcome.result === "escalate") {
535
661
  ctx.ui.notify(`× forge:run-task — preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
536
662
  writeState(cwd, {
537
663
  taskId,
@@ -646,6 +772,15 @@ export async function runTaskPipeline(opts) {
646
772
  persona: phase.personaNoun,
647
773
  });
648
774
  registry.startPhase(taskId, phase.role, currentPhaseIndex);
775
+ // Bridge: register phase in OrchestratorTree.
776
+ const iteration = (opts.resumeFromState?.iterationCounts?.[phase.role] ?? 0) + 1;
777
+ const phaseNodeId = `${taskId}:${phase.role}:${iteration}`;
778
+ tree.startNode(phaseNodeId, {
779
+ parentId: taskId,
780
+ label: `${phase.role}:${iteration}`,
781
+ kind: "leaf",
782
+ promptPreview: taskBody.slice(0, 200),
783
+ });
649
784
  // Capture the first stream-observed model on turn_end (IL10 visibility).
650
785
  // If pi auto-substitutes or setModel silently no-ops, this line will diverge
651
786
  // from requested_model — exactly the diagnostic signal we want.
@@ -680,6 +815,29 @@ export async function runTaskPipeline(opts) {
680
815
  verboseKeys: { messageKey: MESSAGE_KEY },
681
816
  afterEach: refreshStatus,
682
817
  });
818
+ // ── Context governor injection (completes FORGE-S30-T07) ──────
819
+ // Per-phase factories built HERE because only the pipeline knows the
820
+ // `${personaNoun}/${role}` phase key — pi never sets persona/phase on
821
+ // ExtensionContext, and the parent session's registerHookDispatcher
822
+ // governor never sees subagent tool traffic (dormant-governor defect,
823
+ // CART-S02-T03 benchmark). Flag-gated: FORGE_CTX_GOVERNOR=1.
824
+ // buildGovernorFactory — Mechanisms A/B/C/D in the subagent
825
+ // buildForgeCompactionFactory — Mechanism E with warm-tier path opts
826
+ // (previously injected from index.ts with NO opts → warm-tier dead)
827
+ const phaseKey = `${phase.personaNoun}/${phase.role}`;
828
+ const sprintIdForSummaries = /^(.*)-T\d+$/.exec(taskId)?.[1];
829
+ const governorFactories = process.env.FORGE_CTX_GOVERNOR === "1"
830
+ ? [
831
+ buildGovernorFactory({ phaseKey, cwd }),
832
+ buildForgeCompactionFactory({
833
+ cwd,
834
+ phaseKey,
835
+ entityId: taskId,
836
+ sprintId: sprintIdForSummaries,
837
+ }),
838
+ ]
839
+ : [];
840
+ const phaseExtensionFactories = [...(opts.extensionFactories ?? []), ...governorFactories];
683
841
  let result;
684
842
  try {
685
843
  // FORGE-BUG-040: wrap the runForgeSubagent dispatch in the phase
@@ -698,6 +856,7 @@ export async function runTaskPipeline(opts) {
698
856
  modelRegistry: ctx.modelRegistry,
699
857
  signal: opts.signal,
700
858
  customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
859
+ extensionFactories: phaseExtensionFactories.length > 0 ? phaseExtensionFactories : undefined,
701
860
  }));
702
861
  }
703
862
  catch (err) {
@@ -728,7 +887,42 @@ export async function runTaskPipeline(opts) {
728
887
  if (result.stopReason === "aborted" || opts.signal?.aborted) {
729
888
  ctx.ui.notify(`⊘ forge:run-task — ${taskId} phase ${phase.role} cancelled.`, "info");
730
889
  registry.completePhase(taskId, phase.role, "cancelled");
890
+ tree.completeNode(phaseNodeId, "cancelled");
731
891
  registry.confirmCancelled(taskId);
892
+ // Bug B: account the billed tokens of this aborted attempt before returning.
893
+ {
894
+ const abortSprintId = readTaskRecord(taskId, storeCli, cwd)?.sprintId;
895
+ if (abortSprintId) {
896
+ emitIncompletePhaseEvent({
897
+ emitCtx: {
898
+ entityType: "task",
899
+ taskId,
900
+ sprintId: abortSprintId,
901
+ phase,
902
+ iteration: (iterationCounts[phase.role] ?? 0) + 1,
903
+ startMs: phaseStart,
904
+ endMs: Date.now(),
905
+ model: result.model ?? "unknown",
906
+ provider: result.provider ?? "unknown",
907
+ usage: {
908
+ input: result.usage.input,
909
+ output: result.usage.output,
910
+ cacheRead: result.usage.cacheRead,
911
+ cacheWrite: result.usage.cacheWrite,
912
+ },
913
+ judgement: undefined,
914
+ storeCli,
915
+ cwd,
916
+ },
917
+ outcome: "aborted",
918
+ notes: result.errorMessage ?? result.stopReason ?? undefined,
919
+ onDebug: writeDebug,
920
+ });
921
+ }
922
+ else {
923
+ writeDebug({ kind: "incomplete_emit_skipped", reason: "no-sprintId", outcome: "aborted" });
924
+ }
925
+ }
732
926
  // ADR-S21-01: preserve state file so cancelled runs are resumable
733
927
  writeState(cwd, {
734
928
  taskId,
@@ -746,6 +940,40 @@ export async function runTaskPipeline(opts) {
746
940
  ctx.ui.notify(`× forge:run-task — phase ${phase.role} failed (exit ${result.exitCode})` +
747
941
  (result.errorMessage ? `: ${result.errorMessage}` : "") +
748
942
  (result.stopReason ? ` [${result.stopReason}]` : ""), "error");
943
+ // Bug B: account the billed tokens of this failed attempt before returning.
944
+ {
945
+ const failSprintId = readTaskRecord(taskId, storeCli, cwd)?.sprintId;
946
+ if (failSprintId) {
947
+ emitIncompletePhaseEvent({
948
+ emitCtx: {
949
+ entityType: "task",
950
+ taskId,
951
+ sprintId: failSprintId,
952
+ phase,
953
+ iteration: (iterationCounts[phase.role] ?? 0) + 1,
954
+ startMs: phaseStart,
955
+ endMs: Date.now(),
956
+ model: result.model ?? "unknown",
957
+ provider: result.provider ?? "unknown",
958
+ usage: {
959
+ input: result.usage.input,
960
+ output: result.usage.output,
961
+ cacheRead: result.usage.cacheRead,
962
+ cacheWrite: result.usage.cacheWrite,
963
+ },
964
+ judgement: undefined,
965
+ storeCli,
966
+ cwd,
967
+ },
968
+ outcome: "failed",
969
+ notes: result.errorMessage ?? result.stopReason ?? undefined,
970
+ onDebug: writeDebug,
971
+ });
972
+ }
973
+ else {
974
+ writeDebug({ kind: "incomplete_emit_skipped", reason: "no-sprintId", outcome: "failed" });
975
+ }
976
+ }
749
977
  writeState(cwd, {
750
978
  taskId,
751
979
  phaseIndex: currentPhaseIndex,
@@ -854,7 +1082,7 @@ export async function runTaskPipeline(opts) {
854
1082
  const verdict = readVerdict(taskId, phase.role, storeCli, cwd);
855
1083
  if (verdict === "missing") {
856
1084
  ctx.ui.notify(`× forge:run-task — verdict missing for phase ${phase.role} after subagent completed. ` +
857
- "Subagent may have crashed or failed to write summaries. Escalating.", "error");
1085
+ "Subagent may have crashed or failed to write summaries. Halting for advisory.", "error");
858
1086
  writeState(cwd, {
859
1087
  taskId,
860
1088
  phaseIndex: currentPhaseIndex,
@@ -863,8 +1091,35 @@ export async function runTaskPipeline(opts) {
863
1091
  lastError: `verdict missing for ${phase.role}`,
864
1092
  savedAt: new Date().toISOString(),
865
1093
  });
1094
+ // A missing verdict IS a postflight-outputs failure: the canonical
1095
+ // phase summary the subagent must write (e.g. summaries.code_review)
1096
+ // was never recorded, so there is no verdict to route on. review-phase
1097
+ // workflows declare no `outputs` block, so runPostflightGate is a
1098
+ // pass-through here — this readVerdict check is the effective gate.
1099
+ // Route it through the halt-recovery advisor (FORGE-S26-T18), the same
1100
+ // hand-off the postflight-gate-unsatisfied branch uses, instead of a
1101
+ // bare escalation — so the strongest configured model can diagnose the
1102
+ // missing-summary cause. Best-effort, non-fatal (FORGE-S26-T19 parity).
1103
+ const gateFailure = {
1104
+ phase: phase.role,
1105
+ reasonCode: "verdict-missing",
1106
+ detail: `Phase '${phase.role}' completed but no verdict was found in the store. ` +
1107
+ "The canonical phase summary was not written, so the orchestrator has no verdict to route on.",
1108
+ remediation: "Re-run the phase and ensure the subagent's forge_store set-summary call " +
1109
+ 'uses args:["<recordId>", "<phaseKey>"] with the literal phase key as args[1] ' +
1110
+ "(e.g. code_review), and that the call exits zero before the subagent returns.",
1111
+ };
1112
+ const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
1113
+ void runHaltAdvisor({
1114
+ gateFailure,
1115
+ advisorModel,
1116
+ taskId,
1117
+ cwd,
1118
+ ctx: { ui: ctx.ui },
1119
+ forgeRoot,
1120
+ });
866
1121
  return {
867
- status: "failed",
1122
+ status: "halted",
868
1123
  lastPhaseIndex: currentPhaseIndex,
869
1124
  iterationCounts,
870
1125
  lastError: `verdict missing for ${phase.role}`,
@@ -917,8 +1172,57 @@ export async function runTaskPipeline(opts) {
917
1172
  }
918
1173
  // verdict === "approved": fall through to advance
919
1174
  }
1175
+ // Postflight gate: evaluate `outputs` block after subagent returns,
1176
+ // before FSM status advance (FORGE-S26-T19). Hard enforcement in forge-cli;
1177
+ // plugin LLM route treats postflight as advisory. On UNSATISFIED: do not
1178
+ // advance currentPhaseIndex, halt, hand off to existing runHaltAdvisor.
1179
+ {
1180
+ const postflightGatePath = preflightGate.replace("preflight-gate.cjs", "postflight-gate.cjs");
1181
+ const postflightOutcome = runPostflightGate(postflightGatePath, phase.role, taskId, cwd);
1182
+ if (postflightOutcome.result === "unsatisfied") {
1183
+ if (postflightOutcome.gateFailure) {
1184
+ ctx.ui.notify(`× forge:run-task — postflight gate failed for phase ${phase.role} ` +
1185
+ `[${postflightOutcome.gateFailure.reasonCode}]: ${postflightOutcome.gateFailure.detail}`, "error");
1186
+ }
1187
+ else {
1188
+ ctx.ui.notify(`× forge:run-task — postflight gate failed for phase ${phase.role}; halting.`, "error");
1189
+ }
1190
+ // Do NOT advance FSM — write state at current phaseIndex (halted)
1191
+ writeState(cwd, {
1192
+ taskId,
1193
+ phaseIndex: currentPhaseIndex,
1194
+ iterationCounts,
1195
+ halted: true,
1196
+ lastError: `postflight gate exit 1 for ${phase.role}`,
1197
+ savedAt: new Date().toISOString(),
1198
+ });
1199
+ // Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
1200
+ if (postflightOutcome.gateFailure) {
1201
+ const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
1202
+ void runHaltAdvisor({
1203
+ gateFailure: postflightOutcome.gateFailure,
1204
+ advisorModel,
1205
+ taskId,
1206
+ cwd,
1207
+ ctx: { ui: ctx.ui },
1208
+ forgeRoot,
1209
+ });
1210
+ }
1211
+ return {
1212
+ status: "halted",
1213
+ lastPhaseIndex: currentPhaseIndex,
1214
+ iterationCounts,
1215
+ lastError: `postflight gate exit 1 for ${phase.role}`,
1216
+ };
1217
+ }
1218
+ // "ok" or "error" — proceed to advance
1219
+ }
920
1220
  // ── Advance to next phase ─────────────────────────────────────
921
1221
  registry.completePhase(taskId, phase.role, "completed");
1222
+ tree.completeNode(phaseNodeId, "completed");
1223
+ tree.setNodeUsage(phaseNodeId, { input: result.usage.input, output: result.usage.output, cacheRead: result.usage.cacheRead });
1224
+ if (result.model)
1225
+ tree.setNodeModel(phaseNodeId, result.model, result.provider ?? "");
922
1226
  writeState(cwd, {
923
1227
  taskId,
924
1228
  phaseIndex: currentPhaseIndex,
@@ -1058,6 +1362,9 @@ export function registerRunTask(pi, options = {}) {
1058
1362
  // steal arrow keys from ctx.ui.select / ctx.ui.confirm dialogs.
1059
1363
  const registry = getSessionRegistry();
1060
1364
  registry.startSession(taskId);
1365
+ // Bridge: also register in OrchestratorTree for the dashboard overlay.
1366
+ const tree = getOrchestratorTree();
1367
+ tree.startNode(taskId, { label: taskId, kind: "orchestrator" });
1061
1368
  const signal = registry.getAbortSignal(taskId);
1062
1369
  const pipelineResult = await runTaskPipeline({
1063
1370
  taskId,
@@ -1070,19 +1377,23 @@ export function registerRunTask(pi, options = {}) {
1070
1377
  resumeFromState,
1071
1378
  signal,
1072
1379
  forgeToolDefs: options.forgeToolDefs,
1380
+ extensionFactories: options.extensionFactories,
1073
1381
  });
1074
1382
  // ── Handle result ────────────────────────────────────────────────
1075
1383
  if (pipelineResult.status === "completed") {
1076
1384
  registry.completeSession(taskId, "completed");
1385
+ tree.completeNode(taskId, "completed");
1077
1386
  ctx.ui.notify(`〇 forge:run-task — ${taskId} pipeline complete (${PHASES.length} phases).`, "info");
1078
1387
  }
1079
1388
  else if (pipelineResult.status === "cancelled") {
1080
1389
  // confirmCancelled was already called by the pipeline, but
1081
1390
  // completeSession("cancelled") ensures the session ends cleanly.
1082
1391
  registry.completeSession(taskId, "cancelled");
1392
+ tree.completeNode(taskId, "cancelled");
1083
1393
  }
1084
1394
  else {
1085
1395
  registry.completeSession(taskId, "failed");
1396
+ tree.completeNode(taskId, "failed");
1086
1397
  }
1087
1398
  ctx.ui.setStatus?.(STATUS_KEY, undefined);
1088
1399
  ctx.ui.setStatus?.(MESSAGE_KEY, undefined);