@entelligentsia/forgecli 1.0.21 → 1.0.25

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 (232) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/CHANGELOG-forge-plugin.md +118 -0
  3. package/dist/extensions/forgecli/forge-tools.d.ts +1 -0
  4. package/dist/extensions/forgecli/forge-tools.js +73 -0
  5. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  6. package/dist/extensions/forgecli/lib/forge-root.d.ts +5 -0
  7. package/dist/extensions/forgecli/lib/forge-root.js +14 -1
  8. package/dist/extensions/forgecli/lib/forge-root.js.map +1 -1
  9. package/dist/extensions/forgecli/orchestrators/fix-bug.d.ts +1 -0
  10. package/dist/extensions/forgecli/orchestrators/fix-bug.js +26 -0
  11. package/dist/extensions/forgecli/orchestrators/fix-bug.js.map +1 -1
  12. package/dist/extensions/forgecli/orchestrators/run-sprint.js +49 -0
  13. package/dist/extensions/forgecli/orchestrators/run-sprint.js.map +1 -1
  14. package/dist/forge-payload/.base-pack/workflows/_fragments/event-emission-schema.md +4 -0
  15. package/dist/forge-payload/.base-pack/workflows/_fragments/event-vocabulary.md +88 -0
  16. package/dist/forge-payload/.base-pack/workflows/commit_task.md +41 -38
  17. package/dist/forge-payload/.base-pack/workflows/implement_plan.md +3 -3
  18. package/dist/forge-payload/.base-pack/workflows-js/wfl-fix-bug.js +42 -6
  19. package/dist/forge-payload/.base-pack/workflows-js/wfl-run-task.js +32 -1
  20. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  21. package/dist/forge-payload/.schemas/enum-catalog.json +2 -2
  22. package/dist/forge-payload/.schemas/event.schema.json +8 -3
  23. package/dist/forge-payload/.schemas/migrations.json +56 -0
  24. package/dist/forge-payload/integrity.json +3 -3
  25. package/dist/forge-payload/meta/store-schema/event.schema.md +7 -0
  26. package/dist/forge-payload/meta/workflows/_fragments/event-emission-schema.md +4 -0
  27. package/dist/forge-payload/meta/workflows/_fragments/event-vocabulary.md +88 -0
  28. package/dist/forge-payload/meta/workflows/meta-commit.md +46 -43
  29. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +7 -2
  30. package/dist/forge-payload/meta/workflows/meta-implement.md +3 -3
  31. package/dist/forge-payload/meta/workflows/meta-orchestrate.md +4 -1
  32. package/dist/forge-payload/schemas/enum-catalog.json +2 -2
  33. package/dist/forge-payload/schemas/event.schema.json +8 -3
  34. package/dist/forge-payload/schemas/structure-manifest.json +4 -2
  35. package/dist/forge-payload/tools/commit-task.cjs +218 -0
  36. package/dist/forge-payload/tools/store-cli.cjs +6 -1
  37. package/node_modules/@mariozechner/clipboard/package.json +2 -1
  38. package/node_modules/@mariozechner/clipboard-linux-x64-musl/README.md +3 -0
  39. package/node_modules/@mariozechner/clipboard-linux-x64-musl/package.json +25 -0
  40. package/package.json +2 -2
  41. package/dist/extensions/forgecli/add-pipeline.d.ts +0 -19
  42. package/dist/extensions/forgecli/add-pipeline.js +0 -143
  43. package/dist/extensions/forgecli/add-pipeline.js.map +0 -1
  44. package/dist/extensions/forgecli/add-task.d.ts +0 -20
  45. package/dist/extensions/forgecli/add-task.js +0 -154
  46. package/dist/extensions/forgecli/add-task.js.map +0 -1
  47. package/dist/extensions/forgecli/approve.d.ts +0 -22
  48. package/dist/extensions/forgecli/approve.js +0 -152
  49. package/dist/extensions/forgecli/approve.js.map +0 -1
  50. package/dist/extensions/forgecli/banner.d.ts +0 -10
  51. package/dist/extensions/forgecli/banner.js +0 -36
  52. package/dist/extensions/forgecli/banner.js.map +0 -1
  53. package/dist/extensions/forgecli/calibrate.d.ts +0 -64
  54. package/dist/extensions/forgecli/calibrate.js +0 -481
  55. package/dist/extensions/forgecli/calibrate.js.map +0 -1
  56. package/dist/extensions/forgecli/collate.d.ts +0 -22
  57. package/dist/extensions/forgecli/collate.js +0 -134
  58. package/dist/extensions/forgecli/collate.js.map +0 -1
  59. package/dist/extensions/forgecli/commit.d.ts +0 -22
  60. package/dist/extensions/forgecli/commit.js +0 -152
  61. package/dist/extensions/forgecli/commit.js.map +0 -1
  62. package/dist/extensions/forgecli/config-command.d.ts +0 -8
  63. package/dist/extensions/forgecli/config-command.js +0 -67
  64. package/dist/extensions/forgecli/config-command.js.map +0 -1
  65. package/dist/extensions/forgecli/config-layer.d.ts +0 -53
  66. package/dist/extensions/forgecli/config-layer.js +0 -72
  67. package/dist/extensions/forgecli/config-layer.js.map +0 -1
  68. package/dist/extensions/forgecli/config-writer.d.ts +0 -16
  69. package/dist/extensions/forgecli/config-writer.js +0 -69
  70. package/dist/extensions/forgecli/config-writer.js.map +0 -1
  71. package/dist/extensions/forgecli/enhance.d.ts +0 -27
  72. package/dist/extensions/forgecli/enhance.js +0 -199
  73. package/dist/extensions/forgecli/enhance.js.map +0 -1
  74. package/dist/extensions/forgecli/fix-bug.d.ts +0 -85
  75. package/dist/extensions/forgecli/fix-bug.js +0 -1580
  76. package/dist/extensions/forgecli/fix-bug.js.map +0 -1
  77. package/dist/extensions/forgecli/forge-header.d.ts +0 -12
  78. package/dist/extensions/forgecli/forge-header.js +0 -114
  79. package/dist/extensions/forgecli/forge-header.js.map +0 -1
  80. package/dist/extensions/forgecli/forge-init.d.ts +0 -26
  81. package/dist/extensions/forgecli/forge-init.js +0 -514
  82. package/dist/extensions/forgecli/forge-init.js.map +0 -1
  83. package/dist/extensions/forgecli/forge-root.d.ts +0 -10
  84. package/dist/extensions/forgecli/forge-root.js +0 -62
  85. package/dist/extensions/forgecli/forge-root.js.map +0 -1
  86. package/dist/extensions/forgecli/forge-update-command.d.ts +0 -100
  87. package/dist/extensions/forgecli/forge-update-command.js +0 -435
  88. package/dist/extensions/forgecli/forge-update-command.js.map +0 -1
  89. package/dist/extensions/forgecli/friction-emit.d.ts +0 -99
  90. package/dist/extensions/forgecli/friction-emit.js +0 -245
  91. package/dist/extensions/forgecli/friction-emit.js.map +0 -1
  92. package/dist/extensions/forgecli/implement.d.ts +0 -22
  93. package/dist/extensions/forgecli/implement.js +0 -170
  94. package/dist/extensions/forgecli/implement.js.map +0 -1
  95. package/dist/extensions/forgecli/init-context.d.ts +0 -99
  96. package/dist/extensions/forgecli/init-context.js +0 -178
  97. package/dist/extensions/forgecli/init-context.js.map +0 -1
  98. package/dist/extensions/forgecli/init-progress.d.ts +0 -39
  99. package/dist/extensions/forgecli/init-progress.js +0 -117
  100. package/dist/extensions/forgecli/init-progress.js.map +0 -1
  101. package/dist/extensions/forgecli/input-router.d.ts +0 -33
  102. package/dist/extensions/forgecli/input-router.js +0 -136
  103. package/dist/extensions/forgecli/input-router.js.map +0 -1
  104. package/dist/extensions/forgecli/lib/halt-advisor.d.ts +0 -59
  105. package/dist/extensions/forgecli/lib/halt-advisor.js +0 -113
  106. package/dist/extensions/forgecli/lib/halt-advisor.js.map +0 -1
  107. package/dist/extensions/forgecli/lib/orchestrator-preflight.d.ts +0 -46
  108. package/dist/extensions/forgecli/lib/orchestrator-preflight.js +0 -64
  109. package/dist/extensions/forgecli/lib/orchestrator-preflight.js.map +0 -1
  110. package/dist/extensions/forgecli/materialize.d.ts +0 -16
  111. package/dist/extensions/forgecli/materialize.js +0 -195
  112. package/dist/extensions/forgecli/materialize.js.map +0 -1
  113. package/dist/extensions/forgecli/migrate.d.ts +0 -22
  114. package/dist/extensions/forgecli/migrate.js +0 -260
  115. package/dist/extensions/forgecli/migrate.js.map +0 -1
  116. package/dist/extensions/forgecli/migration-engine.d.ts +0 -117
  117. package/dist/extensions/forgecli/migration-engine.js +0 -563
  118. package/dist/extensions/forgecli/migration-engine.js.map +0 -1
  119. package/dist/extensions/forgecli/model-registry.d.ts +0 -61
  120. package/dist/extensions/forgecli/model-registry.js +0 -127
  121. package/dist/extensions/forgecli/model-registry.js.map +0 -1
  122. package/dist/extensions/forgecli/model-resolver.d.ts +0 -32
  123. package/dist/extensions/forgecli/model-resolver.js +0 -65
  124. package/dist/extensions/forgecli/model-resolver.js.map +0 -1
  125. package/dist/extensions/forgecli/model-validator.d.ts +0 -29
  126. package/dist/extensions/forgecli/model-validator.js +0 -107
  127. package/dist/extensions/forgecli/model-validator.js.map +0 -1
  128. package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +0 -26
  129. package/dist/extensions/forgecli/orchestrator-status-bar.js +0 -213
  130. package/dist/extensions/forgecli/orchestrator-status-bar.js.map +0 -1
  131. package/dist/extensions/forgecli/plan.d.ts +0 -22
  132. package/dist/extensions/forgecli/plan.js +0 -167
  133. package/dist/extensions/forgecli/plan.js.map +0 -1
  134. package/dist/extensions/forgecli/quiz-agent.d.ts +0 -17
  135. package/dist/extensions/forgecli/quiz-agent.js +0 -98
  136. package/dist/extensions/forgecli/quiz-agent.js.map +0 -1
  137. package/dist/extensions/forgecli/read-command.d.ts +0 -2
  138. package/dist/extensions/forgecli/read-command.js +0 -100
  139. package/dist/extensions/forgecli/read-command.js.map +0 -1
  140. package/dist/extensions/forgecli/regenerate.d.ts +0 -40
  141. package/dist/extensions/forgecli/regenerate.js +0 -438
  142. package/dist/extensions/forgecli/regenerate.js.map +0 -1
  143. package/dist/extensions/forgecli/remove-command.d.ts +0 -17
  144. package/dist/extensions/forgecli/remove-command.js +0 -124
  145. package/dist/extensions/forgecli/remove-command.js.map +0 -1
  146. package/dist/extensions/forgecli/report-bug.d.ts +0 -25
  147. package/dist/extensions/forgecli/report-bug.js +0 -159
  148. package/dist/extensions/forgecli/report-bug.js.map +0 -1
  149. package/dist/extensions/forgecli/retrospective.d.ts +0 -20
  150. package/dist/extensions/forgecli/retrospective.js +0 -126
  151. package/dist/extensions/forgecli/retrospective.js.map +0 -1
  152. package/dist/extensions/forgecli/review-code.d.ts +0 -35
  153. package/dist/extensions/forgecli/review-code.js +0 -196
  154. package/dist/extensions/forgecli/review-code.js.map +0 -1
  155. package/dist/extensions/forgecli/review-plan.d.ts +0 -35
  156. package/dist/extensions/forgecli/review-plan.js +0 -200
  157. package/dist/extensions/forgecli/review-plan.js.map +0 -1
  158. package/dist/extensions/forgecli/run-sprint.d.ts +0 -27
  159. package/dist/extensions/forgecli/run-sprint.js +0 -716
  160. package/dist/extensions/forgecli/run-sprint.js.map +0 -1
  161. package/dist/extensions/forgecli/run-task.d.ts +0 -204
  162. package/dist/extensions/forgecli/run-task.js +0 -1403
  163. package/dist/extensions/forgecli/run-task.js.map +0 -1
  164. package/dist/extensions/forgecli/skill-curation-flag.d.ts +0 -21
  165. package/dist/extensions/forgecli/skill-curation-flag.js +0 -71
  166. package/dist/extensions/forgecli/skill-curation-flag.js.map +0 -1
  167. package/dist/extensions/forgecli/skill-curator-subagent.d.ts +0 -102
  168. package/dist/extensions/forgecli/skill-curator-subagent.js +0 -339
  169. package/dist/extensions/forgecli/skill-curator-subagent.js.map +0 -1
  170. package/dist/extensions/forgecli/skill-retriever.d.ts +0 -84
  171. package/dist/extensions/forgecli/skill-retriever.js +0 -246
  172. package/dist/extensions/forgecli/skill-retriever.js.map +0 -1
  173. package/dist/extensions/forgecli/skill-usage-tracker.d.ts +0 -91
  174. package/dist/extensions/forgecli/skill-usage-tracker.js +0 -224
  175. package/dist/extensions/forgecli/skill-usage-tracker.js.map +0 -1
  176. package/dist/extensions/forgecli/sprint-intake.d.ts +0 -10
  177. package/dist/extensions/forgecli/sprint-intake.js +0 -91
  178. package/dist/extensions/forgecli/sprint-intake.js.map +0 -1
  179. package/dist/extensions/forgecli/sprint-plan.d.ts +0 -14
  180. package/dist/extensions/forgecli/sprint-plan.js +0 -122
  181. package/dist/extensions/forgecli/sprint-plan.js.map +0 -1
  182. package/dist/extensions/forgecli/status-command.d.ts +0 -19
  183. package/dist/extensions/forgecli/status-command.js +0 -140
  184. package/dist/extensions/forgecli/status-command.js.map +0 -1
  185. package/dist/extensions/forgecli/store-error-remediation.d.ts +0 -65
  186. package/dist/extensions/forgecli/store-error-remediation.js +0 -307
  187. package/dist/extensions/forgecli/store-error-remediation.js.map +0 -1
  188. package/dist/extensions/forgecli/store-query.d.ts +0 -22
  189. package/dist/extensions/forgecli/store-query.js +0 -107
  190. package/dist/extensions/forgecli/store-query.js.map +0 -1
  191. package/dist/extensions/forgecli/store-repair.d.ts +0 -17
  192. package/dist/extensions/forgecli/store-repair.js +0 -123
  193. package/dist/extensions/forgecli/store-repair.js.map +0 -1
  194. package/dist/extensions/forgecli/store-resolver.d.ts +0 -56
  195. package/dist/extensions/forgecli/store-resolver.js +0 -263
  196. package/dist/extensions/forgecli/store-resolver.js.map +0 -1
  197. package/dist/extensions/forgecli/store-validator.d.ts +0 -16
  198. package/dist/extensions/forgecli/store-validator.js +0 -32
  199. package/dist/extensions/forgecli/store-validator.js.map +0 -1
  200. package/dist/extensions/forgecli/test-orchestrate.d.ts +0 -2
  201. package/dist/extensions/forgecli/test-orchestrate.js +0 -182
  202. package/dist/extensions/forgecli/test-orchestrate.js.map +0 -1
  203. package/dist/extensions/forgecli/thread-switcher.d.ts +0 -5
  204. package/dist/extensions/forgecli/thread-switcher.js +0 -189
  205. package/dist/extensions/forgecli/thread-switcher.js.map +0 -1
  206. package/dist/extensions/forgecli/transition-guard.d.ts +0 -20
  207. package/dist/extensions/forgecli/transition-guard.js +0 -89
  208. package/dist/extensions/forgecli/transition-guard.js.map +0 -1
  209. package/dist/extensions/forgecli/update-check.d.ts +0 -37
  210. package/dist/extensions/forgecli/update-check.js +0 -185
  211. package/dist/extensions/forgecli/update-check.js.map +0 -1
  212. package/dist/extensions/forgecli/update-tools.d.ts +0 -23
  213. package/dist/extensions/forgecli/update-tools.js +0 -135
  214. package/dist/extensions/forgecli/update-tools.js.map +0 -1
  215. package/dist/extensions/forgecli/validate.d.ts +0 -22
  216. package/dist/extensions/forgecli/validate.js +0 -152
  217. package/dist/extensions/forgecli/validate.js.map +0 -1
  218. package/dist/extensions/forgecli/viewport-events.d.ts +0 -78
  219. package/dist/extensions/forgecli/viewport-events.js +0 -243
  220. package/dist/extensions/forgecli/viewport-events.js.map +0 -1
  221. package/dist/extensions/forgecli/viewport-renderer.d.ts +0 -83
  222. package/dist/extensions/forgecli/viewport-renderer.js +0 -233
  223. package/dist/extensions/forgecli/viewport-renderer.js.map +0 -1
  224. package/dist/extensions/forgecli/viewport-theme.d.ts +0 -11
  225. package/dist/extensions/forgecli/viewport-theme.js +0 -128
  226. package/dist/extensions/forgecli/viewport-theme.js.map +0 -1
  227. package/dist/extensions/forgecli/whats-new-widget.d.ts +0 -26
  228. package/dist/extensions/forgecli/whats-new-widget.js +0 -376
  229. package/dist/extensions/forgecli/whats-new-widget.js.map +0 -1
  230. package/dist/extensions/forgecli/whats-new.d.ts +0 -120
  231. package/dist/extensions/forgecli/whats-new.js +0 -470
  232. package/dist/extensions/forgecli/whats-new.js.map +0 -1
