@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,357 @@
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
+
8
+ function makeTempProject() {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), "openkit-session-hook-"))
10
+ }
11
+
12
+ function writeState(projectRoot, state) {
13
+ const opencodeDir = path.join(projectRoot, ".opencode")
14
+ fs.mkdirSync(opencodeDir, { recursive: true })
15
+ fs.writeFileSync(path.join(opencodeDir, "workflow-state.json"), `${JSON.stringify(state, null, 2)}\n`, "utf8")
16
+ }
17
+
18
+ function makeQuickState(overrides = {}) {
19
+ return {
20
+ feature_id: "TASK-500",
21
+ feature_slug: "quick-copy-fix",
22
+ mode: "quick",
23
+ mode_reason: "Scoped task",
24
+ routing_profile: {
25
+ work_intent: "maintenance",
26
+ behavior_delta: "preserve",
27
+ dominant_uncertainty: "low_local",
28
+ scope_shape: "local",
29
+ selection_reason: "Scoped task",
30
+ },
31
+ current_stage: "quick_verify",
32
+ status: "in_progress",
33
+ current_owner: "QAAgent",
34
+ artifacts: {
35
+ task_card: null,
36
+ brief: null,
37
+ spec: null,
38
+ architecture: null,
39
+ plan: null,
40
+ migration_report: null,
41
+ qa_report: null,
42
+ adr: [],
43
+ },
44
+ approvals: {
45
+ quick_verified: {
46
+ status: "pending",
47
+ approved_by: null,
48
+ approved_at: null,
49
+ notes: null,
50
+ },
51
+ },
52
+ issues: [],
53
+ retry_count: 0,
54
+ escalated_from: null,
55
+ escalation_reason: null,
56
+ updated_at: "2026-03-21T00:00:00.000Z",
57
+ ...overrides,
58
+ }
59
+ }
60
+
61
+ function writeManifest(projectRoot) {
62
+ const opencodeDir = path.join(projectRoot, ".opencode")
63
+ fs.mkdirSync(opencodeDir, { recursive: true })
64
+ fs.writeFileSync(
65
+ path.join(opencodeDir, "opencode.json"),
66
+ `${JSON.stringify({
67
+ kit: {
68
+ name: "OpenKit AI Software Factory",
69
+ version: "0.1.0",
70
+ entryAgent: "MasterOrchestrator",
71
+ },
72
+ }, null, 2)}\n`,
73
+ "utf8",
74
+ )
75
+ }
76
+
77
+ function writeWorkItemBoard(projectRoot, workItemId, board) {
78
+ const boardPath = path.join(projectRoot, ".opencode", "work-items", workItemId, "tasks.json")
79
+ fs.mkdirSync(path.dirname(boardPath), { recursive: true })
80
+ fs.writeFileSync(boardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
81
+ }
82
+
83
+ function makeFullState(overrides = {}) {
84
+ return {
85
+ feature_id: "FEATURE-700",
86
+ feature_slug: "parallel-runtime-rollout",
87
+ mode: "full",
88
+ mode_reason: "Feature-sized task board rollout",
89
+ routing_profile: {
90
+ work_intent: "feature",
91
+ behavior_delta: "extend",
92
+ dominant_uncertainty: "product",
93
+ scope_shape: "cross_boundary",
94
+ selection_reason: "Feature-sized task board rollout",
95
+ },
96
+ current_stage: "full_implementation",
97
+ status: "in_progress",
98
+ current_owner: "FullstackAgent",
99
+ artifacts: {
100
+ task_card: null,
101
+ brief: "docs/briefs/2026-03-21-parallel-runtime-rollout.md",
102
+ spec: "docs/specs/2026-03-21-parallel-runtime-rollout.md",
103
+ architecture: "docs/architecture/2026-03-21-parallel-runtime-rollout.md",
104
+ plan: "docs/plans/2026-03-21-parallel-runtime-rollout.md",
105
+ migration_report: null,
106
+ qa_report: null,
107
+ adr: [],
108
+ },
109
+ approvals: {
110
+ pm_to_ba: { status: "approved", approved_by: "BAAgent", approved_at: "2026-03-21", notes: "ok" },
111
+ ba_to_architect: { status: "approved", approved_by: "ArchitectAgent", approved_at: "2026-03-21", notes: "ok" },
112
+ architect_to_tech_lead: { status: "approved", approved_by: "TechLeadAgent", approved_at: "2026-03-21", notes: "ok" },
113
+ tech_lead_to_fullstack: { status: "approved", approved_by: "FullstackAgent", approved_at: "2026-03-21", notes: "ok" },
114
+ fullstack_to_qa: { status: "pending", approved_by: null, approved_at: null, notes: null },
115
+ qa_to_done: { status: "pending", approved_by: null, approved_at: null, notes: null },
116
+ },
117
+ issues: [],
118
+ retry_count: 0,
119
+ escalated_from: null,
120
+ escalation_reason: null,
121
+ updated_at: "2026-03-21T00:00:00.000Z",
122
+ work_item_id: "feature-700",
123
+ ...overrides,
124
+ }
125
+ }
126
+
127
+ function writeMetaSkill(projectRoot) {
128
+ const skillDir = path.join(projectRoot, "skills", "using-skills")
129
+ fs.mkdirSync(skillDir, { recursive: true })
130
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), "# using-skills\n", "utf8")
131
+ }
132
+
133
+ function writeFailingPythonShim(projectRoot) {
134
+ const binDir = path.join(projectRoot, "bin")
135
+ const shimPath = path.join(binDir, "python3-shim")
136
+ fs.mkdirSync(binDir, { recursive: true })
137
+ fs.writeFileSync(shimPath, "#!/usr/bin/env bash\nexit 1\n", { encoding: "utf8", mode: 0o755 })
138
+ return shimPath
139
+ }
140
+
141
+ test("session-start emits mode-aware resume hint for quick tasks", () => {
142
+ const projectRoot = makeTempProject()
143
+
144
+ writeManifest(projectRoot)
145
+ writeState(projectRoot, makeQuickState())
146
+
147
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
148
+ cwd: projectRoot,
149
+ encoding: "utf8",
150
+ env: {
151
+ ...process.env,
152
+ OPENKIT_PROJECT_ROOT: projectRoot,
153
+ OPENKIT_SESSION_START_NO_SKILL: "1",
154
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
155
+ },
156
+ })
157
+
158
+ assert.equal(result.status, 0)
159
+ assert.match(result.stdout, /<openkit_runtime_status>/)
160
+ assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.1\.0/)
161
+ assert.match(result.stdout, /startup skill: skipped/)
162
+ assert.match(result.stdout, /node \.opencode\/workflow-state\.js status/)
163
+ assert.match(result.stdout, /node \.opencode\/workflow-state\.js doctor/)
164
+ assert.match(result.stdout, /<workflow_resume_hint>/)
165
+ assert.match(result.stdout, /mode: quick/)
166
+ assert.match(result.stdout, /stage: quick_verify/)
167
+ assert.match(result.stdout, /work item: TASK-500 \(quick-copy-fix\)/)
168
+ })
169
+
170
+ test("session-start reports quick_plan as a resumable quick stage", () => {
171
+ const projectRoot = makeTempProject()
172
+
173
+ writeManifest(projectRoot)
174
+ writeState(
175
+ projectRoot,
176
+ makeQuickState({
177
+ feature_id: "TASK-502",
178
+ feature_slug: "quick-plan-resume",
179
+ current_stage: "quick_plan",
180
+ current_owner: "MasterOrchestrator",
181
+ }),
182
+ )
183
+
184
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
185
+ cwd: projectRoot,
186
+ encoding: "utf8",
187
+ env: {
188
+ ...process.env,
189
+ OPENKIT_PROJECT_ROOT: projectRoot,
190
+ OPENKIT_SESSION_START_NO_SKILL: "1",
191
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
192
+ },
193
+ })
194
+
195
+ assert.equal(result.status, 0)
196
+ assert.match(result.stdout, /<workflow_resume_hint>/)
197
+ assert.match(result.stdout, /mode: quick/)
198
+ assert.match(result.stdout, /stage: quick_plan/)
199
+ assert.match(result.stdout, /owner: MasterOrchestrator/)
200
+ })
201
+
202
+ test("session-start reports loaded startup skill when meta-skill exists", () => {
203
+ const projectRoot = makeTempProject()
204
+
205
+ writeManifest(projectRoot)
206
+ writeMetaSkill(projectRoot)
207
+ writeState(
208
+ projectRoot,
209
+ makeQuickState({
210
+ feature_id: "TASK-501",
211
+ feature_slug: "loaded-skill",
212
+ current_stage: "quick_intake",
213
+ current_owner: "MasterOrchestrator",
214
+ }),
215
+ )
216
+
217
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
218
+ cwd: projectRoot,
219
+ encoding: "utf8",
220
+ env: {
221
+ ...process.env,
222
+ OPENKIT_PROJECT_ROOT: projectRoot,
223
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
224
+ },
225
+ })
226
+
227
+ assert.equal(result.status, 0)
228
+ assert.match(result.stdout, /startup skill: loaded/)
229
+ assert.match(result.stdout, /<skill_system_instruction>/)
230
+ })
231
+
232
+ test("session-start prints canonical resume guidance and inspection commands", () => {
233
+ const projectRoot = makeTempProject()
234
+
235
+ writeManifest(projectRoot)
236
+ writeState(
237
+ projectRoot,
238
+ makeQuickState({
239
+ feature_id: "TASK-503",
240
+ feature_slug: "canonical-resume-guidance",
241
+ current_stage: "quick_build",
242
+ current_owner: "FullstackAgent",
243
+ }),
244
+ )
245
+
246
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
247
+ cwd: projectRoot,
248
+ encoding: "utf8",
249
+ env: {
250
+ ...process.env,
251
+ OPENKIT_PROJECT_ROOT: projectRoot,
252
+ OPENKIT_SESSION_START_NO_SKILL: "1",
253
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
254
+ },
255
+ })
256
+
257
+ assert.equal(result.status, 0)
258
+ assert.match(result.stdout, /help: node \.opencode\/workflow-state\.js status/)
259
+ assert.match(result.stdout, /doctor: node \.opencode\/workflow-state\.js doctor/)
260
+ assert.match(result.stdout, /show: node \.opencode\/workflow-state\.js show/)
261
+ assert.match(result.stdout, /Read first: AGENTS\.md -> context\/navigation\.md -> context\/core\/workflow\.md -> \.opencode\/workflow-state\.json/)
262
+ assert.match(result.stdout, /Then load resume guidance from context\/core\/session-resume\.md\./)
263
+ })
264
+
265
+ test("session-start degrades gracefully when the JSON helper fails", () => {
266
+ const projectRoot = makeTempProject()
267
+
268
+ writeManifest(projectRoot)
269
+ writeState(projectRoot, makeQuickState())
270
+ const pythonShim = writeFailingPythonShim(projectRoot)
271
+
272
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
273
+ cwd: projectRoot,
274
+ encoding: "utf8",
275
+ env: {
276
+ ...process.env,
277
+ OPENKIT_PROJECT_ROOT: projectRoot,
278
+ OPENKIT_SESSION_START_NO_SKILL: "1",
279
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
280
+ OPENKIT_PYTHON_BIN: pythonShim,
281
+ },
282
+ })
283
+
284
+ assert.equal(result.status, 0)
285
+ assert.match(result.stdout, /<openkit_runtime_status>/)
286
+ assert.match(result.stdout, /json helper: degraded/)
287
+ assert.doesNotMatch(result.stdout, /<workflow_resume_hint>/)
288
+ })
289
+
290
+ test("session-start emits task-aware resume hint for active full-delivery work", () => {
291
+ const projectRoot = makeTempProject()
292
+
293
+ writeManifest(projectRoot)
294
+ writeState(projectRoot, makeFullState())
295
+ writeWorkItemBoard(projectRoot, "feature-700", {
296
+ mode: "full",
297
+ current_stage: "full_implementation",
298
+ tasks: [
299
+ {
300
+ task_id: "TASK-700-A",
301
+ title: "Implement diagnostics",
302
+ summary: "Task-aware summaries",
303
+ kind: "implementation",
304
+ status: "in_progress",
305
+ primary_owner: "Dev-A",
306
+ qa_owner: null,
307
+ depends_on: [],
308
+ blocked_by: [],
309
+ artifact_refs: [],
310
+ plan_refs: ["docs/plans/2026-03-21-parallel-runtime-rollout.md"],
311
+ branch_or_worktree: ".worktrees/parallel-agent-rollout/task-700-a",
312
+ created_by: "TechLeadAgent",
313
+ created_at: "2026-03-21T00:00:00.000Z",
314
+ updated_at: "2026-03-21T00:00:00.000Z",
315
+ },
316
+ {
317
+ task_id: "TASK-700-B",
318
+ title: "QA diagnostics",
319
+ summary: "QA active task",
320
+ kind: "qa",
321
+ status: "qa_in_progress",
322
+ primary_owner: "Dev-B",
323
+ qa_owner: "QA-Agent",
324
+ depends_on: [],
325
+ blocked_by: [],
326
+ artifact_refs: [],
327
+ plan_refs: ["docs/plans/2026-03-21-parallel-runtime-rollout.md"],
328
+ branch_or_worktree: ".worktrees/parallel-agent-rollout/task-700-b",
329
+ created_by: "TechLeadAgent",
330
+ created_at: "2026-03-21T00:00:00.000Z",
331
+ updated_at: "2026-03-21T00:00:00.000Z",
332
+ },
333
+ ],
334
+ issues: [],
335
+ })
336
+
337
+ const result = spawnSync(path.resolve(__dirname, "../../hooks/session-start"), {
338
+ cwd: projectRoot,
339
+ encoding: "utf8",
340
+ env: {
341
+ ...process.env,
342
+ OPENKIT_PROJECT_ROOT: projectRoot,
343
+ OPENKIT_SESSION_START_NO_SKILL: "1",
344
+ OPENKIT_WORKFLOW_STATE: path.join(projectRoot, ".opencode", "workflow-state.json"),
345
+ },
346
+ })
347
+
348
+ assert.equal(result.status, 0)
349
+ assert.match(result.stdout, /<workflow_resume_hint>/)
350
+ assert.match(result.stdout, /mode: full/)
351
+ assert.match(result.stdout, /stage: full_implementation/)
352
+ assert.match(result.stdout, /work item: FEATURE-700 \(parallel-runtime-rollout\)/)
353
+ assert.match(result.stdout, /active work item id: feature-700/)
354
+ assert.match(result.stdout, /task board: 2 tasks \| ready 0 \| active 2/)
355
+ assert.match(result.stdout, /active tasks: TASK-700-A \(in_progress, primary: Dev-A\); TASK-700-B \(qa_in_progress, qa: QA-Agent\)/)
356
+ assert.match(result.stdout, /Parallel task support is not yet assumed safe by this hook; confirm with `node \.opencode\/workflow-state\.js doctor` before relying on it\./)
357
+ })
@@ -0,0 +1,124 @@
1
+ const test = require("node:test")
2
+ const assert = require("node:assert/strict")
3
+
4
+ const {
5
+ captureRevision,
6
+ detectMirrorDivergence,
7
+ guardWrite,
8
+ planGuardedMirrorRefresh,
9
+ } = require("../lib/state-guard")
10
+
11
+ function createState(overrides = {}) {
12
+ return {
13
+ work_item_id: "feature-100",
14
+ feature_id: "FEATURE-100",
15
+ feature_slug: "parallel-runtime",
16
+ mode: "full",
17
+ routing_profile: {
18
+ work_intent: "feature",
19
+ behavior_delta: "extend",
20
+ dominant_uncertainty: "product",
21
+ scope_shape: "cross_boundary",
22
+ selection_reason: "feature example",
23
+ },
24
+ current_stage: "full_plan",
25
+ status: "in_progress",
26
+ updated_at: "2026-03-21T00:00:00.000Z",
27
+ ...overrides,
28
+ }
29
+ }
30
+
31
+ test("captureRevision returns a stable revision for equivalent state content", () => {
32
+ const first = createState({ artifacts: { plan: "docs/plans/feature-100.md", migration_report: null, qa_report: null } })
33
+ const second = {
34
+ status: "in_progress",
35
+ current_stage: "full_plan",
36
+ updated_at: "2026-03-21T00:00:00.000Z",
37
+ work_item_id: "feature-100",
38
+ feature_slug: "parallel-runtime",
39
+ feature_id: "FEATURE-100",
40
+ mode: "full",
41
+ routing_profile: {
42
+ selection_reason: "feature example",
43
+ scope_shape: "cross_boundary",
44
+ dominant_uncertainty: "product",
45
+ behavior_delta: "extend",
46
+ work_intent: "feature",
47
+ },
48
+ artifacts: { qa_report: null, migration_report: null, plan: "docs/plans/feature-100.md" },
49
+ }
50
+
51
+ const firstRevision = captureRevision(first)
52
+ const secondRevision = captureRevision(second)
53
+
54
+ assert.equal(typeof firstRevision, "string")
55
+ assert.equal(firstRevision.length > 0, true)
56
+ assert.equal(firstRevision, secondRevision)
57
+ })
58
+
59
+ test("guardWrite rejects compare-and-swap writes when the expected revision is stale", () => {
60
+ const persistedState = createState({ status: "in_progress" })
61
+ const staleState = createState({ status: "queued" })
62
+
63
+ const expectedRevision = captureRevision(staleState)
64
+ const currentRevision = captureRevision(persistedState)
65
+
66
+ let staleWriteError = null
67
+
68
+ try {
69
+ guardWrite({ currentState: persistedState, expectedRevision, nextState: createState({ status: "done" }) })
70
+ } catch (error) {
71
+ staleWriteError = error
72
+ }
73
+
74
+ assert.ok(staleWriteError)
75
+ assert.equal(staleWriteError.code, "STALE_WRITE")
76
+ assert.equal(staleWriteError.currentRevision, currentRevision)
77
+ assert.equal(staleWriteError.expectedRevision, expectedRevision)
78
+ })
79
+
80
+ test("planGuardedMirrorRefresh exposes abstract ordered phases without storage sink names", () => {
81
+ const activeState = createState({ status: "ready_for_qa" })
82
+
83
+ const activePlan = planGuardedMirrorRefresh({
84
+ activeWorkItemId: "feature-100",
85
+ targetWorkItemId: "feature-100",
86
+ nextState: activeState,
87
+ })
88
+
89
+ assert.equal(activePlan.shouldRefreshMirror, true)
90
+ assert.deepEqual(activePlan.phases, ["primary", "replica"])
91
+ assert.equal(activePlan.mirrorRevision, captureRevision(activeState))
92
+
93
+ const inactivePlan = planGuardedMirrorRefresh({
94
+ activeWorkItemId: "feature-100",
95
+ targetWorkItemId: "feature-101",
96
+ nextState: createState({ work_item_id: "feature-101", feature_id: "FEATURE-101" }),
97
+ })
98
+
99
+ assert.equal(inactivePlan.shouldRefreshMirror, false)
100
+ assert.deepEqual(inactivePlan.phases, ["primary"])
101
+ })
102
+
103
+ test("detectMirrorDivergence reports lagging or failed mirror refreshes against the active item revision", () => {
104
+ const activeState = createState({ status: "done" })
105
+ const staleMirrorState = createState({ status: "in_progress" })
106
+
107
+ const laggingMirror = detectMirrorDivergence({
108
+ activeWorkItemId: "feature-100",
109
+ activeState,
110
+ mirrorState: staleMirrorState,
111
+ })
112
+
113
+ assert.equal(laggingMirror.isDiverged, true)
114
+ assert.equal(laggingMirror.reason, "revision_mismatch")
115
+
116
+ const failedMirror = detectMirrorDivergence({
117
+ activeWorkItemId: "feature-100",
118
+ activeState,
119
+ mirrorState: null,
120
+ })
121
+
122
+ assert.equal(failedMirror.isDiverged, true)
123
+ assert.equal(failedMirror.reason, "mirror_missing")
124
+ })
@@ -0,0 +1,204 @@
1
+ const test = require("node:test")
2
+ const assert = require("node:assert/strict")
3
+
4
+ const {
5
+ TASK_STATUS_VALUES,
6
+ validateTaskBoard,
7
+ validateTaskShape,
8
+ validateTaskStatus,
9
+ validateTaskTransition,
10
+ } = require("../lib/task-board-rules")
11
+
12
+ function makeTask(overrides = {}) {
13
+ return {
14
+ task_id: "TASK-1",
15
+ title: "Implement task board helper",
16
+ summary: "Add isolated validation rules",
17
+ kind: "implementation",
18
+ status: "ready",
19
+ primary_owner: null,
20
+ qa_owner: null,
21
+ depends_on: [],
22
+ blocked_by: [],
23
+ artifact_refs: [],
24
+ plan_refs: ["docs/plans/2026-03-21-feature.md"],
25
+ branch_or_worktree: null,
26
+ created_by: "TechLeadAgent",
27
+ created_at: "2026-03-21T00:00:00.000Z",
28
+ updated_at: "2026-03-21T00:00:00.000Z",
29
+ ...overrides,
30
+ }
31
+ }
32
+
33
+ function makeBoard(overrides = {}) {
34
+ return {
35
+ mode: "full",
36
+ current_stage: "full_plan",
37
+ tasks: [makeTask()],
38
+ issues: [],
39
+ ...overrides,
40
+ }
41
+ }
42
+
43
+ test("validateTaskShape accepts the minimum full-delivery task record", () => {
44
+ const task = makeTask()
45
+
46
+ assert.doesNotThrow(() => validateTaskShape(task))
47
+ })
48
+
49
+ test("validateTaskShape rejects records with missing required fields", () => {
50
+ const task = makeTask({ summary: undefined })
51
+
52
+ assert.throws(() => validateTaskShape(task), /summary/)
53
+ })
54
+
55
+ test("validateTaskShape rejects malformed array contents", () => {
56
+ assert.throws(() => validateTaskShape(makeTask({ depends_on: ["TASK-2", ""] })), /depends_on/)
57
+ assert.throws(() => validateTaskShape(makeTask({ blocked_by: [null] })), /blocked_by/)
58
+ assert.throws(() => validateTaskShape(makeTask({ artifact_refs: [42] })), /artifact_refs/)
59
+ assert.throws(() => validateTaskShape(makeTask({ plan_refs: [""] })), /plan_refs/)
60
+ })
61
+
62
+ test("validateTaskShape requires branch_or_worktree in the task contract", () => {
63
+ const task = makeTask()
64
+ delete task.branch_or_worktree
65
+
66
+ assert.throws(() => validateTaskShape(task), /branch_or_worktree/)
67
+ })
68
+
69
+ test("validateTaskStatus allows only documented task-board statuses", () => {
70
+ const expected = [
71
+ "queued",
72
+ "ready",
73
+ "claimed",
74
+ "in_progress",
75
+ "dev_done",
76
+ "qa_ready",
77
+ "qa_in_progress",
78
+ "done",
79
+ "blocked",
80
+ "cancelled",
81
+ ]
82
+
83
+ assert.deepEqual(TASK_STATUS_VALUES, expected)
84
+
85
+ for (const status of expected) {
86
+ assert.equal(validateTaskStatus(status), status)
87
+ }
88
+
89
+ assert.throws(() => validateTaskStatus("idle"), /Unknown task status/)
90
+ })
91
+
92
+ test("validateTaskTransition allows documented happy-path transitions", () => {
93
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "queued" }), "ready"))
94
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "ready" }), "claimed"))
95
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "claimed", primary_owner: "DevA" }), "in_progress"))
96
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "in_progress", primary_owner: "DevA" }), "dev_done"))
97
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "dev_done", qa_owner: "QAAgent" }), "qa_ready"))
98
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "dev_done", qa_owner: null }), "qa_ready", {
99
+ allowQaAssignment: true,
100
+ }))
101
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "qa_ready", qa_owner: "QAAgent" }), "qa_in_progress"))
102
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "qa_ready", qa_owner: null }), "qa_in_progress", {
103
+ allowQaClaim: true,
104
+ }))
105
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "qa_in_progress", qa_owner: "QAAgent" }), "done"))
106
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "qa_in_progress", qa_owner: "QAAgent" }), "blocked"))
107
+ assert.doesNotThrow(() => validateTaskTransition(makeTask({ status: "qa_in_progress", qa_owner: "QAAgent" }), "in_progress", {
108
+ allowQaFailRework: true,
109
+ finding: { task_id: "TASK-1", summary: "Needs rework" },
110
+ }))
111
+ })
112
+
113
+ test("validateTaskTransition rejects invalid edges and missing handoff requirements", () => {
114
+ assert.throws(() => validateTaskTransition(makeTask({ status: "dev_done" }), "done"), /Invalid task transition/)
115
+ assert.throws(() => validateTaskTransition(makeTask({ status: "claimed" }), "dev_done"), /Invalid task transition/)
116
+ assert.throws(() => validateTaskTransition(makeTask({ status: "dev_done", qa_owner: null }), "qa_ready"), /qa_owner|QA assignment/)
117
+ assert.throws(() => validateTaskTransition(makeTask({ status: "qa_ready", qa_owner: null }), "qa_in_progress"), /qa_owner|QA claim/)
118
+ assert.throws(() => validateTaskTransition(makeTask({ status: "qa_in_progress", qa_owner: "QAAgent" }), "claimed"), /QA fail rework/)
119
+ assert.throws(() => validateTaskTransition(makeTask({ status: "qa_in_progress", qa_owner: "QAAgent" }), "claimed", {
120
+ allowQaFailRework: true,
121
+ }), /finding/)
122
+ })
123
+
124
+ test("validateTaskTransition rejects claiming tasks blocked by unresolved dependencies", () => {
125
+ const task = makeTask({
126
+ status: "ready",
127
+ depends_on: ["TASK-0"],
128
+ blocked_by: ["TASK-0"],
129
+ })
130
+
131
+ assert.throws(() => validateTaskTransition(task, "claimed"), /blocked dependency/)
132
+ })
133
+
134
+ test("validateTaskBoard rejects quick-mode task boards", () => {
135
+ assert.throws(() => validateTaskBoard(makeBoard({ mode: "quick", current_stage: "quick_plan" })), /Quick mode/)
136
+ })
137
+
138
+ test("validateTaskBoard rejects migration-mode task boards", () => {
139
+ assert.throws(
140
+ () => validateTaskBoard(makeBoard({ mode: "migration", current_stage: "migration_strategy" })),
141
+ /Migration mode/,
142
+ )
143
+ })
144
+
145
+ test("validateTaskBoard rejects dependency cycles", () => {
146
+ const tasks = [
147
+ makeTask({ task_id: "TASK-1", depends_on: ["TASK-2"], blocked_by: ["TASK-2"] }),
148
+ makeTask({ task_id: "TASK-2", depends_on: ["TASK-1"], blocked_by: ["TASK-1"] }),
149
+ ]
150
+
151
+ assert.throws(() => validateTaskBoard(makeBoard({ tasks })), /Dependency cycle/)
152
+ })
153
+
154
+ test("validateTaskBoard rejects duplicate task identifiers", () => {
155
+ const tasks = [
156
+ makeTask({ task_id: "TASK-1" }),
157
+ makeTask({ task_id: "TASK-1", title: "Duplicate task id" }),
158
+ ]
159
+
160
+ assert.throws(() => validateTaskBoard(makeBoard({ tasks })), /Duplicate task_id/)
161
+ })
162
+
163
+ test("validateTaskBoard rejects unknown dependency and blocker references", () => {
164
+ assert.throws(() => validateTaskBoard(makeBoard({
165
+ tasks: [makeTask({ depends_on: ["TASK-404"] })],
166
+ })), /depends on unknown task/)
167
+
168
+ assert.throws(() => validateTaskBoard(makeBoard({
169
+ tasks: [makeTask({ blocked_by: ["TASK-405"] })],
170
+ })), /blocked by unknown task/)
171
+ })
172
+
173
+ test("validateTaskBoard enforces full-delivery aggregate completion rules", () => {
174
+ const implementationBoard = makeBoard({
175
+ current_stage: "full_implementation",
176
+ tasks: [makeTask({ status: "queued" })],
177
+ })
178
+ assert.throws(() => validateTaskBoard(implementationBoard), /full_implementation/)
179
+
180
+ const qaBoard = makeBoard({
181
+ current_stage: "full_qa",
182
+ tasks: [makeTask({ status: "in_progress", primary_owner: "DevA" })],
183
+ })
184
+ assert.throws(() => validateTaskBoard(qaBoard), /full_qa/)
185
+
186
+ const doneBoard = makeBoard({
187
+ current_stage: "full_done",
188
+ tasks: [makeTask({ status: "qa_ready", qa_owner: "QAAgent" })],
189
+ })
190
+ assert.throws(() => validateTaskBoard(doneBoard), /full_done/)
191
+
192
+ const blockedIssueBoard = makeBoard({
193
+ current_stage: "full_done",
194
+ tasks: [makeTask({ status: "done", qa_owner: "QAAgent" })],
195
+ issues: [{ severity: "high", status: "open", blocks_completion: true }],
196
+ })
197
+ assert.throws(() => validateTaskBoard(blockedIssueBoard), /blocking issue/)
198
+
199
+ assert.doesNotThrow(() => validateTaskBoard(makeBoard({
200
+ current_stage: "full_done",
201
+ tasks: [makeTask({ status: "done", qa_owner: "QAAgent" })],
202
+ issues: [],
203
+ })))
204
+ })