@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,716 +0,0 @@
1
- // run-sprint.ts — /forge:run-sprint native Orchestrator handler (FORGE-S21-T03, Plan 12).
2
- //
3
- // Sprint-level orchestrator that iterates over a sprint's task list,
4
- // delegating per-task execution to runTaskPipeline (extracted from run-task.ts).
5
- //
6
- // The sprint handler does NOT contain its own phase loop; per-phase concerns
7
- // are ALL delegated to runTaskPipeline. The sprint handler owns sprint
8
- // coordination only: resolve sprint, confirm gates, iterate tasks, persist
9
- // sprint state, dispatch architect ceremony, and emit sprint-scoped events.
10
- //
11
- // Plan 12 truth table (§3):
12
- // Clean-complete → architect ceremony (mode=complete) → sprint-complete event
13
- // User-paused → architect ceremony (mode=partial) if ≥1 task done → sprint-complete event (verdict=partial)
14
- // Halted-on-failure → NO ceremony → sprint-halted event
15
- //
16
- // Iron Laws enforced here:
17
- // IL1 — code only under forge-cli/src/extensions/forgecli/
18
- // IL6 — no shell-string interpolation; all external calls via spawnSync argv arrays
19
- // IL7 — every failure path emits ctx.ui.notify and returns; no silent continuation
20
- // IL10 — ALL LLM dispatch goes through runForgeSubagent (NO sendKickoff)
21
- //
22
- // sendKickoff is NEVER called from this file.
23
- // Audit-grep: grep -n "sendKickoff(" run-sprint.ts must return empty.
24
- //
25
- // N-H-D — Ceremony vs per-task model routing:
26
- // The sprint ceremony phase (architect summary / sprint-complete event) uses
27
- // loadLayeredConfig + lookupPersonaModel directly (~lines 216, 227) without
28
- // calling validateModelConfig. This is intentional: the ceremony is a single
29
- // LLM call with a known model and the overhead of full preflight validation
30
- // is not warranted here.
31
- // Per-task dispatch is entirely delegated to runTaskPipeline, which calls
32
- // runOrchestratorPreflight (persona/model config validation) at entry before
33
- // any LLM dispatch. The two paths are deliberately separate.
34
- // Reference: lib/orchestrator-preflight.ts (N-H-D, FORGE-S25-T17).
35
- import { spawnSync } from "node:child_process";
36
- import * as fs from "node:fs";
37
- import * as path from "node:path";
38
- import { fileURLToPath } from "node:url";
39
- import { assertAudience } from "./audience-gate.js";
40
- import { loadLayeredConfig } from "./config-layer.js";
41
- import { loadForgePersona, runForgeSubagent } from "./forge-subagent.js";
42
- import { getSubagentTools } from "./forge-tools.js";
43
- import { emitSyntheticEvent } from "./hook-dispatcher.js";
44
- import { readPersonaDir as readPersonaDirSprint, readPipelineNames as readPipelineNamesSprint, } from "./lib/catalog-helpers.js";
45
- import { discoverForgeConfigCached } from "./lib/forge-config.js";
46
- import { checkMaterialization } from "./lib/manifest-checker.js";
47
- import { readJsonState, sprintStateFilePath, writeJsonState } from "./lib/state-helpers.js";
48
- import { lookupPersonaModel } from "./model-resolver.js";
49
- import { validateModelConfig } from "./model-validator.js";
50
- import { loadWorkflow } from "./parsers/workflow-loader.js";
51
- import { getSessionRegistry } from "./session-registry.js";
52
- import { getOrchestratorTree } from "./orchestrator-tree.js";
53
- import { resolveToCanonicalId, resolveToolDir } from "./store-resolver.js";
54
- import { attachViewportObserver } from "./viewport-events.js";
55
- import { emitEvent, formatLocalTime, isNonInteractive, isoCompact, isStateStale, readState as readTaskState, runTaskPipeline, validateId, } from "./run-task.js";
56
- // FORGE-S25-T16 (N-H-B): sprint state helpers delegate to lib/state-helpers.ts.
57
- function getSprintStatePath(cwd, sprintId) {
58
- if (!validateId(sprintId)) {
59
- throw new Error(`Invalid sprintId for state file path: ${sprintId}`);
60
- }
61
- return sprintStateFilePath(cwd, sprintId);
62
- }
63
- function readSprintState(cwd, sprintId) {
64
- return readJsonState(getSprintStatePath(cwd, sprintId));
65
- }
66
- function writeSprintState(cwd, state) {
67
- writeJsonState(getSprintStatePath(cwd, state.sprintId), state);
68
- }
69
- function deleteSprintState(cwd, sprintId) {
70
- const fp = getSprintStatePath(cwd, sprintId);
71
- try {
72
- if (fs.existsSync(fp))
73
- fs.unlinkSync(fp);
74
- }
75
- catch {
76
- // non-fatal
77
- }
78
- }
79
- function isSprintStateStale(state) {
80
- const savedAt = new Date(state.savedAt).getTime();
81
- const ageMs = Date.now() - savedAt;
82
- const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
83
- return ageMs > sevenDaysMs;
84
- }
85
- function readSprintRecord(sprintId, storeCli, cwd) {
86
- const result = spawnSync("node", [storeCli, "read", "sprint", sprintId], { cwd, encoding: "utf8" });
87
- if (result.status !== 0)
88
- return null;
89
- try {
90
- const raw = typeof result.stdout === "string" ? result.stdout : String(result.stdout);
91
- const record = JSON.parse(raw);
92
- // Validate taskIds is a non-empty array of strings
93
- if (!Array.isArray(record.taskIds) || record.taskIds.length === 0)
94
- return null;
95
- if (!record.taskIds.every((id) => typeof id === "string"))
96
- return null;
97
- return record;
98
- }
99
- catch {
100
- return null;
101
- }
102
- }
103
- async function dispatchSprintCeremony(params) {
104
- const { sprintId, mode, completedTaskIds, pausedAfterIndex, cwd, forgeRoot, ctx, registry, streamFnFactory, forgeToolDefs, } = params;
105
- const startMs = Date.now();
106
- // Materialized workflow path — already shipped from base pack.
107
- const workflowName = "architect_review_sprint_completion";
108
- const personaName = "architect";
109
- let persona;
110
- try {
111
- persona = loadForgePersona(personaName, cwd);
112
- }
113
- catch {
114
- return {
115
- verdict: "revision-required",
116
- durationMs: Date.now() - startMs,
117
- errorMessage: `architect persona not found`,
118
- };
119
- }
120
- const taskLines = [
121
- `# Sprint Completion Review — ${sprintId}`,
122
- ``,
123
- `Mode: ${mode}`,
124
- `Completed tasks: ${completedTaskIds.join(", ") || "(none)"}`,
125
- ];
126
- if (pausedAfterIndex !== undefined) {
127
- taskLines.push(`Paused after task index: ${pausedAfterIndex}`);
128
- }
129
- taskLines.push(``, `Execute the materialized workflow at \`.forge/workflows/${workflowName}.md\`.`, `Do not emit any phase event yourself; the orchestrator owns event emission.`);
130
- const task = taskLines.join("\n");
131
- // Use a dedicated session id (NOT a taskId) so the thread-switcher renders it
132
- // as a sprint-scoped chip distinct from per-task sessions.
133
- const sessionId = `${sprintId}:ceremony`;
134
- registry.startSession(sessionId);
135
- registry.startPhase(sessionId, "ceremony", 0);
136
- // Bridge: register ceremony phase in OrchestratorTree so the dashboard
137
- // shows the ceremony as a leaf under the sprint root.
138
- const tree = getOrchestratorTree();
139
- tree.startNode(sessionId, { parentId: sprintId, label: "ceremony", kind: "leaf" });
140
- let model;
141
- let provider;
142
- let errorMessage;
143
- const observer = attachViewportObserver({
144
- registry,
145
- sessionId,
146
- phaseRole: "ceremony",
147
- displayRole: "ceremony",
148
- beginHeader: `─── sprint ${sprintId} ceremony begin · ${personaName} ───`,
149
- });
150
- // Resolve model routing for the ceremony's architect persona (Plan 16 Slice 2).
151
- // N-B-E: surface schema errors to caller (Decision 9 — orchestrators fail-fast).
152
- // See doc/decisions/layered-config-error-policy.md.
153
- const { merged: ceremonyModelConfig, errors: ceremonyCfgErrors } = loadLayeredConfig(cwd);
154
- if (ceremonyCfgErrors.length > 0) {
155
- for (const e of ceremonyCfgErrors) {
156
- ctx.ui.notify(`× forge:run-sprint ceremony — forge-cli config schema error: ${e}`, "error");
157
- }
158
- return {
159
- verdict: "revision-required",
160
- durationMs: Date.now() - startMs,
161
- errorMessage: `forge-cli config schema errors: ${ceremonyCfgErrors.join("; ")}`,
162
- };
163
- }
164
- const ceremonyModelLookup = lookupPersonaModel(personaName, "default", ceremonyModelConfig);
165
- try {
166
- const result = await runForgeSubagent({
167
- persona,
168
- task,
169
- cwd,
170
- exportTag: `${sprintId}__ceremony`,
171
- forgeRoot,
172
- streamFn: streamFnFactory?.({ kind: "ceremony", persona: personaName }),
173
- // Sprint-scoped prompt-cache key — every subagent spawned across
174
- // the sprint (ceremonies + per-task phases) shares this namespace
175
- // so the system-prompt + persona prefix stays warm.
176
- cacheSessionId: `forge:${sprintId}`,
177
- onEvent: observer.onEvent,
178
- requestedModel: ceremonyModelLookup.model,
179
- modelRegistry: ctx.modelRegistry,
180
- customTools: forgeToolDefs ? getSubagentTools(forgeToolDefs, persona.name) : undefined,
181
- });
182
- model = result.model;
183
- provider = result.provider;
184
- if (result.exitCode !== 0) {
185
- errorMessage = result.errorMessage ?? "architect subagent exited non-zero";
186
- }
187
- }
188
- catch (e) {
189
- const err = e;
190
- errorMessage = err?.message ?? "runForgeSubagent threw";
191
- }
192
- finally {
193
- registry.completeSession(sessionId, errorMessage ? "failed" : "completed");
194
- tree.completeNode(sessionId, errorMessage ? "failed" : "completed");
195
- }
196
- // Parse verdict from store: did the architect actually transition the sprint?
197
- // The store is the source of truth — verdict text in SPRINT_COMPLETION_REVIEW.md
198
- // is human-readable but the store status is authoritative.
199
- let verdict = "revision-required";
200
- const readResult = spawnSync("node", [`${forgeRoot}/tools/store-cli.cjs`, "read", "sprint", sprintId], {
201
- cwd,
202
- encoding: "utf8",
203
- });
204
- if (readResult.status === 0) {
205
- try {
206
- const sprint = JSON.parse(readResult.stdout);
207
- if (sprint.status === "completed")
208
- verdict = "complete";
209
- else if (sprint.status === "partially-completed")
210
- verdict = "partial";
211
- // else: status unchanged → revision-required
212
- }
213
- catch {
214
- // fall through with revision-required
215
- }
216
- }
217
- return {
218
- verdict,
219
- model,
220
- provider,
221
- durationMs: Date.now() - startMs,
222
- errorMessage,
223
- };
224
- }
225
- const SPRINT_STATUS_KEY = "forge:run-sprint";
226
- export function registerRunSprint(pi, options = {}) {
227
- pi.registerCommand("forge:run-sprint", {
228
- description: "Run all tasks in a sprint sequentially. " +
229
- "Usage: /forge:run-sprint <SPRINT_ID>. " +
230
- "Orchestrator archetype: delegates per-task execution to runTaskPipeline.",
231
- async handler(args, ctx) {
232
- const cwd = options.cwd ?? process.cwd();
233
- let sprintId = args.trim();
234
- if (!sprintId) {
235
- ctx.ui.notify("× forge:run-sprint — sprint ID required. Usage: /forge:run-sprint <SPRINT_ID>", "error");
236
- return;
237
- }
238
- // Path traversal validation (advisory #3)
239
- if (!validateId(sprintId)) {
240
- ctx.ui.notify(`× forge:run-sprint — invalid sprint ID format: ${sprintId}`, "error");
241
- return;
242
- }
243
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, `run-sprint ${sprintId}: initializing…`);
244
- // ── Discover forge config ────────────────────────────────────────
245
- const forgeConfig = discoverForgeConfigCached(cwd);
246
- if (!forgeConfig) {
247
- ctx.ui.notify("× forge:run-sprint — no Forge project found at cwd. Run /forge:init first.", "error");
248
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
249
- return;
250
- }
251
- const forgeRoot = forgeConfig.forgeRoot;
252
- // ── Resolve sprint ID (prefix-normalize, suffix-match, NLP fallback) ──
253
- // Handles unprefixed IDs like "S22" → "FORGE-S22".
254
- // Issue #20: unprefixed entity IDs silently poisoned substitutions.
255
- const toolDir = resolveToolDir(forgeRoot);
256
- const resolvedSprintId = await resolveToCanonicalId(sprintId, toolDir, cwd, "sprint", {
257
- ctx,
258
- commandLabel: "forge:run-sprint",
259
- });
260
- if (!resolvedSprintId) {
261
- // Error already emitted by resolver
262
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
263
- return;
264
- }
265
- // Replace raw arg with canonical ID for all subsequent operations.
266
- sprintId = resolvedSprintId;
267
- // Update status with canonical ID so the user sees the resolved form.
268
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, `run-sprint ${sprintId}: ready`);
269
- const storeCli = path.join(forgeRoot, "tools", "store-cli.cjs");
270
- const preflightGate = path.join(forgeRoot, "tools", "preflight-gate.cjs");
271
- // ── Sprint resolution ────────────────────────────────────────────
272
- const sprintRecord = readSprintRecord(sprintId, storeCli, cwd);
273
- if (!sprintRecord) {
274
- ctx.ui.notify(`× forge:run-sprint — could not read sprint ${sprintId} or sprint has no task IDs.`, "error");
275
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
276
- return;
277
- }
278
- const taskIds = sprintRecord.taskIds;
279
- ctx.ui.notify(`▶ forge:run-sprint — sprint ${sprintId}: ${taskIds.length} tasks`, "info");
280
- // ── Audience check (AC B-12) ──────────────────────────────────────
281
- // Read the run_sprint workflow for audience check.
282
- const workflowPath = path.join(cwd, ".forge", "workflows", "run_sprint.md");
283
- let workflowMd;
284
- let workflowAudience = "any";
285
- try {
286
- const loaded = loadWorkflow(workflowPath);
287
- workflowMd = loaded.rawMarkdown;
288
- workflowAudience = loaded.audience;
289
- }
290
- catch {
291
- // Workflow file may not exist — default to orchestrator-only since
292
- // /forge:run-sprint is an orchestrator archetype command.
293
- workflowMd = "";
294
- workflowAudience = "orchestrator-only";
295
- }
296
- if (!assertAudience({ workflowName: "run_sprint", audience: workflowAudience }, ctx)) {
297
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
298
- return;
299
- }
300
- // ── Materialization-marker check (AC B-5) ────────────────────────
301
- if (workflowMd) {
302
- const markerCheck = checkMaterialization(workflowPath, workflowMd);
303
- if (!markerCheck.ok) {
304
- for (const marker of markerCheck.missing) {
305
- ctx.ui.notify(`× workflow regression: ${marker} not found in ${workflowPath}`, "error");
306
- }
307
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
308
- return;
309
- }
310
- }
311
- // ── Pre-flight confirm (AC B-7) ───────────────────────────────────
312
- if (!isNonInteractive()) {
313
- const proceed = await ctx.ui.confirm(`Begin sprint ${sprintId}?`, `Tasks: ${taskIds.join(", ")}`);
314
- if (!proceed) {
315
- ctx.ui.notify("forge:run-sprint — sprint aborted.", "info");
316
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
317
- return;
318
- }
319
- }
320
- // ── Sprint-level resume detection (AC B-11) ──────────────────────
321
- const existingSprintState = readSprintState(cwd, sprintId);
322
- let startTaskIndex = 0;
323
- let completedTaskIds = [];
324
- const resumeTaskStates = new Map();
325
- if (existingSprintState) {
326
- if (isSprintStateStale(existingSprintState)) {
327
- ctx.ui.notify(`⚠ forge:run-sprint — cached sprint state for ${sprintId} is stale (>7 days old, saved at ${formatLocalTime(existingSprintState.savedAt)}). Offering purge.`, "warning");
328
- if (!isNonInteractive()) {
329
- const purge = await ctx.ui.confirm(`Purge stale sprint state for ${sprintId}?`, "The cached state is older than 7 days. Purge and restart from the beginning?");
330
- if (purge) {
331
- deleteSprintState(cwd, sprintId);
332
- }
333
- else {
334
- ctx.ui.notify("forge:run-sprint — stale state kept; aborting.", "info");
335
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
336
- return;
337
- }
338
- }
339
- else {
340
- // Non-interactive: auto-abort on stale state (advisory #9)
341
- ctx.ui.notify("forge:run-sprint — stale sprint state; non-interactive mode auto-aborting.", "info");
342
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
343
- return;
344
- }
345
- }
346
- else {
347
- // Fresh state: offer resume
348
- if (!isNonInteractive()) {
349
- const resume = await ctx.ui.confirm(`Resume sprint ${sprintId}?`, `Cached state found at task ${existingSprintState.taskIndex} (${existingSprintState.completedTaskIds.length} completed). Resume from here?`);
350
- if (resume) {
351
- startTaskIndex = existingSprintState.taskIndex;
352
- completedTaskIds = existingSprintState.completedTaskIds;
353
- ctx.ui.notify(`forge:run-sprint — resuming ${sprintId} from task ${taskIds[startTaskIndex] ?? startTaskIndex}`, "info");
354
- // Collect halted and cancelled task states for mid-task resume
355
- // (REVIEW FIX #2, option b; ADR-S21-01: cancelled states are
356
- // resumable from the beginning of the cancelled phase).
357
- for (const taskId of taskIds.slice(startTaskIndex)) {
358
- const taskState = readTaskState(cwd, taskId);
359
- if (taskState && (taskState.halted || taskState.status === "cancelled")) {
360
- resumeTaskStates.set(taskId, taskState);
361
- }
362
- }
363
- }
364
- else {
365
- deleteSprintState(cwd, sprintId);
366
- }
367
- }
368
- else {
369
- // Non-interactive + existing state: auto-abort (advisory #9)
370
- ctx.ui.notify(`forge:run-sprint — cached sprint state for ${sprintId} found but non-interactive mode; aborting.`, "info");
371
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
372
- return;
373
- }
374
- }
375
- }
376
- // ── Model routing pre-flight (Plan 16 Slice 3) ──────────────────────
377
- // Validate routing config once at sprint start (before any LLM calls).
378
- // N-B-E: surface schema errors first (Decision 9 — orchestrators fail-fast).
379
- // See doc/decisions/layered-config-error-policy.md.
380
- {
381
- const { merged: modelRoutingConfig, errors: schemaErrors } = loadLayeredConfig(cwd);
382
- if (schemaErrors.length > 0) {
383
- for (const e of schemaErrors) {
384
- ctx.ui.notify(`× forge:run-sprint — forge-cli config schema error: ${e}`, "error");
385
- }
386
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
387
- return;
388
- }
389
- const personasDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "forge-payload", ".base-pack", "personas");
390
- const personaCatalogue = readPersonaDirSprint(personasDir);
391
- const forgeCfgPath = path.join(cwd, ".forge", "config.json");
392
- const pipelineCatalogue = readPipelineNamesSprint(forgeCfgPath);
393
- const availableModels = ctx.modelRegistry?.getAvailable?.() ?? [];
394
- const strict = process.env.FORGE_STRICT_MODELS === "1";
395
- const { errors, warnings } = validateModelConfig(personaCatalogue, pipelineCatalogue, modelRoutingConfig, availableModels.map((m) => ({ provider: m.provider, id: m.id })), strict);
396
- for (const w of warnings) {
397
- ctx.ui.notify(`⚠ forge:run-sprint — model routing: ${w.message}`, "warning");
398
- }
399
- if (errors.length > 0) {
400
- for (const e of errors) {
401
- ctx.ui.notify(`× forge:run-sprint — model routing: ${e.message}`, "error");
402
- }
403
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
404
- return;
405
- }
406
- }
407
- // ── Per-task loop (AC B-8) ────────────────────────────────────────
408
- const registry = getSessionRegistry();
409
- const tree = getOrchestratorTree();
410
- let lastModel;
411
- let lastProvider;
412
- const sprintStartMs = Date.now();
413
- // Bridge: register sprint root in OrchestratorTree.
414
- tree.startNode(sprintId, { label: `wfl:run-sprint`, kind: "orchestrator" });
415
- for (let i = startTaskIndex; i < taskIds.length; i++) {
416
- const taskId = taskIds[i];
417
- if (!taskId)
418
- continue;
419
- // ── Skip already-completed tasks ──────────────────────────────
420
- // If the task is already committed/approved in the store, skip the
421
- // phase pipeline and accumulate it as completed. This handles
422
- // re-runs where tasks finished in a prior attempt.
423
- {
424
- const taskReadResult = spawnSync("node", [`${forgeRoot}/tools/store-cli.cjs`, "read", "task", taskId], {
425
- cwd,
426
- encoding: "utf8",
427
- });
428
- if (taskReadResult.status === 0) {
429
- try {
430
- const taskRecord = JSON.parse(taskReadResult.stdout);
431
- if (taskRecord.status === "committed" || taskRecord.status === "completed") {
432
- ctx.ui.notify(`▶ ${sprintId}: task ${i + 1}/${taskIds.length} — ${taskId} already ${taskRecord.status}, skipping.`, "info");
433
- completedTaskIds.push(taskId);
434
- lastModel = taskRecord.model ?? lastModel;
435
- lastProvider = taskRecord.provider ?? lastProvider;
436
- writeSprintState(cwd, {
437
- sprintId,
438
- taskIndex: i + 1,
439
- completedTaskIds,
440
- halted: false,
441
- savedAt: new Date().toISOString(),
442
- });
443
- continue;
444
- }
445
- }
446
- catch {
447
- // Malformed task record — fall through to runTaskPipeline
448
- }
449
- }
450
- }
451
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, `run-sprint ${sprintId}: task ${i + 1}/${taskIds.length} (${taskId})`);
452
- ctx.ui.notify(`▶ ${sprintId}: task ${i + 1}/${taskIds.length} — ${taskId}`, "info");
453
- // Determine resumeFromState for mid-task resume (REVIEW FIX #2).
454
- // If a halted or cancelled task state exists for this task,
455
- // pass it to runTaskPipeline so it resumes from the saved phase.
456
- let resumeFromState = resumeTaskStates.get(taskId);
457
- if (resumeFromState) {
458
- // Validate the state is not corrupt
459
- if (typeof resumeFromState.phaseIndex !== "number" ||
460
- typeof resumeFromState.iterationCounts !== "object") {
461
- ctx.ui.notify(`⚠ forge:run-sprint — corrupt task state for ${taskId}; starting fresh.`, "warning");
462
- resumeFromState = undefined;
463
- }
464
- }
465
- if (resumeFromState) {
466
- const resumephaseRole = resumeFromState.phaseIndex;
467
- const resumeStatus = resumeFromState.status ?? (resumeFromState.halted ? "halted" : "interrupted");
468
- ctx.ui.notify(`▶ forge:run-sprint — resuming ${taskId} from phase ${resumephaseRole} (${resumeStatus})`, "info");
469
- }
470
- // Stale task state fallback: if task state >7d, delete and start fresh
471
- if (resumeFromState && isStateStale(resumeFromState)) {
472
- ctx.ui.notify(`⚠ forge:run-sprint — stale task state for ${taskId} (>7d); starting fresh.`, "warning");
473
- resumeFromState = undefined;
474
- }
475
- // ── Session lifecycle for thread-switcher (REVIEW FIX #2) ──────
476
- registry.startSession(taskId);
477
- // Bridge: register task in OrchestratorTree under sprint root.
478
- tree.startNode(taskId, { parentId: sprintId, label: `▸ wfl:run-task`, kind: "orchestrator" });
479
- const taskSignal = registry.getAbortSignal(taskId);
480
- const taskResult = await runTaskPipeline({
481
- taskId,
482
- cwd,
483
- ctx,
484
- forgeRoot,
485
- storeCli,
486
- preflightGate,
487
- registry,
488
- resumeFromState,
489
- signal: taskSignal,
490
- forgeToolDefs: options.forgeToolDefs,
491
- extensionFactories: options.extensionFactories,
492
- streamFnFactory: options.streamFnFactory
493
- ? (c) => options.streamFnFactory?.({
494
- kind: "task-phase",
495
- persona: c.persona,
496
- phase: c.phase,
497
- taskId: c.taskId,
498
- })
499
- : undefined,
500
- });
501
- // Capture model/provider from last task result (REVIEW FIX #1)
502
- if (taskResult.model)
503
- lastModel = taskResult.model;
504
- if (taskResult.provider)
505
- lastProvider = taskResult.provider;
506
- // ── Handle task result ──────────────────────────────────────
507
- if (taskResult.status === "completed") {
508
- completedTaskIds.push(taskId);
509
- registry.completeSession(taskId, "completed");
510
- tree.completeNode(taskId, "completed");
511
- }
512
- else if (taskResult.status === "cancelled") {
513
- // Task was cancelled by user — mark session, persist sprint
514
- // state, emit sprint-halted with cancellation detail, exit.
515
- registry.completeSession(taskId, "cancelled");
516
- tree.completeNode(taskId, "cancelled");
517
- tree.completeNode(sprintId, "cancelled");
518
- const cancelledEvent = {
519
- eventId: `${isoCompact(sprintStartMs)}_${sprintId}_sprint_halted`,
520
- sprintId,
521
- role: "orchestrator",
522
- action: "sprint-halted",
523
- startTimestamp: new Date(sprintStartMs).toISOString(),
524
- endTimestamp: new Date(Date.now()).toISOString(),
525
- durationMinutes: Math.round(((Date.now() - sprintStartMs) / 60000) * 100) / 100,
526
- model: lastModel ?? "orchestrator",
527
- provider: lastProvider ?? "orchestrator",
528
- type: "sprint-halted",
529
- haltedAtTaskIndex: i,
530
- haltedAtTaskId: taskId,
531
- lastError: "cancelled",
532
- };
533
- emitEvent(storeCli, cwd, sprintId, cancelledEvent);
534
- writeSprintState(cwd, {
535
- sprintId,
536
- taskIndex: i,
537
- completedTaskIds,
538
- halted: true,
539
- lastError: "cancelled",
540
- savedAt: new Date().toISOString(),
541
- });
542
- ctx.ui.notify(`⊘ forge:run-sprint — ${sprintId} halted: task ${taskId} cancelled.`, "info");
543
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
544
- return;
545
- }
546
- else {
547
- // Task halted/escalated/failed: mark session failed, persist sprint state, emit sprint-halted, exit.
548
- registry.completeSession(taskId, "failed");
549
- tree.completeNode(taskId, "failed");
550
- tree.completeNode(sprintId, "failed");
551
- const haltedEvent = {
552
- eventId: `${isoCompact(sprintStartMs)}_${sprintId}_sprint_halted`,
553
- sprintId,
554
- role: "orchestrator",
555
- action: "sprint-halted",
556
- startTimestamp: new Date(sprintStartMs).toISOString(),
557
- endTimestamp: new Date(Date.now()).toISOString(),
558
- durationMinutes: Math.round(((Date.now() - sprintStartMs) / 60000) * 100) / 100,
559
- model: lastModel ?? "orchestrator",
560
- provider: lastProvider ?? "orchestrator",
561
- type: "sprint-halted",
562
- haltedAtTaskIndex: i,
563
- haltedAtTaskId: taskId,
564
- lastError: taskResult.lastError ?? "unknown",
565
- };
566
- emitEvent(storeCli, cwd, sprintId, haltedEvent);
567
- writeSprintState(cwd, {
568
- sprintId,
569
- taskIndex: i,
570
- completedTaskIds,
571
- halted: true,
572
- lastError: taskResult.lastError,
573
- savedAt: new Date().toISOString(),
574
- });
575
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
576
- return;
577
- }
578
- // ── Post-task confirm (AC B-9) ────────────────────────────
579
- // Skip after final task
580
- if (i < taskIds.length - 1 && !isNonInteractive()) {
581
- const proceed = await ctx.ui.confirm(`Continue to next task?`, `${taskIds[i + 1]} is next. ${taskIds.length - i - 1} task(s) remaining.`);
582
- if (!proceed) {
583
- // Persist sprint state for resume.
584
- writeSprintState(cwd, {
585
- sprintId,
586
- taskIndex: i + 1,
587
- completedTaskIds,
588
- halted: false,
589
- savedAt: new Date().toISOString(),
590
- });
591
- // User-paused branch: dispatch ceremony if ≥1 task completed, emit partial event.
592
- const pauseEndMs = Date.now();
593
- let ceremonyResult;
594
- if (completedTaskIds.length > 0) {
595
- // Only dispatch ceremony if at least one task completed.
596
- // A zero-progress pause has nothing to review.
597
- ceremonyResult = await dispatchSprintCeremony({
598
- sprintId,
599
- mode: "partial",
600
- completedTaskIds,
601
- pausedAfterIndex: i,
602
- cwd,
603
- forgeRoot,
604
- ctx,
605
- registry,
606
- streamFnFactory: options.streamFnFactory,
607
- forgeToolDefs: options.forgeToolDefs,
608
- });
609
- }
610
- const pausedEvent = {
611
- eventId: `${isoCompact(sprintStartMs)}_${sprintId}_sprint_complete`,
612
- sprintId,
613
- role: "architect",
614
- action: "sprint-complete",
615
- startTimestamp: new Date(sprintStartMs).toISOString(),
616
- endTimestamp: new Date(pauseEndMs).toISOString(),
617
- durationMinutes: Math.round(((pauseEndMs - sprintStartMs) / 60000) * 100) / 100,
618
- model: ceremonyResult?.model ?? lastModel ?? "orchestrator",
619
- provider: ceremonyResult?.provider ?? lastProvider ?? "orchestrator",
620
- type: "sprint-complete",
621
- taskCount: taskIds.length,
622
- completedTaskIds,
623
- verdict: "partial",
624
- pausedAfterTaskIndex: i,
625
- waveCount: 1,
626
- maxConcurrency: 1,
627
- };
628
- emitEvent(storeCli, cwd, sprintId, pausedEvent);
629
- tree.completeNode(sprintId, "completed");
630
- ctx.ui.notify("forge:run-sprint — sprint paused after task completion.", "info");
631
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
632
- return;
633
- }
634
- }
635
- // Persist sprint state after each task transition (AC B-10)
636
- writeSprintState(cwd, {
637
- sprintId,
638
- taskIndex: i + 1,
639
- completedTaskIds,
640
- halted: false,
641
- savedAt: new Date().toISOString(),
642
- });
643
- }
644
- // ── All tasks complete — clean-complete branch (Plan 12 §3) ──────
645
- const sprintEndMs = Date.now();
646
- // Delete sprint state on successful completion
647
- deleteSprintState(cwd, sprintId);
648
- const ceremony = await dispatchSprintCeremony({
649
- sprintId,
650
- mode: "complete",
651
- completedTaskIds,
652
- cwd,
653
- forgeRoot,
654
- ctx,
655
- registry,
656
- streamFnFactory: options.streamFnFactory,
657
- forgeToolDefs: options.forgeToolDefs,
658
- });
659
- const sprintEvent = {
660
- eventId: `${isoCompact(sprintStartMs)}_${sprintId}_sprint_complete`,
661
- sprintId,
662
- role: "architect",
663
- action: "sprint-complete",
664
- startTimestamp: new Date(sprintStartMs).toISOString(),
665
- endTimestamp: new Date(sprintEndMs).toISOString(),
666
- durationMinutes: Math.round(((sprintEndMs - sprintStartMs) / 60000) * 100) / 100,
667
- model: ceremony.model ?? lastModel ?? "orchestrator",
668
- provider: ceremony.provider ?? lastProvider ?? "orchestrator",
669
- type: "sprint-complete",
670
- taskCount: taskIds.length,
671
- completedTaskIds,
672
- verdict: ceremony.verdict === "revision-required" ? "partial" : ceremony.verdict,
673
- waveCount: 1,
674
- maxConcurrency: 1,
675
- };
676
- const emitResult = emitEvent(storeCli, cwd, sprintId, sprintEvent);
677
- if (!emitResult.ok) {
678
- ctx.ui.notify(`⚠ forge:run-sprint — sprint-complete event emit failed: ${emitResult.stderr.trim()}`, "warning");
679
- }
680
- // ── Emit synthetic sprint-collate-complete event (FORGE-S21-T05) ──
681
- // Fires the in-process hook for post-sprint-hook.ts to consume.
682
- // Best-effort: failure-to-emit notifies but does NOT halt — sprint is
683
- // already complete at this point.
684
- try {
685
- const collateEvent = {
686
- type: "sprint-collate-complete",
687
- sprintId,
688
- cwd,
689
- };
690
- await emitSyntheticEvent(collateEvent, ctx);
691
- }
692
- catch (err) {
693
- const e = err;
694
- ctx.ui.notify(`⚠ forge:run-sprint — sprint-collate-complete synthetic event emit failed: ${e.message ?? "unknown"}`, "warning");
695
- }
696
- ctx.ui.setStatus?.(SPRINT_STATUS_KEY, undefined);
697
- // Sprint root: mark completed in the tree. "revision-required" → failed
698
- // because the architect rejected the sprint; partial → completed (the
699
- // sprint itself finished, the progress indicator shows N/M tasks done).
700
- const sprintTreeStatus = ceremony.verdict === "revision-required" ? "failed" : "completed";
701
- tree.completeNode(sprintId, sprintTreeStatus);
702
- if (ceremony.verdict === "complete") {
703
- ctx.ui.notify(`〇 forge:run-sprint — sprint ${sprintId} complete (${completedTaskIds.length}/${taskIds.length} tasks).`, "info");
704
- }
705
- else if (ceremony.verdict === "revision-required") {
706
- // Architect did not approve; surface to user.
707
- ctx.ui.notify(`▲ forge:run-sprint — sprint ${sprintId} ceremony returned "Revision Required". ` +
708
- `See engineering/sprints/${sprintId}/SPRINT_COMPLETION_REVIEW.md.`, "warning");
709
- }
710
- else {
711
- ctx.ui.notify(`▲ forge:run-sprint — sprint ${sprintId} marked partially-completed by architect.`, "warning");
712
- }
713
- },
714
- });
715
- }
716
- //# sourceMappingURL=run-sprint.js.map