@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.
- package/.opencode/README.md +47 -0
- package/.opencode/install-manifest.json +41 -0
- package/.opencode/lib/artifact-scaffolder.js +111 -0
- package/.opencode/lib/contract-consistency.js +218 -0
- package/.opencode/lib/parallel-execution-rules.js +261 -0
- package/.opencode/lib/runtime-paths.js +95 -0
- package/.opencode/lib/runtime-summary.js +82 -0
- package/.opencode/lib/state-guard.js +99 -0
- package/.opencode/lib/task-board-rules.js +375 -0
- package/.opencode/lib/work-item-store.js +280 -0
- package/.opencode/lib/workflow-state-controller.js +1739 -0
- package/.opencode/lib/workflow-state-rules.js +331 -0
- package/.opencode/opencode.json +93 -0
- package/.opencode/package.json +3 -0
- package/.opencode/tests/artifact-scaffolder.test.js +733 -0
- package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
- package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
- package/.opencode/tests/session-start-hook.test.js +357 -0
- package/.opencode/tests/state-guard.test.js +124 -0
- package/.opencode/tests/task-board-rules.test.js +204 -0
- package/.opencode/tests/work-item-store.test.js +380 -0
- package/.opencode/tests/workflow-behavior.test.js +149 -0
- package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
- package/.opencode/tests/workflow-state-cli.test.js +1275 -0
- package/.opencode/tests/workflow-state-controller.test.js +1038 -0
- package/.opencode/work-items/feature-001/state.json +70 -0
- package/.opencode/work-items/index.json +13 -0
- package/.opencode/workflow-state.js +489 -0
- package/.opencode/workflow-state.json +70 -0
- package/AGENTS.md +265 -0
- package/README.md +401 -0
- package/agents/architect-agent.md +63 -0
- package/agents/ba-agent.md +56 -0
- package/agents/code-reviewer.md +77 -0
- package/agents/fullstack-agent.md +115 -0
- package/agents/master-orchestrator.md +60 -0
- package/agents/pm-agent.md +56 -0
- package/agents/qa-agent.md +124 -0
- package/agents/tech-lead-agent.md +60 -0
- package/assets/install-bundle/README.md +7 -0
- package/assets/install-bundle/opencode/README.md +11 -0
- package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
- package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
- package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
- package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
- package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
- package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
- package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
- package/assets/install-bundle/opencode/commands/delivery.md +45 -0
- package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
- package/assets/install-bundle/opencode/commands/migrate.md +61 -0
- package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
- package/assets/install-bundle/opencode/commands/task.md +46 -0
- package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
- package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
- package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
- package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
- package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
- package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
- package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
- package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
- package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
- package/assets/opencode.json.template +11 -0
- package/assets/openkit-install.json.template +19 -0
- package/bin/openkit.js +9 -0
- package/commands/brainstorm.md +44 -0
- package/commands/delivery.md +45 -0
- package/commands/execute-plan.md +44 -0
- package/commands/migrate.md +61 -0
- package/commands/quick-task.md +45 -0
- package/commands/task.md +46 -0
- package/commands/write-plan.md +50 -0
- package/context/core/approval-gates.md +146 -0
- package/context/core/code-quality.md +42 -0
- package/context/core/issue-routing.md +85 -0
- package/context/core/lane-selection.md +54 -0
- package/context/core/project-config.md +143 -0
- package/context/core/session-resume.md +85 -0
- package/context/core/workflow-state-schema.md +224 -0
- package/context/core/workflow.md +442 -0
- package/context/navigation.md +94 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
- package/docs/architecture/README.md +7 -0
- package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
- package/docs/briefs/README.md +7 -0
- package/docs/governance/README.md +25 -0
- package/docs/governance/adr-policy.md +27 -0
- package/docs/governance/definition-of-done.md +17 -0
- package/docs/governance/naming-conventions.md +21 -0
- package/docs/governance/severity-levels.md +12 -0
- package/docs/maintainer/README.md +51 -0
- package/docs/operations/README.md +79 -0
- package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
- package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
- package/docs/operations/internal-records/README.md +18 -0
- package/docs/operations/runbooks/README.md +23 -0
- package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
- package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
- package/docs/operator/README.md +50 -0
- package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
- package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
- package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
- package/docs/plans/README.md +7 -0
- package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
- package/docs/qa/README.md +7 -0
- package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
- package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
- package/docs/specs/README.md +7 -0
- package/docs/templates/README.md +36 -0
- package/docs/templates/adr-template.md +18 -0
- package/docs/templates/architecture-template.md +31 -0
- package/docs/templates/implementation-plan-template.md +32 -0
- package/docs/templates/migration-baseline-checklist.md +48 -0
- package/docs/templates/migration-plan-template.md +52 -0
- package/docs/templates/migration-report-template.md +74 -0
- package/docs/templates/migration-verify-checklist.md +39 -0
- package/docs/templates/product-brief-template.md +32 -0
- package/docs/templates/qa-report-template.md +37 -0
- package/docs/templates/quick-task-template.md +36 -0
- package/docs/templates/spec-template.md +31 -0
- package/hooks/hooks.json +16 -0
- package/hooks/session-start +162 -0
- package/package.json +24 -0
- package/registry.json +328 -0
- package/skills/brainstorming/SKILL.md +51 -0
- package/skills/code-review/SKILL.md +48 -0
- package/skills/subagent-driven-development/SKILL.md +79 -0
- package/skills/systematic-debugging/SKILL.md +61 -0
- package/skills/test-driven-development/SKILL.md +48 -0
- package/skills/using-skills/SKILL.md +39 -0
- package/skills/verification-before-completion/SKILL.md +137 -0
- package/skills/writing-plans/SKILL.md +68 -0
- package/skills/writing-specs/SKILL.md +47 -0
- package/src/audit/vietnamese-detection.js +259 -0
- package/src/cli/commands/detect-vietnamese.js +24 -0
- package/src/cli/commands/doctor.js +33 -0
- package/src/cli/commands/help.js +33 -0
- package/src/cli/commands/init.js +25 -0
- package/src/cli/commands/install-global.js +26 -0
- package/src/cli/commands/install.js +25 -0
- package/src/cli/commands/run.js +63 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/upgrade.js +25 -0
- package/src/cli/conflict-output.js +19 -0
- package/src/cli/index.js +56 -0
- package/src/global/doctor.js +101 -0
- package/src/global/ensure-install.js +32 -0
- package/src/global/install-state.js +73 -0
- package/src/global/launcher.js +51 -0
- package/src/global/materialize.js +123 -0
- package/src/global/paths.js +85 -0
- package/src/global/uninstall.js +25 -0
- package/src/global/workspace-state.js +63 -0
- package/src/install/asset-manifest.js +284 -0
- package/src/install/conflicts.js +43 -0
- package/src/install/discovery.js +138 -0
- package/src/install/install-state.js +136 -0
- package/src/install/materialize.js +158 -0
- package/src/install/merge-policy.js +145 -0
- package/src/runtime/doctor.js +281 -0
- package/src/runtime/launcher.js +49 -0
- package/src/runtime/opencode-layering.js +135 -0
- package/src/runtime/openkit-managed-summary.js +27 -0
- package/tests/cli/openkit-cli.test.js +417 -0
- package/tests/global/doctor.test.js +130 -0
- package/tests/global/ensure-install.test.js +105 -0
- package/tests/install/discovery.test.js +124 -0
- package/tests/install/install-state.test.js +346 -0
- package/tests/install/materialize.test.js +244 -0
- package/tests/install/merge-policy.test.js +177 -0
- package/tests/runtime/doctor.test.js +430 -0
- package/tests/runtime/launcher.test.js +230 -0
- package/tests/runtime/module-boundary.test.js +16 -0
|
@@ -0,0 +1,1038 @@
|
|
|
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
|
+
assignQaOwner,
|
|
10
|
+
claimTask,
|
|
11
|
+
createTask: createBoardTask,
|
|
12
|
+
linkArtifact,
|
|
13
|
+
reassignTask,
|
|
14
|
+
releaseTask,
|
|
15
|
+
routeRework,
|
|
16
|
+
setRoutingProfile,
|
|
17
|
+
setTaskStatus,
|
|
18
|
+
showState,
|
|
19
|
+
showWorkItemState,
|
|
20
|
+
selectActiveWorkItem,
|
|
21
|
+
setApproval,
|
|
22
|
+
startFeature,
|
|
23
|
+
startTask,
|
|
24
|
+
validateState,
|
|
25
|
+
validateWorkItemBoard,
|
|
26
|
+
} = require("../lib/workflow-state-controller")
|
|
27
|
+
const { readWorkItemIndex, readWorkItemState } = require("../lib/work-item-store")
|
|
28
|
+
|
|
29
|
+
function loadControllerWithWorkItemStoreMocks(mocks) {
|
|
30
|
+
const controllerPath = require.resolve("../lib/workflow-state-controller")
|
|
31
|
+
const workItemStorePath = require.resolve("../lib/work-item-store")
|
|
32
|
+
const originalStore = require(workItemStorePath)
|
|
33
|
+
const patchedStore = { ...originalStore, ...mocks }
|
|
34
|
+
|
|
35
|
+
delete require.cache[controllerPath]
|
|
36
|
+
require.cache[workItemStorePath] = {
|
|
37
|
+
id: workItemStorePath,
|
|
38
|
+
filename: workItemStorePath,
|
|
39
|
+
loaded: true,
|
|
40
|
+
exports: patchedStore,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return require(controllerPath)
|
|
45
|
+
} finally {
|
|
46
|
+
delete require.cache[controllerPath]
|
|
47
|
+
require.cache[workItemStorePath] = {
|
|
48
|
+
id: workItemStorePath,
|
|
49
|
+
filename: workItemStorePath,
|
|
50
|
+
loaded: true,
|
|
51
|
+
exports: originalStore,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeTempDir() {
|
|
57
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "openkit-workflow-state-"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadFixtureState() {
|
|
61
|
+
const fixturePath = path.resolve(__dirname, "../workflow-state.json")
|
|
62
|
+
return JSON.parse(fs.readFileSync(fixturePath, "utf8"))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createTempStateFile() {
|
|
66
|
+
const dir = makeTempDir()
|
|
67
|
+
const opencodeDir = path.join(dir, ".opencode")
|
|
68
|
+
fs.mkdirSync(opencodeDir, { recursive: true })
|
|
69
|
+
const statePath = path.join(opencodeDir, "workflow-state.json")
|
|
70
|
+
fs.writeFileSync(statePath, `${JSON.stringify(loadFixtureState(), null, 2)}\n`, "utf8")
|
|
71
|
+
return statePath
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createTask(overrides = {}) {
|
|
75
|
+
return {
|
|
76
|
+
task_id: "TASK-1",
|
|
77
|
+
title: "Implement controller integration",
|
|
78
|
+
summary: "Add board-aware runtime enforcement",
|
|
79
|
+
kind: "implementation",
|
|
80
|
+
status: "ready",
|
|
81
|
+
primary_owner: null,
|
|
82
|
+
qa_owner: null,
|
|
83
|
+
depends_on: [],
|
|
84
|
+
blocked_by: [],
|
|
85
|
+
artifact_refs: [],
|
|
86
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
87
|
+
branch_or_worktree: null,
|
|
88
|
+
created_by: "TechLeadAgent",
|
|
89
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
90
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
91
|
+
...overrides,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeTaskBoard(statePath, workItemId, board) {
|
|
96
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
97
|
+
const boardPath = path.join(projectRoot, ".opencode", "work-items", workItemId, "tasks.json")
|
|
98
|
+
fs.mkdirSync(path.dirname(boardPath), { recursive: true })
|
|
99
|
+
fs.writeFileSync(boardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
|
|
100
|
+
return boardPath
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function advanceFullWorkItemToPlan(statePath) {
|
|
104
|
+
advanceStage("full_brief", statePath)
|
|
105
|
+
setApproval("pm_to_ba", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
106
|
+
advanceStage("full_spec", statePath)
|
|
107
|
+
setApproval("ba_to_architect", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
108
|
+
advanceStage("full_architecture", statePath)
|
|
109
|
+
setApproval("architect_to_tech_lead", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
110
|
+
advanceStage("full_plan", statePath)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test("validateState accepts the shipped hard-split example state", () => {
|
|
114
|
+
const statePath = path.resolve(__dirname, "../workflow-state.json")
|
|
115
|
+
const result = validateState(statePath)
|
|
116
|
+
|
|
117
|
+
assert.equal(result.state.mode, "full")
|
|
118
|
+
assert.equal(result.state.current_stage, "full_done")
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("startTask initializes quick mode with quick-only approvals", () => {
|
|
122
|
+
const statePath = createTempStateFile()
|
|
123
|
+
const result = startTask("quick", "TASK-123", "update-copy", "Scoped text change", statePath)
|
|
124
|
+
|
|
125
|
+
assert.equal(result.state.mode, "quick")
|
|
126
|
+
assert.equal(result.state.mode_reason, "Scoped text change")
|
|
127
|
+
assert.equal(result.state.current_stage, "quick_intake")
|
|
128
|
+
assert.equal(result.state.current_owner, "MasterOrchestrator")
|
|
129
|
+
assert.deepEqual(result.state.routing_profile, {
|
|
130
|
+
work_intent: "maintenance",
|
|
131
|
+
behavior_delta: "preserve",
|
|
132
|
+
dominant_uncertainty: "low_local",
|
|
133
|
+
scope_shape: "local",
|
|
134
|
+
selection_reason: "Scoped text change",
|
|
135
|
+
})
|
|
136
|
+
assert.deepEqual(Object.keys(result.state.approvals), ["quick_verified"])
|
|
137
|
+
assert.equal(result.state.artifacts.task_card, null)
|
|
138
|
+
assert.equal(result.state.escalated_from, null)
|
|
139
|
+
assert.equal(result.state.escalation_reason, null)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("quick mode requires quick_plan before quick_build", () => {
|
|
143
|
+
const statePath = createTempStateFile()
|
|
144
|
+
|
|
145
|
+
startTask("quick", "TASK-129", "needs-plan", "Bounded quick work", statePath)
|
|
146
|
+
|
|
147
|
+
assert.throws(() => advanceStage("quick_build", statePath), /immediate next stage 'quick_plan'/)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("quick_plan becomes the next stage after quick_intake", () => {
|
|
151
|
+
const statePath = createTempStateFile()
|
|
152
|
+
|
|
153
|
+
startTask("quick", "TASK-130", "plan-stage", "Live quick plan stage", statePath)
|
|
154
|
+
const result = advanceStage("quick_plan", statePath)
|
|
155
|
+
|
|
156
|
+
assert.equal(result.state.current_stage, "quick_plan")
|
|
157
|
+
assert.equal(result.state.current_owner, "MasterOrchestrator")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("quick_done requires quick_verified approval", () => {
|
|
161
|
+
const statePath = createTempStateFile()
|
|
162
|
+
|
|
163
|
+
startTask("quick", "TASK-124", "verify-copy", "Copy verification task", statePath)
|
|
164
|
+
advanceStage("quick_plan", statePath)
|
|
165
|
+
advanceStage("quick_build", statePath)
|
|
166
|
+
advanceStage("quick_verify", statePath)
|
|
167
|
+
|
|
168
|
+
assert.throws(() => advanceStage("quick_done", statePath), /quick_verified/)
|
|
169
|
+
|
|
170
|
+
setApproval("quick_verified", "approved", "system", "2026-03-21", "QA Lite passed", statePath)
|
|
171
|
+
const result = advanceStage("quick_done", statePath)
|
|
172
|
+
|
|
173
|
+
assert.equal(result.state.current_stage, "quick_done")
|
|
174
|
+
assert.equal(result.state.status, "done")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("linkArtifact supports quick task cards", () => {
|
|
178
|
+
const statePath = createTempStateFile()
|
|
179
|
+
const dir = makeTempDir()
|
|
180
|
+
const taskCardPath = path.join(dir, "2026-03-21-update-copy.md")
|
|
181
|
+
|
|
182
|
+
fs.writeFileSync(taskCardPath, "# Quick Task\n", "utf8")
|
|
183
|
+
startTask("quick", "TASK-125", "task-card", "Quick task artifact link", statePath)
|
|
184
|
+
|
|
185
|
+
const result = linkArtifact("task_card", taskCardPath, statePath)
|
|
186
|
+
|
|
187
|
+
assert.equal(result.state.artifacts.task_card, taskCardPath)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test("routeRework escalates quick design flaws into full delivery", () => {
|
|
191
|
+
const statePath = createTempStateFile()
|
|
192
|
+
|
|
193
|
+
startTask("quick", "TASK-126", "needs-spec", "Started as a quick task", statePath)
|
|
194
|
+
const result = routeRework("design_flaw", false, statePath)
|
|
195
|
+
|
|
196
|
+
assert.equal(result.state.mode, "full")
|
|
197
|
+
assert.equal(result.state.current_stage, "full_intake")
|
|
198
|
+
assert.equal(result.state.current_owner, "MasterOrchestrator")
|
|
199
|
+
assert.equal(result.state.escalated_from, "quick")
|
|
200
|
+
assert.match(result.state.escalation_reason, /design_flaw/)
|
|
201
|
+
assert.deepEqual(Object.keys(result.state.approvals), [
|
|
202
|
+
"pm_to_ba",
|
|
203
|
+
"ba_to_architect",
|
|
204
|
+
"architect_to_tech_lead",
|
|
205
|
+
"tech_lead_to_fullstack",
|
|
206
|
+
"fullstack_to_qa",
|
|
207
|
+
"qa_to_done",
|
|
208
|
+
])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test("routeRework keeps full-mode bugs in full implementation", () => {
|
|
212
|
+
const statePath = createTempStateFile()
|
|
213
|
+
|
|
214
|
+
startTask("full", "FEATURE-200", "dashboard-v2", "Feature-sized workflow", statePath)
|
|
215
|
+
const result = routeRework("bug", true, statePath)
|
|
216
|
+
|
|
217
|
+
assert.equal(result.state.mode, "full")
|
|
218
|
+
assert.equal(result.state.current_stage, "full_implementation")
|
|
219
|
+
assert.equal(result.state.current_owner, "FullstackAgent")
|
|
220
|
+
assert.equal(result.state.retry_count, 1)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test("startTask initializes migration mode with migration approvals", () => {
|
|
224
|
+
const statePath = createTempStateFile()
|
|
225
|
+
const result = startTask("migration", "MIGRATE-100", "react-19-upgrade", "Upgrade React stack safely", statePath)
|
|
226
|
+
|
|
227
|
+
assert.equal(result.state.mode, "migration")
|
|
228
|
+
assert.equal(result.state.current_stage, "migration_intake")
|
|
229
|
+
assert.equal(result.state.current_owner, "MasterOrchestrator")
|
|
230
|
+
assert.deepEqual(result.state.routing_profile, {
|
|
231
|
+
work_intent: "modernization",
|
|
232
|
+
behavior_delta: "preserve",
|
|
233
|
+
dominant_uncertainty: "compatibility",
|
|
234
|
+
scope_shape: "adjacent",
|
|
235
|
+
selection_reason: "Upgrade React stack safely",
|
|
236
|
+
})
|
|
237
|
+
assert.deepEqual(Object.keys(result.state.approvals), [
|
|
238
|
+
"baseline_to_strategy",
|
|
239
|
+
"strategy_to_upgrade",
|
|
240
|
+
"upgrade_to_verify",
|
|
241
|
+
"migration_verified",
|
|
242
|
+
])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test("migration mode advances through its canonical stage chain", () => {
|
|
246
|
+
const statePath = createTempStateFile()
|
|
247
|
+
|
|
248
|
+
startTask("migration", "MIGRATE-101", "legacy-refresh", "Legacy stack refresh", statePath)
|
|
249
|
+
|
|
250
|
+
let result = advanceStage("migration_baseline", statePath)
|
|
251
|
+
assert.equal(result.state.current_owner, "ArchitectAgent")
|
|
252
|
+
|
|
253
|
+
setApproval("baseline_to_strategy", "approved", "TechLeadAgent", "2026-03-21", "Baseline approved", statePath)
|
|
254
|
+
result = advanceStage("migration_strategy", statePath)
|
|
255
|
+
assert.equal(result.state.current_owner, "TechLeadAgent")
|
|
256
|
+
|
|
257
|
+
setApproval("strategy_to_upgrade", "approved", "FullstackAgent", "2026-03-21", "Strategy approved", statePath)
|
|
258
|
+
result = advanceStage("migration_upgrade", statePath)
|
|
259
|
+
assert.equal(result.state.current_owner, "FullstackAgent")
|
|
260
|
+
|
|
261
|
+
setApproval("upgrade_to_verify", "approved", "QAAgent", "2026-03-21", "Upgrade ready for QA", statePath)
|
|
262
|
+
result = advanceStage("migration_verify", statePath)
|
|
263
|
+
assert.equal(result.state.current_owner, "QAAgent")
|
|
264
|
+
|
|
265
|
+
setApproval("migration_verified", "approved", "QAAgent", "2026-03-21", "Migration verified", statePath)
|
|
266
|
+
result = advanceStage("migration_done", statePath)
|
|
267
|
+
assert.equal(result.state.current_stage, "migration_done")
|
|
268
|
+
assert.equal(result.state.status, "done")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test("migration design flaws reroute within migration strategy", () => {
|
|
272
|
+
const statePath = createTempStateFile()
|
|
273
|
+
|
|
274
|
+
startTask("migration", "MIGRATE-102", "upgrade-routing", "Upgrade routing", statePath)
|
|
275
|
+
const result = routeRework("design_flaw", false, statePath)
|
|
276
|
+
|
|
277
|
+
assert.equal(result.state.mode, "migration")
|
|
278
|
+
assert.equal(result.state.current_stage, "migration_strategy")
|
|
279
|
+
assert.equal(result.state.current_owner, "TechLeadAgent")
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test("migration requirement gaps escalate into full delivery", () => {
|
|
283
|
+
const statePath = createTempStateFile()
|
|
284
|
+
|
|
285
|
+
startTask("migration", "MIGRATE-103", "upgrade-requirements", "Upgrade with requirement ambiguity", statePath)
|
|
286
|
+
const result = routeRework("requirement_gap", false, statePath)
|
|
287
|
+
|
|
288
|
+
assert.equal(result.state.mode, "full")
|
|
289
|
+
assert.equal(result.state.current_stage, "full_intake")
|
|
290
|
+
assert.equal(result.state.escalated_from, "migration")
|
|
291
|
+
assert.match(result.state.mode_reason, /Promoted from migration mode/)
|
|
292
|
+
assert.match(result.state.escalation_reason, /migration work escalated/i)
|
|
293
|
+
assert.equal(result.state.routing_profile.dominant_uncertainty, "product")
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("routeRework blocks after reaching the retry threshold", () => {
|
|
297
|
+
const statePath = createTempStateFile()
|
|
298
|
+
|
|
299
|
+
startTask("full", "FEATURE-203", "retry-threshold", "Feature workflow with repeated bug", statePath)
|
|
300
|
+
routeRework("bug", true, statePath)
|
|
301
|
+
routeRework("bug", true, statePath)
|
|
302
|
+
const result = routeRework("bug", true, statePath)
|
|
303
|
+
|
|
304
|
+
assert.equal(result.state.retry_count, 3)
|
|
305
|
+
assert.equal(result.state.status, "blocked")
|
|
306
|
+
assert.equal(result.state.current_stage, "full_implementation")
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test("quick mode rejects full-delivery approvals", () => {
|
|
310
|
+
const statePath = createTempStateFile()
|
|
311
|
+
|
|
312
|
+
startTask("quick", "TASK-127", "bad-gate", "Quick workflow gate validation", statePath)
|
|
313
|
+
|
|
314
|
+
assert.throws(
|
|
315
|
+
() => setApproval("pm_to_ba", "approved", "user", "2026-03-21", "Wrong gate", statePath),
|
|
316
|
+
/mode 'quick'/,
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test("quick mode rejects skipping stages", () => {
|
|
321
|
+
const statePath = createTempStateFile()
|
|
322
|
+
|
|
323
|
+
startTask("quick", "TASK-128", "skip-stage", "Quick stage ordering", statePath)
|
|
324
|
+
|
|
325
|
+
assert.throws(() => advanceStage("quick_verify", statePath), /immediate next stage 'quick_plan'/)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("full mode rejects quick stages", () => {
|
|
329
|
+
const statePath = createTempStateFile()
|
|
330
|
+
|
|
331
|
+
startTask("full", "FEATURE-201", "wrong-lane-stage", "Full workflow stage validation", statePath)
|
|
332
|
+
|
|
333
|
+
assert.throws(() => advanceStage("quick_build", statePath), /does not belong to mode 'full'/)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test("setRoutingProfile rejects contradictory routing metadata for quick mode", () => {
|
|
337
|
+
const statePath = createTempStateFile()
|
|
338
|
+
|
|
339
|
+
startTask("quick", "TASK-777", "routing-conflict", "Quick routing profile", statePath)
|
|
340
|
+
|
|
341
|
+
assert.throws(
|
|
342
|
+
() => setRoutingProfile("modernization", "preserve", "compatibility", "adjacent", "Actually a migration", statePath),
|
|
343
|
+
/routing_profile\.work_intent must be 'maintenance' for quick mode/,
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("compatibility startFeature initializes full mode", () => {
|
|
348
|
+
const statePath = createTempStateFile()
|
|
349
|
+
|
|
350
|
+
const result = startFeature("FEATURE-202", "compat-start", statePath)
|
|
351
|
+
|
|
352
|
+
assert.equal(result.state.mode, "full")
|
|
353
|
+
assert.equal(result.state.current_stage, "full_intake")
|
|
354
|
+
assert.match(result.state.mode_reason, /legacy start-feature command/)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test("legacy startTask creates and selects the active work item through backing storage", () => {
|
|
358
|
+
const statePath = createTempStateFile()
|
|
359
|
+
|
|
360
|
+
const result = startTask("quick", "TASK-123", "update-copy", "Scoped text change", statePath)
|
|
361
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
362
|
+
const index = readWorkItemIndex(projectRoot)
|
|
363
|
+
const persistedState = readWorkItemState(projectRoot, "task-123")
|
|
364
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
365
|
+
|
|
366
|
+
assert.equal(result.state.work_item_id, "task-123")
|
|
367
|
+
assert.equal(index.active_work_item_id, "task-123")
|
|
368
|
+
assert.equal(index.work_items.at(-1).work_item_id, "task-123")
|
|
369
|
+
assert.equal(persistedState.work_item_id, "task-123")
|
|
370
|
+
assert.equal(persistedState.feature_id, "TASK-123")
|
|
371
|
+
assert.deepEqual(mirrorState, persistedState)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test("legacy startTask full creates and selects the active full-delivery work item through backing storage", () => {
|
|
375
|
+
const statePath = createTempStateFile()
|
|
376
|
+
|
|
377
|
+
const result = startTask("full", "FEATURE-303", "legacy-full", "Legacy full start-task command", statePath)
|
|
378
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
379
|
+
const index = readWorkItemIndex(projectRoot)
|
|
380
|
+
const persistedState = readWorkItemState(projectRoot, "feature-303")
|
|
381
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
382
|
+
|
|
383
|
+
assert.equal(result.state.work_item_id, "feature-303")
|
|
384
|
+
assert.equal(result.state.mode, "full")
|
|
385
|
+
assert.equal(index.active_work_item_id, "feature-303")
|
|
386
|
+
assert.equal(index.work_items.at(-1).work_item_id, "feature-303")
|
|
387
|
+
assert.equal(persistedState.work_item_id, "feature-303")
|
|
388
|
+
assert.equal(persistedState.feature_id, "FEATURE-303")
|
|
389
|
+
assert.deepEqual(mirrorState, persistedState)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test("startTask builds a fresh work item state without leaking unrelated active-item metadata", () => {
|
|
393
|
+
const statePath = createTempStateFile()
|
|
394
|
+
const activeState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
395
|
+
activeState.unrelated_runtime_note = "should-not-leak"
|
|
396
|
+
fs.writeFileSync(statePath, `${JSON.stringify(activeState, null, 2)}\n`, "utf8")
|
|
397
|
+
|
|
398
|
+
startFeature("FEATURE-202", "compat-start", statePath)
|
|
399
|
+
const result = startTask("quick", "TASK-124", "fresh-shape", "Fresh quick task", statePath)
|
|
400
|
+
|
|
401
|
+
assert.equal(Object.hasOwn(result.state, "unrelated_runtime_note"), false)
|
|
402
|
+
assert.equal(result.state.mode, "quick")
|
|
403
|
+
assert.deepEqual(result.state.issues, [])
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test("legacy startFeature creates and selects the active work item through backing storage", () => {
|
|
407
|
+
const statePath = createTempStateFile()
|
|
408
|
+
|
|
409
|
+
const result = startFeature("FEATURE-202", "compat-start", statePath)
|
|
410
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
411
|
+
const index = readWorkItemIndex(projectRoot)
|
|
412
|
+
const persistedState = readWorkItemState(projectRoot, "feature-202")
|
|
413
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
414
|
+
|
|
415
|
+
assert.equal(result.state.work_item_id, "feature-202")
|
|
416
|
+
assert.equal(index.active_work_item_id, "feature-202")
|
|
417
|
+
assert.equal(index.work_items.at(-1).work_item_id, "feature-202")
|
|
418
|
+
assert.equal(persistedState.work_item_id, "feature-202")
|
|
419
|
+
assert.equal(persistedState.feature_id, "FEATURE-202")
|
|
420
|
+
assert.deepEqual(mirrorState, persistedState)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test("showState reads the active work item through backing storage", () => {
|
|
424
|
+
const statePath = createTempStateFile()
|
|
425
|
+
|
|
426
|
+
startFeature("FEATURE-202", "compat-start", statePath)
|
|
427
|
+
startTask("quick", "TASK-500", "selected-item", "Switch active item", statePath)
|
|
428
|
+
|
|
429
|
+
const result = showState(statePath)
|
|
430
|
+
|
|
431
|
+
assert.equal(result.state.work_item_id, "task-500")
|
|
432
|
+
assert.equal(result.state.feature_id, "TASK-500")
|
|
433
|
+
assert.equal(result.state.current_stage, "quick_intake")
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test("showWorkItemState reads a non-active work item without changing selection", () => {
|
|
437
|
+
const statePath = createTempStateFile()
|
|
438
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
439
|
+
|
|
440
|
+
startFeature("FEATURE-202", "compat-start", statePath)
|
|
441
|
+
startTask("quick", "TASK-500", "selected-item", "Switch active item", statePath)
|
|
442
|
+
|
|
443
|
+
const result = showWorkItemState("feature-202", statePath)
|
|
444
|
+
const index = readWorkItemIndex(projectRoot)
|
|
445
|
+
|
|
446
|
+
assert.equal(result.state.work_item_id, "feature-202")
|
|
447
|
+
assert.equal(result.state.feature_id, "FEATURE-202")
|
|
448
|
+
assert.equal(index.active_work_item_id, "task-500")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test("selectActiveWorkItem refreshes the compatibility mirror after changing the backing-store selection", () => {
|
|
452
|
+
const statePath = createTempStateFile()
|
|
453
|
+
|
|
454
|
+
startFeature("FEATURE-202", "compat-start", statePath)
|
|
455
|
+
startTask("quick", "TASK-500", "selected-item", "Switch active item", statePath)
|
|
456
|
+
|
|
457
|
+
const result = selectActiveWorkItem("feature-202", statePath)
|
|
458
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
459
|
+
|
|
460
|
+
assert.equal(result.state.work_item_id, "feature-202")
|
|
461
|
+
assert.equal(mirrorState.work_item_id, "feature-202")
|
|
462
|
+
assert.equal(mirrorState.feature_id, "FEATURE-202")
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test("selectActiveWorkItem restores the previous active pointer when mirror write fails", () => {
|
|
466
|
+
const statePath = createTempStateFile()
|
|
467
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
468
|
+
|
|
469
|
+
startFeature("FEATURE-202", "compat-start", statePath)
|
|
470
|
+
startTask("quick", "TASK-500", "selected-item", "Switch active item", statePath)
|
|
471
|
+
|
|
472
|
+
const originalStore = require("../lib/work-item-store")
|
|
473
|
+
const controller = loadControllerWithWorkItemStoreMocks({
|
|
474
|
+
writeCompatibilityMirror() {
|
|
475
|
+
throw new Error("Simulated selection mirror failure")
|
|
476
|
+
},
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
assert.throws(() => controller.selectActiveWorkItem("feature-202", statePath), /mirror/i)
|
|
480
|
+
|
|
481
|
+
const index = originalStore.readWorkItemIndex(projectRoot)
|
|
482
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
483
|
+
|
|
484
|
+
assert.equal(index.active_work_item_id, "task-500")
|
|
485
|
+
assert.equal(mirrorState.work_item_id, "task-500")
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test("approving tech_lead_to_fullstack without a valid task board fails", () => {
|
|
489
|
+
const statePath = createTempStateFile()
|
|
490
|
+
|
|
491
|
+
startFeature("FEATURE-600", "board-gate", statePath)
|
|
492
|
+
advanceFullWorkItemToPlan(statePath)
|
|
493
|
+
|
|
494
|
+
assert.throws(
|
|
495
|
+
() => setApproval("tech_lead_to_fullstack", "approved", "user", "2026-03-21", "Approved", statePath),
|
|
496
|
+
/valid task board|tasks\.json/,
|
|
497
|
+
)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test("approving tech_lead_to_fullstack succeeds with a valid initial full_plan task board", () => {
|
|
501
|
+
const statePath = createTempStateFile()
|
|
502
|
+
|
|
503
|
+
startFeature("FEATURE-601", "board-gate-pass", statePath)
|
|
504
|
+
advanceFullWorkItemToPlan(statePath)
|
|
505
|
+
writeTaskBoard(statePath, "feature-601", {
|
|
506
|
+
mode: "full",
|
|
507
|
+
current_stage: "full_plan",
|
|
508
|
+
tasks: [createTask({ status: "queued" }), createTask({ task_id: "TASK-2", title: "Queued follow-up", status: "queued" })],
|
|
509
|
+
issues: [],
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const result = setApproval("tech_lead_to_fullstack", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
513
|
+
|
|
514
|
+
assert.equal(result.state.approvals.tech_lead_to_fullstack.status, "approved")
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
test("advancing full_plan to full_implementation without a valid task board fails", () => {
|
|
518
|
+
const statePath = createTempStateFile()
|
|
519
|
+
|
|
520
|
+
startFeature("FEATURE-602", "missing-board", statePath)
|
|
521
|
+
advanceFullWorkItemToPlan(statePath)
|
|
522
|
+
writeTaskBoard(statePath, "feature-602", {
|
|
523
|
+
mode: "full",
|
|
524
|
+
current_stage: "full_plan",
|
|
525
|
+
tasks: [createTask({ status: "ready" })],
|
|
526
|
+
issues: [],
|
|
527
|
+
})
|
|
528
|
+
setApproval("tech_lead_to_fullstack", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
529
|
+
writeTaskBoard(statePath, "feature-602", {
|
|
530
|
+
mode: "full",
|
|
531
|
+
current_stage: "full_plan",
|
|
532
|
+
tasks: [],
|
|
533
|
+
issues: [],
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
assert.throws(() => advanceStage("full_implementation", statePath), /task board must include at least one task/)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test("advancing full_implementation to full_qa fails when task board has incomplete implementation work", () => {
|
|
540
|
+
const statePath = createTempStateFile()
|
|
541
|
+
|
|
542
|
+
startFeature("FEATURE-603", "qa-handoff", statePath)
|
|
543
|
+
advanceFullWorkItemToPlan(statePath)
|
|
544
|
+
writeTaskBoard(statePath, "feature-603", {
|
|
545
|
+
mode: "full",
|
|
546
|
+
current_stage: "full_plan",
|
|
547
|
+
tasks: [createTask({ status: "in_progress", primary_owner: "DevA" })],
|
|
548
|
+
issues: [],
|
|
549
|
+
})
|
|
550
|
+
setApproval("tech_lead_to_fullstack", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
551
|
+
advanceStage("full_implementation", statePath)
|
|
552
|
+
setApproval("fullstack_to_qa", "approved", "system", "2026-03-21", "Ready for QA", statePath)
|
|
553
|
+
|
|
554
|
+
assert.throws(() => advanceStage("full_qa", statePath), /full_qa/)
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test("quick mode state with tasks.json present is rejected at controller validation time", () => {
|
|
558
|
+
const statePath = createTempStateFile()
|
|
559
|
+
|
|
560
|
+
startTask("quick", "TASK-700", "stale-board", "Quick task should reject task boards", statePath)
|
|
561
|
+
writeTaskBoard(statePath, "task-700", {
|
|
562
|
+
mode: "full",
|
|
563
|
+
current_stage: "full_plan",
|
|
564
|
+
tasks: [createTask()],
|
|
565
|
+
issues: [],
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
assert.throws(() => validateState(statePath), /Quick mode cannot carry a task board/)
|
|
569
|
+
assert.throws(() => showState(statePath), /Quick mode cannot carry a task board/)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
test("migration mode state with tasks.json present is rejected at controller validation time", () => {
|
|
573
|
+
const statePath = createTempStateFile()
|
|
574
|
+
|
|
575
|
+
startTask("migration", "MIGRATE-700", "stale-board", "Migration should reject task boards", statePath)
|
|
576
|
+
writeTaskBoard(statePath, "migrate-700", {
|
|
577
|
+
mode: "full",
|
|
578
|
+
current_stage: "full_plan",
|
|
579
|
+
tasks: [createTask()],
|
|
580
|
+
issues: [],
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
assert.throws(() => validateState(statePath), /Migration mode cannot carry a task board/)
|
|
584
|
+
assert.throws(() => showState(statePath), /Migration mode cannot carry a task board/)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test("valid full-delivery task board passes implementation and QA stage enforcement", () => {
|
|
588
|
+
const statePath = createTempStateFile()
|
|
589
|
+
|
|
590
|
+
startFeature("FEATURE-604", "valid-board", statePath)
|
|
591
|
+
advanceFullWorkItemToPlan(statePath)
|
|
592
|
+
writeTaskBoard(statePath, "feature-604", {
|
|
593
|
+
mode: "full",
|
|
594
|
+
current_stage: "full_plan",
|
|
595
|
+
tasks: [createTask({ status: "ready" })],
|
|
596
|
+
issues: [],
|
|
597
|
+
})
|
|
598
|
+
setApproval("tech_lead_to_fullstack", "approved", "user", "2026-03-21", "Approved", statePath)
|
|
599
|
+
|
|
600
|
+
let result = advanceStage("full_implementation", statePath)
|
|
601
|
+
assert.equal(result.state.current_stage, "full_implementation")
|
|
602
|
+
|
|
603
|
+
writeTaskBoard(statePath, "feature-604", {
|
|
604
|
+
mode: "full",
|
|
605
|
+
current_stage: "full_qa",
|
|
606
|
+
tasks: [createTask({ status: "qa_ready", primary_owner: "DevA", qa_owner: "QAAgent" })],
|
|
607
|
+
issues: [],
|
|
608
|
+
})
|
|
609
|
+
setApproval("fullstack_to_qa", "approved", "system", "2026-03-21", "Ready for QA", statePath)
|
|
610
|
+
|
|
611
|
+
result = advanceStage("full_qa", statePath)
|
|
612
|
+
assert.equal(result.state.current_stage, "full_qa")
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
test("claimTask rejects implicit reassignment and requires explicit reassignTask flow", () => {
|
|
616
|
+
const statePath = createTempStateFile()
|
|
617
|
+
|
|
618
|
+
startFeature("FEATURE-605", "assignment-safety", statePath)
|
|
619
|
+
advanceFullWorkItemToPlan(statePath)
|
|
620
|
+
createBoardTask(
|
|
621
|
+
"feature-605",
|
|
622
|
+
{
|
|
623
|
+
task_id: "TASK-605",
|
|
624
|
+
title: "Safe assignment",
|
|
625
|
+
summary: "Reject unauthorized reassignment",
|
|
626
|
+
kind: "implementation",
|
|
627
|
+
created_by: "TechLeadAgent",
|
|
628
|
+
},
|
|
629
|
+
statePath,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
claimTask("feature-605", "TASK-605", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
|
|
633
|
+
|
|
634
|
+
assert.throws(
|
|
635
|
+
() => claimTask("feature-605", "TASK-605", "Dev-B", statePath, { requestedBy: "TechLeadAgent" }),
|
|
636
|
+
/Implicit reassignment is not allowed; use reassignTask/,
|
|
637
|
+
)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test("reassignTask enforces authority and updates the claimed owner explicitly", () => {
|
|
641
|
+
const statePath = createTempStateFile()
|
|
642
|
+
|
|
643
|
+
startFeature("FEATURE-609", "explicit-reassign", statePath)
|
|
644
|
+
advanceFullWorkItemToPlan(statePath)
|
|
645
|
+
createBoardTask(
|
|
646
|
+
"feature-609",
|
|
647
|
+
{
|
|
648
|
+
task_id: "TASK-609",
|
|
649
|
+
title: "Explicit reassignment",
|
|
650
|
+
summary: "Use explicit reassignment flow",
|
|
651
|
+
kind: "implementation",
|
|
652
|
+
created_by: "TechLeadAgent",
|
|
653
|
+
},
|
|
654
|
+
statePath,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
claimTask("feature-609", "TASK-609", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
|
|
658
|
+
|
|
659
|
+
assert.throws(
|
|
660
|
+
() => reassignTask("feature-609", "TASK-609", "Dev-B", statePath, { requestedBy: "QAAgent" }),
|
|
661
|
+
/Only MasterOrchestrator or TechLeadAgent can reassign primary_owner/,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
const result = reassignTask("feature-609", "TASK-609", "Dev-B", statePath, { requestedBy: "TechLeadAgent" })
|
|
665
|
+
|
|
666
|
+
assert.equal(result.board.tasks[0].primary_owner, "Dev-B")
|
|
667
|
+
assert.equal(result.board.tasks[0].status, "claimed")
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test("releaseTask enforces authority and clears claimed ownership explicitly", () => {
|
|
671
|
+
const statePath = createTempStateFile()
|
|
672
|
+
|
|
673
|
+
startFeature("FEATURE-610", "explicit-release", statePath)
|
|
674
|
+
advanceFullWorkItemToPlan(statePath)
|
|
675
|
+
createBoardTask(
|
|
676
|
+
"feature-610",
|
|
677
|
+
{
|
|
678
|
+
task_id: "TASK-610",
|
|
679
|
+
title: "Explicit release",
|
|
680
|
+
summary: "Use explicit release flow",
|
|
681
|
+
kind: "implementation",
|
|
682
|
+
created_by: "TechLeadAgent",
|
|
683
|
+
},
|
|
684
|
+
statePath,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
claimTask("feature-610", "TASK-610", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
|
|
688
|
+
|
|
689
|
+
assert.throws(
|
|
690
|
+
() => releaseTask("feature-610", "TASK-610", statePath, { requestedBy: "QAAgent" }),
|
|
691
|
+
/Only MasterOrchestrator or TechLeadAgent can release primary_owner/,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
const result = releaseTask("feature-610", "TASK-610", statePath, { requestedBy: "TechLeadAgent" })
|
|
695
|
+
|
|
696
|
+
assert.equal(result.board.tasks[0].primary_owner, null)
|
|
697
|
+
assert.equal(result.board.tasks[0].status, "ready")
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
test("releaseTask allows the current task owner to release their claimed task", () => {
|
|
701
|
+
const statePath = createTempStateFile()
|
|
702
|
+
|
|
703
|
+
startFeature("FEATURE-612", "owner-release", statePath)
|
|
704
|
+
advanceFullWorkItemToPlan(statePath)
|
|
705
|
+
createBoardTask(
|
|
706
|
+
"feature-612",
|
|
707
|
+
{
|
|
708
|
+
task_id: "TASK-612",
|
|
709
|
+
title: "Owner release",
|
|
710
|
+
summary: "Current owner can explicitly release task",
|
|
711
|
+
kind: "implementation",
|
|
712
|
+
created_by: "TechLeadAgent",
|
|
713
|
+
},
|
|
714
|
+
statePath,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
claimTask("feature-612", "TASK-612", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
|
|
718
|
+
|
|
719
|
+
const result = releaseTask("feature-612", "TASK-612", statePath, { requestedBy: "Dev-A" })
|
|
720
|
+
|
|
721
|
+
assert.equal(result.board.tasks[0].primary_owner, null)
|
|
722
|
+
assert.equal(result.board.tasks[0].status, "ready")
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
test("assignQaOwner rejects reassignment from the wrong authority", () => {
|
|
726
|
+
const statePath = createTempStateFile()
|
|
727
|
+
|
|
728
|
+
startFeature("FEATURE-606", "qa-assignment-safety", statePath)
|
|
729
|
+
advanceFullWorkItemToPlan(statePath)
|
|
730
|
+
createBoardTask(
|
|
731
|
+
"feature-606",
|
|
732
|
+
{
|
|
733
|
+
task_id: "TASK-606",
|
|
734
|
+
title: "Safe QA assignment",
|
|
735
|
+
summary: "Reject unauthorized QA reassignment",
|
|
736
|
+
kind: "implementation",
|
|
737
|
+
created_by: "TechLeadAgent",
|
|
738
|
+
},
|
|
739
|
+
statePath,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
claimTask("feature-606", "TASK-606", "Dev-A", statePath, { requestedBy: "TechLeadAgent" })
|
|
743
|
+
setTaskStatus("feature-606", "TASK-606", "in_progress", statePath)
|
|
744
|
+
setTaskStatus("feature-606", "TASK-606", "dev_done", statePath)
|
|
745
|
+
assignQaOwner("feature-606", "TASK-606", "QA-Agent", statePath, { requestedBy: "TechLeadAgent" })
|
|
746
|
+
|
|
747
|
+
assert.throws(
|
|
748
|
+
() => assignQaOwner("feature-606", "TASK-606", "QA-Agent-2", statePath, { requestedBy: "QAAgent" }),
|
|
749
|
+
/Only MasterOrchestrator or TechLeadAgent can reassign qa_owner/,
|
|
750
|
+
)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
test("setTaskStatus rejects QA-fail local rework without a task-scoped finding and reroute decision", () => {
|
|
754
|
+
const statePath = createTempStateFile()
|
|
755
|
+
|
|
756
|
+
startFeature("FEATURE-607", "qa-fail-guardrails", statePath)
|
|
757
|
+
advanceFullWorkItemToPlan(statePath)
|
|
758
|
+
createBoardTask(
|
|
759
|
+
"feature-607",
|
|
760
|
+
{
|
|
761
|
+
task_id: "TASK-607",
|
|
762
|
+
title: "QA fail guardrails",
|
|
763
|
+
summary: "Require local rework metadata",
|
|
764
|
+
kind: "implementation",
|
|
765
|
+
status: "qa_in_progress",
|
|
766
|
+
primary_owner: "Dev-A",
|
|
767
|
+
qa_owner: "QA-Agent",
|
|
768
|
+
created_by: "TechLeadAgent",
|
|
769
|
+
},
|
|
770
|
+
statePath,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
assert.throws(
|
|
774
|
+
() => setTaskStatus("feature-607", "TASK-607", "claimed", statePath, { requestedBy: "QAAgent" }),
|
|
775
|
+
/task-scoped finding/,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
assert.throws(
|
|
779
|
+
() =>
|
|
780
|
+
setTaskStatus("feature-607", "TASK-607", "claimed", statePath, {
|
|
781
|
+
requestedBy: "QAAgent",
|
|
782
|
+
finding: {
|
|
783
|
+
issue_id: "ISSUE-607",
|
|
784
|
+
task_id: "TASK-607",
|
|
785
|
+
title: "Regression found",
|
|
786
|
+
summary: "Fix one task-local bug",
|
|
787
|
+
type: "bug",
|
|
788
|
+
severity: "medium",
|
|
789
|
+
rooted_in: "implementation",
|
|
790
|
+
recommended_owner: "FullstackAgent",
|
|
791
|
+
evidence: "Targeted QA reproduction",
|
|
792
|
+
artifact_refs: ["docs/qa/2026-03-21-feature-607.md"],
|
|
793
|
+
affects_tasks: ["TASK-607"],
|
|
794
|
+
blocks_parallel_work: false,
|
|
795
|
+
},
|
|
796
|
+
}),
|
|
797
|
+
/rerouteDecision/,
|
|
798
|
+
)
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
test("QA-fail local rework applies the reroute decision to work-item runtime state", () => {
|
|
802
|
+
const statePath = createTempStateFile()
|
|
803
|
+
|
|
804
|
+
startFeature("FEATURE-611", "qa-reroute-applied", statePath)
|
|
805
|
+
advanceFullWorkItemToPlan(statePath)
|
|
806
|
+
createBoardTask(
|
|
807
|
+
"feature-611",
|
|
808
|
+
{
|
|
809
|
+
task_id: "TASK-611",
|
|
810
|
+
title: "Apply reroute decision",
|
|
811
|
+
summary: "Persist QA local rework route",
|
|
812
|
+
kind: "implementation",
|
|
813
|
+
status: "qa_in_progress",
|
|
814
|
+
primary_owner: "Dev-A",
|
|
815
|
+
qa_owner: "QA-Agent",
|
|
816
|
+
created_by: "TechLeadAgent",
|
|
817
|
+
},
|
|
818
|
+
statePath,
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
const result = setTaskStatus("feature-611", "TASK-611", "claimed", statePath, {
|
|
822
|
+
requestedBy: "QAAgent",
|
|
823
|
+
finding: {
|
|
824
|
+
issue_id: "ISSUE-611",
|
|
825
|
+
task_id: "TASK-611",
|
|
826
|
+
title: "Regression found",
|
|
827
|
+
summary: "Fix one task-local bug",
|
|
828
|
+
type: "bug",
|
|
829
|
+
severity: "medium",
|
|
830
|
+
rooted_in: "implementation",
|
|
831
|
+
recommended_owner: "FullstackAgent",
|
|
832
|
+
evidence: "Targeted QA reproduction",
|
|
833
|
+
artifact_refs: ["docs/qa/2026-03-21-feature-611.md"],
|
|
834
|
+
affects_tasks: ["TASK-611"],
|
|
835
|
+
blocks_parallel_work: false,
|
|
836
|
+
},
|
|
837
|
+
rerouteDecision: {
|
|
838
|
+
stage: "full_implementation",
|
|
839
|
+
owner: "FullstackAgent",
|
|
840
|
+
decided_by: "TechLeadAgent",
|
|
841
|
+
reason: "Return only the failing task to implementation",
|
|
842
|
+
},
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
assert.equal(result.state.current_stage, "full_implementation")
|
|
846
|
+
assert.equal(result.state.current_owner, "FullstackAgent")
|
|
847
|
+
assert.equal(result.state.status, "in_progress")
|
|
848
|
+
assert.equal(result.board.tasks[0].status, "claimed")
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
test("QA-fail local rework rejects invalid reroute decisions atomically before any task write", () => {
|
|
852
|
+
const statePath = createTempStateFile()
|
|
853
|
+
|
|
854
|
+
startFeature("FEATURE-613", "atomic-qa-reroute", statePath)
|
|
855
|
+
advanceFullWorkItemToPlan(statePath)
|
|
856
|
+
createBoardTask(
|
|
857
|
+
"feature-613",
|
|
858
|
+
{
|
|
859
|
+
task_id: "TASK-613",
|
|
860
|
+
title: "Atomic QA reroute",
|
|
861
|
+
summary: "Invalid reroute must not partially mutate task",
|
|
862
|
+
kind: "implementation",
|
|
863
|
+
status: "qa_in_progress",
|
|
864
|
+
primary_owner: "Dev-A",
|
|
865
|
+
qa_owner: "QA-Agent",
|
|
866
|
+
created_by: "TechLeadAgent",
|
|
867
|
+
},
|
|
868
|
+
statePath,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
assert.throws(
|
|
872
|
+
() =>
|
|
873
|
+
setTaskStatus("feature-613", "TASK-613", "claimed", statePath, {
|
|
874
|
+
requestedBy: "QAAgent",
|
|
875
|
+
finding: {
|
|
876
|
+
issue_id: "ISSUE-613",
|
|
877
|
+
task_id: "TASK-613",
|
|
878
|
+
title: "Regression found",
|
|
879
|
+
summary: "Fix one task-local bug",
|
|
880
|
+
type: "bug",
|
|
881
|
+
severity: "medium",
|
|
882
|
+
rooted_in: "implementation",
|
|
883
|
+
recommended_owner: "FullstackAgent",
|
|
884
|
+
evidence: "Targeted QA reproduction",
|
|
885
|
+
artifact_refs: ["docs/qa/2026-03-21-feature-613.md"],
|
|
886
|
+
affects_tasks: ["TASK-613"],
|
|
887
|
+
blocks_parallel_work: false,
|
|
888
|
+
},
|
|
889
|
+
rerouteDecision: {
|
|
890
|
+
stage: "full_done",
|
|
891
|
+
owner: "MasterOrchestrator",
|
|
892
|
+
decided_by: "TechLeadAgent",
|
|
893
|
+
reason: "Invalid reroute target",
|
|
894
|
+
},
|
|
895
|
+
}),
|
|
896
|
+
/field 'stage'/,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
const workItem = validateWorkItemBoard("feature-613", statePath)
|
|
900
|
+
assert.equal(workItem.state.current_stage, "full_plan")
|
|
901
|
+
assert.equal(workItem.state.current_owner, "TechLeadAgent")
|
|
902
|
+
assert.equal(workItem.board.tasks[0].status, "qa_in_progress")
|
|
903
|
+
assert.equal(workItem.board.tasks[0].primary_owner, "Dev-A")
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
test("createTask rejects invalid parallel worktree metadata", () => {
|
|
907
|
+
const statePath = createTempStateFile()
|
|
908
|
+
|
|
909
|
+
startFeature("FEATURE-608", "invalid-worktree-metadata", statePath)
|
|
910
|
+
advanceFullWorkItemToPlan(statePath)
|
|
911
|
+
|
|
912
|
+
assert.throws(
|
|
913
|
+
() =>
|
|
914
|
+
createBoardTask(
|
|
915
|
+
"feature-608",
|
|
916
|
+
{
|
|
917
|
+
task_id: "TASK-608",
|
|
918
|
+
title: "Parallel worktree metadata",
|
|
919
|
+
summary: "Reject invalid protected branch metadata",
|
|
920
|
+
kind: "implementation",
|
|
921
|
+
created_by: "TechLeadAgent",
|
|
922
|
+
worktree_metadata: {
|
|
923
|
+
task_id: "TASK-608",
|
|
924
|
+
branch: "main",
|
|
925
|
+
worktree_path: ".worktrees/task-608-parallel",
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
statePath,
|
|
929
|
+
),
|
|
930
|
+
/must not target main/,
|
|
931
|
+
)
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test("createTask rejects task-board creation before full_plan", () => {
|
|
935
|
+
const statePath = createTempStateFile()
|
|
936
|
+
|
|
937
|
+
startFeature("FEATURE-614", "board-stage-gate", statePath)
|
|
938
|
+
|
|
939
|
+
assert.throws(
|
|
940
|
+
() =>
|
|
941
|
+
createBoardTask(
|
|
942
|
+
"feature-614",
|
|
943
|
+
{
|
|
944
|
+
task_id: "TASK-614",
|
|
945
|
+
title: "Too early task board",
|
|
946
|
+
summary: "Task boards should wait until planning",
|
|
947
|
+
kind: "implementation",
|
|
948
|
+
created_by: "TechLeadAgent",
|
|
949
|
+
},
|
|
950
|
+
statePath,
|
|
951
|
+
),
|
|
952
|
+
/full_plan/,
|
|
953
|
+
)
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
test("startTask rejects stale overwrites when an existing work item changes between guarded reads", () => {
|
|
957
|
+
const statePath = createTempStateFile()
|
|
958
|
+
startTask("quick", "TASK-800", "stale-write", "Guard controller mutation paths", statePath)
|
|
959
|
+
|
|
960
|
+
const originalStore = require("../lib/work-item-store")
|
|
961
|
+
let readCount = 0
|
|
962
|
+
const controller = loadControllerWithWorkItemStoreMocks({
|
|
963
|
+
readWorkItemState(projectRoot, workItemId) {
|
|
964
|
+
const state = originalStore.readWorkItemState(projectRoot, workItemId)
|
|
965
|
+
if (workItemId === "task-800") {
|
|
966
|
+
readCount += 1
|
|
967
|
+
if (readCount === 2) {
|
|
968
|
+
originalStore.writeWorkItemState(projectRoot, workItemId, {
|
|
969
|
+
...state,
|
|
970
|
+
status: "blocked",
|
|
971
|
+
updated_at: "2026-03-21T01:00:00.000Z",
|
|
972
|
+
})
|
|
973
|
+
return originalStore.readWorkItemState(projectRoot, workItemId)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return state
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
assert.throws(
|
|
981
|
+
() => controller.startTask("quick", "TASK-800", "stale-write", "Guard controller mutation paths", statePath),
|
|
982
|
+
(error) => error.code === "STALE_WRITE" && /expected revision/i.test(error.message),
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
const persistedState = showState(statePath).state
|
|
986
|
+
assert.equal(persistedState.status, "blocked")
|
|
987
|
+
assert.equal(persistedState.current_stage, "quick_intake")
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
test("controller rolls back active-item writes when mirror refresh fails after the primary state write", () => {
|
|
991
|
+
const statePath = createTempStateFile()
|
|
992
|
+
startTask("quick", "TASK-801", "mirror-stale", "Guard mirror refresh ordering", statePath)
|
|
993
|
+
|
|
994
|
+
const originalStore = require("../lib/work-item-store")
|
|
995
|
+
let observedPrimaryWriteBeforeMirrorFailure = false
|
|
996
|
+
const controller = loadControllerWithWorkItemStoreMocks({
|
|
997
|
+
writeCompatibilityMirror(projectRoot) {
|
|
998
|
+
const activeState = originalStore.readWorkItemState(projectRoot, "task-801")
|
|
999
|
+
observedPrimaryWriteBeforeMirrorFailure ||= activeState.current_stage === "quick_plan"
|
|
1000
|
+
throw new Error("Simulated mirror write failure")
|
|
1001
|
+
},
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
assert.throws(() => controller.advanceStage("quick_plan", statePath), /mirror/i)
|
|
1005
|
+
|
|
1006
|
+
assert.equal(observedPrimaryWriteBeforeMirrorFailure, true)
|
|
1007
|
+
|
|
1008
|
+
const persistedState = originalStore.readWorkItemState(path.dirname(path.dirname(statePath)), "task-801")
|
|
1009
|
+
assert.equal(persistedState.current_stage, "quick_intake")
|
|
1010
|
+
|
|
1011
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
1012
|
+
assert.equal(mirrorState.current_stage, "quick_intake")
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
test("controller restores primary and mirror state when index write fails late", () => {
|
|
1016
|
+
const statePath = createTempStateFile()
|
|
1017
|
+
startTask("quick", "TASK-802", "index-rollback", "Guard late index failures", statePath)
|
|
1018
|
+
|
|
1019
|
+
const projectRoot = path.dirname(path.dirname(statePath))
|
|
1020
|
+
const originalStore = require("../lib/work-item-store")
|
|
1021
|
+
let observedMirrorWriteBeforeIndexFailure = false
|
|
1022
|
+
const controller = loadControllerWithWorkItemStoreMocks({
|
|
1023
|
+
writeWorkItemIndex(root, index) {
|
|
1024
|
+
observedMirrorWriteBeforeIndexFailure = Boolean(index.active_work_item_id)
|
|
1025
|
+
throw new Error("Simulated index write failure")
|
|
1026
|
+
},
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
assert.throws(() => controller.advanceStage("quick_plan", statePath), /index write failure/)
|
|
1030
|
+
|
|
1031
|
+
assert.equal(observedMirrorWriteBeforeIndexFailure, true)
|
|
1032
|
+
|
|
1033
|
+
const persistedState = originalStore.readWorkItemState(projectRoot, "task-802")
|
|
1034
|
+
const mirrorState = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
1035
|
+
|
|
1036
|
+
assert.equal(persistedState.current_stage, "quick_intake")
|
|
1037
|
+
assert.equal(mirrorState.current_stage, "quick_intake")
|
|
1038
|
+
})
|