@duypham93/openkit 0.2.0

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 (178) hide show
  1. package/.opencode/README.md +47 -0
  2. package/.opencode/install-manifest.json +41 -0
  3. package/.opencode/lib/artifact-scaffolder.js +111 -0
  4. package/.opencode/lib/contract-consistency.js +218 -0
  5. package/.opencode/lib/parallel-execution-rules.js +261 -0
  6. package/.opencode/lib/runtime-paths.js +95 -0
  7. package/.opencode/lib/runtime-summary.js +82 -0
  8. package/.opencode/lib/state-guard.js +99 -0
  9. package/.opencode/lib/task-board-rules.js +375 -0
  10. package/.opencode/lib/work-item-store.js +280 -0
  11. package/.opencode/lib/workflow-state-controller.js +1739 -0
  12. package/.opencode/lib/workflow-state-rules.js +331 -0
  13. package/.opencode/opencode.json +93 -0
  14. package/.opencode/package.json +3 -0
  15. package/.opencode/tests/artifact-scaffolder.test.js +733 -0
  16. package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
  17. package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
  18. package/.opencode/tests/session-start-hook.test.js +357 -0
  19. package/.opencode/tests/state-guard.test.js +124 -0
  20. package/.opencode/tests/task-board-rules.test.js +204 -0
  21. package/.opencode/tests/work-item-store.test.js +380 -0
  22. package/.opencode/tests/workflow-behavior.test.js +149 -0
  23. package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
  24. package/.opencode/tests/workflow-state-cli.test.js +1275 -0
  25. package/.opencode/tests/workflow-state-controller.test.js +1038 -0
  26. package/.opencode/work-items/feature-001/state.json +70 -0
  27. package/.opencode/work-items/index.json +13 -0
  28. package/.opencode/workflow-state.js +489 -0
  29. package/.opencode/workflow-state.json +70 -0
  30. package/AGENTS.md +265 -0
  31. package/README.md +401 -0
  32. package/agents/architect-agent.md +63 -0
  33. package/agents/ba-agent.md +56 -0
  34. package/agents/code-reviewer.md +77 -0
  35. package/agents/fullstack-agent.md +115 -0
  36. package/agents/master-orchestrator.md +60 -0
  37. package/agents/pm-agent.md +56 -0
  38. package/agents/qa-agent.md +124 -0
  39. package/agents/tech-lead-agent.md +60 -0
  40. package/assets/install-bundle/README.md +7 -0
  41. package/assets/install-bundle/opencode/README.md +11 -0
  42. package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
  43. package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
  44. package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
  45. package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
  46. package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
  47. package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
  48. package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
  49. package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
  50. package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
  51. package/assets/install-bundle/opencode/commands/delivery.md +45 -0
  52. package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
  53. package/assets/install-bundle/opencode/commands/migrate.md +61 -0
  54. package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
  55. package/assets/install-bundle/opencode/commands/task.md +46 -0
  56. package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
  57. package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
  58. package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
  59. package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
  60. package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
  61. package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
  62. package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
  63. package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
  64. package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
  65. package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
  66. package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
  67. package/assets/opencode.json.template +11 -0
  68. package/assets/openkit-install.json.template +19 -0
  69. package/bin/openkit.js +9 -0
  70. package/commands/brainstorm.md +44 -0
  71. package/commands/delivery.md +45 -0
  72. package/commands/execute-plan.md +44 -0
  73. package/commands/migrate.md +61 -0
  74. package/commands/quick-task.md +45 -0
  75. package/commands/task.md +46 -0
  76. package/commands/write-plan.md +50 -0
  77. package/context/core/approval-gates.md +146 -0
  78. package/context/core/code-quality.md +42 -0
  79. package/context/core/issue-routing.md +85 -0
  80. package/context/core/lane-selection.md +54 -0
  81. package/context/core/project-config.md +143 -0
  82. package/context/core/session-resume.md +85 -0
  83. package/context/core/workflow-state-schema.md +224 -0
  84. package/context/core/workflow.md +442 -0
  85. package/context/navigation.md +94 -0
  86. package/docs/adr/README.md +6 -0
  87. package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
  88. package/docs/architecture/README.md +7 -0
  89. package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
  90. package/docs/briefs/README.md +7 -0
  91. package/docs/governance/README.md +25 -0
  92. package/docs/governance/adr-policy.md +27 -0
  93. package/docs/governance/definition-of-done.md +17 -0
  94. package/docs/governance/naming-conventions.md +21 -0
  95. package/docs/governance/severity-levels.md +12 -0
  96. package/docs/maintainer/README.md +51 -0
  97. package/docs/operations/README.md +79 -0
  98. package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
  99. package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
  100. package/docs/operations/internal-records/README.md +18 -0
  101. package/docs/operations/runbooks/README.md +23 -0
  102. package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
  103. package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
  104. package/docs/operator/README.md +50 -0
  105. package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
  106. package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
  107. package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
  108. package/docs/plans/README.md +7 -0
  109. package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
  110. package/docs/qa/README.md +7 -0
  111. package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
  112. package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
  113. package/docs/specs/README.md +7 -0
  114. package/docs/templates/README.md +36 -0
  115. package/docs/templates/adr-template.md +18 -0
  116. package/docs/templates/architecture-template.md +31 -0
  117. package/docs/templates/implementation-plan-template.md +32 -0
  118. package/docs/templates/migration-baseline-checklist.md +48 -0
  119. package/docs/templates/migration-plan-template.md +52 -0
  120. package/docs/templates/migration-report-template.md +74 -0
  121. package/docs/templates/migration-verify-checklist.md +39 -0
  122. package/docs/templates/product-brief-template.md +32 -0
  123. package/docs/templates/qa-report-template.md +37 -0
  124. package/docs/templates/quick-task-template.md +36 -0
  125. package/docs/templates/spec-template.md +31 -0
  126. package/hooks/hooks.json +16 -0
  127. package/hooks/session-start +162 -0
  128. package/package.json +24 -0
  129. package/registry.json +328 -0
  130. package/skills/brainstorming/SKILL.md +51 -0
  131. package/skills/code-review/SKILL.md +48 -0
  132. package/skills/subagent-driven-development/SKILL.md +79 -0
  133. package/skills/systematic-debugging/SKILL.md +61 -0
  134. package/skills/test-driven-development/SKILL.md +48 -0
  135. package/skills/using-skills/SKILL.md +39 -0
  136. package/skills/verification-before-completion/SKILL.md +137 -0
  137. package/skills/writing-plans/SKILL.md +68 -0
  138. package/skills/writing-specs/SKILL.md +47 -0
  139. package/src/audit/vietnamese-detection.js +259 -0
  140. package/src/cli/commands/detect-vietnamese.js +24 -0
  141. package/src/cli/commands/doctor.js +33 -0
  142. package/src/cli/commands/help.js +33 -0
  143. package/src/cli/commands/init.js +25 -0
  144. package/src/cli/commands/install-global.js +26 -0
  145. package/src/cli/commands/install.js +25 -0
  146. package/src/cli/commands/run.js +63 -0
  147. package/src/cli/commands/uninstall.js +32 -0
  148. package/src/cli/commands/upgrade.js +25 -0
  149. package/src/cli/conflict-output.js +19 -0
  150. package/src/cli/index.js +56 -0
  151. package/src/global/doctor.js +101 -0
  152. package/src/global/ensure-install.js +32 -0
  153. package/src/global/install-state.js +73 -0
  154. package/src/global/launcher.js +51 -0
  155. package/src/global/materialize.js +123 -0
  156. package/src/global/paths.js +85 -0
  157. package/src/global/uninstall.js +25 -0
  158. package/src/global/workspace-state.js +63 -0
  159. package/src/install/asset-manifest.js +284 -0
  160. package/src/install/conflicts.js +43 -0
  161. package/src/install/discovery.js +138 -0
  162. package/src/install/install-state.js +136 -0
  163. package/src/install/materialize.js +158 -0
  164. package/src/install/merge-policy.js +145 -0
  165. package/src/runtime/doctor.js +281 -0
  166. package/src/runtime/launcher.js +49 -0
  167. package/src/runtime/opencode-layering.js +135 -0
  168. package/src/runtime/openkit-managed-summary.js +27 -0
  169. package/tests/cli/openkit-cli.test.js +417 -0
  170. package/tests/global/doctor.test.js +130 -0
  171. package/tests/global/ensure-install.test.js +105 -0
  172. package/tests/install/discovery.test.js +124 -0
  173. package/tests/install/install-state.test.js +346 -0
  174. package/tests/install/materialize.test.js +244 -0
  175. package/tests/install/merge-policy.test.js +177 -0
  176. package/tests/runtime/doctor.test.js +430 -0
  177. package/tests/runtime/launcher.test.js +230 -0
  178. package/tests/runtime/module-boundary.test.js +16 -0
