@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,733 @@
1
+ const test = require("node:test")
2
+ const assert = require("node:assert/strict")
3
+ const fs = require("fs")
4
+ const os = require("os")
5
+ const path = require("path")
6
+ const { spawnSync } = require("child_process")
7
+ const { scaffoldAndLinkArtifact } = require("../lib/workflow-state-controller")
8
+
9
+ function makeTempProject() {
10
+ return fs.mkdtempSync(path.join(os.tmpdir(), "openkit-artifact-scaffold-"))
11
+ }
12
+
13
+ function setupTempRuntime(projectRoot) {
14
+ const opencodeDir = path.join(projectRoot, ".opencode")
15
+ const templatesDir = path.join(projectRoot, "docs", "templates")
16
+ const tasksDir = path.join(projectRoot, "docs", "tasks")
17
+ const plansDir = path.join(projectRoot, "docs", "plans")
18
+
19
+ fs.mkdirSync(opencodeDir, { recursive: true })
20
+ fs.mkdirSync(templatesDir, { recursive: true })
21
+ fs.mkdirSync(tasksDir, { recursive: true })
22
+ fs.mkdirSync(plansDir, { recursive: true })
23
+
24
+ const fixtureState = JSON.parse(
25
+ fs.readFileSync(path.resolve(__dirname, "../workflow-state.json"), "utf8"),
26
+ )
27
+ fixtureState.feature_id = "TASK-700"
28
+ fixtureState.feature_slug = "scaffold-target"
29
+ fixtureState.mode = "quick"
30
+ fixtureState.mode_reason = "Scaffold testing"
31
+ fixtureState.routing_profile = {
32
+ work_intent: "maintenance",
33
+ behavior_delta: "preserve",
34
+ dominant_uncertainty: "low_local",
35
+ scope_shape: "local",
36
+ selection_reason: "Scaffold testing",
37
+ }
38
+ fixtureState.current_stage = "quick_plan"
39
+ fixtureState.status = "in_progress"
40
+ fixtureState.current_owner = "MasterOrchestrator"
41
+ fixtureState.artifacts.task_card = null
42
+ fixtureState.artifacts.brief = null
43
+ fixtureState.artifacts.spec = null
44
+ fixtureState.artifacts.architecture = null
45
+ fixtureState.artifacts.plan = null
46
+ fixtureState.artifacts.migration_report = null
47
+ fixtureState.artifacts.qa_report = null
48
+ fixtureState.artifacts.adr = []
49
+ fixtureState.approvals = {
50
+ quick_verified: {
51
+ status: "pending",
52
+ approved_by: null,
53
+ approved_at: null,
54
+ notes: null,
55
+ },
56
+ }
57
+
58
+ fs.writeFileSync(
59
+ path.join(opencodeDir, "workflow-state.json"),
60
+ `${JSON.stringify(fixtureState, null, 2)}\n`,
61
+ "utf8",
62
+ )
63
+ fs.writeFileSync(
64
+ path.join(templatesDir, "quick-task-template.md"),
65
+ [
66
+ "---",
67
+ "artifact_type: quick_task_card",
68
+ "feature_id: TASK-000",
69
+ "feature_slug: example-task",
70
+ "---",
71
+ "",
72
+ "# Quick Task: <Task Name>",
73
+ "",
74
+ ].join("\n"),
75
+ "utf8",
76
+ )
77
+ fs.writeFileSync(
78
+ path.join(templatesDir, "implementation-plan-template.md"),
79
+ [
80
+ "---",
81
+ "artifact_type: implementation_plan",
82
+ "feature_id: FEATURE-000",
83
+ "feature_slug: example-feature",
84
+ "source_architecture: docs/architecture/YYYY-MM-DD-example-feature.md",
85
+ "---",
86
+ "",
87
+ "# Implementation Plan: <Feature Name>",
88
+ "",
89
+ ].join("\n"),
90
+ "utf8",
91
+ )
92
+ fs.writeFileSync(
93
+ path.join(templatesDir, "migration-plan-template.md"),
94
+ [
95
+ "---",
96
+ "artifact_type: migration_plan",
97
+ "feature_id: FEATURE-000",
98
+ "feature_slug: example-migration",
99
+ "source_architecture: docs/architecture/YYYY-MM-DD-example-migration.md",
100
+ "---",
101
+ "",
102
+ "# Migration Plan: <Migration Name>",
103
+ "",
104
+ ].join("\n"),
105
+ "utf8",
106
+ )
107
+ fs.writeFileSync(
108
+ path.join(templatesDir, "migration-report-template.md"),
109
+ [
110
+ "---",
111
+ "artifact_type: migration_report",
112
+ "feature_id: FEATURE-000",
113
+ "feature_slug: example-migration",
114
+ "source_architecture: docs/architecture/YYYY-MM-DD-example-migration.md",
115
+ "source_plan: docs/plans/YYYY-MM-DD-example-migration.md",
116
+ "---",
117
+ "",
118
+ "# Migration Report: <Migration Name>",
119
+ "",
120
+ ].join("\n"),
121
+ "utf8",
122
+ )
123
+ }
124
+
125
+ function setupTempRuntimeWithRealTemplates(projectRoot) {
126
+ setupTempRuntime(projectRoot)
127
+
128
+ const quickTemplate = fs.readFileSync(
129
+ path.resolve(__dirname, "../../docs/templates/quick-task-template.md"),
130
+ "utf8",
131
+ )
132
+ const planTemplate = fs.readFileSync(
133
+ path.resolve(__dirname, "../../docs/templates/implementation-plan-template.md"),
134
+ "utf8",
135
+ )
136
+ const migrationPlanTemplate = fs.readFileSync(
137
+ path.resolve(__dirname, "../../docs/templates/migration-plan-template.md"),
138
+ "utf8",
139
+ )
140
+ const migrationReportTemplate = fs.readFileSync(
141
+ path.resolve(__dirname, "../../docs/templates/migration-report-template.md"),
142
+ "utf8",
143
+ )
144
+
145
+ fs.writeFileSync(path.join(projectRoot, "docs", "templates", "quick-task-template.md"), quickTemplate, "utf8")
146
+ fs.writeFileSync(
147
+ path.join(projectRoot, "docs", "templates", "implementation-plan-template.md"),
148
+ planTemplate,
149
+ "utf8",
150
+ )
151
+ fs.writeFileSync(
152
+ path.join(projectRoot, "docs", "templates", "migration-plan-template.md"),
153
+ migrationPlanTemplate,
154
+ "utf8",
155
+ )
156
+ fs.writeFileSync(
157
+ path.join(projectRoot, "docs", "templates", "migration-report-template.md"),
158
+ migrationReportTemplate,
159
+ "utf8",
160
+ )
161
+ }
162
+
163
+ test("scaffold-artifact creates a quick task card and links it into state", () => {
164
+ const projectRoot = makeTempProject()
165
+ setupTempRuntime(projectRoot)
166
+
167
+ const result = spawnSync(
168
+ "node",
169
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "task_card", "copy-fix"],
170
+ {
171
+ cwd: projectRoot,
172
+ encoding: "utf8",
173
+ },
174
+ )
175
+
176
+ assert.equal(result.status, 0)
177
+ assert.match(result.stdout, /Created artifact 'task_card'/)
178
+
179
+ const state = JSON.parse(fs.readFileSync(path.join(projectRoot, ".opencode", "workflow-state.json"), "utf8"))
180
+ assert.match(state.artifacts.task_card, /docs\/tasks\/\d{4}-\d{2}-\d{2}-copy-fix\.md$/)
181
+ assert.equal(fs.existsSync(path.join(projectRoot, state.artifacts.task_card)), true)
182
+ })
183
+
184
+ test("scaffold-artifact substitutes real checked-in templates correctly", () => {
185
+ const projectRoot = makeTempProject()
186
+ setupTempRuntimeWithRealTemplates(projectRoot)
187
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
188
+
189
+ let result = spawnSync(
190
+ "node",
191
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "task_card", "real-template-task"],
192
+ {
193
+ cwd: projectRoot,
194
+ encoding: "utf8",
195
+ },
196
+ )
197
+
198
+ assert.equal(result.status, 0)
199
+
200
+ let state = JSON.parse(fs.readFileSync(statePath, "utf8"))
201
+ let taskCardContent = fs.readFileSync(path.join(projectRoot, state.artifacts.task_card), "utf8")
202
+ assert.match(taskCardContent, /feature_id: TASK-700/)
203
+ assert.match(taskCardContent, /feature_slug: scaffold-target/)
204
+ assert.match(taskCardContent, /# Quick Task: Real Template Task/)
205
+
206
+ state.feature_id = "FEATURE-705"
207
+ state.feature_slug = "real-template-plan"
208
+ state.mode = "full"
209
+ state.mode_reason = "Real template plan scaffold"
210
+ state.routing_profile = {
211
+ work_intent: "feature",
212
+ behavior_delta: "extend",
213
+ dominant_uncertainty: "product",
214
+ scope_shape: "cross_boundary",
215
+ selection_reason: "Real template plan scaffold",
216
+ }
217
+ state.current_stage = "full_plan"
218
+ state.current_owner = "TechLeadAgent"
219
+ state.artifacts.task_card = null
220
+ state.artifacts.architecture = "docs/architecture/2026-03-21-real-template-plan.md"
221
+ state.approvals = {
222
+ pm_to_ba: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
223
+ ba_to_architect: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
224
+ architect_to_tech_lead: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
225
+ tech_lead_to_fullstack: { status: "pending", approved_by: null, approved_at: null, notes: null },
226
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
227
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
228
+ }
229
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
230
+ fs.rmSync(path.join(projectRoot, ".opencode", "work-items"), { recursive: true, force: true })
231
+
232
+ result = spawnSync(
233
+ "node",
234
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "real-template-plan"],
235
+ {
236
+ cwd: projectRoot,
237
+ encoding: "utf8",
238
+ },
239
+ )
240
+
241
+ assert.equal(result.status, 0)
242
+
243
+ state = JSON.parse(fs.readFileSync(statePath, "utf8"))
244
+ const planContent = fs.readFileSync(path.join(projectRoot, state.artifacts.plan), "utf8")
245
+ assert.match(planContent, /feature_id: FEATURE-705/)
246
+ assert.match(planContent, /feature_slug: real-template-plan/)
247
+ assert.match(planContent, /source_architecture: docs\/architecture\/2026-03-21-real-template-plan\.md/)
248
+ assert.match(planContent, /# Implementation Plan: Real Template Plan/)
249
+ })
250
+
251
+ test("scaffold-artifact creates an implementation plan and links it into state", () => {
252
+ const projectRoot = makeTempProject()
253
+ setupTempRuntime(projectRoot)
254
+
255
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
256
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
257
+ state.feature_id = "FEATURE-701"
258
+ state.feature_slug = "scaffold-plan"
259
+ state.mode = "full"
260
+ state.mode_reason = "Plan scaffold testing"
261
+ state.routing_profile = {
262
+ work_intent: "feature",
263
+ behavior_delta: "extend",
264
+ dominant_uncertainty: "product",
265
+ scope_shape: "cross_boundary",
266
+ selection_reason: "Plan scaffold testing",
267
+ }
268
+ state.current_stage = "full_plan"
269
+ state.current_owner = "TechLeadAgent"
270
+ state.artifacts.architecture = "docs/architecture/2026-03-21-scaffold-plan.md"
271
+ state.approvals = {
272
+ pm_to_ba: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
273
+ ba_to_architect: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
274
+ architect_to_tech_lead: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
275
+ tech_lead_to_fullstack: { status: "pending", approved_by: null, approved_at: null, notes: null },
276
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
277
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
278
+ }
279
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
280
+
281
+ const result = spawnSync(
282
+ "node",
283
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "scaffold-plan"],
284
+ {
285
+ cwd: projectRoot,
286
+ encoding: "utf8",
287
+ },
288
+ )
289
+
290
+ assert.equal(result.status, 0)
291
+ const nextState = JSON.parse(fs.readFileSync(statePath, "utf8"))
292
+ assert.match(nextState.artifacts.plan, /docs\/plans\/\d{4}-\d{2}-\d{2}-scaffold-plan\.md$/)
293
+ assert.equal(fs.existsSync(path.join(projectRoot, nextState.artifacts.plan)), true)
294
+ const planContent = fs.readFileSync(path.join(projectRoot, nextState.artifacts.plan), "utf8")
295
+ assert.match(planContent, /source_architecture: docs\/architecture\/2026-03-21-scaffold-plan\.md/)
296
+ })
297
+
298
+ test("scaffold-artifact rejects unsupported kinds without mutating state", () => {
299
+ const projectRoot = makeTempProject()
300
+ setupTempRuntime(projectRoot)
301
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
302
+ const before = fs.readFileSync(statePath, "utf8")
303
+
304
+ const result = spawnSync(
305
+ "node",
306
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "brief", "new-brief"],
307
+ {
308
+ cwd: projectRoot,
309
+ encoding: "utf8",
310
+ },
311
+ )
312
+
313
+ assert.equal(result.status, 1)
314
+ assert.match(result.stderr, /Unsupported scaffold kind 'brief'/)
315
+ assert.equal(fs.readFileSync(statePath, "utf8"), before)
316
+ })
317
+
318
+ test("scaffold-artifact refuses to overwrite an already populated slot", () => {
319
+ const projectRoot = makeTempProject()
320
+ setupTempRuntime(projectRoot)
321
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
322
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
323
+ state.artifacts.task_card = "docs/tasks/2026-03-21-existing.md"
324
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
325
+ const before = fs.readFileSync(statePath, "utf8")
326
+
327
+ const result = spawnSync(
328
+ "node",
329
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "task_card", "copy-fix"],
330
+ {
331
+ cwd: projectRoot,
332
+ encoding: "utf8",
333
+ },
334
+ )
335
+
336
+ assert.equal(result.status, 1)
337
+ assert.match(result.stderr, /already linked for artifact kind 'task_card'/)
338
+ assert.equal(fs.readFileSync(statePath, "utf8"), before)
339
+ })
340
+
341
+ test("scaffold-artifact resolves repo-relative paths from the state project root", () => {
342
+ const projectRoot = makeTempProject()
343
+ setupTempRuntime(projectRoot)
344
+ const externalStatePath = path.join(projectRoot, ".opencode", "workflow-state.json")
345
+
346
+ const result = spawnSync(
347
+ "node",
348
+ [
349
+ path.resolve(__dirname, "../workflow-state.js"),
350
+ "--state",
351
+ externalStatePath,
352
+ "scaffold-artifact",
353
+ "task_card",
354
+ "copy-fix-from-external-cwd",
355
+ ],
356
+ {
357
+ cwd: os.tmpdir(),
358
+ encoding: "utf8",
359
+ },
360
+ )
361
+
362
+ assert.equal(result.status, 0)
363
+
364
+ const state = JSON.parse(fs.readFileSync(externalStatePath, "utf8"))
365
+ assert.match(state.artifacts.task_card, /copy-fix-from-external-cwd\.md$/)
366
+ assert.equal(fs.existsSync(path.join(projectRoot, state.artifacts.task_card)), true)
367
+ })
368
+
369
+ test("scaffold-artifact rejects invalid slugs without writing files", () => {
370
+ const projectRoot = makeTempProject()
371
+ setupTempRuntime(projectRoot)
372
+
373
+ const result = spawnSync(
374
+ "node",
375
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "task_card", "../../escape"],
376
+ {
377
+ cwd: projectRoot,
378
+ encoding: "utf8",
379
+ },
380
+ )
381
+
382
+ assert.equal(result.status, 1)
383
+ assert.match(result.stderr, /artifact slug must use lowercase kebab-case/)
384
+ assert.equal(fs.readdirSync(path.join(projectRoot, "docs", "tasks")).length, 0)
385
+ })
386
+
387
+ test("scaffold-artifact removes created files if state linking fails", () => {
388
+ const projectRoot = makeTempProject()
389
+ setupTempRuntime(projectRoot)
390
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
391
+
392
+ assert.throws(
393
+ () =>
394
+ scaffoldAndLinkArtifact("task_card", "cleanup-on-failure", statePath, {
395
+ beforeLink: () => {
396
+ throw new Error("simulated link failure")
397
+ },
398
+ }),
399
+ /simulated link failure/,
400
+ )
401
+
402
+ assert.equal(fs.readdirSync(path.join(projectRoot, "docs", "tasks")).length, 0)
403
+ })
404
+
405
+ test("scaffold-artifact rejects task cards outside quick mode", () => {
406
+ const projectRoot = makeTempProject()
407
+ setupTempRuntime(projectRoot)
408
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
409
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
410
+ state.feature_id = "FEATURE-702"
411
+ state.feature_slug = "wrong-lane-task-card"
412
+ state.mode = "full"
413
+ state.routing_profile = {
414
+ work_intent: "feature",
415
+ behavior_delta: "extend",
416
+ dominant_uncertainty: "product",
417
+ scope_shape: "cross_boundary",
418
+ selection_reason: "wrong lane task card",
419
+ }
420
+ state.current_stage = "full_plan"
421
+ state.current_owner = "TechLeadAgent"
422
+ state.approvals = {
423
+ pm_to_ba: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
424
+ ba_to_architect: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
425
+ architect_to_tech_lead: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
426
+ tech_lead_to_fullstack: { status: "pending", approved_by: null, approved_at: null, notes: null },
427
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
428
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
429
+ }
430
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
431
+
432
+ const result = spawnSync(
433
+ "node",
434
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "task_card", "wrong-lane-task-card"],
435
+ {
436
+ cwd: projectRoot,
437
+ encoding: "utf8",
438
+ },
439
+ )
440
+
441
+ assert.equal(result.status, 1)
442
+ assert.match(result.stderr, /Artifact scaffold kind 'task_card' requires quick mode/)
443
+ })
444
+
445
+ test("scaffold-artifact rejects plans without a linked architecture artifact", () => {
446
+ const projectRoot = makeTempProject()
447
+ setupTempRuntime(projectRoot)
448
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
449
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
450
+ state.feature_id = "FEATURE-703"
451
+ state.feature_slug = "missing-architecture"
452
+ state.mode = "full"
453
+ state.routing_profile = {
454
+ work_intent: "feature",
455
+ behavior_delta: "extend",
456
+ dominant_uncertainty: "product",
457
+ scope_shape: "cross_boundary",
458
+ selection_reason: "missing architecture",
459
+ }
460
+ state.current_stage = "full_plan"
461
+ state.current_owner = "TechLeadAgent"
462
+ state.artifacts.architecture = null
463
+ state.approvals = {
464
+ pm_to_ba: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
465
+ ba_to_architect: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
466
+ architect_to_tech_lead: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
467
+ tech_lead_to_fullstack: { status: "pending", approved_by: null, approved_at: null, notes: null },
468
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
469
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
470
+ }
471
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
472
+
473
+ const result = spawnSync(
474
+ "node",
475
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "missing-architecture"],
476
+ {
477
+ cwd: projectRoot,
478
+ encoding: "utf8",
479
+ },
480
+ )
481
+
482
+ assert.equal(result.status, 1)
483
+ assert.match(result.stderr, /Artifact scaffold kind 'plan' requires a linked architecture artifact/)
484
+ assert.equal(fs.readdirSync(path.join(projectRoot, "docs", "plans")).length, 0)
485
+ })
486
+
487
+ test("scaffold-artifact rejects plans outside full_plan stage", () => {
488
+ const projectRoot = makeTempProject()
489
+ setupTempRuntime(projectRoot)
490
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
491
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
492
+ state.feature_id = "FEATURE-704"
493
+ state.feature_slug = "wrong-plan-stage"
494
+ state.mode = "full"
495
+ state.routing_profile = {
496
+ work_intent: "feature",
497
+ behavior_delta: "extend",
498
+ dominant_uncertainty: "product",
499
+ scope_shape: "cross_boundary",
500
+ selection_reason: "wrong plan stage",
501
+ }
502
+ state.current_stage = "full_architecture"
503
+ state.current_owner = "ArchitectAgent"
504
+ state.artifacts.architecture = "docs/architecture/2026-03-21-wrong-plan-stage.md"
505
+ state.approvals = {
506
+ pm_to_ba: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
507
+ ba_to_architect: { status: "approved", approved_by: "user", approved_at: "2026-03-21", notes: null },
508
+ architect_to_tech_lead: { status: "pending", approved_by: null, approved_at: null, notes: null },
509
+ tech_lead_to_fullstack: { status: "pending", approved_by: null, approved_at: null, notes: null },
510
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
511
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
512
+ }
513
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
514
+
515
+ const result = spawnSync(
516
+ "node",
517
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "wrong-plan-stage"],
518
+ {
519
+ cwd: projectRoot,
520
+ encoding: "utf8",
521
+ },
522
+ )
523
+
524
+ assert.equal(result.status, 1)
525
+ assert.match(result.stderr, /Artifact scaffold kind 'plan' requires current stage 'full_plan'/)
526
+ assert.equal(fs.readdirSync(path.join(projectRoot, "docs", "plans")).length, 0)
527
+ })
528
+
529
+ test("scaffold-artifact creates a migration plan in migration_strategy stage", () => {
530
+ const projectRoot = makeTempProject()
531
+ setupTempRuntimeWithRealTemplates(projectRoot)
532
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
533
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
534
+
535
+ state.feature_id = "MIGRATE-705"
536
+ state.feature_slug = "react-19-upgrade"
537
+ state.mode = "migration"
538
+ state.mode_reason = "Migration scaffold testing"
539
+ state.routing_profile = {
540
+ work_intent: "modernization",
541
+ behavior_delta: "preserve",
542
+ dominant_uncertainty: "compatibility",
543
+ scope_shape: "adjacent",
544
+ selection_reason: "Migration scaffold testing",
545
+ }
546
+ state.current_stage = "migration_strategy"
547
+ state.current_owner = "TechLeadAgent"
548
+ state.artifacts.task_card = null
549
+ state.artifacts.brief = null
550
+ state.artifacts.spec = null
551
+ state.artifacts.architecture = "docs/architecture/2026-03-21-react-19-upgrade.md"
552
+ state.artifacts.migration_report = null
553
+ state.artifacts.qa_report = null
554
+ state.artifacts.adr = []
555
+ state.approvals = {
556
+ baseline_to_strategy: { status: "approved", approved_by: "TechLeadAgent", approved_at: "2026-03-21", notes: null },
557
+ strategy_to_upgrade: { status: "pending", approved_by: null, approved_at: null, notes: null },
558
+ upgrade_to_verify: { status: "pending", approved_by: null, approved_at: null, notes: null },
559
+ migration_verified: { status: "pending", approved_by: null, approved_at: null, notes: null },
560
+ }
561
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
562
+
563
+ const result = spawnSync(
564
+ "node",
565
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "react-19-upgrade"],
566
+ {
567
+ cwd: projectRoot,
568
+ encoding: "utf8",
569
+ },
570
+ )
571
+
572
+ assert.equal(result.status, 0)
573
+
574
+ const nextState = JSON.parse(fs.readFileSync(statePath, "utf8"))
575
+ const planContent = fs.readFileSync(path.join(projectRoot, nextState.artifacts.plan), "utf8")
576
+ assert.match(planContent, /artifact_type: migration_plan/)
577
+ assert.match(planContent, /feature_id: MIGRATE-705/)
578
+ assert.match(planContent, /feature_slug: react-19-upgrade/)
579
+ assert.match(planContent, /# Migration Plan: React 19 Upgrade/)
580
+ })
581
+
582
+ test("scaffold-artifact rejects migration plans outside migration_strategy stage", () => {
583
+ const projectRoot = makeTempProject()
584
+ setupTempRuntime(projectRoot)
585
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
586
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
587
+
588
+ state.feature_id = "MIGRATE-706"
589
+ state.feature_slug = "wrong-migration-stage"
590
+ state.mode = "migration"
591
+ state.routing_profile = {
592
+ work_intent: "modernization",
593
+ behavior_delta: "preserve",
594
+ dominant_uncertainty: "compatibility",
595
+ scope_shape: "adjacent",
596
+ selection_reason: "wrong migration stage",
597
+ }
598
+ state.current_stage = "migration_baseline"
599
+ state.current_owner = "ArchitectAgent"
600
+ state.artifacts.task_card = null
601
+ state.artifacts.brief = null
602
+ state.artifacts.spec = null
603
+ state.artifacts.architecture = "docs/architecture/2026-03-21-wrong-migration-stage.md"
604
+ state.artifacts.plan = null
605
+ state.artifacts.migration_report = null
606
+ state.artifacts.qa_report = null
607
+ state.artifacts.adr = []
608
+ state.approvals = {
609
+ baseline_to_strategy: { status: "pending", approved_by: null, approved_at: null, notes: null },
610
+ strategy_to_upgrade: { status: "pending", approved_by: null, approved_at: null, notes: null },
611
+ upgrade_to_verify: { status: "pending", approved_by: null, approved_at: null, notes: null },
612
+ migration_verified: { status: "pending", approved_by: null, approved_at: null, notes: null },
613
+ }
614
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
615
+
616
+ const result = spawnSync(
617
+ "node",
618
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "plan", "wrong-migration-stage"],
619
+ {
620
+ cwd: projectRoot,
621
+ encoding: "utf8",
622
+ },
623
+ )
624
+
625
+ assert.equal(result.status, 1)
626
+ assert.match(result.stderr, /Artifact scaffold kind 'plan' requires current stage 'migration_strategy'/)
627
+ })
628
+
629
+ test("scaffold-artifact creates a migration report in migration_baseline stage", () => {
630
+ const projectRoot = makeTempProject()
631
+ setupTempRuntimeWithRealTemplates(projectRoot)
632
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
633
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
634
+
635
+ state.feature_id = "MIGRATE-707"
636
+ state.feature_slug = "legacy-stack-refresh"
637
+ state.mode = "migration"
638
+ state.mode_reason = "Migration report scaffold testing"
639
+ state.routing_profile = {
640
+ work_intent: "modernization",
641
+ behavior_delta: "preserve",
642
+ dominant_uncertainty: "compatibility",
643
+ scope_shape: "adjacent",
644
+ selection_reason: "Migration report scaffold testing",
645
+ }
646
+ state.current_stage = "migration_baseline"
647
+ state.current_owner = "ArchitectAgent"
648
+ state.artifacts.task_card = null
649
+ state.artifacts.brief = null
650
+ state.artifacts.spec = null
651
+ state.artifacts.architecture = "docs/architecture/2026-03-21-legacy-stack-refresh.md"
652
+ state.artifacts.plan = "docs/plans/2026-03-21-legacy-stack-refresh.md"
653
+ state.artifacts.migration_report = null
654
+ state.artifacts.qa_report = null
655
+ state.artifacts.adr = []
656
+ state.approvals = {
657
+ baseline_to_strategy: { status: "pending", approved_by: null, approved_at: null, notes: null },
658
+ strategy_to_upgrade: { status: "pending", approved_by: null, approved_at: null, notes: null },
659
+ upgrade_to_verify: { status: "pending", approved_by: null, approved_at: null, notes: null },
660
+ migration_verified: { status: "pending", approved_by: null, approved_at: null, notes: null },
661
+ }
662
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
663
+
664
+ const result = spawnSync(
665
+ "node",
666
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "migration_report", "legacy-stack-refresh-report"],
667
+ {
668
+ cwd: projectRoot,
669
+ encoding: "utf8",
670
+ },
671
+ )
672
+
673
+ assert.equal(result.status, 0)
674
+
675
+ const nextState = JSON.parse(fs.readFileSync(statePath, "utf8"))
676
+ const reportContent = fs.readFileSync(path.join(projectRoot, nextState.artifacts.migration_report), "utf8")
677
+ assert.match(reportContent, /artifact_type: migration_report/)
678
+ assert.match(reportContent, /feature_id: MIGRATE-707/)
679
+ assert.match(reportContent, /feature_slug: legacy-stack-refresh/)
680
+ assert.match(reportContent, /source_architecture: docs\/architecture\/2026-03-21-legacy-stack-refresh\.md/)
681
+ assert.match(reportContent, /source_plan: docs\/plans\/2026-03-21-legacy-stack-refresh\.md/)
682
+ assert.match(reportContent, /# Migration Report: Legacy Stack Refresh Report/)
683
+ })
684
+
685
+ test("scaffold-artifact rejects migration reports outside migration baseline or strategy", () => {
686
+ const projectRoot = makeTempProject()
687
+ setupTempRuntime(projectRoot)
688
+ const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
689
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
690
+
691
+ state.feature_id = "MIGRATE-708"
692
+ state.feature_slug = "wrong-report-stage"
693
+ state.mode = "migration"
694
+ state.routing_profile = {
695
+ work_intent: "modernization",
696
+ behavior_delta: "preserve",
697
+ dominant_uncertainty: "compatibility",
698
+ scope_shape: "adjacent",
699
+ selection_reason: "wrong report stage",
700
+ }
701
+ state.current_stage = "migration_upgrade"
702
+ state.current_owner = "FullstackAgent"
703
+ state.artifacts.task_card = null
704
+ state.artifacts.brief = null
705
+ state.artifacts.spec = null
706
+ state.artifacts.architecture = null
707
+ state.artifacts.plan = null
708
+ state.artifacts.migration_report = null
709
+ state.artifacts.qa_report = null
710
+ state.artifacts.adr = []
711
+ state.approvals = {
712
+ baseline_to_strategy: { status: "approved", approved_by: "TechLeadAgent", approved_at: "2026-03-21", notes: null },
713
+ strategy_to_upgrade: { status: "approved", approved_by: "FullstackAgent", approved_at: "2026-03-21", notes: null },
714
+ upgrade_to_verify: { status: "pending", approved_by: null, approved_at: null, notes: null },
715
+ migration_verified: { status: "pending", approved_by: null, approved_at: null, notes: null },
716
+ }
717
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
718
+
719
+ const result = spawnSync(
720
+ "node",
721
+ [path.resolve(__dirname, "../workflow-state.js"), "scaffold-artifact", "migration_report", "wrong-report-stage"],
722
+ {
723
+ cwd: projectRoot,
724
+ encoding: "utf8",
725
+ },
726
+ )
727
+
728
+ assert.equal(result.status, 1)
729
+ assert.match(
730
+ result.stderr,
731
+ /Artifact scaffold kind 'migration_report' requires current stage 'migration_baseline' or 'migration_strategy'/,
732
+ )
733
+ })