@@ -1,1580 +0,0 @@
1
- // fix-bug.ts — /forge:fix-bug Orchestrator native handler (FORGE-S21-T07).
2
- //
3
- // Promotes /forge:fix-bug from stub to a full TS-driven Orchestrator-archetype
4
- // native handler. Reads `.forge/workflows/fix_bug.md`, chains the bug-specific
5
- // phase sequence (triage → plan-fix → review-plan → implement → review-code →
6
- // approve → commit) by spawning a fresh runForgeSubagent per phase (IL10).
7
- //
8
- // Iron Laws enforced here:
9
- // IL1 — code only under forge-cli/src/extensions/forgecli/
10
- // IL6 — no shell-string interpolation; all external calls via spawnSync argv arrays
11
- // IL7 — every failure path emits ctx.ui.notify and returns; no silent continuation
12
- // IL10 — ALL LLM dispatch goes through runForgeSubagent (NO sendKickoff calls here)
13
- //
14
- // sendKickoff is NEVER called from this file.
15
- // Audit-grep: grep -n "sendKickoff(" fix-bug.ts must return empty.
16
- //
17
- // N-H-C — bugId dual-assignment lifecycle:
18
- // Phase 1 (handler entry, ~line 1372): bugId = `PENDING-${Date.now()}`, isNewBug = true.
19
- // A temporary placeholder; the timestamp is later used to find the real bug record.
20
- // Phase 2 (pre-init, ~line 1495–1500): preCreateBug() writes a minimal bug record with a
21
- // real FORGE-BUG-NNN ID so the triage subagent has a stable ID to reference.
22
- // If preCreateBug fails, the PENDING- placeholder is kept for fallback capture.
23
- // Phase 3 (post-triage, ~line 962–989): capture real ID from BugCreated events emitted by
24
- // the triage subagent; fall back to listing the most-recent bug after pipelineStart if
25
- // event capture fails. The PENDING- prefix is used throughout as a guard for
26
- // state-write paths (see ~line 176 and CallerContextStore guards).
27
- // Reference: PENDING- prefix semantics defined in CallerContextStore guards.
28
- //
29
- // N-H-H — Preflight gate design (closed by FORGE-S25-T17):
30
- // Entry-level: runOrchestratorPreflight is called at runBugPipeline entry (~line 523).
31
- // Validates persona/model config before any LLM dispatch (mirrors run-task.ts design).
32
- // Per-phase: runPreflightGate (store-cli gate) is called per phase (~line 667).
33
- // Evaluates declarative gate conditions from the workflow's gate block.
34
- // This two-level design ensures both structural validity (model/persona config) and
35
- // store-state validity (predecessor verdicts, status guards) are checked.
36
- // Reference: lib/orchestrator-preflight.ts (N-H-H, FORGE-S25-T17).
37
- //
38
- // N-H-E tag: see inline comment at the materialization skip (~line 707 / checkMaterialization).
39
- import { spawnSync } from "node:child_process";
40
- import * as fs from "node:fs";
41
- import * as path from "node:path";
42
- import { fileURLToPath } from "node:url";
43
- import { assertAudience, CallerContextStore } from "./audience-gate.js";
44
- // ModelRegistry/AuthStorage no longer instantiated here — use ctx.modelRegistry
45
- // so extension-registered providers (registered against the live session) are
46
- // visible to validateModelConfig. Creating a fresh registry here would miss
47
- // them and produce spurious MODEL_UNAVAILABLE warnings (FORGE-BUG-001).
48
- import { loadLayeredConfig } from "./config-layer.js";
49
- import { loadForgePersona, runForgeSubagent } from "./forge-subagent.js";
50
- import { getSubagentTools } from "./forge-tools.js";
51
- import { readPersonaDir as readPersonaDirBug, readPipelineNames as readPipelineNamesBug, } from "./lib/catalog-helpers.js";
52
- import { discoverForgeConfigCached } from "./lib/forge-config.js";
53
- import { resolveAdvisorModel, runHaltAdvisor } from "./lib/halt-advisor.js";
54
- import { checkMaterialization } from "./lib/manifest-checker.js";
55
- import { runOrchestratorPreflight } from "./lib/orchestrator-preflight.js";
56
- import { resolveModelForPhase } from "./model-resolver.js";
57
- import { loadWorkflow } from "./parsers/workflow-loader.js";
58
- import { buildPhaseEvent, buildSummariesBlock, drainFrictionFile, emitEvent, emitIncompletePhaseEvent, findPredecessorIndex, formatLocalTime, isNonInteractive, judgementFromSummary, runPreflightGateWithData, validateId, } from "./run-task.js";
59
- import { getSessionRegistry } from "./session-registry.js";
60
- import { getOrchestratorTree } from "./orchestrator-tree.js";
61
- import { OrchestratorTranscriptWriter } from "./subagent/orchestrator-transcript.js";
62
- import { resolveToCanonicalId, resolveToolDir } from "./store-resolver.js";
63
- import { attachViewportObserver } from "./viewport-events.js";
64
- import { fmtPhaseSummary } from "./viewport-renderer.js";
65
- // ── Bug phase descriptor table ──────────────────────────────────────────────
66
- //
67
- // Decoded from .forge/workflows/fix_bug.md and the task prompt's BUG_PHASES.
68
- // triage / plan-fix / implement all read the same fix_bug.md body — the
69
- // workflow handles all three phases through prose.
70
- // FORGE-S25-T16: readPersonaDirBug / readPipelineNamesBug extracted to
71
- // lib/catalog-helpers.ts and imported above with aliases (H-4, N-H-G).
72
- export const BUG_PHASES = [
73
- // FORGE-BUG-040: each phase points at its own phase-scoped subagent workflow.
74
- // Previously triage/plan-fix/implement all pointed at fix_bug.md (the
75
- // orchestrator-only body), which caused the triage subagent to execute
76
- // the full lifecycle in a single invocation. plan-fix and implement reuse
77
- // plan_task.md / implement_plan.md (bug-mode) per meta-fix-bug.md
78
- // § Pipeline Phases — the bug-mode entity-kind detection is built into
79
- // those workflows already.
80
- { role: "triage", workflowFile: "triage", personaNoun: "bug-fixer", isReview: false, maxIterations: 1 },
81
- { role: "plan-fix", workflowFile: "plan_task", personaNoun: "engineer", isReview: false, maxIterations: 1 },
82
- { role: "review-plan", workflowFile: "review_plan", personaNoun: "supervisor", isReview: true, maxIterations: 3 },
83
- { role: "implement", workflowFile: "implement_plan", personaNoun: "engineer", isReview: false, maxIterations: 1 },
84
- { role: "review-code", workflowFile: "review_code", personaNoun: "supervisor", isReview: true, maxIterations: 3 },
85
- { role: "approve", workflowFile: "architect_approve", personaNoun: "architect", isReview: true, maxIterations: 3 },
86
- { role: "commit", workflowFile: "commit_task", personaNoun: "engineer", isReview: false, maxIterations: 1 },
87
- ];
88
- // FORGE-BUG-040: BUG_SUMMARY_KEY_BY_ROLE lives in
89
- // subagent/phase-summary-map.ts so the new phase-guard.ts can import
90
- // it without dragging fix-bug.ts into a forge-tools import cycle.
91
- // Re-exported here for backwards-compatibility with existing call sites.
92
- export { BUG_SUMMARY_KEY_BY_ROLE } from "./subagent/phase-summary-map.js";
93
- import { BUG_SUMMARY_KEY_BY_ROLE } from "./subagent/phase-summary-map.js";
94
- // Bug-event type tokens — explicit mapping per review finding #3.
95
- // Non-review phases always emit the pass token. Review phases select
96
- // pass or fail based on ec.judgement.verdict.
97
- export const BUG_TYPE_TOKENS = {
98
- triage: { pass: "bug-triaged", fail: "bug-triaged" },
99
- "plan-fix": { pass: "fix-planned", fail: "fix-planned" },
100
- "review-plan": { pass: "fix-review-passed", fail: "fix-review-failed" },
101
- implement: { pass: "fix-implemented", fail: "fix-implemented" },
102
- "review-code": { pass: "fix-code-review-passed", fail: "fix-code-review-failed" },
103
- approve: { pass: "fix-approved", fail: "fix-revision-requested" },
104
- commit: { pass: "bug-committed", fail: "bug-commit-failed" },
105
- };
106
- // ── Bug FSM transitions ────────────────────────────────────────────────────
107
- // Mirrors store-cli BUG_TRANSITIONS. Terminal: `fixed`.
108
- // `approved` and `verified` enum values were dropped in forge v0.44.0
109
- // (FORGE-BUG-002 trap). The canonical source is store-cli.cjs.
110
- const BUG_TERMINAL_STATES = new Set(["fixed"]);
111
- function bugStateFilePath(cwd, bugId, sessionId) {
112
- if (!validateId(bugId)) {
113
- throw new Error(`Invalid bugId for state file path: ${bugId}`);
114
- }
115
- const suffix = sessionId ?? process.env.FORGE_SESSION_ID ?? `${process.pid}`;
116
- return path.join(cwd, ".forge", "cache", `fix-bug-state-${bugId}-${suffix}.json`);
117
- }
118
- export function readBugState(cwd, bugId, sessionId) {
119
- // If a specific session ID is given, read that file directly.
120
- if (sessionId || process.env.FORGE_SESSION_ID) {
121
- const fp = bugStateFilePath(cwd, bugId, sessionId);
122
- try {
123
- if (!fs.existsSync(fp))
124
- return null;
125
- const raw = fs.readFileSync(fp, "utf8");
126
- return JSON.parse(raw);
127
- }
128
- catch {
129
- return null;
130
- }
131
- }
132
- // No specific session — glob for the most recent matching state file.
133
- // Single-writer assumption: normally only one session per bug.
134
- const cacheDir = path.join(cwd, ".forge", "cache");
135
- const prefix = `fix-bug-state-${bugId}-`;
136
- let bestFile = null;
137
- let bestMtime = 0;
138
- try {
139
- const entries = fs.readdirSync(cacheDir);
140
- for (const entry of entries) {
141
- if (!entry.startsWith(prefix) || !entry.endsWith(".json"))
142
- continue;
143
- const fp = path.join(cacheDir, entry);
144
- try {
145
- const st = fs.statSync(fp);
146
- if (st.mtimeMs > bestMtime) {
147
- bestMtime = st.mtimeMs;
148
- bestFile = fp;
149
- }
150
- }
151
- catch { }
152
- }
153
- }
154
- catch {
155
- return null;
156
- }
157
- if (!bestFile)
158
- return null;
159
- try {
160
- const raw = fs.readFileSync(bestFile, "utf8");
161
- return JSON.parse(raw);
162
- }
163
- catch {
164
- return null;
165
- }
166
- }
167
- export function writeBugState(cwd, state) {
168
- // Guard: never write state for PENDING bugIds — wait for real bugId capture.
169
- if (state.bugId.startsWith("PENDING-"))
170
- return;
171
- const fp = bugStateFilePath(cwd, state.bugId);
172
- const dir = path.dirname(fp);
173
- fs.mkdirSync(dir, { recursive: true });
174
- fs.writeFileSync(fp, JSON.stringify(state, null, 2), "utf8");
175
- }
176
- export function deleteBugState(cwd, bugId) {
177
- // Clean up all state files for this bug (all sessions)
178
- const cacheDir = path.join(cwd, ".forge", "cache");
179
- const statePrefix = `fix-bug-state-${bugId}-`;
180
- const debugPrefix = `fix-bug-debug-${bugId}`;
181
- try {
182
- const entries = fs.readdirSync(cacheDir);
183
- for (const entry of entries) {
184
- if ((entry.startsWith(statePrefix) && entry.endsWith(".json")) || entry.startsWith(debugPrefix)) {
185
- try {
186
- fs.unlinkSync(path.join(cacheDir, entry));
187
- }
188
- catch {
189
- /* non-fatal */
190
- }
191
- }
192
- }
193
- }
194
- catch {
195
- // non-fatal
196
- }
197
- }
198
- export function isBugStateStale(state) {
199
- const savedAt = new Date(state.savedAt).getTime();
200
- const ageMs = Date.now() - savedAt;
201
- const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
202
- return ageMs > sevenDaysMs;
203
- }
204
- export function readBugRecord(bugId, storeCli, cwd) {
205
- const result = spawnSync("node", [storeCli, "read", "bug", bugId], { cwd, encoding: "utf8" });
206
- if (result.status !== 0)
207
- return null;
208
- try {
209
- const raw = typeof result.stdout === "string" ? result.stdout : String(result.stdout);
210
- return JSON.parse(raw);
211
- }
212
- catch {
213
- return null;
214
- }
215
- }
216
- // Pre-assigns a real FORGE-BUG-NNN ID by listing existing bugs and incrementing.
217
- // Returns the next ID in sequence, e.g. "FORGE-BUG-003" if bugs 001 and 002 exist.
218
- export function assignNextBugId(storeCli, cwd) {
219
- const result = spawnSync("node", [storeCli, "list", "bug", "--json"], { cwd, encoding: "utf8" });
220
- let maxNum = 0;
221
- if (result.status === 0 && result.stdout) {
222
- try {
223
- const bugs = JSON.parse(result.stdout);
224
- if (Array.isArray(bugs)) {
225
- for (const b of bugs) {
226
- const m = String(b.bugId ?? "").match(/FORGE-BUG-(\d+)/);
227
- if (m) {
228
- const n = parseInt(m[1], 10);
229
- if (n > maxNum)
230
- maxNum = n;
231
- }
232
- }
233
- }
234
- }
235
- catch {
236
- /* empty store — start from 1 */
237
- }
238
- }
239
- const next = maxNum + 1;
240
- return `FORGE-BUG-${String(next).padStart(3, "0")}`;
241
- }
242
- // Pre-creates a minimal bug record so the subagent has a real ID to work with.
243
- export function preCreateBug(bugId, title, storeCli, cwd) {
244
- const data = {
245
- bugId,
246
- title,
247
- severity: "minor",
248
- status: "reported",
249
- path: `engineering/bugs/${bugId}`,
250
- reportedAt: new Date().toISOString(),
251
- };
252
- const result = spawnSync("node", [storeCli, "write", "bug", JSON.stringify(data)], { cwd, encoding: "utf8" });
253
- return result.status === 0;
254
- }
255
- export function readBugVerdict(bugRecord, phaseRole, summaryKeyByRole) {
256
- if (!bugRecord)
257
- return "missing";
258
- // Approve phase: read approve summary verdict (set via set-bug-summary).
259
- // The forge v0.44.0 contract makes summaries.approve.verdict the canonical
260
- // approve signal for bugs — `bug.status` does NOT carry an "approved"
261
- // value (that enum was dropped). See read-verdict.cjs §
262
- // BUG_PHASE_VERDICT_SOURCE for the matching plugin-side wiring.
263
- if (phaseRole === "approve") {
264
- const summaryKey = summaryKeyByRole["approve"];
265
- if (summaryKey) {
266
- const summaries = bugRecord.summaries ?? {};
267
- const blob = summaries[summaryKey];
268
- if (blob && typeof blob === "object") {
269
- const verdict = blob?.verdict;
270
- if (typeof verdict === "string") {
271
- if (verdict === "approved")
272
- return "approved";
273
- if (verdict === "revision")
274
- return "revision";
275
- }
276
- }
277
- }
278
- return "missing";
279
- }
280
- // Commit phase: read bug status directly. Terminal target is `fixed`.
281
- if (phaseRole === "commit") {
282
- if (bugRecord.status === "fixed")
283
- return "approved";
284
- // in-progress means commit did not advance status — treat as revision-needed.
285
- if (bugRecord.status === "in-progress")
286
- return "revision";
287
- return "missing";
288
- }
289
- // Review phases: read from summaries via key map.
290
- const summaryKey = summaryKeyByRole[phaseRole];
291
- if (!summaryKey)
292
- return "missing";
293
- const summaries = bugRecord.summaries ?? {};
294
- const blob = summaries[summaryKey];
295
- if (!blob || typeof blob !== "object")
296
- return "missing";
297
- const verdict = blob?.verdict;
298
- if (typeof verdict !== "string")
299
- return "missing";
300
- if (verdict === "approved")
301
- return "approved";
302
- if (verdict === "revision")
303
- return "revision";
304
- return "missing";
305
- }
306
- // ── Bug body composition ──────────────────────────────────────────────────
307
- export function composeBugBody(subWorkflowMd, bugId, phaseRole, bugStatusBeforePhase, summariesBlock) {
308
- // Entity-kind override block prepended before workflow body.
309
- // Conforms to forge v0.44.x meta-fix-bug contract:
310
- // - bug.status enum is {reported, triaged, in-progress, fixed}; `fixed` is terminal.
311
- // - `approved` and `verified` are NOT valid bug status values (dropped in v0.44.0).
312
- // - Approve phase: NO status write. Architect writes summaries.approve.verdict
313
- // via set-bug-summary; verdict signal IS the summary (read by
314
- // read-verdict.cjs § BUG_PHASE_VERDICT_SOURCE).
315
- // - Commit phase: status → fixed (the only status transition post-triage).
316
- //
317
- // Earlier revisions of this prompt told the architect to write
318
- // `update-status bug ... approved` and the engineer to write `... verified`.
319
- // Those instructions produced the FORGE-BUG-002 trap (LLM-translation of
320
- // task-shaped approve workflow → illegal transition through a terminal state).
321
- // The new contract removes the trap at its source.
322
- const entityKindLines = [
323
- `Bug ID: ${bugId}`,
324
- "",
325
- "⚠ ENTITY KIND OVERRIDE: This is a bug, not a task.",
326
- "- All `update-status` calls must use entity kind `bug` (not `task`).",
327
- "- Approve phase: NO status write. Write the approval verdict via set-bug-summary:",
328
- ` node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary ${bugId} approve <APPROVE-SUMMARY.json>`,
329
- ` The summary's "verdict" field MUST be "approved" or "revision". The downstream commit gate reads this, not bug.status.`,
330
- `- Commit phase: on successful git commit, run \`node "$FORGE_ROOT/tools/store-cli.cjs" update-status bug ${bugId} status fixed\` (terminal).`,
331
- `- Do NOT write "approved" or "verified" to bug.status — those values were removed from the schema in forge v0.44.0.`,
332
- `- Do NOT reference task-specific status values (e.g., "committed") or task entity kind.`,
333
- "- CRITICAL: All `set-summary` calls must use `set-bug-summary` (not `set-summary`).",
334
- ` e.g. node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary ${bugId} review_plan <jsonFile>`,
335
- `- Preflight gate: use \`--bug\` flag (not \`--task\`). e.g. node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-plan --bug ${bugId}`,
336
- "- Skip re-running preflight-gate — the orchestrator already checked it. Proceed directly to the review.",
337
- 'Any workflow text that says "task" should be read as "bug" for this context.',
338
- ];
339
- // Phase-specific reinforcement when the orchestrator can name the current status.
340
- if (phaseRole === "approve" && bugStatusBeforePhase) {
341
- entityKindLines.push(`- Approve phase (reinforce): bug.status is currently '${bugStatusBeforePhase}' and MUST NOT change in this phase. Record verdict in summaries.approve only.`);
342
- }
343
- if (phaseRole === "commit" && bugStatusBeforePhase) {
344
- entityKindLines.push(`- Commit phase: after the git commit lands, transition bug.status from '${bugStatusBeforePhase}' to 'fixed'.`);
345
- }
346
- // FORGE-BUG-040: the triage-phase hint block previously prepended here
347
- // compensated for the orchestrator-only fix_bug.md being delivered to
348
- // the triage subagent. With the new phase-scoped triage.md sub-workflow,
349
- // the route-field contract and Path A/B criteria are documented natively
350
- // in the workflow body — no compose-time injection required.
351
- const parts = [
352
- `Read the workflow below and follow it. Bug ID: ${bugId}.`,
353
- "",
354
- "---",
355
- "",
356
- entityKindLines.join("\n"),
357
- "",
358
- "---",
359
- "",
360
- ];
361
- if (summariesBlock) {
362
- parts.push(summariesBlock, "", "---", "");
363
- }
364
- parts.push(subWorkflowMd.trim());
365
- return parts.join("\n");
366
- }
367
- // ── BugId capture via tool_execution_end ──────────────────────────────────
368
- const BUG_WRITE_TOOL_NAMES = new Set(["write", "store-cli", "bash", "forge_store"]);
369
- /**
370
- * Scan tool_execution_end events to extract the bugId written by a triage
371
- * subagent. Returns the LAST matching tool call's bugId, or null if none found.
372
- *
373
- * In pi runtime, the forge_store tool is registered as "forge_store" (not
374
- * "store-cli"). In Claude Code runtime, subagents may shell out via Bash.
375
- * This function covers all three paths.
376
- */
377
- export function extractBugIdFromEvents(events) {
378
- let lastBugId = null;
379
- for (const event of events) {
380
- if (!event.toolName)
381
- continue;
382
- // Check for store-cli write bug calls (Claude Code runtime)
383
- if (event.toolName === "store-cli") {
384
- const result = event.result;
385
- if (typeof result === "string") {
386
- const match = result.match(/FORGE-BUG-\d+/);
387
- if (match)
388
- lastBugId = match[0];
389
- }
390
- else if (result && typeof result === "object") {
391
- const obj = result;
392
- if (typeof obj.bugId === "string" && obj.bugId.startsWith("FORGE-BUG-")) {
393
- lastBugId = obj.bugId;
394
- }
395
- }
396
- }
397
- // Check for forge_store tool calls (pi runtime)
398
- // The pi extension registers the tool as "forge_store", not "store-cli".
399
- if (event.toolName === "forge_store" && event.result != null) {
400
- const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
401
- const match = output.match(/FORGE-BUG-\d+/);
402
- if (match)
403
- lastBugId = match[0];
404
- }
405
- // Also check for write operations to .forge/store/bugs/
406
- if (event.toolName === "write" && typeof event.result === "string") {
407
- const match = event.result.match(/(FORGE-BUG-\d+)/);
408
- if (match)
409
- lastBugId = match[0];
410
- }
411
- // Bash events: subagents shelling out via Bash may run "store-cli write bug".
412
- // Only match when output includes store-cli, write, and bug together
413
- // to avoid false positives from unrelated Bash commands that happen to
414
- // mention a bug ID in a different context.
415
- if (event.toolName === "bash" && event.result != null) {
416
- const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
417
- if (output.includes("store-cli") && output.includes("write") && output.includes("bug")) {
418
- const match = output.match(/FORGE-BUG-\d+/);
419
- if (match)
420
- lastBugId = match[0];
421
- }
422
- }
423
- }
424
- return lastBugId;
425
- }
426
- const STATUS_KEY = "forge:fix-bug";
427
- const MESSAGE_KEY = "forge:fix-bug:message";
428
- export async function runBugPipeline(opts) {
429
- const { bugId: initialBugId, originalArg, isNewBug, cwd, ctx, forgeRoot, storeCli, preflightGate, registry, resumeFromState, } = opts;
430
- const tree = getOrchestratorTree();
431
- // Mutable bugId — for new bugs, pre-assign a real FORGE-BUG-NNN ID
432
- // before triage so the subagent never needs to create or discover one.
433
- // This replaces the fragile PENDING→capture pattern where the subagent was
434
- // expected to create the bug record and we'd fish the ID from events.
435
- let bugId = initialBugId;
436
- let currentPhaseIndex = resumeFromState?.phaseIndex ?? 0;
437
- const iterationCounts = resumeFromState?.iterationCounts ?? {};
438
- let lastModel;
439
- let lastProvider;
440
- // ── Per-persona model routing (Plan 16) ─────────────────────────────────
441
- // Load layered routing config once at bug-pipeline entry. Empty / absent
442
- // config produces inherit for every phase — no behaviour change. Pipeline
443
- // name "fix-bug" lets users configure per-phase overrides distinctly from
444
- // task pipelines under pipelines["fix-bug"] in their routing config.
445
- // N-B-E: surface schema errors to caller (Decision 9 — orchestrators fail-fast).
446
- // See doc/decisions/layered-config-error-policy.md.
447
- const { merged: modelRoutingConfig, errors: layeredConfigErrors } = loadLayeredConfig(cwd);
448
- if (layeredConfigErrors.length > 0) {
449
- for (const e of layeredConfigErrors) {
450
- ctx.ui.notify(`× forge:fix-bug — forge-cli config schema error: ${e}`, "error");
451
- }
452
- return {
453
- status: "failed",
454
- lastPhaseIndex: currentPhaseIndex,
455
- iterationCounts,
456
- lastError: `forge-cli config schema errors: ${layeredConfigErrors.join("; ")}`,
457
- };
458
- }
459
- // Pre-flight validation — same shape as run-task / run-sprint.
460
- // FORGE-S25-T17: delegated to lib/orchestrator-preflight.ts (H-13).
461
- {
462
- const personasDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "forge-payload", ".base-pack", "personas");
463
- const personaCatalogue = readPersonaDirBug(personasDir);
464
- const forgeCfgPath = path.join(cwd, ".forge", "config.json");
465
- const pipelineCatalogue = readPipelineNamesBug(forgeCfgPath);
466
- const availableModels = ctx.modelRegistry?.getAvailable?.() ?? [];
467
- const preflightResult = runOrchestratorPreflight({
468
- mode: "task",
469
- ctx,
470
- notifyPrefix: "forge:fix-bug",
471
- personaCatalogue,
472
- pipelineCatalogue,
473
- modelRoutingConfig,
474
- availableModels: availableModels.map((m) => ({ provider: m.provider, id: m.id })),
475
- });
476
- if (!preflightResult.proceed) {
477
- return {
478
- ...preflightResult.result,
479
- lastPhaseIndex: currentPhaseIndex,
480
- iterationCounts,
481
- };
482
- }
483
- }
484
- // ── Orchestrator transcript ──────────────────────────────────────────
485
- // One JSONL file per pipeline run, ISO-prefixed in its filename so
486
- // review-loop iterations (plan → review → plan → review) preserve
487
- // their own logs instead of overwriting each other. Captures every
488
- // ctx.ui.notify line plus structured phase-boundary events.
489
- const orchTranscript = new OrchestratorTranscriptWriter({
490
- cwd,
491
- entityKind: "bug",
492
- entityId: bugId,
493
- });
494
- const __origNotify = ctx.ui.notify.bind(ctx.ui);
495
- ctx.ui.notify = ((msg, level) => {
496
- __origNotify(msg, level);
497
- orchTranscript.record({
498
- kind: "notify",
499
- ts: new Date().toISOString(),
500
- level: (level ?? "info"),
501
- message: typeof msg === "string" ? msg : String(msg),
502
- });
503
- });
504
- const pipelineStartMs = Date.now();
505
- try {
506
- while (currentPhaseIndex < BUG_PHASES.length) {
507
- // ── Between-phase cancellation gate ────────────────────────────
508
- if (opts.signal?.aborted) {
509
- ctx.ui.notify(`⊘ forge:fix-bug — ${bugId} cancelled by user.`, "info");
510
- registry.completePhase(bugId, BUG_PHASES[currentPhaseIndex]?.role ?? "unknown", "cancelled");
511
- registry.confirmCancelled(bugId);
512
- // ADR-S21-01: preserve state file so cancelled runs are resumable
513
- writeBugState(cwd, {
514
- bugId,
515
- phaseIndex: currentPhaseIndex,
516
- iterationCounts,
517
- halted: false,
518
- status: "cancelled",
519
- lastError: undefined,
520
- savedAt: new Date().toISOString(),
521
- });
522
- return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
523
- }
524
- const phase = BUG_PHASES[currentPhaseIndex];
525
- if (!phase) {
526
- ctx.ui.notify(`× forge:fix-bug — invalid phase index ${currentPhaseIndex}`, "error");
527
- return {
528
- status: "failed",
529
- lastPhaseIndex: currentPhaseIndex,
530
- iterationCounts,
531
- lastError: `invalid phase index ${currentPhaseIndex}`,
532
- };
533
- }
534
- ctx.ui.setStatus?.(STATUS_KEY, `fix-bug ${bugId}: phase ${currentPhaseIndex + 1}/${BUG_PHASES.length} (${phase.role})`);
535
- ctx.ui.notify(`→ ${bugId}: ${phase.role} (phase ${currentPhaseIndex + 1}/${BUG_PHASES.length})`, "info");
536
- orchTranscript.record({
537
- kind: "phase-start",
538
- ts: new Date().toISOString(),
539
- phase: phase.role,
540
- phaseIndex: currentPhaseIndex,
541
- phaseCount: BUG_PHASES.length,
542
- attempt: (iterationCounts[phase.role] ?? 0) + 1,
543
- workflowFile: phase.workflowFile,
544
- persona: phase.personaNoun,
545
- });
546
- const subWorkflowPath = path.join(cwd, ".forge", "workflows", `${phase.workflowFile}.md`);
547
- // ── Read sub-workflow ─────────────────────────────────────────
548
- let subWorkflowMd;
549
- let subWorkflowAudience = "any";
550
- try {
551
- const loaded = loadWorkflow(subWorkflowPath);
552
- subWorkflowMd = loaded.rawMarkdown;
553
- subWorkflowAudience = loaded.audience;
554
- }
555
- catch (err) {
556
- const e = err;
557
- ctx.ui.notify(`× forge:fix-bug — failed to read sub-workflow for ${phase.role}: ${e.message ?? "unknown"}`, "error");
558
- writeBugState(cwd, {
559
- bugId,
560
- phaseIndex: currentPhaseIndex,
561
- iterationCounts,
562
- halted: true,
563
- lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
564
- savedAt: new Date().toISOString(),
565
- });
566
- return {
567
- status: "failed",
568
- lastPhaseIndex: currentPhaseIndex,
569
- iterationCounts,
570
- lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
571
- };
572
- }
573
- // ── 6a. Phase skip (state-aware, defense-in-depth) ─────────────
574
- // Belt-and-suspenders alongside the explicit summaries.triage.route
575
- // branch (handled in section 6c below). Some subagents in some
576
- // runtimes still go end-to-end during triage instead of just triaging
577
- // — rather than roll back the work they did, skip non-review phases
578
- // whose output is already reflected in the bug status. Review phases
579
- // are never skipped — they are quality gates that must always run.
580
- //
581
- // Post-v0.44.0: terminal status is `fixed` only. `approved` and
582
- // `verified` are no longer valid bug status values; references
583
- // removed.
584
- const PHASE_SKIP_STATES = {
585
- "plan-fix": new Set(["fixed"]),
586
- implement: new Set(["fixed"]),
587
- commit: new Set(["fixed"]), // commit writes the terminal status; skip if already there
588
- };
589
- const bugNow = readBugRecord(bugId, storeCli, cwd);
590
- const skipStates = PHASE_SKIP_STATES[phase.role];
591
- if (skipStates && bugNow?.status && skipStates.has(bugNow.status) && !phase.isReview) {
592
- ctx.ui.notify(`⊘ forge:fix-bug — skipping ${phase.role}: bug ${bugId} is already '${bugNow.status}' (work already done).`, "info");
593
- // Write a synthetic "approved" summary so downstream `after` predecessor
594
- // verdict checks find a verdict and don't block review phases.
595
- const summaryKey = BUG_SUMMARY_KEY_BY_ROLE[phase.role];
596
- if (summaryKey) {
597
- const synthSummary = {
598
- objective: `Phase ${phase.role} skipped — bug already ${bugNow.status}`,
599
- findings: ["Subagent completed fix during triage (Path A); phase output implicitly satisfied."],
600
- // Non-review phases should have verdict "n/a" — the phase
601
- // didn't produce a gate verdict. This matches the `after
602
- // <phase> = n/a` preflight gate contract. Review phases
603
- // use "approved" since they are gate phases.
604
- verdict: phase.isReview ? "approved" : "n/a",
605
- written_at: new Date().toISOString(),
606
- };
607
- const synthFile = path.join(cwd, ".forge", "cache", `synthetic-summary-${bugId}-${summaryKey}.json`);
608
- fs.writeFileSync(synthFile, JSON.stringify(synthSummary, null, 2), "utf8");
609
- const synthResult = spawnSync("node", [storeCli, "set-bug-summary", bugId, summaryKey, synthFile], {
610
- cwd,
611
- encoding: "utf8",
612
- });
613
- if (synthResult.status !== 0) {
614
- ctx.ui.notify(`⚠ forge:fix-bug — synthetic summary write failed for ${phase.role}: ${String(synthResult.stderr).trim()}`, "warning");
615
- }
616
- try {
617
- fs.unlinkSync(synthFile);
618
- }
619
- catch {
620
- /* non-fatal */
621
- }
622
- }
623
- currentPhaseIndex++;
624
- continue;
625
- }
626
- // ── 6b. Preflight gate ────────────────────────────────────────
627
- // Skip preflight gate for triage phase of new bugs (PENDING- placeholder)
628
- // because the bug record doesn't exist yet — gates referencing bug fields
629
- // would always fail.
630
- //
631
- // Also skip for review phases when the bug is already in a terminal
632
- // state ("fixed"). Path A bugs get fixed during triage, then the
633
- // preflight gate's `forbid bug.status == fixed` and `after implement
634
- // = n/a` checks block review-code/review-plan even though we
635
- // deliberately want to run those reviews. The review subagent handles
636
- // the already-fixed scenario internally.
637
- const pendingBugId = bugId.startsWith("PENDING-");
638
- const bugAlreadyFixed = bugNow?.status === "fixed" && phase.isReview;
639
- if (!pendingBugId && !bugAlreadyFixed && fs.existsSync(preflightGate)) {
640
- const preflightOutcome = runPreflightGateWithData(preflightGate, phase.role, bugId, cwd, "bug");
641
- if (preflightOutcome.result === "halt") {
642
- // Render structured failure reason if available.
643
- if (preflightOutcome.gateFailure) {
644
- ctx.ui.notify(`× forge:fix-bug — preflight gate failed for phase ${phase.role} ` +
645
- `[${preflightOutcome.gateFailure.reasonCode}]: ${preflightOutcome.gateFailure.detail}`, "error");
646
- }
647
- else {
648
- ctx.ui.notify(`× forge:fix-bug — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
649
- }
650
- writeBugState(cwd, {
651
- bugId,
652
- phaseIndex: currentPhaseIndex,
653
- iterationCounts,
654
- halted: true,
655
- lastError: `preflight gate exit 1 for ${phase.role}`,
656
- savedAt: new Date().toISOString(),
657
- });
658
- // Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
659
- if (preflightOutcome.gateFailure) {
660
- const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
661
- void runHaltAdvisor({
662
- gateFailure: preflightOutcome.gateFailure,
663
- advisorModel,
664
- taskId: bugId,
665
- cwd,
666
- ctx: { ui: ctx.ui },
667
- forgeRoot,
668
- });
669
- }
670
- return {
671
- status: "halted",
672
- lastPhaseIndex: currentPhaseIndex,
673
- iterationCounts,
674
- lastError: `preflight gate exit 1 for ${phase.role}`,
675
- };
676
- }
677
- if (preflightOutcome.result === "escalate") {
678
- ctx.ui.notify(`× forge:fix-bug — preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
679
- writeBugState(cwd, {
680
- bugId,
681
- phaseIndex: currentPhaseIndex,
682
- iterationCounts,
683
- halted: true,
684
- lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
685
- savedAt: new Date().toISOString(),
686
- });
687
- return {
688
- status: "escalated",
689
- lastPhaseIndex: currentPhaseIndex,
690
- iterationCounts,
691
- lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
692
- };
693
- }
694
- }
695
- // ── 6. Materialization-marker check ───────────────────────────
696
- // FORGE-BUG-040: every BUG phase is now a true `audience: subagent`
697
- // sub-workflow — triage / plan-fix / implement no longer alias to
698
- // fix_bug.md. The marker check is therefore unconditional; a missing
699
- // marker is a hard failure on the first dispatch.
700
- {
701
- const markerCheck = checkMaterialization(subWorkflowPath, subWorkflowMd);
702
- if (!markerCheck.ok) {
703
- for (const marker of markerCheck.missing) {
704
- ctx.ui.notify(`× workflow regression: ${marker} not found in ${subWorkflowPath}`, "error");
705
- }
706
- return {
707
- status: "failed",
708
- lastPhaseIndex: currentPhaseIndex,
709
- iterationCounts,
710
- lastError: `materialization markers missing: ${markerCheck.missing.join(", ")}`,
711
- };
712
- }
713
- }
714
- // ── 5. Audience check ─────────────────────────────────────────
715
- // FORGE-BUG-040: every BUG phase is a true `audience: subagent`
716
- // workflow now; the previous `fix_bug.md` audience-bypass is gone.
717
- const audienceOk = CallerContextStore.asSubagent(phase.role, () => assertAudience({ workflowName: phase.workflowFile, audience: subWorkflowAudience }, ctx));
718
- if (!audienceOk) {
719
- writeBugState(cwd, {
720
- bugId,
721
- phaseIndex: currentPhaseIndex,
722
- iterationCounts,
723
- halted: true,
724
- lastError: `audience check failed for ${phase.workflowFile}`,
725
- savedAt: new Date().toISOString(),
726
- });
727
- return {
728
- status: "failed",
729
- lastPhaseIndex: currentPhaseIndex,
730
- iterationCounts,
731
- lastError: `audience check failed for ${phase.workflowFile}`,
732
- };
733
- }
734
- // ── Persona load ──────────────────────────────────────────────
735
- let persona;
736
- try {
737
- persona = loadForgePersona(phase.personaNoun, cwd);
738
- }
739
- catch (err) {
740
- const e = err;
741
- ctx.ui.notify(`× forge:fix-bug — persona '${phase.personaNoun}' not found for phase ${phase.role}: ${e.message ?? "unknown"}. ` +
742
- "Run /forge:regenerate to materialize persona files.", "error");
743
- writeBugState(cwd, {
744
- bugId,
745
- phaseIndex: currentPhaseIndex,
746
- iterationCounts,
747
- halted: true,
748
- lastError: `persona load failed: ${e.message ?? "unknown"}`,
749
- savedAt: new Date().toISOString(),
750
- });
751
- return {
752
- status: "failed",
753
- lastPhaseIndex: currentPhaseIndex,
754
- iterationCounts,
755
- lastError: `persona load failed: ${e.message ?? "unknown"}`,
756
- };
757
- }
758
- // ── Read bug record for current status ────────────────────────
759
- // Skip for PENDING bugIds (bug doesn't exist yet).
760
- const bugRecordBefore = pendingBugId ? null : readBugRecord(bugId, storeCli, cwd);
761
- const bugStatusBeforePhase = bugRecordBefore?.status;
762
- // ── 4. Dispatch via runForgeSubagent (IL10) ───────────────────
763
- // NEVER sendKickoff here — that would reproduce issue #30.
764
- // Carry forward prior phase summaries (forge-cli#19).
765
- const bugSummariesBlock = currentPhaseIndex > 0
766
- ? buildSummariesBlock(bugRecordBefore?.summaries) || undefined
767
- : undefined;
768
- let bugBody = composeBugBody(subWorkflowMd, bugId, phase.role, bugStatusBeforePhase, bugSummariesBlock);
769
- // For new bugs in triage, prepend the original free-form text so the
770
- // subagent knows the user-provided bug description to triage.
771
- // The bug record already exists (pre-created with status "reported"),
772
- // so the subagent should update it, not create a new one.
773
- if (phase.role === "triage" && isNewBug && originalArg) {
774
- bugBody = `Bug description: ${originalArg}\n\n---\n\n${bugBody}`;
775
- }
776
- // Phase-scoped progress counters
777
- const phaseStart = Date.now();
778
- // Track tool_execution_end events for bugId capture (Findings #1, #2).
779
- const toolExecutionEvents = [];
780
- // Stabilization debug log
781
- // Skip for PENDING bugIds — create after real bugId is captured.
782
- // Disable entirely with FORGE_DEBUG_LOG=0.
783
- const debugLogDisabled = process.env.FORGE_DEBUG_LOG === "0";
784
- let debugLogPath = null;
785
- let writeDebug = () => { };
786
- if (!pendingBugId && !debugLogDisabled) {
787
- debugLogPath = path.join(cwd, ".forge", "cache", `fix-bug-debug-${bugId}.jsonl`);
788
- writeDebug = (rec) => {
789
- try {
790
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
791
- // Cap at 10 MB: truncate head when size exceeds the cap.
792
- try {
793
- const st = fs.statSync(debugLogPath);
794
- if (st.size > 10 * 1024 * 1024) {
795
- const all = fs.readFileSync(debugLogPath, "utf8");
796
- const lines = all.split("\n");
797
- // Keep last 80% of lines
798
- const keep = Math.floor(lines.length * 0.8);
799
- fs.writeFileSync(debugLogPath, lines.slice(-keep).join("\n"), "utf8");
800
- }
801
- }
802
- catch {
803
- /* file may not exist yet */
804
- }
805
- fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
806
- }
807
- catch {
808
- // non-fatal; debug log is best-effort
809
- }
810
- };
811
- }
812
- writeDebug({ kind: "phase_start", phaseIndex: currentPhaseIndex });
813
- registry.startPhase(bugId, phase.role, currentPhaseIndex);
814
- // Bridge: register phase in OrchestratorTree
815
- const iteration = (iterationCounts[phase.role] ?? 0) + 1;
816
- const phaseNodeId = `${bugId}:${phase.role}:${iteration}`;
817
- tree.startNode(phaseNodeId, {
818
- parentId: bugId,
819
- label: `${phase.role}:${iteration}`,
820
- kind: "leaf",
821
- promptPreview: bugBody.slice(0, 200),
822
- });
823
- const refreshStatus = () => {
824
- if (process.env.FORGE_VERBOSE !== "1")
825
- return;
826
- const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
827
- const tail = observer.state.lastTool ? ` · ${observer.state.lastTool}` : "";
828
- ctx.ui.setStatus?.(STATUS_KEY, `fix-bug ${bugId}: ${phase.role} · t${observer.state.turn} · tools ${observer.state.toolCount}${observer.state.errCount ? ` · err ${observer.state.errCount}` : ""} · ${elapsed}s${tail}`);
829
- };
830
- const observer = attachViewportObserver({
831
- registry,
832
- sessionId: bugId,
833
- phaseRole: phase.role,
834
- beginHeader: `─── phase ${phase.role} begin ───`,
835
- writeDebug,
836
- notify: (msg, level) => ctx.ui.notify(msg, level),
837
- setStatusVerbose: process.env.FORGE_VERBOSE === "1" ? (k, v) => ctx.ui.setStatus?.(k, v) : undefined,
838
- verboseKeys: { messageKey: `${STATUS_KEY}:message` },
839
- afterEach: refreshStatus,
840
- });
841
- // Wrap the observer's onEvent to also capture tool_execution_end events
842
- // for bugId capture downstream (findings #1, #2), plus the first turn_end
843
- // per phase (IL10 visibility — stream-observed model id).
844
- let modelObservedLogged = false;
845
- const onSubagentEvent = (event) => {
846
- if (event?.type === "tool_execution_end") {
847
- toolExecutionEvents.push({ toolName: event.toolName, result: event.result });
848
- }
849
- if (!modelObservedLogged && event?.type === "turn_end" && event.message?.model) {
850
- modelObservedLogged = true;
851
- writeDebug({
852
- kind: "model_observed",
853
- provider: event.message.provider ?? null,
854
- model: event.message.model,
855
- });
856
- }
857
- observer.onEvent(event);
858
- };
859
- // Per-phase model resolution. When config is absent or cascade bottoms
860
- // out, resolves to inherit (model: undefined) — setModel is skipped and
861
- // pi's current model is used. IL10 still holds: result.model below is
862
- // the stream-observed runtime model, not whatever we requested here.
863
- const modelResolution = resolveModelForPhase("fix-bug", phase.role, phase.personaNoun, modelRoutingConfig);
864
- writeDebug({
865
- kind: "requested_model",
866
- requested: modelResolution.model ?? null,
867
- source: modelResolution.source,
868
- persona: phase.personaNoun,
869
- });
870
- let result;
871
- try {
872
- // FORGE-BUG-040: wrap the runForgeSubagent dispatch in the phase
873
- // caller context so downstream tool calls (forge_preflight,
874
- // forge_store update-status / set-bug-summary / set-summary / emit)
875
- // can verify the caller's phase matches the phase named in the
876
- // tool's arguments. This is the single setter of phase context
877
- // for the bug pipeline; the audience-test wrap above is a
878
- // short-lived test, not the canonical dispatch context.
879
- result = await CallerContextStore.asSubagent(phase.role, () => runForgeSubagent({
880
- persona,
881
- task: bugBody,
882
- cwd,
883
- exportTag: `${bugId}__${phase.role}`,
884
- // Sprint-scoped if the bug is attached to one, else bug-scoped.
885
- // Keeps every phase of this bug-fix pipeline in a single cache
886
- // namespace so the system-prompt + persona prefix stays warm
887
- // across the ~10-minute phases.
888
- cacheSessionId: typeof bugRecordBefore?.sprintId === "string"
889
- ? `forge:${bugRecordBefore.sprintId}`
890
- : `forge:bug:${bugId}`,
891
- onEvent: onSubagentEvent,
892
- requestedModel: modelResolution.model,
893
- modelRegistry: ctx.modelRegistry,
894
- signal: opts.signal,
895
- customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
896
- }));
897
- }
898
- catch (err) {
899
- const e = err;
900
- ctx.ui.notify(`× forge:fix-bug — runForgeSubagent threw for phase ${phase.role}: ${e.message ?? "unknown"}`, "error");
901
- writeBugState(cwd, {
902
- bugId,
903
- phaseIndex: currentPhaseIndex,
904
- iterationCounts,
905
- halted: true,
906
- lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
907
- savedAt: new Date().toISOString(),
908
- });
909
- return {
910
- status: "failed",
911
- lastPhaseIndex: currentPhaseIndex,
912
- iterationCounts,
913
- lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
914
- };
915
- }
916
- // ── Post-subagent abort detection ─────────────────────────────────
917
- if (result.stopReason === "aborted" || opts.signal?.aborted) {
918
- ctx.ui.notify(`⊘ forge:fix-bug — ${bugId} phase ${phase.role} cancelled.`, "info");
919
- registry.completePhase(bugId, phase.role, "cancelled");
920
- tree.completeNode(phaseNodeId, "cancelled");
921
- registry.confirmCancelled(bugId);
922
- // Bug B parity with run-task: account billed tokens of the aborted attempt.
923
- // sprintId "bugs" = routing key for bug events (matches success path).
924
- // The optional `type` token is omitted — verdict carries the outcome.
925
- emitIncompletePhaseEvent({
926
- emitCtx: {
927
- entityType: "bug",
928
- bugId,
929
- sprintId: "bugs",
930
- phase,
931
- iteration: (iterationCounts[phase.role] ?? 0) + 1,
932
- startMs: phaseStart,
933
- endMs: Date.now(),
934
- model: result.model ?? "unknown",
935
- provider: result.provider ?? "unknown",
936
- usage: {
937
- input: result.usage.input,
938
- output: result.usage.output,
939
- cacheRead: result.usage.cacheRead,
940
- cacheWrite: result.usage.cacheWrite,
941
- },
942
- judgement: undefined,
943
- storeCli,
944
- cwd,
945
- },
946
- outcome: "aborted",
947
- notes: result.errorMessage ?? result.stopReason ?? undefined,
948
- onDebug: writeDebug,
949
- });
950
- // ADR-S21-01: preserve state file so cancelled runs are resumable
951
- writeBugState(cwd, {
952
- bugId,
953
- phaseIndex: currentPhaseIndex,
954
- iterationCounts,
955
- halted: false,
956
- status: "cancelled",
957
- lastError: undefined,
958
- savedAt: new Date().toISOString(),
959
- });
960
- return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
961
- }
962
- // ── Halt-on-failure ───────────────────────────────────────────
963
- if (result.exitCode !== 0) {
964
- ctx.ui.notify(`× forge:fix-bug — phase ${phase.role} failed (exit ${result.exitCode})` +
965
- (result.errorMessage ? `: ${result.errorMessage}` : "") +
966
- (result.stopReason ? ` [${result.stopReason}]` : ""), "error");
967
- // Bug B parity with run-task: account billed tokens of the failed attempt.
968
- emitIncompletePhaseEvent({
969
- emitCtx: {
970
- entityType: "bug",
971
- bugId,
972
- sprintId: "bugs",
973
- phase,
974
- iteration: (iterationCounts[phase.role] ?? 0) + 1,
975
- startMs: phaseStart,
976
- endMs: Date.now(),
977
- model: result.model ?? "unknown",
978
- provider: result.provider ?? "unknown",
979
- usage: {
980
- input: result.usage.input,
981
- output: result.usage.output,
982
- cacheRead: result.usage.cacheRead,
983
- cacheWrite: result.usage.cacheWrite,
984
- },
985
- judgement: undefined,
986
- storeCli,
987
- cwd,
988
- },
989
- outcome: "failed",
990
- notes: result.errorMessage ?? result.stopReason ?? undefined,
991
- onDebug: writeDebug,
992
- });
993
- writeBugState(cwd, {
994
- bugId,
995
- phaseIndex: currentPhaseIndex,
996
- iterationCounts,
997
- halted: true,
998
- lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
999
- savedAt: new Date().toISOString(),
1000
- });
1001
- return {
1002
- status: "failed",
1003
- lastPhaseIndex: currentPhaseIndex,
1004
- iterationCounts,
1005
- lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
1006
- };
1007
- }
1008
- // Capture model/provider from subagent result.
1009
- if (result.model)
1010
- lastModel = result.model;
1011
- if (result.provider)
1012
- lastProvider = result.provider;
1013
- // ── BugId capture after triage phase (Finding #1, #2) ──────────
1014
- // For new bugs, the triage subagent creates the bug record via store-cli.
1015
- // We capture the bugId by scanning tool_execution_end events.
1016
- if (phase.role === "triage" && isNewBug && bugId.startsWith("PENDING-")) {
1017
- const capturedBugId = extractBugIdFromEvents(toolExecutionEvents);
1018
- if (capturedBugId) {
1019
- ctx.ui.notify(`forge:fix-bug — captured bug ID: ${capturedBugId}`, "info");
1020
- bugId = capturedBugId;
1021
- }
1022
- else {
1023
- // Fallback: list bugs and find the most recent one created after pipeline start.
1024
- const listResult = spawnSync("node", [storeCli, "list", "bug", "--json"], { cwd, encoding: "utf8" });
1025
- if (listResult.status === 0 && listResult.stdout) {
1026
- try {
1027
- const bugs = JSON.parse(listResult.stdout);
1028
- if (Array.isArray(bugs)) {
1029
- // Find most recent bug whose reportedAt is after the pipeline start
1030
- const pipelineStartIso = new Date(parseInt(bugId.replace("PENDING-", ""))).toISOString();
1031
- const recent = bugs
1032
- .filter((b) => b.reportedAt && b.reportedAt >= pipelineStartIso)
1033
- .sort((a, b) => String(b.reportedAt).localeCompare(String(a.reportedAt)))[0];
1034
- if (recent &&
1035
- recent.bugId &&
1036
- typeof recent.bugId === "string" &&
1037
- recent.bugId.startsWith("FORGE-BUG-")) {
1038
- bugId = recent.bugId;
1039
- ctx.ui.notify(`forge:fix-bug — captured bug ID via store fallback: ${bugId}`, "info");
1040
- }
1041
- }
1042
- }
1043
- catch {
1044
- /* parse failure — fall through to assertion */
1045
- }
1046
- }
1047
- }
1048
- // Defensive guard: if bugId is still PENDING after triage, pipeline cannot proceed.
1049
- if (bugId.startsWith("PENDING-")) {
1050
- ctx.ui.notify("× forge:fix-bug — failed to capture real bug ID after triage. Cannot proceed with PENDING placeholder.", "error");
1051
- return {
1052
- status: "failed",
1053
- lastPhaseIndex: currentPhaseIndex,
1054
- iterationCounts,
1055
- lastError: "bugId still PENDING after triage",
1056
- };
1057
- }
1058
- // Re-initialize debug log now that real bugId is available.
1059
- if (!debugLogDisabled) {
1060
- debugLogPath = path.join(cwd, ".forge", "cache", `fix-bug-debug-${bugId}.jsonl`);
1061
- const savedWriteDebug = writeDebug;
1062
- writeDebug = (rec) => {
1063
- try {
1064
- fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
1065
- try {
1066
- const st = fs.statSync(debugLogPath);
1067
- if (st.size > 10 * 1024 * 1024) {
1068
- const all = fs.readFileSync(debugLogPath, "utf8");
1069
- const lines = all.split("\n");
1070
- const keep = Math.floor(lines.length * 0.8);
1071
- fs.writeFileSync(debugLogPath, lines.slice(-keep).join("\n"), "utf8");
1072
- }
1073
- }
1074
- catch {
1075
- /* file may not exist yet */
1076
- }
1077
- fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
1078
- }
1079
- catch {
1080
- // non-fatal
1081
- }
1082
- };
1083
- writeDebug({ kind: "bugid_captured", bugId });
1084
- }
1085
- }
1086
- {
1087
- const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
1088
- const { turn, toolCount, errCount, cumUsage, cumCompression } = observer.state;
1089
- ctx.ui.notify(`✓ ${phase.role}: ${turn} turn${turn === 1 ? "" : "s"} · ${toolCount} tool call${toolCount === 1 ? "" : "s"}${errCount ? ` · ${errCount} err` : ""} · ${elapsed}s`, "info");
1090
- orchTranscript.record({
1091
- kind: "phase-end",
1092
- ts: new Date().toISOString(),
1093
- phase: phase.role,
1094
- phaseIndex: currentPhaseIndex,
1095
- attempt: (iterationCounts[phase.role] ?? 0) + 1,
1096
- verdict: "n/a",
1097
- elapsedMs: Date.now() - phaseStart,
1098
- turns: turn,
1099
- toolCount,
1100
- errCount,
1101
- });
1102
- registry.appendTail(bugId, phase.role, fmtPhaseSummary({
1103
- role: phase.role,
1104
- turns: turn,
1105
- tools: toolCount,
1106
- errors: errCount,
1107
- wallSeconds: elapsed,
1108
- usage: cumUsage,
1109
- model: result.model,
1110
- provider: result.provider,
1111
- compression: cumCompression.tokensSaved > 0 ? cumCompression : undefined,
1112
- }));
1113
- }
1114
- // ── Slice-2: orchestrator emits phase event ──────────────────
1115
- // sprintId for bug event emission is the literal "bugs" (routing key),
1116
- // matching the convention in .forge/workflows/fix_bug.md.
1117
- const phaseEndMs = Date.now();
1118
- const bugRecord = readBugRecord(bugId, storeCli, cwd);
1119
- const sprintId = "bugs"; // routing key for bug events — not a sprint reference
1120
- const phaseIteration = (iterationCounts[phase.role] ?? 0) + 1;
1121
- // Read summary judgement for review phases (using bug summary key map)
1122
- const judgement = phase.isReview
1123
- ? judgementFromSummary(bugRecord ?? null, phase.role, BUG_SUMMARY_KEY_BY_ROLE)
1124
- : undefined;
1125
- const emitCtx = {
1126
- entityType: "bug",
1127
- bugId,
1128
- sprintId, // routing key "bugs" — not a sprint reference
1129
- phase,
1130
- iteration: phaseIteration,
1131
- startMs: phaseStart,
1132
- endMs: phaseEndMs,
1133
- model: result.model ?? "unknown",
1134
- provider: result.provider ?? "unknown",
1135
- usage: {
1136
- input: result.usage.input,
1137
- output: result.usage.output,
1138
- cacheRead: result.usage.cacheRead,
1139
- cacheWrite: result.usage.cacheWrite,
1140
- },
1141
- judgement,
1142
- storeCli,
1143
- cwd,
1144
- };
1145
- const phaseEvent = buildPhaseEvent(emitCtx);
1146
- // Set bug event type based on BUG_TYPE_TOKENS mapping.
1147
- const typeTokenEntry = BUG_TYPE_TOKENS[phase.role];
1148
- if (typeTokenEntry) {
1149
- if (phase.isReview && judgement?.verdict === "revision") {
1150
- phaseEvent.type = typeTokenEntry.fail;
1151
- }
1152
- else {
1153
- phaseEvent.type = typeTokenEntry.pass;
1154
- }
1155
- }
1156
- const emitResult = emitEvent(storeCli, cwd, sprintId, phaseEvent);
1157
- if (!emitResult.ok) {
1158
- ctx.ui.notify(`⚠ forge:fix-bug — phase event emit failed for ${phase.role}: ${emitResult.stderr.trim()}`, "warning");
1159
- writeDebug({ kind: "emit_failed", stderr: emitResult.stderr });
1160
- }
1161
- else {
1162
- writeDebug({ kind: "emit_ok", eventId: phaseEvent.eventId });
1163
- }
1164
- // Drain friction file for this phase.
1165
- const frictionPath = path.join(cwd, ".forge", "cache", `FRICTION-${phase.role}.jsonl`);
1166
- const drain = drainFrictionFile(frictionPath, emitCtx);
1167
- if (drain.emitted + drain.failed > 0) {
1168
- writeDebug({ kind: "friction_drain", ...drain });
1169
- if (drain.failed > 0) {
1170
- ctx.ui.notify(`⚠ forge:fix-bug — friction drain for ${phase.role}: ${drain.emitted} ok, ${drain.failed} failed`, "warning");
1171
- }
1172
- }
1173
- // ── AC §C.16: Bug FSM canonical-enum assertion ────────────────
1174
- // After each phase that could transition bug status, validate the new
1175
- // status via store-cli (single source of truth). Surface a warning (not halt) if invalid.
1176
- const currentBugRecordForAssert = readBugRecord(bugId, storeCli, cwd);
1177
- if (currentBugRecordForAssert && currentBugRecordForAssert.status) {
1178
- // Defer to store-cli's isLegalTransition as authoritative guard.
1179
- // Only warn on statuses store-cli itself would reject.
1180
- const validateResult = spawnSync("node", [storeCli, "validate", "bug", JSON.stringify(currentBugRecordForAssert)], { cwd, encoding: "utf8" });
1181
- if (validateResult.status !== 0) {
1182
- const detail = typeof validateResult.stderr === "string" ? validateResult.stderr.trim() : "unknown";
1183
- ctx.ui.notify(`⚠ forge:fix-bug — bug ${bugId} validation warning: ${detail}`, "warning");
1184
- writeDebug({ kind: "fsm_assertion_warning", bugId, status: currentBugRecordForAssert.status, detail });
1185
- }
1186
- }
1187
- // ── 6b. Verdict check (review phases only) ────────────────────
1188
- if (phase.isReview) {
1189
- // Re-read bug record for latest status after subagent ran
1190
- const updatedBugRecord = readBugRecord(bugId, storeCli, cwd);
1191
- const verdict = readBugVerdict(updatedBugRecord, phase.role, BUG_SUMMARY_KEY_BY_ROLE);
1192
- if (verdict === "missing") {
1193
- ctx.ui.notify(`× forge:fix-bug — verdict missing for phase ${phase.role} after subagent completed. Halting for advisory.`, "error");
1194
- writeBugState(cwd, {
1195
- bugId,
1196
- phaseIndex: currentPhaseIndex,
1197
- iterationCounts,
1198
- halted: true,
1199
- lastError: `verdict missing for ${phase.role}`,
1200
- savedAt: new Date().toISOString(),
1201
- });
1202
- // A missing verdict IS a postflight-outputs failure: the canonical
1203
- // phase summary the subagent must write (e.g. summaries.code_review,
1204
- // linked via set-bug-summary) was never recorded, so there is no
1205
- // verdict to route on. Route it through the halt-recovery advisor
1206
- // (FORGE-S26-T18) — the same hand-off the preflight/postflight gate
1207
- // failures use — instead of a bare escalation. Best-effort, non-fatal.
1208
- const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
1209
- void runHaltAdvisor({
1210
- gateFailure: {
1211
- phase: phase.role,
1212
- reasonCode: "verdict-missing",
1213
- detail: `Phase '${phase.role}' completed but no verdict was found in the store. ` +
1214
- "The canonical phase summary was not written, so the orchestrator has no verdict to route on.",
1215
- remediation: "Re-run the phase and ensure the subagent's forge_store set-bug-summary call " +
1216
- 'uses args:["<bugId>", "<phaseKey>"] with the literal phase key as args[1] ' +
1217
- "(e.g. code_review), and that the call exits zero before the subagent returns.",
1218
- },
1219
- advisorModel,
1220
- taskId: bugId,
1221
- cwd,
1222
- ctx: { ui: ctx.ui },
1223
- forgeRoot,
1224
- });
1225
- return {
1226
- status: "halted",
1227
- lastPhaseIndex: currentPhaseIndex,
1228
- iterationCounts,
1229
- lastError: `verdict missing for ${phase.role}`,
1230
- };
1231
- }
1232
- if (verdict === "revision") {
1233
- iterationCounts[phase.role] = (iterationCounts[phase.role] ?? 0) + 1;
1234
- if (iterationCounts[phase.role] >= phase.maxIterations) {
1235
- ctx.ui.notify(`× forge:fix-bug — revision cap reached for phase ${phase.role} ` +
1236
- `(${iterationCounts[phase.role]}/${phase.maxIterations} iterations). Escalating.`, "error");
1237
- writeBugState(cwd, {
1238
- bugId,
1239
- phaseIndex: currentPhaseIndex,
1240
- iterationCounts,
1241
- halted: true,
1242
- lastError: `revision cap reached for ${phase.role}`,
1243
- savedAt: new Date().toISOString(),
1244
- });
1245
- return {
1246
- status: "escalated",
1247
- lastPhaseIndex: currentPhaseIndex,
1248
- iterationCounts,
1249
- lastError: `revision cap reached for ${phase.role}`,
1250
- };
1251
- }
1252
- // Transition bug back to in-progress before re-dispatching implement.
1253
- // This is required for review-code → implement and approve → implement loops.
1254
- const currentBugStatus = updatedBugRecord?.status;
1255
- if (currentBugStatus === "fixed" || currentBugStatus === "approved") {
1256
- const transitionResult = spawnSync("node", [storeCli, "update-status", "bug", bugId, "status", "in-progress"], { cwd, encoding: "utf8" });
1257
- if (transitionResult.status !== 0) {
1258
- ctx.ui.notify(`⚠ forge:fix-bug — failed to transition bug ${bugId} from ${currentBugStatus} to in-progress: ${transitionResult.stderr ?? "unknown"}`, "warning");
1259
- }
1260
- else {
1261
- ctx.ui.notify(`⟳ forge:fix-bug — transitioned bug ${bugId}: ${currentBugStatus} → in-progress`, "info");
1262
- }
1263
- }
1264
- const predIndex = findPredecessorIndex(BUG_PHASES, currentPhaseIndex);
1265
- ctx.ui.notify(`⟳ forge:fix-bug — ${phase.role} returned revision; looping to ${BUG_PHASES[predIndex]?.role ?? predIndex} ` +
1266
- `(attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`, "info");
1267
- orchTranscript.record({
1268
- kind: "phase-loopback",
1269
- ts: new Date().toISOString(),
1270
- fromPhase: phase.role,
1271
- toPhase: BUG_PHASES[predIndex]?.role ?? String(predIndex),
1272
- fromPhaseIndex: currentPhaseIndex,
1273
- toPhaseIndex: predIndex,
1274
- reason: `${phase.role} returned revision (attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`,
1275
- });
1276
- writeBugState(cwd, {
1277
- bugId,
1278
- phaseIndex: predIndex,
1279
- iterationCounts,
1280
- halted: false,
1281
- savedAt: new Date().toISOString(),
1282
- });
1283
- currentPhaseIndex = predIndex;
1284
- continue;
1285
- }
1286
- // verdict === "approved": fall through to advance
1287
- }
1288
- // ── Advance to next phase ─────────────────────────────────────
1289
- registry.completePhase(bugId, phase.role, "completed");
1290
- tree.completeNode(phaseNodeId, "completed");
1291
- tree.setNodeUsage(phaseNodeId, { input: result.usage.input, output: result.usage.output, cacheRead: result.usage.cacheRead });
1292
- if (result.model)
1293
- tree.setNodeModel(phaseNodeId, result.model, result.provider ?? "");
1294
- writeBugState(cwd, {
1295
- bugId,
1296
- phaseIndex: currentPhaseIndex,
1297
- iterationCounts,
1298
- halted: false,
1299
- savedAt: new Date().toISOString(),
1300
- });
1301
- // ── 6c. Path A / Path B branch (post-triage) ──────────────────
1302
- // Per meta-fix-bug.md § Triage Judgement (forge v0.44.0+), the
1303
- // triage subagent records the route decision in
1304
- // bug.summaries.triage.route. The orchestrator reads it after
1305
- // triage returns and selects the downstream phase list:
1306
- // Path A (short-circuit): skip plan-fix + review-plan
1307
- // Path B (default, full loop): run all phases
1308
- //
1309
- // If route is missing or malformed, default to Path B (the safe
1310
- // choice — running extra phases never produces an unsafe outcome).
1311
- // The PHASE_SKIP_STATES heuristic at section 6a remains as
1312
- // defense-in-depth for cases where the field is missing but the
1313
- // bug status proves the work happened.
1314
- if (phase.role === "triage") {
1315
- const bugAfterTriage = readBugRecord(bugId, storeCli, cwd);
1316
- const triageSummary = bugAfterTriage?.summaries?.triage;
1317
- const route = triageSummary?.route;
1318
- if (route === "A") {
1319
- const skipUntilIndex = BUG_PHASES.findIndex((p) => p.role === "implement");
1320
- if (skipUntilIndex > currentPhaseIndex + 1) {
1321
- ctx.ui.notify(`⊘ forge:fix-bug — Path A selected by triage; skipping plan-fix and review-plan.`, "info");
1322
- currentPhaseIndex = skipUntilIndex;
1323
- continue;
1324
- }
1325
- }
1326
- // route === "B", missing, or any other value → fall through to standard advance
1327
- }
1328
- currentPhaseIndex++;
1329
- }
1330
- // ── All phases complete ───────────────────────────────────────────
1331
- deleteBugState(cwd, bugId);
1332
- orchTranscript.record({
1333
- kind: "pipeline-end",
1334
- ts: new Date().toISOString(),
1335
- outcome: "complete",
1336
- elapsedMs: Date.now() - pipelineStartMs,
1337
- });
1338
- return {
1339
- status: "completed",
1340
- lastPhaseIndex: BUG_PHASES.length - 1,
1341
- iterationCounts,
1342
- model: lastModel,
1343
- provider: lastProvider,
1344
- };
1345
- }
1346
- finally {
1347
- ctx.ui.notify = __origNotify;
1348
- }
1349
- }
1350
- export function registerFixBug(pi, options = {}) {
1351
- pi.registerCommand("forge:fix-bug", {
1352
- description: "Run the full bug-fix pipeline (triage → plan-fix → review-plan → implement → review-code → approve → commit). " +
1353
- "Usage: /forge:fix-bug <BUG_ID_OR_SUMMARY>. " +
1354
- "Orchestrator archetype: each phase is an isolated subagent session (IL10).",
1355
- async handler(args, ctx) {
1356
- const cwd = options.cwd ?? process.cwd();
1357
- const rawArg = args.trim();
1358
- if (!rawArg) {
1359
- ctx.ui.notify("× forge:fix-bug — bug ID or summary required. Usage: /forge:fix-bug <BUG_ID_OR_SUMMARY>", "error");
1360
- return;
1361
- }
1362
- ctx.ui.setStatus?.(STATUS_KEY, `fix-bug: initializing…`);
1363
- // ── Discover forge config ────────────────────────────────────────
1364
- const forgeConfig = discoverForgeConfigCached(cwd);
1365
- if (!forgeConfig) {
1366
- ctx.ui.notify("× forge:fix-bug — no Forge project found at cwd. Run /forge:init first.", "error");
1367
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1368
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1369
- return;
1370
- }
1371
- const forgeRoot = forgeConfig.forgeRoot;
1372
- // Tool paths
1373
- const storeCli = path.join(forgeRoot, "tools", "store-cli.cjs");
1374
- const preflightGate = path.join(forgeRoot, "tools", "preflight-gate.cjs");
1375
- // ── Determine bugId ────────────────────────────────────────────
1376
- let bugId;
1377
- let isNewBug = false;
1378
- // Check if arg looks like it could be a bug ID (prefixed or unprefixed).
1379
- // Covers: FORGE-BUG-042, BUG-042, B042.
1380
- const looksLikeBugId = /^(?:[A-Z0-9]+-)?(?:BUG-?\d+|B\d+)$/i.test(rawArg) || /^BUG-\d+$/i.test(rawArg);
1381
- if (/^FORGE-BUG-\d+$/.test(rawArg)) {
1382
- // Canonical bug ID — verify it exists
1383
- bugId = rawArg;
1384
- const bugRecord = readBugRecord(bugId, storeCli, cwd);
1385
- if (!bugRecord) {
1386
- ctx.ui.notify(`× forge:fix-bug — bug ${bugId} not found in store.`, "error");
1387
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1388
- return;
1389
- }
1390
- // Check if bug is already in a terminal state
1391
- if (BUG_TERMINAL_STATES.has(bugRecord.status ?? "")) {
1392
- ctx.ui.notify(`× forge:fix-bug — bug ${bugId} is already in terminal state '${bugRecord.status}'. No further processing.`, "error");
1393
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1394
- return;
1395
- }
1396
- }
1397
- else if (looksLikeBugId) {
1398
- // Unprefixed bug ID — resolve through the store cascade.
1399
- // Issue #20: unprefixed entity IDs silently poisoned substitutions.
1400
- const toolDir = resolveToolDir(forgeRoot);
1401
- const resolvedBugId = await resolveToCanonicalId(rawArg, toolDir, cwd, "bug", {
1402
- ctx,
1403
- commandLabel: "forge:fix-bug",
1404
- });
1405
- if (!resolvedBugId) {
1406
- // Error already emitted by resolver
1407
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1408
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1409
- return;
1410
- }
1411
- bugId = resolvedBugId;
1412
- // Re-verify the resolved bug exists
1413
- const bugRecord = readBugRecord(bugId, storeCli, cwd);
1414
- if (!bugRecord) {
1415
- ctx.ui.notify(`× forge:fix-bug — bug ${bugId} not found in store.`, "error");
1416
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1417
- return;
1418
- }
1419
- if (BUG_TERMINAL_STATES.has(bugRecord.status ?? "")) {
1420
- ctx.ui.notify(`× forge:fix-bug — bug ${bugId} is already in terminal state '${bugRecord.status}'. No further processing.`, "error");
1421
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1422
- return;
1423
- }
1424
- }
1425
- else {
1426
- // Free-form text — defer bug creation to triage-phase subagent
1427
- // Use a temporary bugId placeholder; will be captured from subagent events
1428
- bugId = `PENDING-${Date.now()}`;
1429
- isNewBug = true;
1430
- }
1431
- // ── Pre-flight confirm ───────────────────────────────────────────
1432
- if (!isNonInteractive()) {
1433
- const confirmMsg = isNewBug
1434
- ? `Fix bug: "${rawArg.slice(0, 80)}"? A bug record will be created during triage.`
1435
- : `Fix bug ${bugId}?`;
1436
- const proceed = await ctx.ui.confirm(`Fix bug?`, confirmMsg);
1437
- if (!proceed) {
1438
- ctx.ui.notify("forge:fix-bug — cancelled.", "info");
1439
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1440
- return;
1441
- }
1442
- }
1443
- // ── Resume detection ─────────────────────────────────────────────
1444
- const registry = getSessionRegistry();
1445
- const existing = isNewBug ? null : readBugState(cwd, bugId);
1446
- let resumeFromState;
1447
- if (existing) {
1448
- if (isBugStateStale(existing)) {
1449
- ctx.ui.notify(`⚠ forge:fix-bug — cached state for ${bugId} is stale (>7 days old, saved at ${formatLocalTime(existing.savedAt)}). Offering purge.`, "warning");
1450
- if (!isNonInteractive()) {
1451
- const purge = await ctx.ui.confirm(`Purge stale state for ${bugId}?`, "The cached state is older than 7 days. Purge and restart from the beginning?");
1452
- if (purge) {
1453
- deleteBugState(cwd, bugId);
1454
- }
1455
- else {
1456
- ctx.ui.notify("forge:fix-bug — stale state kept; aborting.", "info");
1457
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1458
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1459
- return;
1460
- }
1461
- }
1462
- else {
1463
- ctx.ui.notify("forge:fix-bug — stale state; non-interactive mode auto-aborting.", "info");
1464
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1465
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1466
- return;
1467
- }
1468
- }
1469
- else {
1470
- // ADR-S21-01: offer resume for ALL non-stale states — halted=true
1471
- // (explicit failure), halted=false (cancelled/interrupted), and
1472
- // any state with existing.status set.
1473
- const stateStatus = existing.status ?? (existing.halted ? "halted" : "interrupted");
1474
- const statusLabel = stateStatus === "cancelled" ? "cancelled" : stateStatus === "halted" ? "halted" : "interrupted";
1475
- const phaseRole = BUG_PHASES[existing.phaseIndex]?.role ?? existing.phaseIndex;
1476
- if (!isNonInteractive()) {
1477
- const resume = await ctx.ui.confirm(`Resume ${bugId}?`, `Cached state — phase ${existing.phaseIndex} (${phaseRole}), ${statusLabel}, ` +
1478
- `saved at ${formatLocalTime(existing.savedAt)}. Resume from here?`);
1479
- if (resume) {
1480
- resumeFromState = existing;
1481
- ctx.ui.notify(`forge:fix-bug — resuming ${bugId} from phase ${phaseRole} (${statusLabel})`, "info");
1482
- }
1483
- else {
1484
- deleteBugState(cwd, bugId);
1485
- }
1486
- }
1487
- else {
1488
- // Non-interactive: auto-resume from state (no confirmation).
1489
- // Cancelled/interrupted states are valid resume points.
1490
- resumeFromState = existing;
1491
- ctx.ui.notify(`forge:fix-bug — resuming ${bugId} from phase ${phaseRole} (${statusLabel})`, "info");
1492
- }
1493
- }
1494
- }
1495
- // For new bugs, triage phase will create the bug record.
1496
- // After triage, we need to capture the bugId from the subagent events.
1497
- // This is handled inside runBugPipeline via onEvent interception.
1498
- // For now, we pass the temporary bugId; runBugPipeline will update it.
1499
- // ── Materialization check (top-level workflow) ──────────────────
1500
- const workflowPath = path.join(cwd, ".forge", "workflows", "fix_bug.md");
1501
- if (fs.existsSync(workflowPath)) {
1502
- try {
1503
- const loaded = loadWorkflow(workflowPath);
1504
- // AC#12: Top-level audience check for the fix_bug.md workflow.
1505
- // The orchestrator ITSELF runs fix_bug.md (not a subagent), so check
1506
- // from orchestrator context. Using asSubagent would falsely reject
1507
- // orchestrator-only workflows called by the orchestrator.
1508
- const topAudienceOk = CallerContextStore.asOrchestrator(() => assertAudience({ workflowName: "fix_bug", audience: loaded.audience }, ctx));
1509
- if (!topAudienceOk) {
1510
- ctx.ui.notify("× forge:fix-bug — audience check failed for top-level fix_bug workflow.", "error");
1511
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1512
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1513
- return;
1514
- }
1515
- // Note: no materialization-marker check here. fix_bug.md is the
1516
- // orchestrator workflow (prose algorithm), not a sub-workflow that
1517
- // subagents run directly. Per-phase sub-workflows (architect_approve,
1518
- // review_code, etc.) each get their own materialization check inside
1519
- // runBugPipeline at line ~481, which is the correct guard layer.
1520
- }
1521
- catch {
1522
- // Workflow file exists but couldn't be read — non-fatal, continue
1523
- }
1524
- }
1525
- // ── Pre-assign real bug ID for new bugs ────────────────────────
1526
- // Previously this was done inside runBugPipeline, but the session registry
1527
- // needs the real ID before startSession is called.
1528
- if (isNewBug && bugId.startsWith("PENDING-")) {
1529
- const realBugId = assignNextBugId(storeCli, cwd);
1530
- const title = rawArg && !rawArg.startsWith("@") ? rawArg.slice(0, 120) : "New bug (pending triage)";
1531
- if (preCreateBug(realBugId, title, storeCli, cwd)) {
1532
- ctx.ui.notify(`forge:fix-bug — pre-assigned bug ID: ${realBugId}`, "info");
1533
- bugId = realBugId;
1534
- }
1535
- else {
1536
- ctx.ui.notify("× forge:fix-bug — failed to pre-create bug record. Falling back to PENDING capture.", "error");
1537
- }
1538
- }
1539
- // Register session
1540
- registry.startSession(bugId);
1541
- // Bridge: register bug in OrchestratorTree.
1542
- const tree = getOrchestratorTree();
1543
- tree.startNode(bugId, { label: `fix-bug ${bugId}`, kind: "orchestrator" });
1544
- // ── Delegate to pipeline ─────────────────────────────────────────
1545
- // ── Delegate to pipeline ─────────────────────────────────────────
1546
- const signal = registry.getAbortSignal(bugId);
1547
- const pipelineResult = await runBugPipeline({
1548
- bugId,
1549
- originalArg: isNewBug ? rawArg : undefined,
1550
- isNewBug,
1551
- cwd,
1552
- ctx,
1553
- forgeRoot,
1554
- storeCli,
1555
- preflightGate,
1556
- registry,
1557
- resumeFromState,
1558
- signal,
1559
- forgeToolDefs: options.forgeToolDefs,
1560
- });
1561
- // ── Handle result ────────────────────────────────────────────────
1562
- if (pipelineResult.status === "completed") {
1563
- registry.completeSession(bugId, "completed");
1564
- tree.completeNode(bugId, "completed");
1565
- ctx.ui.notify(`〇 forge:fix-bug — ${bugId} pipeline complete (${BUG_PHASES.length} phases).`, "info");
1566
- }
1567
- else if (pipelineResult.status === "cancelled") {
1568
- registry.completeSession(bugId, "cancelled");
1569
- tree.completeNode(bugId, "cancelled");
1570
- }
1571
- else {
1572
- registry.completeSession(bugId, "failed");
1573
- tree.completeNode(bugId, "failed");
1574
- }
1575
- ctx.ui.setStatus?.(STATUS_KEY, undefined);
1576
- ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
1577
- },
1578
- });
1579
- }
1580
- //# sourceMappingURL=fix-bug.js.map