@@ -0,0 +1,1739 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+
4
+ const {
5
+ resolveKitRoot,
6
+ resolveProjectRoot,
7
+ resolveRuntimeRoot,
8
+ resolveStatePath,
9
+ } = require("./runtime-paths")
10
+
11
+ const {
12
+ bootstrapRuntimeStore,
13
+ bootstrapLegacyWorkflowState,
14
+ deriveWorkItemId,
15
+ readWorkItemIndex,
16
+ readWorkItemState,
17
+ resolveWorkItemPaths,
18
+ setActiveWorkItem,
19
+ writeCompatibilityMirror,
20
+ writeWorkItemIndex,
21
+ writeWorkItemState,
22
+ } = require("./work-item-store")
23
+ const {
24
+ ARTIFACT_KINDS,
25
+ ESCALATION_RETRY_THRESHOLD,
26
+ ISSUE_SEVERITIES,
27
+ ISSUE_TYPES,
28
+ MODE_VALUES,
29
+ RECOMMENDED_OWNERS,
30
+ ROOTED_IN_VALUES,
31
+ ROUTING_BEHAVIOR_DELTA_VALUES,
32
+ ROUTING_DOMINANT_UNCERTAINTY_VALUES,
33
+ ROUTING_SCOPE_SHAPE_VALUES,
34
+ ROUTING_WORK_INTENT_VALUES,
35
+ STAGE_OWNERS,
36
+ STAGE_SEQUENCE,
37
+ STATUS_VALUES,
38
+ createDefaultRoutingProfile,
39
+ createEmptyApprovals,
40
+ createEmptyArtifacts,
41
+ getApprovalGatesForMode,
42
+ getInitialStageForMode,
43
+ getModeForStage,
44
+ getNextStage,
45
+ getReworkRoute,
46
+ getTransitionGate,
47
+ } = require("./workflow-state-rules")
48
+ const { getContractConsistencyReport: buildContractConsistencyReport } = require("./contract-consistency")
49
+ const { SUPPORTED_SCAFFOLDS, scaffoldArtifact } = require("./artifact-scaffolder")
50
+ const { captureRevision, guardWrite, planGuardedMirrorRefresh } = require("./state-guard")
51
+ const {
52
+ VALID_ASSIGNMENT_AUTHORITIES,
53
+ decideQaFailLocalRework,
54
+ validateParallelAssignments,
55
+ validateReassignmentAuthority,
56
+ validateWorktreeMetadata,
57
+ } = require("./parallel-execution-rules")
58
+ const { validateTaskBoard, validateTaskShape, validateTaskStatus, validateTaskTransition } = require("./task-board-rules")
59
+
60
+ function fail(message) {
61
+ const error = new Error(message)
62
+ error.isWorkflowStateError = true
63
+ throw error
64
+ }
65
+
66
+ function formatModeLabel(mode) {
67
+ return typeof mode === "string" && mode.length > 0 ? `${mode.charAt(0).toUpperCase()}${mode.slice(1)}` : "Unknown"
68
+ }
69
+
70
+ function timestamp() {
71
+ return new Date().toISOString()
72
+ }
73
+
74
+ function readState(customStatePath) {
75
+ const statePath = resolveStatePath(customStatePath)
76
+
77
+ let raw
78
+ try {
79
+ raw = fs.readFileSync(statePath, "utf8")
80
+ } catch (error) {
81
+ fail(`Unable to read workflow state file at '${statePath}': ${error.message}`)
82
+ }
83
+
84
+ try {
85
+ return {
86
+ statePath,
87
+ state: JSON.parse(raw),
88
+ }
89
+ } catch (error) {
90
+ fail(`Malformed workflow state JSON at '${statePath}': ${error.message}`)
91
+ }
92
+ }
93
+
94
+ function writeState(statePath, state) {
95
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
96
+ }
97
+
98
+ function createEmptyWorkItemIndex() {
99
+ return {
100
+ active_work_item_id: null,
101
+ work_items: [],
102
+ }
103
+ }
104
+
105
+ function ensureWorkItemStoreReady(customStatePath) {
106
+ const runtimeRoot = resolveRuntimeRoot(customStatePath)
107
+ const projectRoot = resolveProjectRoot(customStatePath)
108
+
109
+ if (runtimeRoot !== projectRoot) {
110
+ const runtimePaths = resolveWorkItemPaths(runtimeRoot, "__bootstrap__")
111
+ const projectPaths = resolveWorkItemPaths(projectRoot, "__bootstrap__")
112
+
113
+ if (!fs.existsSync(runtimePaths.indexPath) && fs.existsSync(projectPaths.workflowStatePath)) {
114
+ fs.mkdirSync(path.dirname(runtimePaths.workflowStatePath), { recursive: true })
115
+ fs.copyFileSync(projectPaths.workflowStatePath, runtimePaths.workflowStatePath)
116
+
117
+ if (fs.existsSync(projectPaths.workItemsDir) && !fs.existsSync(runtimePaths.workItemsDir)) {
118
+ fs.cpSync(projectPaths.workItemsDir, runtimePaths.workItemsDir, { recursive: true })
119
+ }
120
+ }
121
+ }
122
+
123
+ bootstrapRuntimeStore(runtimeRoot)
124
+
125
+ return runtimeRoot
126
+ }
127
+
128
+ function upsertWorkItemIndexEntry(index, state, workItemId, relativeStatePath) {
129
+ const nextEntry = {
130
+ work_item_id: workItemId,
131
+ feature_id: state.feature_id,
132
+ feature_slug: state.feature_slug,
133
+ mode: state.mode,
134
+ status: state.status,
135
+ state_path: relativeStatePath,
136
+ }
137
+
138
+ const existingIndex = index.work_items.findIndex((entry) => entry.work_item_id === workItemId)
139
+ if (existingIndex === -1) {
140
+ index.work_items.push(nextEntry)
141
+ } else {
142
+ index.work_items[existingIndex] = nextEntry
143
+ }
144
+
145
+ return nextEntry
146
+ }
147
+
148
+ function readManagedState(customStatePath, workItemId = null) {
149
+ const statePath = resolveStatePath(customStatePath)
150
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
151
+ const index = readWorkItemIndex(projectRoot)
152
+ const resolvedWorkItemId = workItemId ?? index.active_work_item_id
153
+
154
+ if (!resolvedWorkItemId) {
155
+ fail("Active work item pointer missing")
156
+ }
157
+
158
+ return {
159
+ statePath,
160
+ projectRoot,
161
+ index,
162
+ workItemId: resolvedWorkItemId,
163
+ state: readWorkItemState(projectRoot, resolvedWorkItemId),
164
+ }
165
+ }
166
+
167
+ function persistManagedState(customStatePath, state, options = {}) {
168
+ const statePath = resolveStatePath(customStatePath)
169
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
170
+ const workItemId = options.workItemId ?? deriveWorkItemId(state)
171
+ const workItemPaths = resolveWorkItemPaths(projectRoot, workItemId)
172
+ const index = fs.existsSync(workItemPaths.indexPath)
173
+ ? readWorkItemIndex(projectRoot)
174
+ : createEmptyWorkItemIndex()
175
+ const hasPersistedState = fs.existsSync(workItemPaths.statePath)
176
+ const currentPersistedState = hasPersistedState ? readWorkItemState(projectRoot, workItemId) : null
177
+ const previousIndexSnapshot = JSON.parse(JSON.stringify(index))
178
+ const hadMirrorState = fs.existsSync(statePath)
179
+ const previousMirrorState = hadMirrorState ? readState(statePath).state : null
180
+
181
+ if (currentPersistedState) {
182
+ guardWrite({
183
+ currentState: currentPersistedState,
184
+ expectedRevision: options.expectedRevision,
185
+ nextState: state,
186
+ })
187
+ }
188
+
189
+ const persistedState = writeWorkItemState(projectRoot, workItemId, state)
190
+ try {
191
+ validateManagedState(persistedState, projectRoot, workItemId)
192
+
193
+ const nextActiveWorkItemId = options.activateWorkItemId ?? workItemId
194
+ const mirrorRefreshPlan = planGuardedMirrorRefresh({
195
+ activeWorkItemId: nextActiveWorkItemId,
196
+ targetWorkItemId: workItemId,
197
+ nextState: persistedState,
198
+ })
199
+
200
+ if (mirrorRefreshPlan.shouldRefreshMirror) {
201
+ const preflightMirrorState = fs.existsSync(statePath) ? readState(statePath).state : null
202
+
203
+ if (preflightMirrorState && options.expectedMirrorRevision) {
204
+ guardWrite({
205
+ currentState: preflightMirrorState,
206
+ expectedRevision: options.expectedMirrorRevision,
207
+ nextState: persistedState,
208
+ })
209
+ }
210
+
211
+ writeCompatibilityMirror(projectRoot, persistedState)
212
+
213
+ const mirrorState = readState(statePath).state
214
+ const mirrorRevision = captureRevision(mirrorState)
215
+
216
+ if (mirrorRevision !== mirrorRefreshPlan.mirrorRevision) {
217
+ fail(
218
+ `Compatibility mirror refresh revision conflict for active work item '${workItemId}'; expected ${mirrorRefreshPlan.mirrorRevision} but found ${mirrorRevision}`,
219
+ )
220
+ }
221
+ }
222
+
223
+ upsertWorkItemIndexEntry(index, persistedState, workItemId, workItemPaths.relativeStatePath)
224
+ index.active_work_item_id = nextActiveWorkItemId
225
+ writeWorkItemIndex(projectRoot, index)
226
+ } catch (error) {
227
+ if (currentPersistedState) {
228
+ writeWorkItemState(projectRoot, workItemId, currentPersistedState)
229
+ } else if (fs.existsSync(workItemPaths.statePath)) {
230
+ fs.rmSync(workItemPaths.statePath)
231
+ }
232
+
233
+ if (previousMirrorState) {
234
+ writeCompatibilityMirror(projectRoot, previousMirrorState)
235
+ } else if (!hadMirrorState && fs.existsSync(statePath)) {
236
+ fs.rmSync(statePath)
237
+ }
238
+
239
+ try {
240
+ writeWorkItemIndex(projectRoot, previousIndexSnapshot)
241
+ } catch (_rollbackError) {
242
+ // Preserve the original failure after best-effort rollback.
243
+ }
244
+ throw error
245
+ }
246
+
247
+ return {
248
+ statePath,
249
+ projectRoot,
250
+ index,
251
+ state: persistedState,
252
+ }
253
+ }
254
+
255
+ function getTaskBoardPath(projectRoot, workItemId) {
256
+ return path.join(resolveWorkItemPaths(projectRoot, workItemId).workItemDir, "tasks.json")
257
+ }
258
+
259
+ function readTaskBoardIfExists(projectRoot, workItemId) {
260
+ const taskBoardPath = getTaskBoardPath(projectRoot, workItemId)
261
+ if (!fs.existsSync(taskBoardPath)) {
262
+ return null
263
+ }
264
+
265
+ try {
266
+ return JSON.parse(fs.readFileSync(taskBoardPath, "utf8"))
267
+ } catch (error) {
268
+ fail(`Malformed JSON at '${taskBoardPath}': ${error.message}`)
269
+ }
270
+ }
271
+
272
+ function writeTaskBoard(projectRoot, workItemId, board) {
273
+ const taskBoardPath = getTaskBoardPath(projectRoot, workItemId)
274
+ fs.mkdirSync(path.dirname(taskBoardPath), { recursive: true })
275
+ fs.writeFileSync(taskBoardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
276
+ return board
277
+ }
278
+
279
+ function buildBoardView(state, board) {
280
+ return {
281
+ mode: state.mode,
282
+ current_stage: state.current_stage,
283
+ tasks: board?.tasks ?? [],
284
+ issues: board?.issues ?? [],
285
+ }
286
+ }
287
+
288
+ function readWorkItemContext(workItemId, customStatePath) {
289
+ ensureString(workItemId, "work_item_id")
290
+ const statePath = resolveStatePath(customStatePath)
291
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
292
+ const state = readWorkItemState(projectRoot, workItemId)
293
+ validateStateObject(state)
294
+ validateManagedState(state, projectRoot, workItemId)
295
+
296
+ return {
297
+ statePath,
298
+ projectRoot,
299
+ workItemId,
300
+ state,
301
+ }
302
+ }
303
+
304
+ function requireFullModeWorkItem(state, workItemId) {
305
+ if (state.mode !== "full") {
306
+ fail(`Work item '${workItemId}' must be in full mode to use a task board`)
307
+ }
308
+ }
309
+
310
+ function isTaskBoardStageAllowed(stage) {
311
+ return ["full_plan", "full_implementation", "full_qa", "full_done"].includes(stage)
312
+ }
313
+
314
+ function requireTaskBoardStage(state, workItemId, action = "use a task board") {
315
+ if (!isTaskBoardStageAllowed(state.current_stage)) {
316
+ fail(
317
+ `Work item '${workItemId}' must reach 'full_plan' before it can ${action}; current stage is '${state.current_stage}'`,
318
+ )
319
+ }
320
+ }
321
+
322
+ function buildBoardForValidation(state, board) {
323
+ validateParallelAssignments(board.tasks ?? [])
324
+
325
+ return {
326
+ ...board,
327
+ mode: state.mode,
328
+ current_stage: state.current_stage,
329
+ issues: board.issues ?? [],
330
+ }
331
+ }
332
+
333
+ function withTaskBoard(workItemId, customStatePath, mutator) {
334
+ const context = readWorkItemContext(workItemId, customStatePath)
335
+ const { state, projectRoot } = context
336
+ requireFullModeWorkItem(state, workItemId)
337
+ requireTaskBoardStage(state, workItemId)
338
+
339
+ const existingBoard = readTaskBoardIfExists(projectRoot, workItemId)
340
+ const baseBoard = buildBoardView(state, existingBoard)
341
+ const nextBoard = mutator(JSON.parse(JSON.stringify(baseBoard)), context)
342
+ const validatedBoard = validateTaskBoard(buildBoardForValidation(state, nextBoard))
343
+ writeTaskBoard(projectRoot, workItemId, validatedBoard)
344
+
345
+ return {
346
+ ...context,
347
+ board: validatedBoard,
348
+ }
349
+ }
350
+
351
+ function findTask(board, taskId) {
352
+ ensureString(taskId, "task_id")
353
+ const task = board.tasks.find((entry) => entry.task_id === taskId)
354
+ if (!task) {
355
+ fail(`Unknown task '${taskId}'`)
356
+ }
357
+ return task
358
+ }
359
+
360
+ function buildTaskRecord(taskInput) {
361
+ ensureObject(taskInput, "task")
362
+ ensureString(taskInput.task_id, "task.task_id")
363
+ ensureString(taskInput.title, "task.title")
364
+ ensureString(taskInput.kind, "task.kind")
365
+
366
+ if (taskInput.worktree_metadata !== undefined) {
367
+ validateWorktreeMetadata(taskInput.worktree_metadata)
368
+ }
369
+
370
+ const now = timestamp()
371
+
372
+ return validateTaskShape({
373
+ task_id: taskInput.task_id,
374
+ title: taskInput.title,
375
+ summary: taskInput.summary ?? taskInput.title,
376
+ kind: taskInput.kind,
377
+ status: taskInput.status ?? "ready",
378
+ primary_owner: taskInput.primary_owner ?? null,
379
+ qa_owner: taskInput.qa_owner ?? null,
380
+ depends_on: taskInput.depends_on ?? [],
381
+ blocked_by: taskInput.blocked_by ?? [],
382
+ artifact_refs: taskInput.artifact_refs ?? [],
383
+ plan_refs: taskInput.plan_refs ?? [],
384
+ branch_or_worktree: taskInput.branch_or_worktree ?? taskInput.worktree_metadata?.worktree_path ?? null,
385
+ created_by: taskInput.created_by ?? "TechLeadAgent",
386
+ created_at: taskInput.created_at ?? now,
387
+ updated_at: taskInput.updated_at ?? now,
388
+ })
389
+ }
390
+
391
+ function validateManagedState(state, projectRoot, workItemId, options = {}) {
392
+ const taskBoard = readTaskBoardIfExists(projectRoot, workItemId)
393
+
394
+ if (state.mode !== "full") {
395
+ if (taskBoard !== null) {
396
+ fail(`${formatModeLabel(state.mode)} mode cannot carry a task board; task boards are full-delivery only`)
397
+ }
398
+ return null
399
+ }
400
+
401
+ if (taskBoard === null) {
402
+ return null
403
+ }
404
+
405
+ requireTaskBoardStage(state, workItemId, "carry a task board")
406
+
407
+ const boardStage = options.boardStage ?? taskBoard.current_stage ?? state.current_stage
408
+ validateTaskBoard({
409
+ ...taskBoard,
410
+ mode: state.mode,
411
+ current_stage: boardStage,
412
+ })
413
+
414
+ return taskBoard
415
+ }
416
+
417
+ function requireValidTaskBoard(state, projectRoot, workItemId, boardStage, reason) {
418
+ const taskBoard = readTaskBoardIfExists(projectRoot, workItemId)
419
+
420
+ if (state.mode !== "full") {
421
+ if (taskBoard !== null) {
422
+ fail(`${formatModeLabel(state.mode)} mode cannot carry a task board; task boards are full-delivery only`)
423
+ }
424
+ fail(reason)
425
+ }
426
+
427
+ if (taskBoard === null) {
428
+ fail(reason)
429
+ }
430
+
431
+ validateTaskBoard({
432
+ ...taskBoard,
433
+ mode: state.mode,
434
+ current_stage: boardStage,
435
+ })
436
+ }
437
+
438
+ function readJsonIfExists(filePath) {
439
+ if (!fs.existsSync(filePath)) {
440
+ return null
441
+ }
442
+
443
+ try {
444
+ return JSON.parse(fs.readFileSync(filePath, "utf8"))
445
+ } catch (error) {
446
+ fail(`Malformed JSON at '${filePath}': ${error.message}`)
447
+ }
448
+ }
449
+
450
+ function getManifestPaths(projectRoot, manifest) {
451
+ const kit = manifest?.kit ?? {}
452
+ const registryPath = kit.registry?.path
453
+ ? path.resolve(projectRoot, kit.registry.path)
454
+ : path.join(projectRoot, "registry.json")
455
+ const installManifestPath = kit.installManifest?.path
456
+ ? path.resolve(projectRoot, kit.installManifest.path)
457
+ : path.join(projectRoot, ".opencode", "install-manifest.json")
458
+
459
+ return {
460
+ registryPath,
461
+ installManifestPath,
462
+ }
463
+ }
464
+
465
+ function tryReadJson(filePath) {
466
+ if (!fs.existsSync(filePath)) {
467
+ return { exists: false, readable: false, data: null }
468
+ }
469
+
470
+ try {
471
+ return {
472
+ exists: true,
473
+ readable: true,
474
+ data: JSON.parse(fs.readFileSync(filePath, "utf8")),
475
+ }
476
+ } catch (_error) {
477
+ return {
478
+ exists: true,
479
+ readable: false,
480
+ data: null,
481
+ }
482
+ }
483
+ }
484
+
485
+ function getRegistry(customStatePath) {
486
+ const projectRoot = resolveProjectRoot(customStatePath)
487
+ const manifestPath = path.join(projectRoot, ".opencode", "opencode.json")
488
+ const manifest = readJsonIfExists(manifestPath)
489
+ const { registryPath } = getManifestPaths(projectRoot, manifest)
490
+ const registry = readJsonIfExists(registryPath)
491
+
492
+ if (!registry) {
493
+ fail(`Unable to read registry metadata at '${registryPath}'`)
494
+ }
495
+
496
+ return {
497
+ projectRoot,
498
+ manifestPath,
499
+ registryPath,
500
+ manifest,
501
+ registry,
502
+ }
503
+ }
504
+
505
+ function getInstallManifest(customStatePath) {
506
+ const projectRoot = resolveProjectRoot(customStatePath)
507
+ const manifestPath = path.join(projectRoot, ".opencode", "opencode.json")
508
+ const manifest = readJsonIfExists(manifestPath)
509
+ const { installManifestPath } = getManifestPaths(projectRoot, manifest)
510
+ const installManifest = readJsonIfExists(installManifestPath)
511
+
512
+ if (!installManifest) {
513
+ fail(`Unable to read install manifest at '${installManifestPath}'`)
514
+ }
515
+
516
+ return {
517
+ projectRoot,
518
+ manifestPath,
519
+ installManifestPath,
520
+ manifest,
521
+ installManifest,
522
+ }
523
+ }
524
+
525
+ function listProfiles(customStatePath) {
526
+ const { registry } = getRegistry(customStatePath)
527
+ return registry.profiles ?? []
528
+ }
529
+
530
+ function getProfile(profileName, customStatePath) {
531
+ ensureString(profileName, "profile name")
532
+ const { registry } = getRegistry(customStatePath)
533
+ const profiles = registry.profiles ?? []
534
+ const profile = profiles.find((entry) => entry.name === profileName)
535
+
536
+ if (!profile) {
537
+ fail(`Unknown profile '${profileName}'`)
538
+ }
539
+
540
+ return profile
541
+ }
542
+
543
+ function syncInstallManifest(profileName, customStatePath) {
544
+ const profile = getProfile(profileName, customStatePath)
545
+ const { installManifestPath, installManifest } = getInstallManifest(customStatePath)
546
+
547
+ const nextManifest = JSON.parse(JSON.stringify(installManifest))
548
+ nextManifest.installation = nextManifest.installation ?? {}
549
+ nextManifest.installation.activeProfile = profile.name
550
+ writeState(installManifestPath, nextManifest)
551
+
552
+ return {
553
+ installManifestPath,
554
+ installManifest: nextManifest,
555
+ profile,
556
+ }
557
+ }
558
+
559
+ function ensureObject(value, label) {
560
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
561
+ fail(`${label} must be an object`)
562
+ }
563
+ }
564
+
565
+ function ensureArray(value, label) {
566
+ if (!Array.isArray(value)) {
567
+ fail(`${label} must be an array`)
568
+ }
569
+ }
570
+
571
+ function ensureString(value, label) {
572
+ if (typeof value !== "string" || value.length === 0) {
573
+ fail(`${label} must be a non-empty string`)
574
+ }
575
+ }
576
+
577
+ function ensureNullableString(value, label) {
578
+ if (value !== null && typeof value !== "string") {
579
+ fail(`${label} must be a string or null`)
580
+ }
581
+ }
582
+
583
+ function ensureKnown(value, allowedValues, label) {
584
+ if (!allowedValues.includes(value)) {
585
+ fail(`${label} must be one of: ${allowedValues.join(", ")}`)
586
+ }
587
+ }
588
+
589
+ function ensureGateShape(gateName, gateValue) {
590
+ ensureObject(gateValue, `approvals.${gateName}`)
591
+ for (const key of ["status", "approved_by", "approved_at", "notes"]) {
592
+ if (!(key in gateValue)) {
593
+ fail(`approvals.${gateName}.${key} is required`)
594
+ }
595
+ }
596
+
597
+ ensureKnown(gateValue.status, ["pending", "approved", "rejected"], `approvals.${gateName}.status`)
598
+ ensureNullableString(gateValue.approved_by, `approvals.${gateName}.approved_by`)
599
+ ensureNullableString(gateValue.approved_at, `approvals.${gateName}.approved_at`)
600
+ ensureNullableString(gateValue.notes, `approvals.${gateName}.notes`)
601
+ }
602
+
603
+ function ensureIssueShape(issue, index) {
604
+ ensureObject(issue, `issues[${index}]`)
605
+
606
+ for (const key of [
607
+ "issue_id",
608
+ "title",
609
+ "type",
610
+ "severity",
611
+ "rooted_in",
612
+ "recommended_owner",
613
+ "evidence",
614
+ "artifact_refs",
615
+ ]) {
616
+ if (!(key in issue)) {
617
+ fail(`issues[${index}].${key} is required`)
618
+ }
619
+ }
620
+
621
+ ensureString(issue.issue_id, `issues[${index}].issue_id`)
622
+ ensureString(issue.title, `issues[${index}].title`)
623
+ ensureKnown(issue.type, ISSUE_TYPES, `issues[${index}].type`)
624
+ ensureKnown(issue.severity, ISSUE_SEVERITIES, `issues[${index}].severity`)
625
+ ensureKnown(issue.rooted_in, ROOTED_IN_VALUES, `issues[${index}].rooted_in`)
626
+ ensureString(issue.recommended_owner, `issues[${index}].recommended_owner`)
627
+ ensureString(issue.evidence, `issues[${index}].evidence`)
628
+
629
+ const allowedOwners = RECOMMENDED_OWNERS[issue.type] ?? []
630
+ if (!allowedOwners.includes(issue.recommended_owner)) {
631
+ fail(`issues[${index}].recommended_owner must be one of: ${allowedOwners.join(", ")}`)
632
+ }
633
+
634
+ if (typeof issue.artifact_refs === "string") {
635
+ issue.artifact_refs = [issue.artifact_refs]
636
+ }
637
+
638
+ ensureArray(issue.artifact_refs, `issues[${index}].artifact_refs`)
639
+ }
640
+
641
+ function validateArtifacts(artifacts) {
642
+ ensureObject(artifacts, "artifacts")
643
+ for (const key of ["task_card", "brief", "spec", "architecture", "plan", "migration_report", "qa_report", "adr"]) {
644
+ if (!(key in artifacts)) {
645
+ fail(`artifacts.${key} is required`)
646
+ }
647
+ }
648
+
649
+ ensureNullableString(artifacts.task_card, "artifacts.task_card")
650
+ ensureNullableString(artifacts.brief, "artifacts.brief")
651
+ ensureNullableString(artifacts.spec, "artifacts.spec")
652
+ ensureNullableString(artifacts.architecture, "artifacts.architecture")
653
+ ensureNullableString(artifacts.plan, "artifacts.plan")
654
+ ensureNullableString(artifacts.migration_report, "artifacts.migration_report")
655
+ ensureNullableString(artifacts.qa_report, "artifacts.qa_report")
656
+ ensureArray(artifacts.adr, "artifacts.adr")
657
+ }
658
+
659
+ function validateRoutingProfile(routingProfile) {
660
+ ensureObject(routingProfile, "routing_profile")
661
+
662
+ for (const key of [
663
+ "work_intent",
664
+ "behavior_delta",
665
+ "dominant_uncertainty",
666
+ "scope_shape",
667
+ "selection_reason",
668
+ ]) {
669
+ if (!(key in routingProfile)) {
670
+ fail(`routing_profile.${key} is required`)
671
+ }
672
+ }
673
+
674
+ ensureKnown(routingProfile.work_intent, ROUTING_WORK_INTENT_VALUES, "routing_profile.work_intent")
675
+ ensureKnown(routingProfile.behavior_delta, ROUTING_BEHAVIOR_DELTA_VALUES, "routing_profile.behavior_delta")
676
+ ensureKnown(
677
+ routingProfile.dominant_uncertainty,
678
+ ROUTING_DOMINANT_UNCERTAINTY_VALUES,
679
+ "routing_profile.dominant_uncertainty",
680
+ )
681
+ ensureKnown(routingProfile.scope_shape, ROUTING_SCOPE_SHAPE_VALUES, "routing_profile.scope_shape")
682
+ ensureString(routingProfile.selection_reason, "routing_profile.selection_reason")
683
+ }
684
+
685
+ function validateRoutingProfileForMode(mode, routingProfile) {
686
+ if (mode === "quick") {
687
+ if (routingProfile.work_intent !== "maintenance") {
688
+ fail("routing_profile.work_intent must be 'maintenance' for quick mode")
689
+ }
690
+
691
+ if (routingProfile.behavior_delta !== "preserve") {
692
+ fail("routing_profile.behavior_delta must be 'preserve' for quick mode")
693
+ }
694
+
695
+ if (routingProfile.dominant_uncertainty !== "low_local") {
696
+ fail("routing_profile.dominant_uncertainty must be 'low_local' for quick mode")
697
+ }
698
+
699
+ if (routingProfile.scope_shape === "cross_boundary") {
700
+ fail("routing_profile.scope_shape cannot be 'cross_boundary' for quick mode")
701
+ }
702
+
703
+ return
704
+ }
705
+
706
+ if (mode === "migration") {
707
+ if (routingProfile.work_intent !== "modernization") {
708
+ fail("routing_profile.work_intent must be 'modernization' for migration mode")
709
+ }
710
+
711
+ if (routingProfile.behavior_delta !== "preserve") {
712
+ fail("routing_profile.behavior_delta must be 'preserve' for migration mode")
713
+ }
714
+
715
+ if (routingProfile.dominant_uncertainty !== "compatibility") {
716
+ fail("routing_profile.dominant_uncertainty must be 'compatibility' for migration mode")
717
+ }
718
+
719
+ return
720
+ }
721
+
722
+ const supportsFullMode =
723
+ routingProfile.dominant_uncertainty === "product" ||
724
+ routingProfile.behavior_delta !== "preserve" ||
725
+ routingProfile.work_intent === "feature" ||
726
+ routingProfile.scope_shape === "cross_boundary"
727
+
728
+ if (!supportsFullMode) {
729
+ fail(
730
+ "routing_profile must reflect product uncertainty, changed behavior, feature intent, or cross-boundary scope for full mode",
731
+ )
732
+ }
733
+ }
734
+
735
+ function validateArtifactSignatureForMode(mode, artifacts) {
736
+ if (mode === "quick") {
737
+ for (const key of ["brief", "spec", "architecture", "plan", "migration_report", "qa_report"]) {
738
+ if (artifacts[key] !== null) {
739
+ fail(`artifacts.${key} must be null in quick mode`)
740
+ }
741
+ }
742
+
743
+ if (artifacts.adr.length > 0) {
744
+ fail("artifacts.adr must stay empty in quick mode")
745
+ }
746
+
747
+ return
748
+ }
749
+
750
+ if (mode === "migration") {
751
+ for (const key of ["task_card", "brief", "spec", "qa_report"]) {
752
+ if (artifacts[key] !== null) {
753
+ fail(`artifacts.${key} must be null in migration mode`)
754
+ }
755
+ }
756
+
757
+ return
758
+ }
759
+
760
+ for (const key of ["task_card", "migration_report"]) {
761
+ if (artifacts[key] !== null) {
762
+ fail(`artifacts.${key} must be null in full mode`)
763
+ }
764
+ }
765
+ }
766
+
767
+ function validateApprovals(mode, approvals) {
768
+ ensureObject(approvals, "approvals")
769
+
770
+ const requiredGates = getApprovalGatesForMode(mode)
771
+ const approvalKeys = Object.keys(approvals)
772
+
773
+ for (const gate of requiredGates) {
774
+ if (!(gate in approvals)) {
775
+ fail(`approvals.${gate} is required for mode '${mode}'`)
776
+ }
777
+ ensureGateShape(gate, approvals[gate])
778
+ }
779
+
780
+ for (const gate of approvalKeys) {
781
+ if (!requiredGates.includes(gate)) {
782
+ fail(`approvals.${gate} is not valid for mode '${mode}'`)
783
+ }
784
+ }
785
+ }
786
+
787
+ function validateStateObject(state, options = {}) {
788
+ ensureObject(state, "workflow state")
789
+
790
+ for (const key of [
791
+ "feature_id",
792
+ "feature_slug",
793
+ "mode",
794
+ "mode_reason",
795
+ "routing_profile",
796
+ "current_stage",
797
+ "status",
798
+ "current_owner",
799
+ "artifacts",
800
+ "approvals",
801
+ "issues",
802
+ "retry_count",
803
+ "escalated_from",
804
+ "escalation_reason",
805
+ "updated_at",
806
+ ]) {
807
+ if (!(key in state)) {
808
+ fail(`${key} is required`)
809
+ }
810
+ }
811
+
812
+ if (state.feature_id !== null) {
813
+ ensureString(state.feature_id, "feature_id")
814
+ }
815
+
816
+ if (state.feature_slug !== null) {
817
+ ensureString(state.feature_slug, "feature_slug")
818
+ }
819
+
820
+ ensureKnown(state.mode, MODE_VALUES, "mode")
821
+ ensureString(state.mode_reason, "mode_reason")
822
+ validateRoutingProfile(state.routing_profile)
823
+ validateRoutingProfileForMode(state.mode, state.routing_profile)
824
+ ensureKnown(state.current_stage, STAGE_SEQUENCE, "current_stage")
825
+ ensureKnown(state.status, STATUS_VALUES, "status")
826
+
827
+ const stageMode = getModeForStage(state.current_stage)
828
+ if (stageMode !== state.mode) {
829
+ fail(`current_stage '${state.current_stage}' does not belong to mode '${state.mode}'`)
830
+ }
831
+
832
+ if (options.strictOwner !== false) {
833
+ const expectedOwner = STAGE_OWNERS[state.current_stage]
834
+ if (state.current_owner !== expectedOwner) {
835
+ fail(`current_owner must be '${expectedOwner}' for stage '${state.current_stage}'`)
836
+ }
837
+ }
838
+
839
+ validateArtifacts(state.artifacts)
840
+ validateArtifactSignatureForMode(state.mode, state.artifacts)
841
+ validateApprovals(state.mode, state.approvals)
842
+
843
+ ensureArray(state.issues, "issues")
844
+ state.issues.forEach((issue, index) => ensureIssueShape(issue, index))
845
+
846
+ if (typeof state.retry_count !== "number" || Number.isNaN(state.retry_count) || state.retry_count < 0) {
847
+ fail("retry_count must be a non-negative number")
848
+ }
849
+
850
+ if (state.escalated_from !== null) {
851
+ ensureKnown(state.escalated_from, ["quick", "migration"], "escalated_from")
852
+ }
853
+
854
+ ensureNullableString(state.escalation_reason, "escalation_reason")
855
+ ensureNullableString(state.updated_at, "updated_at")
856
+
857
+ if (state.escalated_from === null && state.escalation_reason !== null) {
858
+ fail("escalation_reason must be null when escalated_from is null")
859
+ }
860
+
861
+ if ((state.escalated_from === "quick" || state.escalated_from === "migration") && state.mode !== "full") {
862
+ fail("mode must be 'full' when escalated_from records an escalated lane")
863
+ }
864
+
865
+ return state
866
+ }
867
+
868
+ function createFreshState({ workItemId, mode, featureId, featureSlug, modeReason, updatedAt }) {
869
+ const initialStage = getInitialStageForMode(mode)
870
+
871
+ return {
872
+ work_item_id: workItemId,
873
+ feature_id: featureId,
874
+ feature_slug: featureSlug,
875
+ mode,
876
+ mode_reason: modeReason,
877
+ routing_profile: createDefaultRoutingProfile(mode, modeReason),
878
+ current_stage: initialStage,
879
+ status: "in_progress",
880
+ current_owner: STAGE_OWNERS[initialStage],
881
+ artifacts: createEmptyArtifacts(),
882
+ approvals: createEmptyApprovals(mode),
883
+ issues: [],
884
+ retry_count: 0,
885
+ escalated_from: null,
886
+ escalation_reason: null,
887
+ updated_at: updatedAt,
888
+ }
889
+ }
890
+
891
+ function setRoutingProfile(workIntent, behaviorDelta, dominantUncertainty, scopeShape, selectionReason, customStatePath) {
892
+ ensureKnown(workIntent, ROUTING_WORK_INTENT_VALUES, "routing_profile.work_intent")
893
+ ensureKnown(behaviorDelta, ROUTING_BEHAVIOR_DELTA_VALUES, "routing_profile.behavior_delta")
894
+ ensureKnown(
895
+ dominantUncertainty,
896
+ ROUTING_DOMINANT_UNCERTAINTY_VALUES,
897
+ "routing_profile.dominant_uncertainty",
898
+ )
899
+ ensureKnown(scopeShape, ROUTING_SCOPE_SHAPE_VALUES, "routing_profile.scope_shape")
900
+ ensureString(selectionReason, "routing_profile.selection_reason")
901
+
902
+ return mutate(customStatePath, (state) => {
903
+ state.routing_profile = {
904
+ work_intent: workIntent,
905
+ behavior_delta: behaviorDelta,
906
+ dominant_uncertainty: dominantUncertainty,
907
+ scope_shape: scopeShape,
908
+ selection_reason: selectionReason,
909
+ }
910
+ return state
911
+ })
912
+ }
913
+
914
+ function mutate(customStatePath, mutator) {
915
+ const context = readManagedState(customStatePath)
916
+ const { state, workItemId, index, projectRoot } = context
917
+ const expectedRevision = captureRevision(state)
918
+ const expectedMirrorRevision = index.active_work_item_id === workItemId ? expectedRevision : null
919
+ validateStateObject(state)
920
+ validateManagedState(state, projectRoot, workItemId)
921
+ const nextState = mutator(JSON.parse(JSON.stringify(state)), context)
922
+ nextState.updated_at = timestamp()
923
+ validateStateObject(nextState)
924
+ return persistManagedState(customStatePath, nextState, {
925
+ expectedRevision,
926
+ expectedMirrorRevision,
927
+ workItemId,
928
+ activateWorkItemId: index.active_work_item_id ?? workItemId,
929
+ })
930
+ }
931
+
932
+ function mutateWorkItem(workItemId, customStatePath, mutator) {
933
+ const context = readWorkItemContext(workItemId, customStatePath)
934
+ const { state, projectRoot } = context
935
+ const index = readWorkItemIndex(projectRoot)
936
+ const expectedRevision = captureRevision(state)
937
+ const expectedMirrorRevision = index.active_work_item_id === workItemId ? expectedRevision : null
938
+
939
+ validateStateObject(state)
940
+ validateManagedState(state, projectRoot, workItemId)
941
+
942
+ const nextState = mutator(JSON.parse(JSON.stringify(state)), context)
943
+ nextState.updated_at = timestamp()
944
+ validateStateObject(nextState)
945
+
946
+ return persistManagedState(customStatePath, nextState, {
947
+ expectedRevision,
948
+ expectedMirrorRevision,
949
+ workItemId,
950
+ activateWorkItemId: index.active_work_item_id ?? workItemId,
951
+ })
952
+ }
953
+
954
+ function showState(customStatePath) {
955
+ const { statePath, state, projectRoot, workItemId } = readManagedState(customStatePath)
956
+ validateStateObject(state)
957
+ validateManagedState(state, projectRoot, workItemId)
958
+ return { statePath, state }
959
+ }
960
+
961
+ function showWorkItemState(workItemId, customStatePath) {
962
+ ensureString(workItemId, "work_item_id")
963
+ const { statePath, projectRoot, state } = readManagedState(customStatePath, workItemId)
964
+ validateStateObject(state)
965
+ validateManagedState(state, projectRoot, workItemId)
966
+
967
+ return {
968
+ statePath,
969
+ workItemStatePath: resolveWorkItemPaths(projectRoot, workItemId).statePath,
970
+ state,
971
+ }
972
+ }
973
+
974
+ function createWorkItem(mode, featureId, featureSlug, modeReason, customStatePath) {
975
+ return startTask(mode, featureId, featureSlug, modeReason, customStatePath)
976
+ }
977
+
978
+ function listWorkItems(customStatePath) {
979
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
980
+ const index = readWorkItemIndex(projectRoot)
981
+ return {
982
+ projectRoot,
983
+ index,
984
+ workItems: index.work_items,
985
+ }
986
+ }
987
+
988
+ function listTasks(workItemId, customStatePath) {
989
+ const context = readWorkItemContext(workItemId, customStatePath)
990
+ const { state, projectRoot } = context
991
+ requireFullModeWorkItem(state, workItemId)
992
+ requireTaskBoardStage(state, workItemId, "view a task board")
993
+
994
+ const board = buildBoardView(state, readTaskBoardIfExists(projectRoot, workItemId))
995
+ return {
996
+ ...context,
997
+ board,
998
+ tasks: board.tasks,
999
+ }
1000
+ }
1001
+
1002
+ function createTask(workItemId, taskInput, customStatePath) {
1003
+ return withTaskBoard(workItemId, customStatePath, (board) => {
1004
+ const task = buildTaskRecord(taskInput)
1005
+
1006
+ if (board.tasks.some((entry) => entry.task_id === task.task_id)) {
1007
+ fail(`Duplicate task_id '${task.task_id}' in task board`)
1008
+ }
1009
+
1010
+ board.tasks.push(task)
1011
+ return board
1012
+ })
1013
+ }
1014
+
1015
+ function claimTask(workItemId, taskId, owner, customStatePath, options = {}) {
1016
+ ensureString(owner, "owner")
1017
+
1018
+ return withTaskBoard(workItemId, customStatePath, (board) => {
1019
+ const task = findTask(board, taskId)
1020
+
1021
+ if (task.primary_owner && task.primary_owner !== owner) {
1022
+ fail("Implicit reassignment is not allowed; use reassignTask")
1023
+ }
1024
+
1025
+ validateReassignmentAuthority({
1026
+ task,
1027
+ ownerField: "primary_owner",
1028
+ requestedBy: options.requestedBy,
1029
+ nextOwner: owner,
1030
+ })
1031
+
1032
+ validateTaskTransition(task, "claimed")
1033
+ task.primary_owner = owner
1034
+ task.status = "claimed"
1035
+ task.updated_at = timestamp()
1036
+ return board
1037
+ })
1038
+ }
1039
+
1040
+ function assignQaOwner(workItemId, taskId, qaOwner, customStatePath, options = {}) {
1041
+ ensureString(qaOwner, "qa_owner")
1042
+
1043
+ return withTaskBoard(workItemId, customStatePath, (board) => {
1044
+ const task = findTask(board, taskId)
1045
+
1046
+ validateReassignmentAuthority({
1047
+ task,
1048
+ ownerField: "qa_owner",
1049
+ requestedBy: options.requestedBy,
1050
+ nextOwner: qaOwner,
1051
+ })
1052
+
1053
+ task.qa_owner = qaOwner
1054
+ task.updated_at = timestamp()
1055
+ return board
1056
+ })
1057
+ }
1058
+
1059
+ function reassignTask(workItemId, taskId, owner, customStatePath, options = {}) {
1060
+ ensureString(owner, "owner")
1061
+
1062
+ return withTaskBoard(workItemId, customStatePath, (board) => {
1063
+ const task = findTask(board, taskId)
1064
+
1065
+ validateReassignmentAuthority({
1066
+ task,
1067
+ ownerField: "primary_owner",
1068
+ requestedBy: options.requestedBy,
1069
+ nextOwner: owner,
1070
+ })
1071
+
1072
+ task.primary_owner = owner
1073
+ if (task.status === "ready") {
1074
+ task.status = "claimed"
1075
+ }
1076
+ task.updated_at = timestamp()
1077
+ return board
1078
+ })
1079
+ }
1080
+
1081
+ function releaseTask(workItemId, taskId, customStatePath, options = {}) {
1082
+ ensureString(options.requestedBy, "requestedBy")
1083
+
1084
+ return withTaskBoard(workItemId, customStatePath, (board) => {
1085
+ const task = findTask(board, taskId)
1086
+ const allowedAuthorities = VALID_ASSIGNMENT_AUTHORITIES.primary_owner
1087
+ const isCurrentOwner = task.primary_owner === options.requestedBy
1088
+
1089
+ if (!allowedAuthorities.includes(options.requestedBy) && !isCurrentOwner) {
1090
+ fail(`Only ${allowedAuthorities.join(" or ")} can release primary_owner`)
1091
+ }
1092
+
1093
+ task.primary_owner = null
1094
+ if (task.status === "claimed") {
1095
+ task.status = "ready"
1096
+ }
1097
+ task.updated_at = timestamp()
1098
+ return board
1099
+ })
1100
+ }
1101
+
1102
+ function setTaskStatus(workItemId, taskId, nextStatus, customStatePath, options = {}) {
1103
+ validateTaskStatus(nextStatus)
1104
+
1105
+ const targetContext = readWorkItemContext(workItemId, customStatePath)
1106
+ const { state: targetState, projectRoot } = targetContext
1107
+ validateManagedState(targetState, projectRoot, workItemId)
1108
+ const targetBoard = buildBoardView(targetState, readTaskBoardIfExists(projectRoot, workItemId))
1109
+ const targetTask = findTask(targetBoard, taskId)
1110
+
1111
+ if (targetTask.status === "qa_in_progress" && (nextStatus === "claimed" || nextStatus === "in_progress")) {
1112
+ decideQaFailLocalRework({
1113
+ mode: "full",
1114
+ task: targetTask,
1115
+ finding: options.finding,
1116
+ rerouteDecision: options.rerouteDecision,
1117
+ })
1118
+ }
1119
+
1120
+ const shouldApplyQaFailReroute = nextStatus === "claimed" || nextStatus === "in_progress"
1121
+ let rerouteDecision = null
1122
+
1123
+ const result = withTaskBoard(workItemId, customStatePath, (board) => {
1124
+ const task = findTask(board, taskId)
1125
+
1126
+ if (task.status === "qa_in_progress" && (nextStatus === "claimed" || nextStatus === "in_progress")) {
1127
+ const decision = decideQaFailLocalRework({
1128
+ mode: "full",
1129
+ task,
1130
+ finding: options.finding,
1131
+ rerouteDecision: options.rerouteDecision,
1132
+ })
1133
+ rerouteDecision = decision.route
1134
+
1135
+ validateTaskTransition(task, nextStatus, {
1136
+ allowQaFailRework: true,
1137
+ finding: options.finding,
1138
+ })
1139
+ } else {
1140
+ validateTaskTransition(task, nextStatus)
1141
+ }
1142
+
1143
+ task.status = nextStatus
1144
+ task.updated_at = timestamp()
1145
+ return board
1146
+ })
1147
+
1148
+ if (shouldApplyQaFailReroute && rerouteDecision) {
1149
+ const rerouted = mutateWorkItem(workItemId, customStatePath, (state) => {
1150
+ state.current_stage = rerouteDecision.stage
1151
+ state.current_owner = rerouteDecision.owner
1152
+ state.status = "in_progress"
1153
+ return state
1154
+ })
1155
+
1156
+ return {
1157
+ ...rerouted,
1158
+ board: result.board,
1159
+ }
1160
+ }
1161
+
1162
+ return result
1163
+ }
1164
+
1165
+ function validateWorkItemBoard(workItemId, customStatePath) {
1166
+ const context = readWorkItemContext(workItemId, customStatePath)
1167
+ const { state, projectRoot } = context
1168
+ requireFullModeWorkItem(state, workItemId)
1169
+ const board = readTaskBoardIfExists(projectRoot, workItemId)
1170
+
1171
+ if (!board) {
1172
+ fail(`Task board missing for work item '${workItemId}'`)
1173
+ }
1174
+
1175
+ return {
1176
+ ...context,
1177
+ board: validateTaskBoard(buildBoardForValidation(state, board)),
1178
+ }
1179
+ }
1180
+
1181
+ function selectActiveWorkItem(workItemId, customStatePath) {
1182
+ ensureString(workItemId, "work_item_id")
1183
+ const statePath = resolveStatePath(customStatePath)
1184
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
1185
+ const previousIndex = readWorkItemIndex(projectRoot)
1186
+ const previousActiveWorkItemId = previousIndex.active_work_item_id
1187
+
1188
+ try {
1189
+ setActiveWorkItem(projectRoot, workItemId)
1190
+ const nextActiveState = readWorkItemState(projectRoot, workItemId)
1191
+ const mirrorRefreshPlan = planGuardedMirrorRefresh({
1192
+ activeWorkItemId: workItemId,
1193
+ targetWorkItemId: workItemId,
1194
+ nextState: nextActiveState,
1195
+ })
1196
+ const preflightMirrorState = fs.existsSync(statePath) ? readState(statePath).state : null
1197
+
1198
+ if (preflightMirrorState) {
1199
+ guardWrite({
1200
+ currentState: preflightMirrorState,
1201
+ expectedRevision: captureRevision(preflightMirrorState),
1202
+ nextState: nextActiveState,
1203
+ })
1204
+ }
1205
+
1206
+ writeCompatibilityMirror(projectRoot, nextActiveState)
1207
+ const state = readState(statePath).state
1208
+ const mirrorRevision = captureRevision(state)
1209
+
1210
+ if (mirrorRevision !== mirrorRefreshPlan.mirrorRevision) {
1211
+ fail(
1212
+ `Compatibility mirror refresh revision conflict for active work item '${workItemId}'; expected ${mirrorRefreshPlan.mirrorRevision} but found ${mirrorRevision}`,
1213
+ )
1214
+ }
1215
+
1216
+ validateStateObject(state)
1217
+ validateManagedState(state, projectRoot, workItemId)
1218
+
1219
+ return {
1220
+ statePath,
1221
+ state,
1222
+ }
1223
+ } catch (error) {
1224
+ if (previousActiveWorkItemId !== null && previousActiveWorkItemId !== workItemId) {
1225
+ setActiveWorkItem(projectRoot, previousActiveWorkItemId)
1226
+ }
1227
+ throw error
1228
+ }
1229
+ }
1230
+
1231
+ function getRuntimeStatus(customStatePath) {
1232
+ const { statePath, state } = showState(customStatePath)
1233
+
1234
+ const projectRoot = resolveProjectRoot(customStatePath)
1235
+ const runtimeRoot = resolveRuntimeRoot(customStatePath)
1236
+ const kitRoot = resolveKitRoot(projectRoot)
1237
+ const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
1238
+ const manifest = readJsonIfExists(manifestPath)
1239
+ const { registryPath, installManifestPath } = getManifestPaths(kitRoot, manifest)
1240
+ const installManifest = readJsonIfExists(installManifestPath)
1241
+ const hooksConfigPath = path.join(kitRoot, "hooks", "hooks.json")
1242
+ const sessionStartPath = path.join(kitRoot, "hooks", "session-start")
1243
+ const metaSkillPath = path.join(kitRoot, "skills", "using-skills", "SKILL.md")
1244
+ const kit = manifest?.kit ?? {}
1245
+
1246
+ return {
1247
+ projectRoot,
1248
+ runtimeRoot,
1249
+ kitRoot,
1250
+ statePath,
1251
+ manifestPath,
1252
+ registryPath,
1253
+ installManifestPath,
1254
+ hooksConfigPath,
1255
+ sessionStartPath,
1256
+ metaSkillPath,
1257
+ kitName: kit.name ?? "Unknown kit",
1258
+ kitVersion: kit.version ?? "unknown",
1259
+ entryAgent: kit.entryAgent ?? "unknown",
1260
+ activeProfile: installManifest?.installation?.activeProfile ?? kit.activeProfile ?? "unknown",
1261
+ installManifest,
1262
+ state,
1263
+ }
1264
+ }
1265
+
1266
+ function getVersionInfo(customStatePath) {
1267
+ const runtime = getRuntimeStatus(customStatePath)
1268
+ return {
1269
+ kitName: runtime.kitName,
1270
+ kitVersion: runtime.kitVersion,
1271
+ activeProfile: runtime.activeProfile,
1272
+ }
1273
+ }
1274
+
1275
+ function runDoctor(customStatePath) {
1276
+ const statePath = resolveStatePath(customStatePath)
1277
+ const projectRoot = resolveProjectRoot(customStatePath)
1278
+ const runtimeRoot = resolveRuntimeRoot(customStatePath)
1279
+ const kitRoot = resolveKitRoot(projectRoot)
1280
+ const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
1281
+ const manifestInfo = tryReadJson(manifestPath)
1282
+ const manifest = manifestInfo.data
1283
+ const { registryPath, installManifestPath } = getManifestPaths(kitRoot, manifest)
1284
+ const registryInfo = tryReadJson(registryPath)
1285
+ const installManifestInfo = tryReadJson(installManifestPath)
1286
+ const installManifest = installManifestInfo.data
1287
+ const hooksConfigPath = path.join(kitRoot, "hooks", "hooks.json")
1288
+ const sessionStartPath = path.join(kitRoot, "hooks", "session-start")
1289
+ const metaSkillPath = path.join(kitRoot, "skills", "using-skills", "SKILL.md")
1290
+ const workflowStateCliPath = path.join(kitRoot, ".opencode", "workflow-state.js")
1291
+
1292
+ let stateValid = false
1293
+ let state = null
1294
+ let kitName = "Unknown kit"
1295
+ let kitVersion = "unknown"
1296
+ let entryAgent = "unknown"
1297
+
1298
+ if (manifest?.kit) {
1299
+ kitName = manifest.kit.name ?? kitName
1300
+ kitVersion = manifest.kit.version ?? kitVersion
1301
+ entryAgent = manifest.kit.entryAgent ?? entryAgent
1302
+ }
1303
+
1304
+ if (fs.existsSync(statePath)) {
1305
+ try {
1306
+ const result = showState(customStatePath)
1307
+ state = result.state
1308
+ stateValid = true
1309
+ } catch (_error) {
1310
+ stateValid = false
1311
+ }
1312
+ }
1313
+
1314
+ const runtime = {
1315
+ projectRoot,
1316
+ runtimeRoot,
1317
+ kitRoot,
1318
+ statePath,
1319
+ manifestPath,
1320
+ registryPath,
1321
+ installManifestPath,
1322
+ workflowStateCliPath,
1323
+ hooksConfigPath,
1324
+ sessionStartPath,
1325
+ metaSkillPath,
1326
+ kitName,
1327
+ kitVersion,
1328
+ entryAgent,
1329
+ activeProfile: installManifest?.installation?.activeProfile ?? manifest?.kit?.activeProfile ?? "unknown",
1330
+ installManifest,
1331
+ state,
1332
+ }
1333
+
1334
+ const checks = [
1335
+ { label: "manifest file found", ok: fs.existsSync(manifestPath) },
1336
+ { label: "workflow state file found", ok: fs.existsSync(statePath) },
1337
+ { label: "workflow state is valid", ok: stateValid },
1338
+ { label: "registry file found", ok: fs.existsSync(registryPath) },
1339
+ { label: "registry metadata is readable", ok: !registryInfo.exists || registryInfo.readable },
1340
+ { label: "install manifest found", ok: fs.existsSync(installManifestPath) },
1341
+ {
1342
+ label: "install manifest is readable",
1343
+ ok: !installManifestInfo.exists || installManifestInfo.readable,
1344
+ },
1345
+ { label: "workflow state CLI found", ok: fs.existsSync(workflowStateCliPath) },
1346
+ { label: "hooks config found", ok: fs.existsSync(hooksConfigPath) },
1347
+ { label: "session-start hook found", ok: fs.existsSync(sessionStartPath) },
1348
+ { label: "meta-skill found", ok: fs.existsSync(metaSkillPath) },
1349
+ {
1350
+ label: "active profile exists in registry",
1351
+ ok:
1352
+ !installManifestInfo.readable ||
1353
+ !registryInfo.readable ||
1354
+ (registryInfo.data?.profiles ?? []).some(
1355
+ (profile) => profile.name === (installManifest?.installation?.activeProfile ?? manifest?.kit?.activeProfile),
1356
+ ),
1357
+ },
1358
+ {
1359
+ label: "manifest and install manifest profiles agree",
1360
+ ok:
1361
+ !installManifestInfo.readable ||
1362
+ !manifest?.kit?.activeProfile ||
1363
+ manifest.kit.activeProfile === installManifest?.installation?.activeProfile,
1364
+ },
1365
+ ]
1366
+
1367
+ const contractReport = buildContractConsistencyReport({ projectRoot: kitRoot, manifest })
1368
+ checks.push(...contractReport.checks)
1369
+
1370
+ const summary = checks.reduce(
1371
+ (counts, check) => {
1372
+ if (check.ok) {
1373
+ counts.ok += 1
1374
+ } else {
1375
+ counts.error += 1
1376
+ }
1377
+ return counts
1378
+ },
1379
+ { ok: 0, warn: 0, error: 0 },
1380
+ )
1381
+
1382
+ return {
1383
+ runtime,
1384
+ checks,
1385
+ summary,
1386
+ }
1387
+ }
1388
+
1389
+ function getContractConsistencyReport(customStatePath) {
1390
+ const projectRoot = resolveProjectRoot(customStatePath)
1391
+ const kitRoot = resolveKitRoot(projectRoot)
1392
+ const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
1393
+ const manifest = readJsonIfExists(manifestPath)
1394
+
1395
+ return buildContractConsistencyReport({ projectRoot: kitRoot, manifest })
1396
+ }
1397
+
1398
+ function validateState(customStatePath) {
1399
+ const { statePath, state, projectRoot, workItemId } = readManagedState(customStatePath)
1400
+ validateStateObject(state)
1401
+ validateManagedState(state, projectRoot, workItemId)
1402
+ return { statePath, state }
1403
+ }
1404
+
1405
+ function startTask(mode, featureId, featureSlug, modeReason, customStatePath) {
1406
+ ensureKnown(mode, MODE_VALUES, "mode")
1407
+ ensureString(featureId, "feature_id")
1408
+ ensureString(featureSlug, "feature_slug")
1409
+ ensureString(modeReason, "mode_reason")
1410
+
1411
+ const workItemId = deriveWorkItemId({ feature_id: featureId })
1412
+ const projectRoot = ensureWorkItemStoreReady(customStatePath)
1413
+ const workItemPaths = resolveWorkItemPaths(projectRoot, workItemId)
1414
+ const index = readWorkItemIndex(projectRoot)
1415
+ const existingState = fs.existsSync(workItemPaths.statePath) ? readWorkItemState(projectRoot, workItemId) : null
1416
+ const expectedRevision = existingState ? captureRevision(existingState) : null
1417
+ const expectedMirrorRevision = index.active_work_item_id === workItemId && existingState ? expectedRevision : null
1418
+ const nextState = createFreshState({
1419
+ workItemId,
1420
+ mode,
1421
+ featureId,
1422
+ featureSlug,
1423
+ modeReason,
1424
+ updatedAt: timestamp(),
1425
+ })
1426
+
1427
+ validateStateObject(nextState)
1428
+ return persistManagedState(customStatePath, nextState, {
1429
+ expectedRevision,
1430
+ expectedMirrorRevision,
1431
+ workItemId,
1432
+ activateWorkItemId: workItemId,
1433
+ })
1434
+ }
1435
+
1436
+ function startFeature(featureId, featureSlug, customStatePath) {
1437
+ return startTask(
1438
+ "full",
1439
+ featureId,
1440
+ featureSlug,
1441
+ "Started with legacy start-feature command; defaulting to Full Delivery mode",
1442
+ customStatePath,
1443
+ )
1444
+ }
1445
+
1446
+ function setApproval(gate, status, approvedBy, approvedAt, notes, customStatePath) {
1447
+ ensureKnown(status, ["pending", "approved", "rejected"], "status")
1448
+
1449
+ return mutate(customStatePath, (state, context) => {
1450
+ const allowedGates = getApprovalGatesForMode(state.mode)
1451
+ ensureKnown(gate, allowedGates, `gate for mode '${state.mode}'`)
1452
+
1453
+ const { projectRoot, workItemId } = context
1454
+ if (status === "approved" && gate === "tech_lead_to_fullstack") {
1455
+ requireValidTaskBoard(
1456
+ state,
1457
+ projectRoot,
1458
+ workItemId,
1459
+ "full_plan",
1460
+ "A valid task board is required before approving 'tech_lead_to_fullstack' or entering 'full_implementation'",
1461
+ )
1462
+ }
1463
+
1464
+ state.approvals[gate] = {
1465
+ status,
1466
+ approved_by: approvedBy ?? null,
1467
+ approved_at: approvedAt ?? null,
1468
+ notes: notes ?? null,
1469
+ }
1470
+ return state
1471
+ })
1472
+ }
1473
+
1474
+ function advanceStage(targetStage, customStatePath) {
1475
+ ensureKnown(targetStage, STAGE_SEQUENCE, "target stage")
1476
+
1477
+ return mutate(customStatePath, (state, context) => {
1478
+ const { projectRoot, workItemId } = context
1479
+ if (getModeForStage(targetStage) !== state.mode) {
1480
+ fail(`target stage '${targetStage}' does not belong to mode '${state.mode}'`)
1481
+ }
1482
+
1483
+ const nextStage = getNextStage(state.mode, state.current_stage)
1484
+ if (!nextStage) {
1485
+ fail(`Stage '${state.current_stage}' cannot advance further`)
1486
+ }
1487
+
1488
+ if (targetStage !== nextStage) {
1489
+ fail(`advance-stage only allows the immediate next stage '${nextStage}', not '${targetStage}'`)
1490
+ }
1491
+
1492
+ if (targetStage === "full_implementation") {
1493
+ requireValidTaskBoard(
1494
+ state,
1495
+ projectRoot,
1496
+ workItemId,
1497
+ "full_implementation",
1498
+ "A valid task board is required before entering 'full_implementation'",
1499
+ )
1500
+ }
1501
+
1502
+ if (targetStage === "full_qa") {
1503
+ requireValidTaskBoard(
1504
+ state,
1505
+ projectRoot,
1506
+ workItemId,
1507
+ "full_qa",
1508
+ "A valid task board is required before entering 'full_qa'",
1509
+ )
1510
+ }
1511
+
1512
+ const requiredGate = getTransitionGate(state.mode, state.current_stage, targetStage)
1513
+ if (requiredGate && state.approvals[requiredGate].status !== "approved") {
1514
+ fail(`Cannot advance from '${state.current_stage}' to '${targetStage}' until gate '${requiredGate}' is approved`)
1515
+ }
1516
+
1517
+ state.current_stage = targetStage
1518
+ state.current_owner = STAGE_OWNERS[targetStage]
1519
+ state.status = targetStage.endsWith("_done") ? "done" : "in_progress"
1520
+ return state
1521
+ })
1522
+ }
1523
+
1524
+ function linkArtifact(kind, artifactPath, customStatePath) {
1525
+ ensureKnown(kind, ARTIFACT_KINDS, "artifact kind")
1526
+ ensureString(artifactPath, "artifact path")
1527
+
1528
+ const projectRoot = resolveProjectRoot(customStatePath)
1529
+ const resolvedArtifactPath = path.isAbsolute(artifactPath)
1530
+ ? artifactPath
1531
+ : path.resolve(projectRoot, artifactPath)
1532
+ if (!fs.existsSync(resolvedArtifactPath)) {
1533
+ fail(`Artifact path does not exist: '${artifactPath}'`)
1534
+ }
1535
+
1536
+ return mutate(customStatePath, (state) => {
1537
+ if (kind === "adr") {
1538
+ if (!state.artifacts.adr.includes(artifactPath)) {
1539
+ state.artifacts.adr.push(artifactPath)
1540
+ }
1541
+ return state
1542
+ }
1543
+
1544
+ state.artifacts[kind] = artifactPath
1545
+ return state
1546
+ })
1547
+ }
1548
+
1549
+ function scaffoldAndLinkArtifact(kind, slug, customStatePath, options = {}) {
1550
+ ensureString(kind, "artifact kind")
1551
+ ensureString(slug, "artifact slug")
1552
+
1553
+ if (!SUPPORTED_SCAFFOLDS[kind]) {
1554
+ fail(`Unsupported scaffold kind '${kind}'`)
1555
+ }
1556
+
1557
+ const { statePath, state } = readState(customStatePath)
1558
+ validateStateObject(state)
1559
+
1560
+ if (kind !== "adr" && state.artifacts[kind] !== null) {
1561
+ fail(`Artifact already linked for artifact kind '${kind}'`)
1562
+ }
1563
+
1564
+ if (kind === "task_card" && state.mode !== "quick") {
1565
+ fail(`Artifact scaffold kind 'task_card' requires quick mode`)
1566
+ }
1567
+
1568
+ if (kind === "plan") {
1569
+ if (state.mode !== "full" && state.mode !== "migration") {
1570
+ fail(`Artifact scaffold kind 'plan' requires full or migration mode`)
1571
+ }
1572
+
1573
+ if (state.mode === "full" && state.current_stage !== "full_plan") {
1574
+ fail(`Artifact scaffold kind 'plan' requires current stage 'full_plan'`)
1575
+ }
1576
+
1577
+ if (state.mode === "migration" && state.current_stage !== "migration_strategy") {
1578
+ fail(`Artifact scaffold kind 'plan' requires current stage 'migration_strategy'`)
1579
+ }
1580
+
1581
+ if (typeof state.artifacts.architecture !== "string" || state.artifacts.architecture.length === 0) {
1582
+ fail(`Artifact scaffold kind 'plan' requires a linked architecture artifact`)
1583
+ }
1584
+ }
1585
+
1586
+ if (kind === "migration_report") {
1587
+ if (state.mode !== "migration") {
1588
+ fail(`Artifact scaffold kind 'migration_report' requires migration mode`)
1589
+ }
1590
+
1591
+ if (state.current_stage !== "migration_baseline" && state.current_stage !== "migration_strategy") {
1592
+ fail(`Artifact scaffold kind 'migration_report' requires current stage 'migration_baseline' or 'migration_strategy'`)
1593
+ }
1594
+ }
1595
+
1596
+ const projectRoot = resolveProjectRoot(customStatePath)
1597
+ const featureId = state.feature_id
1598
+ const featureSlug = state.feature_slug
1599
+
1600
+ if (typeof featureId !== "string" || featureId.length === 0) {
1601
+ fail("feature_id must be set before scaffolding an artifact")
1602
+ }
1603
+
1604
+ if (typeof featureSlug !== "string" || featureSlug.length === 0) {
1605
+ fail("feature_slug must be set before scaffolding an artifact")
1606
+ }
1607
+
1608
+ const scaffoldResult = scaffoldArtifact({
1609
+ projectRoot,
1610
+ kind,
1611
+ mode: state.mode,
1612
+ slug,
1613
+ featureId,
1614
+ featureSlug,
1615
+ sourceArchitecture:
1616
+ kind === "plan" || kind === "migration_report" ? state.artifacts.architecture : null,
1617
+ sourcePlan: kind === "migration_report" ? state.artifacts.plan : null,
1618
+ })
1619
+
1620
+ try {
1621
+ if (typeof options.beforeLink === "function") {
1622
+ options.beforeLink(scaffoldResult)
1623
+ }
1624
+
1625
+ const next = linkArtifact(kind, scaffoldResult.artifactPath, statePath)
1626
+
1627
+ return {
1628
+ ...next,
1629
+ artifactPath: scaffoldResult.artifactPath,
1630
+ }
1631
+ } catch (error) {
1632
+ const createdPath = path.join(projectRoot, scaffoldResult.artifactPath)
1633
+ if (fs.existsSync(createdPath)) {
1634
+ fs.rmSync(createdPath)
1635
+ }
1636
+ throw error
1637
+ }
1638
+ }
1639
+
1640
+ function recordIssue(issue, customStatePath) {
1641
+ return mutate(customStatePath, (state) => {
1642
+ const nextIssue = { ...issue }
1643
+ if (typeof nextIssue.artifact_refs === "string") {
1644
+ nextIssue.artifact_refs = [nextIssue.artifact_refs]
1645
+ }
1646
+ ensureIssueShape(nextIssue, state.issues.length)
1647
+ state.issues.push(nextIssue)
1648
+ state.status = "blocked"
1649
+ return state
1650
+ })
1651
+ }
1652
+
1653
+ function clearIssues(customStatePath) {
1654
+ return mutate(customStatePath, (state) => {
1655
+ state.issues = []
1656
+ if (!state.current_stage.endsWith("_done")) {
1657
+ state.status = "in_progress"
1658
+ }
1659
+ return state
1660
+ })
1661
+ }
1662
+
1663
+ function routeRework(issueType, repeatFailedFix, customStatePath) {
1664
+ ensureKnown(issueType, ISSUE_TYPES, "issue_type")
1665
+
1666
+ return mutate(customStatePath, (state) => {
1667
+ const previousMode = state.mode
1668
+ const route = getReworkRoute(state.mode, issueType)
1669
+ if (!route) {
1670
+ fail(`No rework route exists for issue type '${issueType}' in mode '${state.mode}'`)
1671
+ }
1672
+
1673
+ if (route.escalate) {
1674
+ state.mode = route.mode
1675
+ state.mode_reason = `Promoted from ${previousMode} mode after '${issueType}' QA finding`
1676
+ state.routing_profile = createDefaultRoutingProfile(route.mode, state.mode_reason)
1677
+ state.current_stage = route.stage
1678
+ state.current_owner = route.owner
1679
+ state.status = "in_progress"
1680
+ state.approvals = createEmptyApprovals(route.mode)
1681
+ state.escalated_from = previousMode
1682
+ state.escalation_reason = `${previousMode} work escalated to ${route.mode === "full" ? "Full Delivery" : route.mode} because QA reported '${issueType}'`
1683
+ } else {
1684
+ state.current_stage = route.stage
1685
+ state.current_owner = route.owner
1686
+ state.status = "in_progress"
1687
+ }
1688
+
1689
+ if (repeatFailedFix) {
1690
+ state.retry_count += 1
1691
+ if (state.retry_count >= ESCALATION_RETRY_THRESHOLD) {
1692
+ state.status = "blocked"
1693
+ }
1694
+ }
1695
+
1696
+ return state
1697
+ })
1698
+ }
1699
+
1700
+ module.exports = {
1701
+ advanceStage,
1702
+ assignQaOwner,
1703
+ clearIssues,
1704
+ claimTask,
1705
+ createTask,
1706
+ createWorkItem,
1707
+ ESCALATION_RETRY_THRESHOLD,
1708
+ getContractConsistencyReport,
1709
+ getInstallManifest,
1710
+ getProfile,
1711
+ getRegistry,
1712
+ getRuntimeStatus,
1713
+ getVersionInfo,
1714
+ linkArtifact,
1715
+ listTasks,
1716
+ listWorkItems,
1717
+ listProfiles,
1718
+ readState,
1719
+ recordIssue,
1720
+ reassignTask,
1721
+ releaseTask,
1722
+ resolveStatePath,
1723
+ routeRework,
1724
+ runDoctor,
1725
+ scaffoldAndLinkArtifact,
1726
+ selectActiveWorkItem,
1727
+ setRoutingProfile,
1728
+ setTaskStatus,
1729
+ setApproval,
1730
+ showState,
1731
+ showWorkItemState,
1732
+ syncInstallManifest,
1733
+ startFeature,
1734
+ startTask,
1735
+ validateWorkItemBoard,
1736
+ validateState,
1737
+ validateStateObject,
1738
+ writeState,
1739
+ }