@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,369 @@
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
+
7
+ const {
8
+ advanceStage,
9
+ claimTask,
10
+ createTask: createBoardTask,
11
+ listTasks,
12
+ reassignTask,
13
+ releaseTask,
14
+ selectActiveWorkItem,
15
+ setApproval,
16
+ setTaskStatus,
17
+ showState,
18
+ showWorkItemState,
19
+ startFeature,
20
+ startTask,
21
+ validateWorkItemBoard,
22
+ } = require("../lib/workflow-state-controller")
23
+ const { readWorkItemIndex, readWorkItemState } = require("../lib/work-item-store")
24
+
25
+ function makeTempDir() {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), "openkit-multi-work-item-"))
27
+ }
28
+
29
+ function loadFixtureState() {
30
+ const fixturePath = path.resolve(__dirname, "../workflow-state.json")
31
+ return JSON.parse(fs.readFileSync(fixturePath, "utf8"))
32
+ }
33
+
34
+ function createTempStateFile() {
35
+ const dir = makeTempDir()
36
+ const opencodeDir = path.join(dir, ".opencode")
37
+ fs.mkdirSync(opencodeDir, { recursive: true })
38
+ const statePath = path.join(opencodeDir, "workflow-state.json")
39
+ fs.writeFileSync(statePath, `${JSON.stringify(loadFixtureState(), null, 2)}\n`, "utf8")
40
+ return statePath
41
+ }
42
+
43
+ function createTask(overrides = {}) {
44
+ return {
45
+ task_id: "TASK-1",
46
+ title: "Implement feature slice",
47
+ summary: "Controller-backed board validation",
48
+ kind: "implementation",
49
+ status: "ready",
50
+ primary_owner: null,
51
+ qa_owner: null,
52
+ depends_on: [],
53
+ blocked_by: [],
54
+ artifact_refs: [],
55
+ plan_refs: ["docs/plans/2026-03-21-feature.md"],
56
+ branch_or_worktree: null,
57
+ created_by: "TechLeadAgent",
58
+ created_at: "2026-03-21T00:00:00.000Z",
59
+ updated_at: "2026-03-21T00:00:00.000Z",
60
+ ...overrides,
61
+ }
62
+ }
63
+
64
+ function writeTaskBoard(statePath, workItemId, board) {
65
+ const projectRoot = path.dirname(path.dirname(statePath))
66
+ const boardPath = path.join(projectRoot, ".opencode", "work-items", workItemId, "tasks.json")
67
+ fs.mkdirSync(path.dirname(boardPath), { recursive: true })
68
+ fs.writeFileSync(boardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
69
+ }
70
+
71
+ function advanceFullWorkItemToPlan(statePath) {
72
+ advanceStage("full_brief", statePath)
73
+ setApproval("pm_to_ba", "approved", "user", "2026-03-21", "Approved", statePath)
74
+ advanceStage("full_spec", statePath)
75
+ setApproval("ba_to_architect", "approved", "user", "2026-03-21", "Approved", statePath)
76
+ advanceStage("full_architecture", statePath)
77
+ setApproval("architect_to_tech_lead", "approved", "user", "2026-03-21", "Approved", statePath)
78
+ advanceStage("full_plan", statePath)
79
+ }
80
+
81
+ test("compatibility mirror refresh happens after the active work item pointer changes", () => {
82
+ const statePath = createTempStateFile()
83
+ const projectRoot = path.dirname(path.dirname(statePath))
84
+
85
+ startFeature("FEATURE-300", "first-feature", statePath)
86
+ startTask("quick", "TASK-300", "second-item", "Select a new quick task", statePath)
87
+
88
+ const indexBeforeSelection = readWorkItemIndex(projectRoot)
89
+ const mirrorBeforeSelection = JSON.parse(fs.readFileSync(statePath, "utf8"))
90
+ assert.equal(indexBeforeSelection.active_work_item_id, "task-300")
91
+ assert.equal(mirrorBeforeSelection.work_item_id, "task-300")
92
+
93
+ const result = selectActiveWorkItem("feature-300", statePath)
94
+ const indexAfterSelection = readWorkItemIndex(projectRoot)
95
+ const mirrorAfterSelection = JSON.parse(fs.readFileSync(statePath, "utf8"))
96
+ const activePerItemState = readWorkItemState(projectRoot, "feature-300")
97
+
98
+ assert.equal(result.state.work_item_id, "feature-300")
99
+ assert.equal(indexAfterSelection.active_work_item_id, "feature-300")
100
+ assert.deepEqual(mirrorAfterSelection, activePerItemState)
101
+ })
102
+
103
+ test("selectActiveWorkItem rewrites a stale compatibility mirror from the active per-item state", () => {
104
+ const statePath = createTempStateFile()
105
+ const projectRoot = path.dirname(path.dirname(statePath))
106
+
107
+ startFeature("FEATURE-305", "mirror-refresh", statePath)
108
+ startTask("quick", "TASK-305", "active-quick", "Use quick item as active selection", statePath)
109
+
110
+ fs.writeFileSync(
111
+ statePath,
112
+ `${JSON.stringify({
113
+ feature_id: "STALE-ITEM",
114
+ feature_slug: "stale-mirror",
115
+ mode: "full",
116
+ mode_reason: "stale",
117
+ routing_profile: {
118
+ work_intent: "feature",
119
+ behavior_delta: "extend",
120
+ dominant_uncertainty: "product",
121
+ scope_shape: "cross_boundary",
122
+ selection_reason: "stale",
123
+ },
124
+ current_stage: "full_done",
125
+ status: "done",
126
+ current_owner: "MasterOrchestrator",
127
+ artifacts: {
128
+ task_card: null,
129
+ brief: null,
130
+ spec: null,
131
+ architecture: null,
132
+ plan: null,
133
+ migration_report: null,
134
+ qa_report: null,
135
+ adr: [],
136
+ },
137
+ approvals: {
138
+ pm_to_ba: { status: "approved", approved_by: null, approved_at: null, notes: null },
139
+ ba_to_architect: { status: "approved", approved_by: null, approved_at: null, notes: null },
140
+ architect_to_tech_lead: { status: "approved", approved_by: null, approved_at: null, notes: null },
141
+ tech_lead_to_fullstack: { status: "approved", approved_by: null, approved_at: null, notes: null },
142
+ fullstack_to_qa: { status: "approved", approved_by: null, approved_at: null, notes: null },
143
+ qa_to_done: { status: "approved", approved_by: null, approved_at: null, notes: null },
144
+ },
145
+ issues: [],
146
+ retry_count: 0,
147
+ escalated_from: null,
148
+ escalation_reason: null,
149
+ updated_at: "2026-03-21T00:00:00.000Z",
150
+ work_item_id: "stale-item",
151
+ }, null, 2)}\n`,
152
+ "utf8",
153
+ )
154
+
155
+ const result = selectActiveWorkItem("task-305", statePath)
156
+ const mirrorAfterRefresh = JSON.parse(fs.readFileSync(statePath, "utf8"))
157
+ const activePerItemState = readWorkItemState(projectRoot, "task-305")
158
+
159
+ assert.equal(result.state.work_item_id, "task-305")
160
+ assert.equal(mirrorAfterRefresh.work_item_id, "task-305")
161
+ assert.deepEqual(mirrorAfterRefresh, activePerItemState)
162
+ })
163
+
164
+ test("per-item reads remain stable while active selection changes", () => {
165
+ const statePath = createTempStateFile()
166
+
167
+ startFeature("FEATURE-301", "feature-one", statePath)
168
+ startTask("quick", "TASK-301", "task-two", "Second tracked item", statePath)
169
+
170
+ const featureStateBefore = showWorkItemState("feature-301", statePath)
171
+ assert.equal(featureStateBefore.state.feature_id, "FEATURE-301")
172
+
173
+ selectActiveWorkItem("feature-301", statePath)
174
+
175
+ const activeState = showState(statePath)
176
+ const taskState = showWorkItemState("task-301", statePath)
177
+
178
+ assert.equal(activeState.state.work_item_id, "feature-301")
179
+ assert.equal(taskState.state.work_item_id, "task-301")
180
+ assert.equal(taskState.state.feature_id, "TASK-301")
181
+ })
182
+
183
+ test("active selection fails validation when switched to a quick work item carrying tasks.json", () => {
184
+ const statePath = createTempStateFile()
185
+
186
+ startFeature("FEATURE-302", "feature-valid", statePath)
187
+ startTask("quick", "TASK-302", "quick-invalid", "Quick item with stale board", statePath)
188
+ writeTaskBoard(statePath, "task-302", {
189
+ mode: "full",
190
+ current_stage: "full_plan",
191
+ tasks: [createTask()],
192
+ issues: [],
193
+ })
194
+
195
+ assert.throws(() => showState(statePath), /Quick mode cannot carry a task board/)
196
+
197
+ const featureState = showWorkItemState("feature-302", statePath)
198
+ assert.equal(featureState.state.work_item_id, "feature-302")
199
+ })
200
+
201
+ test("active selection fails validation when switched to a migration work item carrying tasks.json", () => {
202
+ const statePath = createTempStateFile()
203
+
204
+ startFeature("FEATURE-303", "feature-valid", statePath)
205
+ startTask("migration", "MIGRATE-302", "migration-invalid", "Migration item with stale board", statePath)
206
+ writeTaskBoard(statePath, "migrate-302", {
207
+ mode: "full",
208
+ current_stage: "full_plan",
209
+ tasks: [createTask()],
210
+ issues: [],
211
+ })
212
+
213
+ assert.throws(() => showState(statePath), /Migration mode cannot carry a task board/)
214
+
215
+ const featureState = showWorkItemState("feature-303", statePath)
216
+ assert.equal(featureState.state.work_item_id, "feature-303")
217
+ })
218
+
219
+ test("task-board helpers operate on a specific full work item without changing active selection", () => {
220
+ const statePath = createTempStateFile()
221
+
222
+ startFeature("FEATURE-400", "board-target", statePath)
223
+ selectActiveWorkItem("feature-400", statePath)
224
+ advanceFullWorkItemToPlan(statePath)
225
+ startTask("quick", "TASK-400", "active-quick-item", "Keep quick item active", statePath)
226
+
227
+ createBoardTask(
228
+ "feature-400",
229
+ {
230
+ task_id: "TASK-401",
231
+ title: "Controller board task",
232
+ summary: "Keep board edits scoped to the selected work item",
233
+ kind: "implementation",
234
+ plan_refs: ["docs/plans/2026-03-21-feature.md"],
235
+ created_by: "TechLeadAgent",
236
+ },
237
+ statePath,
238
+ )
239
+
240
+ const activeState = showState(statePath)
241
+ const featureTasks = listTasks("feature-400", statePath)
242
+ const validatedBoard = validateWorkItemBoard("feature-400", statePath)
243
+
244
+ assert.equal(activeState.state.work_item_id, "task-400")
245
+ assert.equal(featureTasks.tasks.length, 1)
246
+ assert.equal(featureTasks.tasks[0].task_id, "TASK-401")
247
+ assert.equal(validatedBoard.board.tasks[0].task_id, "TASK-401")
248
+ })
249
+
250
+ test("invalid worktree metadata does not persist partial task-board writes", () => {
251
+ const statePath = createTempStateFile()
252
+
253
+ startFeature("FEATURE-401", "rollback-invalid-worktree", statePath)
254
+ advanceFullWorkItemToPlan(statePath)
255
+
256
+ assert.throws(
257
+ () =>
258
+ createBoardTask(
259
+ "feature-401",
260
+ {
261
+ task_id: "TASK-401",
262
+ title: "Unsafe worktree",
263
+ summary: "Reject protected branch metadata",
264
+ kind: "implementation",
265
+ created_by: "TechLeadAgent",
266
+ worktree_metadata: {
267
+ task_id: "TASK-401",
268
+ branch: "main",
269
+ worktree_path: ".worktrees/task-401-parallel",
270
+ },
271
+ },
272
+ statePath,
273
+ ),
274
+ /must not target main/,
275
+ )
276
+
277
+ assert.equal(fs.existsSync(path.join(path.dirname(path.dirname(statePath)), ".opencode", "work-items", "feature-401", "tasks.json")), false)
278
+ })
279
+
280
+ test("qa-fail local rework keeps mutations scoped to the targeted work item", () => {
281
+ const statePath = createTempStateFile()
282
+
283
+ startFeature("FEATURE-402", "full-target", statePath)
284
+ selectActiveWorkItem("feature-402", statePath)
285
+ advanceFullWorkItemToPlan(statePath)
286
+ startTask("quick", "TASK-402", "active-quick", "Keep quick item active", statePath)
287
+
288
+ createBoardTask(
289
+ "feature-402",
290
+ {
291
+ task_id: "TASK-402A",
292
+ title: "Scoped local rework",
293
+ summary: "Only mutate target work item",
294
+ kind: "implementation",
295
+ status: "qa_in_progress",
296
+ primary_owner: "Dev-A",
297
+ qa_owner: "QA-Agent",
298
+ created_by: "TechLeadAgent",
299
+ },
300
+ statePath,
301
+ )
302
+
303
+ const result = setTaskStatus("feature-402", "TASK-402A", "claimed", statePath, {
304
+ requestedBy: "QAAgent",
305
+ finding: {
306
+ issue_id: "ISSUE-402",
307
+ task_id: "TASK-402A",
308
+ title: "Regression found during QA",
309
+ summary: "Fix one task-local implementation bug",
310
+ type: "bug",
311
+ severity: "medium",
312
+ rooted_in: "implementation",
313
+ recommended_owner: "FullstackAgent",
314
+ evidence: "Targeted regression reproduced in QA.",
315
+ artifact_refs: ["docs/qa/2026-03-21-feature-402.md"],
316
+ affects_tasks: ["TASK-402A"],
317
+ blocks_parallel_work: false,
318
+ },
319
+ rerouteDecision: {
320
+ stage: "full_implementation",
321
+ owner: "FullstackAgent",
322
+ decided_by: "TechLeadAgent",
323
+ reason: "Return only the failing task to implementation",
324
+ },
325
+ })
326
+
327
+ const activeState = showState(statePath)
328
+ const featureState = showWorkItemState("feature-402", statePath)
329
+ const board = listTasks("feature-402", statePath)
330
+
331
+ assert.equal(result.board.tasks[0].status, "claimed")
332
+ assert.equal(activeState.state.work_item_id, "task-402")
333
+ assert.equal(featureState.state.work_item_id, "feature-402")
334
+ assert.equal(board.tasks[0].status, "claimed")
335
+ assert.equal(featureState.state.current_stage, "full_implementation")
336
+ assert.equal(featureState.state.current_owner, "FullstackAgent")
337
+ })
338
+
339
+ test("explicit release and reassign flows stay scoped to the targeted work item", () => {
340
+ const statePath = createTempStateFile()
341
+
342
+ startFeature("FEATURE-403", "targeted-assignment", statePath)
343
+ selectActiveWorkItem("feature-403", statePath)
344
+ advanceFullWorkItemToPlan(statePath)
345
+ startTask("quick", "TASK-403", "active-quick", "Keep quick item active", statePath)
346
+
347
+ createBoardTask(
348
+ "feature-403",
349
+ {
350
+ task_id: "TASK-403A",
351
+ title: "Scoped reassignment",
352
+ summary: "Only mutate target work item assignments",
353
+ kind: "implementation",
354
+ created_by: "TechLeadAgent",
355
+ },
356
+ statePath,
357
+ )
358
+
359
+ claimTask("feature-403", "TASK-403A", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
360
+ reassignTask("feature-403", "TASK-403A", "Dev-B", statePath, { requestedBy: "TechLeadAgent" })
361
+ const releaseResult = releaseTask("feature-403", "TASK-403A", statePath, { requestedBy: "TechLeadAgent" })
362
+
363
+ const activeState = showState(statePath)
364
+ const featureTasks = listTasks("feature-403", statePath)
365
+
366
+ assert.equal(activeState.state.work_item_id, "task-403")
367
+ assert.equal(releaseResult.board.tasks[0].status, "ready")
368
+ assert.equal(featureTasks.tasks[0].primary_owner, null)
369
+ })
@@ -0,0 +1,259 @@
1
+ const test = require("node:test")
2
+ const assert = require("node:assert/strict")
3
+
4
+ const {
5
+ VALID_ASSIGNMENT_AUTHORITIES,
6
+ decideQaFailLocalRework,
7
+ validateFailureIsolation,
8
+ validateParallelAssignments,
9
+ validateReassignmentAuthority,
10
+ validateTaskScopedFinding,
11
+ validateWorktreeMetadata,
12
+ } = require("../lib/parallel-execution-rules")
13
+
14
+ function makeTask(overrides = {}) {
15
+ return {
16
+ task_id: "TASK-11",
17
+ title: "Parallel execution rules",
18
+ status: "in_progress",
19
+ primary_owner: "Dev-A",
20
+ qa_owner: null,
21
+ ...overrides,
22
+ }
23
+ }
24
+
25
+ function makeFinding(overrides = {}) {
26
+ return {
27
+ issue_id: "ISSUE-11",
28
+ task_id: "TASK-11",
29
+ title: "Regression found during QA",
30
+ summary: "Fix task-local implementation bug",
31
+ type: "bug",
32
+ severity: "medium",
33
+ rooted_in: "implementation",
34
+ recommended_owner: "FullstackAgent",
35
+ evidence: "Targeted regression reproduced in QA.",
36
+ artifact_refs: ["docs/plans/2026-03-21-parallel-rollout.md"],
37
+ affects_tasks: ["TASK-11"],
38
+ blocks_parallel_work: false,
39
+ ...overrides,
40
+ }
41
+ }
42
+
43
+ test("primary-owner assignment validation is per task, not global across tasks", () => {
44
+ assert.doesNotThrow(() => validateParallelAssignments([
45
+ makeTask({ task_id: "TASK-11", primary_owner: "Dev-A", status: "in_progress" }),
46
+ makeTask({ task_id: "TASK-12", primary_owner: "Dev-B", status: "claimed" }),
47
+ makeTask({ task_id: "TASK-13", primary_owner: "Dev-A", status: "done" }),
48
+ ]))
49
+
50
+ assert.doesNotThrow(() => validateParallelAssignments([
51
+ makeTask({ task_id: "TASK-11", primary_owner: "Dev-A", status: "in_progress" }),
52
+ makeTask({ task_id: "TASK-12", primary_owner: "Dev-A", status: "claimed" }),
53
+ ]))
54
+
55
+ assert.throws(() => validateParallelAssignments([
56
+ makeTask({ task_id: "TASK-11", primary_owner: ["Dev-A"] }),
57
+ ]), /primary_owner.*non-empty string/)
58
+ })
59
+
60
+ test("qa-owner assignment validation is per task, not global across tasks", () => {
61
+ assert.doesNotThrow(() => validateParallelAssignments([
62
+ makeTask({ task_id: "TASK-11", status: "qa_in_progress", qa_owner: "QA-Agent" }),
63
+ makeTask({ task_id: "TASK-12", status: "done", qa_owner: "QA-Agent" }),
64
+ ]))
65
+
66
+ assert.doesNotThrow(() => validateParallelAssignments([
67
+ makeTask({ task_id: "TASK-11", status: "qa_ready", qa_owner: "QA-Agent" }),
68
+ makeTask({ task_id: "TASK-12", status: "qa_in_progress", qa_owner: "QA-Agent" }),
69
+ ]))
70
+
71
+ assert.throws(() => validateParallelAssignments([
72
+ makeTask({ task_id: "TASK-11", status: "qa_ready", qa_owner: ["QA-Agent"] }),
73
+ ]), /qa_owner.*non-empty string/)
74
+ })
75
+
76
+ test("reassignment authority allows initial assignment but restricts reassignment", () => {
77
+ assert.deepEqual(VALID_ASSIGNMENT_AUTHORITIES.primary_owner, ["MasterOrchestrator", "TechLeadAgent"])
78
+ assert.deepEqual(VALID_ASSIGNMENT_AUTHORITIES.qa_owner, ["MasterOrchestrator", "TechLeadAgent"])
79
+
80
+ assert.doesNotThrow(() => validateReassignmentAuthority({
81
+ task: makeTask({ primary_owner: null, status: "ready" }),
82
+ ownerField: "primary_owner",
83
+ requestedBy: "TechLeadAgent",
84
+ nextOwner: "Dev-A",
85
+ }))
86
+
87
+ assert.doesNotThrow(() => validateReassignmentAuthority({
88
+ task: makeTask({ primary_owner: "Dev-A", status: "in_progress" }),
89
+ ownerField: "primary_owner",
90
+ requestedBy: "TechLeadAgent",
91
+ nextOwner: "Dev-B",
92
+ }))
93
+
94
+ assert.doesNotThrow(() => validateReassignmentAuthority({
95
+ task: makeTask({ primary_owner: "Dev-A", status: "in_progress" }),
96
+ ownerField: "primary_owner",
97
+ requestedBy: "MasterOrchestrator",
98
+ nextOwner: "Dev-B",
99
+ }))
100
+
101
+ assert.doesNotThrow(() => validateReassignmentAuthority({
102
+ task: makeTask({ qa_owner: "QA-Agent", status: "qa_ready" }),
103
+ ownerField: "qa_owner",
104
+ requestedBy: "TechLeadAgent",
105
+ nextOwner: "QA-Agent-2",
106
+ }))
107
+
108
+ assert.throws(() => validateReassignmentAuthority({
109
+ task: makeTask({ qa_owner: null, status: "dev_done" }),
110
+ ownerField: "qa_owner",
111
+ requestedBy: "QAAgent",
112
+ nextOwner: "QA-Agent",
113
+ }), /MasterOrchestrator or TechLeadAgent.*qa_owner/)
114
+ })
115
+
116
+ test("task-scoped finding validation enforces routing-safe shape", () => {
117
+ assert.doesNotThrow(() => validateTaskScopedFinding(makeFinding(), makeTask()))
118
+
119
+ assert.throws(() => validateTaskScopedFinding(makeFinding({ task_id: "TASK-99" }), makeTask()), /task-scoped finding.*TASK-11/)
120
+ assert.throws(() => validateTaskScopedFinding(makeFinding({ artifact_refs: [] }), makeTask()), /artifact_refs/)
121
+ assert.throws(() => validateTaskScopedFinding(makeFinding({ affects_tasks: ["TASK-11", "TASK-12"] }), makeTask()), /affects_tasks/)
122
+ })
123
+
124
+ test("QA fail local rework routing allows only isolated implementation bugs", () => {
125
+ const decision = decideQaFailLocalRework({
126
+ mode: "full",
127
+ task: makeTask({ status: "qa_in_progress", primary_owner: "Dev-A", qa_owner: "QA-Agent" }),
128
+ finding: makeFinding(),
129
+ rerouteDecision: {
130
+ stage: "full_qa",
131
+ owner: "QAAgent",
132
+ decided_by: "TechLeadAgent",
133
+ reason: "Keep overall feature QA active while routing one task back for local rework",
134
+ },
135
+ })
136
+
137
+ assert.deepEqual(decision, {
138
+ allowed: true,
139
+ route: {
140
+ stage: "full_qa",
141
+ owner: "QAAgent",
142
+ decided_by: "TechLeadAgent",
143
+ reason: "Keep overall feature QA active while routing one task back for local rework",
144
+ },
145
+ })
146
+
147
+ const implementationDecision = decideQaFailLocalRework({
148
+ mode: "full",
149
+ task: makeTask({ status: "qa_in_progress", primary_owner: "Dev-A", qa_owner: "QA-Agent" }),
150
+ finding: makeFinding(),
151
+ rerouteDecision: {
152
+ stage: "full_implementation",
153
+ owner: "FullstackAgent",
154
+ decided_by: "MasterOrchestrator",
155
+ reason: "Aggregate risk requires implementation-stage reroute",
156
+ },
157
+ })
158
+
159
+ assert.deepEqual(implementationDecision.route, {
160
+ stage: "full_implementation",
161
+ owner: "FullstackAgent",
162
+ decided_by: "MasterOrchestrator",
163
+ reason: "Aggregate risk requires implementation-stage reroute",
164
+ })
165
+
166
+ assert.throws(() => decideQaFailLocalRework({
167
+ mode: "quick",
168
+ task: makeTask({ status: "qa_in_progress" }),
169
+ finding: makeFinding(),
170
+ }), /full mode/)
171
+
172
+ assert.throws(() => decideQaFailLocalRework({
173
+ mode: "full",
174
+ task: makeTask({ status: "qa_in_progress" }),
175
+ finding: makeFinding({ type: "design_flaw", rooted_in: "architecture", recommended_owner: "ArchitectAgent" }),
176
+ }), /design or requirements findings must not stay in local rework/)
177
+
178
+ assert.throws(() => decideQaFailLocalRework({
179
+ mode: "full",
180
+ task: makeTask({ status: "qa_in_progress" }),
181
+ finding: makeFinding(),
182
+ rerouteDecision: {
183
+ stage: "full_implementation",
184
+ owner: "FullstackAgent",
185
+ decided_by: "QAAgent",
186
+ reason: "QA should not self-authorize reroute decisions",
187
+ },
188
+ }), /field 'decided_by'.*MasterOrchestrator.*TechLeadAgent.*QAAgent/)
189
+
190
+ assert.throws(() => decideQaFailLocalRework({
191
+ mode: "full",
192
+ task: makeTask({ status: "qa_in_progress" }),
193
+ finding: makeFinding(),
194
+ rerouteDecision: {
195
+ stage: "full_done",
196
+ owner: "MasterOrchestrator",
197
+ decided_by: "MasterOrchestrator",
198
+ reason: "Invalid reroute target",
199
+ },
200
+ }), /field 'stage'.*full_qa.*full_implementation.*full_done/)
201
+
202
+ assert.throws(() => decideQaFailLocalRework({
203
+ mode: "full",
204
+ task: makeTask({ status: "qa_in_progress" }),
205
+ finding: makeFinding(),
206
+ rerouteDecision: {
207
+ stage: "full_qa",
208
+ owner: "MasterOrchestrator",
209
+ decided_by: "MasterOrchestrator",
210
+ reason: "Invalid reroute owner",
211
+ },
212
+ }), /field 'owner'.*QAAgent.*FullstackAgent.*MasterOrchestrator/)
213
+
214
+ assert.throws(() => decideQaFailLocalRework({
215
+ mode: "full",
216
+ task: makeTask({ status: "qa_in_progress" }),
217
+ finding: makeFinding(),
218
+ rerouteDecision: {
219
+ stage: "full_qa",
220
+ owner: "QAAgent",
221
+ decided_by: "MasterOrchestrator",
222
+ reason: "",
223
+ },
224
+ }), /reason.*non-empty string/)
225
+ })
226
+
227
+ test("failure isolation rules reject shared or blocking failures", () => {
228
+ assert.doesNotThrow(() => validateFailureIsolation(makeFinding(), makeTask()))
229
+
230
+ assert.throws(() => validateFailureIsolation(makeFinding({ blocks_parallel_work: true }), makeTask()), /blocks parallel work/)
231
+ assert.throws(() => validateFailureIsolation(makeFinding({ affects_tasks: ["TASK-11", "TASK-12"] }), makeTask()), /isolated to exactly one task/)
232
+ assert.throws(() => validateFailureIsolation(makeFinding({ severity: "critical" }), makeTask()), /critical failures require orchestrator escalation/)
233
+ })
234
+
235
+ test("worktree and branch metadata validation requires task-specific isolation metadata", () => {
236
+ assert.doesNotThrow(() => validateWorktreeMetadata({
237
+ task_id: "TASK-11",
238
+ branch: "task/TASK-11-parallel-execution",
239
+ worktree_path: ".worktrees/task-11-parallel-execution",
240
+ }))
241
+
242
+ assert.throws(() => validateWorktreeMetadata({
243
+ task_id: "TASK-11",
244
+ branch: "main",
245
+ worktree_path: ".worktrees/task-11-parallel-execution",
246
+ }), /branch.*must not target main/)
247
+
248
+ assert.throws(() => validateWorktreeMetadata({
249
+ task_id: "TASK-11",
250
+ branch: "task/TASK-99-other-work",
251
+ worktree_path: ".worktrees/task-11-parallel-execution",
252
+ }), /branch.*TASK-11/)
253
+
254
+ assert.throws(() => validateWorktreeMetadata({
255
+ task_id: "TASK-11",
256
+ branch: "task/TASK-11-parallel-execution",
257
+ worktree_path: "parallel-agent-rollout",
258
+ }), /worktree_path/)
259
+ })