@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,1275 @@
|
|
|
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-workflow-cli-"))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setupTempRuntime(projectRoot) {
|
|
13
|
+
const opencodeDir = path.join(projectRoot, ".opencode")
|
|
14
|
+
const hooksDir = path.join(projectRoot, "hooks")
|
|
15
|
+
const skillsDir = path.join(projectRoot, "skills", "using-skills")
|
|
16
|
+
const contextCoreDir = path.join(projectRoot, "context", "core")
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(opencodeDir, { recursive: true })
|
|
19
|
+
fs.mkdirSync(hooksDir, { recursive: true })
|
|
20
|
+
fs.mkdirSync(skillsDir, { recursive: true })
|
|
21
|
+
fs.mkdirSync(contextCoreDir, { recursive: true })
|
|
22
|
+
|
|
23
|
+
const fixtureState = JSON.parse(
|
|
24
|
+
fs.readFileSync(path.resolve(__dirname, "../workflow-state.json"), "utf8"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(path.join(opencodeDir, "workflow-state.json"), `${JSON.stringify(fixtureState, null, 2)}\n`, "utf8")
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(opencodeDir, "opencode.json"),
|
|
30
|
+
`${JSON.stringify({
|
|
31
|
+
kit: {
|
|
32
|
+
name: "OpenKit AI Software Factory",
|
|
33
|
+
version: "0.1.0",
|
|
34
|
+
entryAgent: "MasterOrchestrator",
|
|
35
|
+
registry: {
|
|
36
|
+
path: "registry.json",
|
|
37
|
+
schema: "openkit/component-registry@1",
|
|
38
|
+
},
|
|
39
|
+
installManifest: {
|
|
40
|
+
path: ".opencode/install-manifest.json",
|
|
41
|
+
schema: "openkit/install-manifest@1",
|
|
42
|
+
},
|
|
43
|
+
activeProfile: "openkit-core",
|
|
44
|
+
},
|
|
45
|
+
}, null, 2)}\n`,
|
|
46
|
+
"utf8",
|
|
47
|
+
)
|
|
48
|
+
fs.writeFileSync(
|
|
49
|
+
path.join(projectRoot, "registry.json"),
|
|
50
|
+
`${JSON.stringify({
|
|
51
|
+
schema: "openkit/component-registry@1",
|
|
52
|
+
registryVersion: 1,
|
|
53
|
+
profiles: [
|
|
54
|
+
{
|
|
55
|
+
id: "profile.openkit-core",
|
|
56
|
+
name: "openkit-core",
|
|
57
|
+
description: "Core profile",
|
|
58
|
+
componentRefs: ["agents", "runtime", "docs"],
|
|
59
|
+
defaultForRepository: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "profile.runtime-docs-surface",
|
|
63
|
+
name: "runtime-docs-surface",
|
|
64
|
+
description: "Runtime and docs surface",
|
|
65
|
+
componentRefs: ["runtime", "hooks", "docs"],
|
|
66
|
+
defaultForRepository: false,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}, null, 2)}\n`,
|
|
70
|
+
"utf8",
|
|
71
|
+
)
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(opencodeDir, "install-manifest.json"),
|
|
74
|
+
`${JSON.stringify({
|
|
75
|
+
schema: "openkit/install-manifest@1",
|
|
76
|
+
manifestVersion: 1,
|
|
77
|
+
installation: {
|
|
78
|
+
activeProfile: "openkit-core",
|
|
79
|
+
},
|
|
80
|
+
}, null, 2)}\n`,
|
|
81
|
+
"utf8",
|
|
82
|
+
)
|
|
83
|
+
fs.writeFileSync(path.join(hooksDir, "hooks.json"), '{"hooks":{}}\n', "utf8")
|
|
84
|
+
fs.writeFileSync(path.join(hooksDir, "session-start"), "#!/usr/bin/env bash\n", "utf8")
|
|
85
|
+
fs.writeFileSync(path.join(skillsDir, "SKILL.md"), "# using-skills\n", "utf8")
|
|
86
|
+
fs.writeFileSync(path.join(opencodeDir, "workflow-state.js"), "#!/usr/bin/env node\n", "utf8")
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(contextCoreDir, "workflow.md"),
|
|
89
|
+
[
|
|
90
|
+
"# Workflow",
|
|
91
|
+
"",
|
|
92
|
+
"Quick Task+ is the live semantics of the quick lane, not a third lane.",
|
|
93
|
+
"Mode enums remain `quick`, `migration`, and `full`.",
|
|
94
|
+
"Commands remain `/task`, `/quick-task`, `/migrate`, `/delivery`, and `/write-plan`.",
|
|
95
|
+
"Migration is the dedicated upgrade and modernization lane.",
|
|
96
|
+
"Migration work must stay free of task boards.",
|
|
97
|
+
"Migration must preserve behavior first and decouple blockers before broad upgrade work.",
|
|
98
|
+
"Lane tie breaker: product uncertainty chooses full, compatibility uncertainty chooses migration, low local uncertainty chooses quick.",
|
|
99
|
+
"Lane Decision Matrix: use examples to choose the lane when wording alone is not enough.",
|
|
100
|
+
"Do not invent a quick task board; quick work stays task-board free.",
|
|
101
|
+
"Full Delivery owns the execution task board when one exists.",
|
|
102
|
+
"Quick stages: `quick_intake -> quick_plan -> quick_build -> quick_verify -> quick_done`.",
|
|
103
|
+
"Migration stages: `migration_intake -> migration_baseline -> migration_strategy -> migration_upgrade -> migration_verify -> migration_done`.",
|
|
104
|
+
"Full stages: `full_intake -> full_brief -> full_spec -> full_architecture -> full_plan -> full_implementation -> full_qa -> full_done`.",
|
|
105
|
+
"Quick approvals: `quick_verified`.",
|
|
106
|
+
"Migration approvals: `baseline_to_strategy`, `strategy_to_upgrade`, `upgrade_to_verify`, `migration_verified`.",
|
|
107
|
+
"Full approvals: `pm_to_ba`, `ba_to_architect`, `architect_to_tech_lead`, `tech_lead_to_fullstack`, `fullstack_to_qa`, `qa_to_done`.",
|
|
108
|
+
"Quick artifacts: `task_card`; migration artifacts may include `architecture`, `plan`, `migration_report`; full artifacts: `brief`, `spec`, `architecture`, `plan`, `qa_report`, `adr`.",
|
|
109
|
+
"",
|
|
110
|
+
].join("\n"),
|
|
111
|
+
"utf8",
|
|
112
|
+
)
|
|
113
|
+
fs.writeFileSync(
|
|
114
|
+
path.join(contextCoreDir, "workflow-state-schema.md"),
|
|
115
|
+
[
|
|
116
|
+
"# Workflow State Schema",
|
|
117
|
+
"",
|
|
118
|
+
"Modes: `quick`, `migration`, `full`.",
|
|
119
|
+
"Quick stages: `quick_intake`, `quick_plan`, `quick_build`, `quick_verify`, `quick_done`.",
|
|
120
|
+
"Migration stages: `migration_intake`, `migration_baseline`, `migration_strategy`, `migration_upgrade`, `migration_verify`, `migration_done`.",
|
|
121
|
+
"Full stages: `full_intake`, `full_brief`, `full_spec`, `full_architecture`, `full_plan`, `full_implementation`, `full_qa`, `full_done`.",
|
|
122
|
+
"Artifact keys: `task_card`, `brief`, `spec`, `architecture`, `plan`, `migration_report`, `qa_report`, `adr`.",
|
|
123
|
+
"Routing profile keys: `work_intent`, `behavior_delta`, `dominant_uncertainty`, `scope_shape`, `selection_reason`.",
|
|
124
|
+
"Quick approvals: `quick_verified`.",
|
|
125
|
+
"Migration approvals: `baseline_to_strategy`, `strategy_to_upgrade`, `upgrade_to_verify`, `migration_verified`.",
|
|
126
|
+
"Full approvals: `pm_to_ba`, `ba_to_architect`, `architect_to_tech_lead`, `tech_lead_to_fullstack`, `fullstack_to_qa`, `qa_to_done`.",
|
|
127
|
+
"Compatibility mirror behavior remains active for the current work item.",
|
|
128
|
+
"",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
"utf8",
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function runCli(projectRoot, args) {
|
|
135
|
+
return spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), ...args], {
|
|
136
|
+
cwd: projectRoot,
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function moveFullWorkItemToPlan(projectRoot, workItemId) {
|
|
142
|
+
let result = runCli(projectRoot, ["activate-work-item", workItemId])
|
|
143
|
+
assert.equal(result.status, 0)
|
|
144
|
+
|
|
145
|
+
const stageApprovals = new Map([
|
|
146
|
+
["full_brief", ["pm_to_ba", "approved", "user", "2026-03-21", "Approved"]],
|
|
147
|
+
["full_spec", ["ba_to_architect", "approved", "user", "2026-03-21", "Approved"]],
|
|
148
|
+
["full_architecture", ["architect_to_tech_lead", "approved", "user", "2026-03-21", "Approved"]],
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
for (const stage of ["full_brief", "full_spec", "full_architecture"]) {
|
|
152
|
+
result = runCli(projectRoot, ["advance-stage", stage])
|
|
153
|
+
assert.equal(result.status, 0)
|
|
154
|
+
|
|
155
|
+
result = runCli(projectRoot, ["set-approval", ...stageApprovals.get(stage)])
|
|
156
|
+
assert.equal(result.status, 0)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
result = runCli(projectRoot, ["advance-stage", "full_plan"])
|
|
160
|
+
assert.equal(result.status, 0)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeTaskBoard(projectRoot, workItemId, board) {
|
|
164
|
+
const boardPath = path.join(projectRoot, ".opencode", "work-items", workItemId, "tasks.json")
|
|
165
|
+
fs.mkdirSync(path.dirname(boardPath), { recursive: true })
|
|
166
|
+
fs.writeFileSync(boardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function makeFullTaskBoard(overrides = {}) {
|
|
170
|
+
return {
|
|
171
|
+
mode: "full",
|
|
172
|
+
current_stage: "full_implementation",
|
|
173
|
+
tasks: [
|
|
174
|
+
{
|
|
175
|
+
task_id: "TASK-BOARD-1",
|
|
176
|
+
title: "Implement diagnostics",
|
|
177
|
+
summary: "Add runtime task summaries",
|
|
178
|
+
kind: "implementation",
|
|
179
|
+
status: "in_progress",
|
|
180
|
+
primary_owner: "Dev-A",
|
|
181
|
+
qa_owner: null,
|
|
182
|
+
depends_on: [],
|
|
183
|
+
blocked_by: [],
|
|
184
|
+
artifact_refs: [],
|
|
185
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
186
|
+
branch_or_worktree: ".worktrees/parallel-agent-rollout/task-board-1",
|
|
187
|
+
created_by: "TechLeadAgent",
|
|
188
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
189
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
task_id: "TASK-BOARD-2",
|
|
193
|
+
title: "Verify diagnostics",
|
|
194
|
+
summary: "Exercise QA handoff",
|
|
195
|
+
kind: "qa",
|
|
196
|
+
status: "qa_in_progress",
|
|
197
|
+
primary_owner: "Dev-B",
|
|
198
|
+
qa_owner: "QA-Agent",
|
|
199
|
+
depends_on: [],
|
|
200
|
+
blocked_by: [],
|
|
201
|
+
artifact_refs: [],
|
|
202
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
203
|
+
branch_or_worktree: ".worktrees/parallel-agent-rollout/task-board-2",
|
|
204
|
+
created_by: "TechLeadAgent",
|
|
205
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
206
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
task_id: "TASK-BOARD-3",
|
|
210
|
+
title: "Document drift checks",
|
|
211
|
+
summary: "Leave one ready task for summary counts",
|
|
212
|
+
kind: "documentation",
|
|
213
|
+
status: "ready",
|
|
214
|
+
primary_owner: null,
|
|
215
|
+
qa_owner: null,
|
|
216
|
+
depends_on: [],
|
|
217
|
+
blocked_by: [],
|
|
218
|
+
artifact_refs: [],
|
|
219
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
220
|
+
branch_or_worktree: null,
|
|
221
|
+
created_by: "TechLeadAgent",
|
|
222
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
223
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
issues: [],
|
|
227
|
+
...overrides,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test("status command prints workflow and runtime summary", () => {
|
|
232
|
+
const projectRoot = makeTempProject()
|
|
233
|
+
setupTempRuntime(projectRoot)
|
|
234
|
+
|
|
235
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "status"], {
|
|
236
|
+
cwd: projectRoot,
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
assert.equal(result.status, 0)
|
|
241
|
+
assert.match(result.stdout, /OpenKit runtime status:/)
|
|
242
|
+
assert.match(result.stdout, /kit: OpenKit AI Software Factory v0\.1\.0/)
|
|
243
|
+
assert.match(result.stdout, /entry agent: MasterOrchestrator/)
|
|
244
|
+
assert.match(result.stdout, /active profile: openkit-core/)
|
|
245
|
+
assert.match(result.stdout, /registry: .*registry\.json/)
|
|
246
|
+
assert.match(result.stdout, /install manifest: .*\.opencode\/install-manifest\.json/)
|
|
247
|
+
assert.match(result.stdout, /mode: full/)
|
|
248
|
+
assert.match(result.stdout, /stage: full_done/)
|
|
249
|
+
assert.match(result.stdout, /status: done/)
|
|
250
|
+
assert.match(result.stdout, /work item: FEATURE-001 \(task-intake-dashboard\)/)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test("status command reflects quick_plan as a live quick stage", () => {
|
|
254
|
+
const projectRoot = makeTempProject()
|
|
255
|
+
setupTempRuntime(projectRoot)
|
|
256
|
+
|
|
257
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
258
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
259
|
+
state.feature_id = "TASK-600"
|
|
260
|
+
state.feature_slug = "quick-plan-status"
|
|
261
|
+
state.mode = "quick"
|
|
262
|
+
state.mode_reason = "Bounded quick work"
|
|
263
|
+
state.routing_profile = {
|
|
264
|
+
work_intent: "maintenance",
|
|
265
|
+
behavior_delta: "preserve",
|
|
266
|
+
dominant_uncertainty: "low_local",
|
|
267
|
+
scope_shape: "local",
|
|
268
|
+
selection_reason: "Bounded quick work",
|
|
269
|
+
}
|
|
270
|
+
state.current_stage = "quick_plan"
|
|
271
|
+
state.status = "in_progress"
|
|
272
|
+
state.current_owner = "MasterOrchestrator"
|
|
273
|
+
state.approvals = {
|
|
274
|
+
quick_verified: {
|
|
275
|
+
status: "pending",
|
|
276
|
+
approved_by: null,
|
|
277
|
+
approved_at: null,
|
|
278
|
+
notes: null,
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
state.artifacts.task_card = null
|
|
282
|
+
state.artifacts.brief = null
|
|
283
|
+
state.artifacts.spec = null
|
|
284
|
+
state.artifacts.architecture = null
|
|
285
|
+
state.artifacts.plan = null
|
|
286
|
+
state.artifacts.qa_report = null
|
|
287
|
+
state.artifacts.adr = []
|
|
288
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
289
|
+
|
|
290
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "status"], {
|
|
291
|
+
cwd: projectRoot,
|
|
292
|
+
encoding: "utf8",
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
assert.equal(result.status, 0)
|
|
296
|
+
assert.match(result.stdout, /mode: quick/)
|
|
297
|
+
assert.match(result.stdout, /stage: quick_plan/)
|
|
298
|
+
assert.match(result.stdout, /owner: MasterOrchestrator/)
|
|
299
|
+
assert.match(result.stdout, /work item: TASK-600 \(quick-plan-status\)/)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test("status command reflects migration_strategy as a live migration stage", () => {
|
|
303
|
+
const projectRoot = makeTempProject()
|
|
304
|
+
setupTempRuntime(projectRoot)
|
|
305
|
+
|
|
306
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
307
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
308
|
+
state.feature_id = "MIGRATE-600"
|
|
309
|
+
state.feature_slug = "react-19-migration"
|
|
310
|
+
state.mode = "migration"
|
|
311
|
+
state.mode_reason = "Framework upgrade"
|
|
312
|
+
state.routing_profile = {
|
|
313
|
+
work_intent: "modernization",
|
|
314
|
+
behavior_delta: "preserve",
|
|
315
|
+
dominant_uncertainty: "compatibility",
|
|
316
|
+
scope_shape: "adjacent",
|
|
317
|
+
selection_reason: "Framework upgrade",
|
|
318
|
+
}
|
|
319
|
+
state.current_stage = "migration_strategy"
|
|
320
|
+
state.status = "in_progress"
|
|
321
|
+
state.current_owner = "TechLeadAgent"
|
|
322
|
+
state.approvals = {
|
|
323
|
+
baseline_to_strategy: {
|
|
324
|
+
status: "approved",
|
|
325
|
+
approved_by: "TechLeadAgent",
|
|
326
|
+
approved_at: "2026-03-21",
|
|
327
|
+
notes: null,
|
|
328
|
+
},
|
|
329
|
+
strategy_to_upgrade: {
|
|
330
|
+
status: "pending",
|
|
331
|
+
approved_by: null,
|
|
332
|
+
approved_at: null,
|
|
333
|
+
notes: null,
|
|
334
|
+
},
|
|
335
|
+
upgrade_to_verify: {
|
|
336
|
+
status: "pending",
|
|
337
|
+
approved_by: null,
|
|
338
|
+
approved_at: null,
|
|
339
|
+
notes: null,
|
|
340
|
+
},
|
|
341
|
+
migration_verified: {
|
|
342
|
+
status: "pending",
|
|
343
|
+
approved_by: null,
|
|
344
|
+
approved_at: null,
|
|
345
|
+
notes: null,
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
state.artifacts.task_card = null
|
|
349
|
+
state.artifacts.brief = null
|
|
350
|
+
state.artifacts.spec = null
|
|
351
|
+
state.artifacts.architecture = "docs/architecture/2026-03-21-react-19-migration.md"
|
|
352
|
+
state.artifacts.plan = null
|
|
353
|
+
state.artifacts.qa_report = null
|
|
354
|
+
state.artifacts.adr = []
|
|
355
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
356
|
+
|
|
357
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "status"], {
|
|
358
|
+
cwd: projectRoot,
|
|
359
|
+
encoding: "utf8",
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
assert.equal(result.status, 0)
|
|
363
|
+
assert.match(result.stdout, /mode: migration/)
|
|
364
|
+
assert.match(result.stdout, /stage: migration_strategy/)
|
|
365
|
+
assert.match(result.stdout, /owner: TechLeadAgent/)
|
|
366
|
+
assert.match(result.stdout, /work item: MIGRATE-600 \(react-19-migration\)/)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test("status command fails when the active managed work item is invalid", () => {
|
|
370
|
+
const projectRoot = makeTempProject()
|
|
371
|
+
setupTempRuntime(projectRoot)
|
|
372
|
+
|
|
373
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
374
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
375
|
+
state.feature_id = "TASK-601"
|
|
376
|
+
state.feature_slug = "quick-invalid-board"
|
|
377
|
+
state.work_item_id = "task-601"
|
|
378
|
+
state.mode = "quick"
|
|
379
|
+
state.mode_reason = "Invalid quick item for status path"
|
|
380
|
+
state.routing_profile = {
|
|
381
|
+
work_intent: "maintenance",
|
|
382
|
+
behavior_delta: "preserve",
|
|
383
|
+
dominant_uncertainty: "low_local",
|
|
384
|
+
scope_shape: "local",
|
|
385
|
+
selection_reason: "Invalid quick item for status path",
|
|
386
|
+
}
|
|
387
|
+
state.current_stage = "quick_plan"
|
|
388
|
+
state.status = "in_progress"
|
|
389
|
+
state.current_owner = "MasterOrchestrator"
|
|
390
|
+
state.artifacts.task_card = null
|
|
391
|
+
state.artifacts.brief = null
|
|
392
|
+
state.artifacts.spec = null
|
|
393
|
+
state.artifacts.architecture = null
|
|
394
|
+
state.artifacts.plan = null
|
|
395
|
+
state.artifacts.migration_report = null
|
|
396
|
+
state.artifacts.qa_report = null
|
|
397
|
+
state.artifacts.adr = []
|
|
398
|
+
state.approvals = {
|
|
399
|
+
quick_verified: {
|
|
400
|
+
status: "pending",
|
|
401
|
+
approved_by: null,
|
|
402
|
+
approved_at: null,
|
|
403
|
+
notes: null,
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
407
|
+
writeTaskBoard(projectRoot, "task-601", {
|
|
408
|
+
mode: "full",
|
|
409
|
+
current_stage: "full_plan",
|
|
410
|
+
tasks: [
|
|
411
|
+
{
|
|
412
|
+
task_id: "TASK-1",
|
|
413
|
+
title: "Invalid quick board",
|
|
414
|
+
summary: "Should make status fail through managed validation",
|
|
415
|
+
kind: "implementation",
|
|
416
|
+
status: "ready",
|
|
417
|
+
primary_owner: null,
|
|
418
|
+
qa_owner: null,
|
|
419
|
+
depends_on: [],
|
|
420
|
+
blocked_by: [],
|
|
421
|
+
artifact_refs: [],
|
|
422
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
423
|
+
branch_or_worktree: null,
|
|
424
|
+
created_by: "TechLeadAgent",
|
|
425
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
426
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
issues: [],
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const result = runCli(projectRoot, ["status"])
|
|
433
|
+
|
|
434
|
+
assert.equal(result.status, 1)
|
|
435
|
+
assert.match(result.stderr, /Quick mode cannot carry a task board/)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
test("doctor command reports runtime diagnostics", () => {
|
|
439
|
+
const projectRoot = makeTempProject()
|
|
440
|
+
setupTempRuntime(projectRoot)
|
|
441
|
+
|
|
442
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "doctor"], {
|
|
443
|
+
cwd: projectRoot,
|
|
444
|
+
encoding: "utf8",
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
assert.equal(result.status, 0)
|
|
448
|
+
assert.match(result.stdout, /OpenKit doctor:/)
|
|
449
|
+
assert.match(result.stdout, /active profile: openkit-core/)
|
|
450
|
+
assert.match(result.stdout, /registry: .*registry\.json/)
|
|
451
|
+
assert.match(result.stdout, /install manifest: .*\.opencode\/install-manifest\.json/)
|
|
452
|
+
assert.match(result.stdout, /\[ok\] manifest file found/)
|
|
453
|
+
assert.match(result.stdout, /\[ok\] workflow state file found/)
|
|
454
|
+
assert.match(result.stdout, /\[ok\] workflow state is valid/)
|
|
455
|
+
assert.match(result.stdout, /\[ok\] registry file found/)
|
|
456
|
+
assert.match(result.stdout, /\[ok\] install manifest found/)
|
|
457
|
+
assert.match(result.stdout, /\[ok\] workflow state CLI found/)
|
|
458
|
+
assert.match(result.stdout, /\[ok\] hooks config found/)
|
|
459
|
+
assert.match(result.stdout, /\[ok\] session-start hook found/)
|
|
460
|
+
assert.match(result.stdout, /\[ok\] meta-skill found/)
|
|
461
|
+
assert.match(result.stdout, /\[ok\] active profile exists in registry/)
|
|
462
|
+
assert.match(result.stdout, /\[ok\] workflow contract doc found/)
|
|
463
|
+
assert.match(result.stdout, /\[ok\] workflow schema matches runtime stage sequences/)
|
|
464
|
+
assert.doesNotMatch(result.stdout, /\[error\]/)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
test("doctor command reports missing state as diagnostics", () => {
|
|
468
|
+
const projectRoot = makeTempProject()
|
|
469
|
+
setupTempRuntime(projectRoot)
|
|
470
|
+
fs.rmSync(path.join(projectRoot, ".opencode", "workflow-state.json"))
|
|
471
|
+
|
|
472
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "doctor"], {
|
|
473
|
+
cwd: projectRoot,
|
|
474
|
+
encoding: "utf8",
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
assert.equal(result.status, 1)
|
|
478
|
+
assert.match(result.stdout, /OpenKit doctor:/)
|
|
479
|
+
assert.match(result.stdout, /\[error\] workflow state file found/)
|
|
480
|
+
assert.match(result.stdout, /\[error\] workflow state is valid/)
|
|
481
|
+
assert.match(result.stdout, /Summary: .* [1-9][0-9]* error/)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test("doctor reports malformed registry as diagnostics", () => {
|
|
485
|
+
const projectRoot = makeTempProject()
|
|
486
|
+
setupTempRuntime(projectRoot)
|
|
487
|
+
fs.writeFileSync(path.join(projectRoot, "registry.json"), "{not-valid-json}\n", "utf8")
|
|
488
|
+
|
|
489
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "doctor"], {
|
|
490
|
+
cwd: projectRoot,
|
|
491
|
+
encoding: "utf8",
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
assert.equal(result.status, 1)
|
|
495
|
+
assert.match(result.stdout, /OpenKit doctor:/)
|
|
496
|
+
assert.match(result.stdout, /\[error\] registry metadata is readable/)
|
|
497
|
+
assert.match(result.stdout, /Summary: .* [1-9][0-9]* error/)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test("doctor reports when active profile is missing from registry", () => {
|
|
501
|
+
const projectRoot = makeTempProject()
|
|
502
|
+
setupTempRuntime(projectRoot)
|
|
503
|
+
|
|
504
|
+
const manifestPath = path.join(projectRoot, ".opencode", "install-manifest.json")
|
|
505
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
|
506
|
+
manifest.installation.activeProfile = "missing-profile"
|
|
507
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
|
508
|
+
|
|
509
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "doctor"], {
|
|
510
|
+
cwd: projectRoot,
|
|
511
|
+
encoding: "utf8",
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
assert.equal(result.status, 1)
|
|
515
|
+
assert.match(result.stdout, /\[error\] active profile exists in registry/)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test("doctor reports when manifest and install-manifest active profiles diverge", () => {
|
|
519
|
+
const projectRoot = makeTempProject()
|
|
520
|
+
setupTempRuntime(projectRoot)
|
|
521
|
+
|
|
522
|
+
const opencodePath = path.join(projectRoot, ".opencode", "opencode.json")
|
|
523
|
+
const opencodeManifest = JSON.parse(fs.readFileSync(opencodePath, "utf8"))
|
|
524
|
+
opencodeManifest.kit.activeProfile = "runtime-docs-surface"
|
|
525
|
+
fs.writeFileSync(opencodePath, `${JSON.stringify(opencodeManifest, null, 2)}\n`, "utf8")
|
|
526
|
+
|
|
527
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "doctor"], {
|
|
528
|
+
cwd: projectRoot,
|
|
529
|
+
encoding: "utf8",
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
assert.equal(result.status, 1)
|
|
533
|
+
assert.match(result.stdout, /\[error\] manifest and install manifest profiles agree/)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test("profiles command lists available registry profiles", () => {
|
|
537
|
+
const projectRoot = makeTempProject()
|
|
538
|
+
setupTempRuntime(projectRoot)
|
|
539
|
+
|
|
540
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "profiles"], {
|
|
541
|
+
cwd: projectRoot,
|
|
542
|
+
encoding: "utf8",
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
assert.equal(result.status, 0)
|
|
546
|
+
assert.match(result.stdout, /OpenKit profiles:/)
|
|
547
|
+
assert.match(result.stdout, /\* openkit-core - Core profile/)
|
|
548
|
+
assert.match(result.stdout, / runtime-docs-surface - Runtime and docs surface/)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
test("show-profile command prints profile details", () => {
|
|
552
|
+
const projectRoot = makeTempProject()
|
|
553
|
+
setupTempRuntime(projectRoot)
|
|
554
|
+
|
|
555
|
+
const result = spawnSync(
|
|
556
|
+
"node",
|
|
557
|
+
[path.resolve(__dirname, "../workflow-state.js"), "show-profile", "runtime-docs-surface"],
|
|
558
|
+
{
|
|
559
|
+
cwd: projectRoot,
|
|
560
|
+
encoding: "utf8",
|
|
561
|
+
},
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
assert.equal(result.status, 0)
|
|
565
|
+
assert.match(result.stdout, /Profile: runtime-docs-surface/)
|
|
566
|
+
assert.match(result.stdout, /default: no/)
|
|
567
|
+
assert.match(result.stdout, /components: runtime, hooks, docs/)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test("version command prints kit metadata version", () => {
|
|
571
|
+
const projectRoot = makeTempProject()
|
|
572
|
+
setupTempRuntime(projectRoot)
|
|
573
|
+
|
|
574
|
+
const result = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "version"], {
|
|
575
|
+
cwd: projectRoot,
|
|
576
|
+
encoding: "utf8",
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
assert.equal(result.status, 0)
|
|
580
|
+
assert.match(result.stdout, /OpenKit version: 0\.1\.0/)
|
|
581
|
+
assert.match(result.stdout, /active profile: openkit-core/)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
test("sync-install-manifest updates the active profile", () => {
|
|
585
|
+
const projectRoot = makeTempProject()
|
|
586
|
+
setupTempRuntime(projectRoot)
|
|
587
|
+
|
|
588
|
+
const result = spawnSync(
|
|
589
|
+
"node",
|
|
590
|
+
[path.resolve(__dirname, "../workflow-state.js"), "sync-install-manifest", "runtime-docs-surface"],
|
|
591
|
+
{
|
|
592
|
+
cwd: projectRoot,
|
|
593
|
+
encoding: "utf8",
|
|
594
|
+
},
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
assert.equal(result.status, 0)
|
|
598
|
+
assert.match(result.stdout, /Updated install manifest profile to 'runtime-docs-surface'/)
|
|
599
|
+
|
|
600
|
+
const manifest = JSON.parse(
|
|
601
|
+
fs.readFileSync(path.join(projectRoot, ".opencode", "install-manifest.json"), "utf8"),
|
|
602
|
+
)
|
|
603
|
+
assert.equal(manifest.installation.activeProfile, "runtime-docs-surface")
|
|
604
|
+
|
|
605
|
+
const statusResult = spawnSync("node", [path.resolve(__dirname, "../workflow-state.js"), "status"], {
|
|
606
|
+
cwd: projectRoot,
|
|
607
|
+
encoding: "utf8",
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
assert.equal(statusResult.status, 0)
|
|
611
|
+
assert.match(statusResult.stdout, /active profile: runtime-docs-surface/)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
test("help output includes multi-work-item and task-board commands", () => {
|
|
615
|
+
const projectRoot = makeTempProject()
|
|
616
|
+
setupTempRuntime(projectRoot)
|
|
617
|
+
|
|
618
|
+
const result = runCli(projectRoot, ["help"])
|
|
619
|
+
|
|
620
|
+
assert.equal(result.status, 0)
|
|
621
|
+
assert.match(result.stdout, /create-work-item/)
|
|
622
|
+
assert.match(result.stdout, /list-work-items/)
|
|
623
|
+
assert.match(result.stdout, /show-work-item <work_item_id>/)
|
|
624
|
+
assert.match(result.stdout, /activate-work-item <work_item_id>/)
|
|
625
|
+
assert.match(result.stdout, /list-tasks <work_item_id>/)
|
|
626
|
+
assert.match(result.stdout, /create-task <work_item_id> <task_id> <title> <kind>/)
|
|
627
|
+
assert.match(result.stdout, /claim-task <work_item_id> <task_id> <owner>/)
|
|
628
|
+
assert.match(result.stdout, /assign-qa-owner <work_item_id> <task_id> <qa_owner>/)
|
|
629
|
+
assert.match(result.stdout, /set-task-status <work_item_id> <task_id> <status>/)
|
|
630
|
+
assert.match(result.stdout, /validate-work-item-board <work_item_id>/)
|
|
631
|
+
assert.match(result.stdout, /status/)
|
|
632
|
+
assert.match(result.stdout, /doctor/)
|
|
633
|
+
assert.match(result.stdout, /show/)
|
|
634
|
+
assert.match(result.stdout, /start-feature <feature_id> <feature_slug>/)
|
|
635
|
+
assert.match(result.stdout, /start-task <mode> <feature_id> <feature_slug> <mode_reason>/)
|
|
636
|
+
assert.match(result.stdout, /create-work-item <mode> <feature_id> <feature_slug> <mode_reason>/)
|
|
637
|
+
assert.match(result.stdout, /set-routing-profile <work_intent> <behavior_delta> <dominant_uncertainty> <scope_shape> <selection_reason>/)
|
|
638
|
+
assert.doesNotMatch(result.stdout, /show-task <work_item_id> <task_id>/)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test("set-routing-profile updates explicit lane routing metadata", () => {
|
|
642
|
+
const projectRoot = makeTempProject()
|
|
643
|
+
setupTempRuntime(projectRoot)
|
|
644
|
+
|
|
645
|
+
let result = runCli(projectRoot, ["start-task", "migration", "MIGRATE-950", "routing-profile", "Compatibility upgrade"])
|
|
646
|
+
assert.equal(result.status, 0)
|
|
647
|
+
|
|
648
|
+
result = runCli(projectRoot, [
|
|
649
|
+
"set-routing-profile",
|
|
650
|
+
"modernization",
|
|
651
|
+
"preserve",
|
|
652
|
+
"compatibility",
|
|
653
|
+
"adjacent",
|
|
654
|
+
"Compatibility modernization with preserved behavior",
|
|
655
|
+
])
|
|
656
|
+
|
|
657
|
+
assert.equal(result.status, 0)
|
|
658
|
+
assert.match(result.stdout, /Updated routing profile for mode 'migration'/)
|
|
659
|
+
|
|
660
|
+
result = runCli(projectRoot, ["show"])
|
|
661
|
+
assert.equal(result.status, 0)
|
|
662
|
+
assert.match(result.stdout, /"routing_profile": \{/)
|
|
663
|
+
assert.match(result.stdout, /"dominant_uncertainty": "compatibility"/)
|
|
664
|
+
assert.match(result.stdout, /"selection_reason": "Compatibility modernization with preserved behavior"/)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
test("status command shows task-aware runtime summary for active full-delivery work", () => {
|
|
668
|
+
const projectRoot = makeTempProject()
|
|
669
|
+
setupTempRuntime(projectRoot)
|
|
670
|
+
|
|
671
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
672
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
673
|
+
state.current_stage = "full_implementation"
|
|
674
|
+
state.status = "in_progress"
|
|
675
|
+
state.current_owner = "FullstackAgent"
|
|
676
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
677
|
+
|
|
678
|
+
writeTaskBoard(projectRoot, "feature-001", makeFullTaskBoard())
|
|
679
|
+
|
|
680
|
+
const result = runCli(projectRoot, ["status"])
|
|
681
|
+
|
|
682
|
+
assert.equal(result.status, 0)
|
|
683
|
+
assert.match(result.stdout, /active work item id: feature-001/)
|
|
684
|
+
assert.match(result.stdout, /work items tracked: 1/)
|
|
685
|
+
assert.match(result.stdout, /task board: 3 tasks \| ready 1 \| active 2/)
|
|
686
|
+
assert.match(result.stdout, /active tasks: TASK-BOARD-1 \(in_progress, primary: Dev-A\); TASK-BOARD-2 \(qa_in_progress, qa: QA-Agent\)/)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
test("show command includes task-aware context before state JSON for active full-delivery work", () => {
|
|
690
|
+
const projectRoot = makeTempProject()
|
|
691
|
+
setupTempRuntime(projectRoot)
|
|
692
|
+
|
|
693
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
694
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
695
|
+
state.current_stage = "full_implementation"
|
|
696
|
+
state.status = "in_progress"
|
|
697
|
+
state.current_owner = "FullstackAgent"
|
|
698
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
699
|
+
|
|
700
|
+
writeTaskBoard(projectRoot, "feature-001", makeFullTaskBoard())
|
|
701
|
+
|
|
702
|
+
const result = runCli(projectRoot, ["show"])
|
|
703
|
+
|
|
704
|
+
assert.equal(result.status, 0)
|
|
705
|
+
assert.match(result.stdout, /Workflow state:/)
|
|
706
|
+
assert.match(result.stdout, /active work item id: feature-001/)
|
|
707
|
+
assert.match(result.stdout, /task board: 3 tasks \| ready 1 \| active 2/)
|
|
708
|
+
assert.match(result.stdout, /"current_stage": "full_implementation"/)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
test("doctor command reports task-aware runtime diagnostics and mirror safety for active full-delivery work", () => {
|
|
712
|
+
const projectRoot = makeTempProject()
|
|
713
|
+
setupTempRuntime(projectRoot)
|
|
714
|
+
|
|
715
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
716
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
717
|
+
state.current_stage = "full_implementation"
|
|
718
|
+
state.status = "in_progress"
|
|
719
|
+
state.current_owner = "FullstackAgent"
|
|
720
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
721
|
+
|
|
722
|
+
writeTaskBoard(projectRoot, "feature-001", makeFullTaskBoard())
|
|
723
|
+
|
|
724
|
+
const result = runCli(projectRoot, ["doctor"])
|
|
725
|
+
|
|
726
|
+
assert.equal(result.status, 0)
|
|
727
|
+
assert.match(result.stdout, /active work item id: feature-001/)
|
|
728
|
+
assert.match(result.stdout, /work items tracked: 1/)
|
|
729
|
+
assert.match(result.stdout, /task board: 3 tasks \| ready 1 \| active 2/)
|
|
730
|
+
assert.match(result.stdout, /\[ok\] active work item pointer resolves to stored state/)
|
|
731
|
+
assert.match(result.stdout, /\[ok\] compatibility mirror matches active work item state/)
|
|
732
|
+
assert.match(result.stdout, /\[ok\] active work item task board is valid/)
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
test("doctor reports missing task board for active full work item as an error", () => {
|
|
736
|
+
const projectRoot = makeTempProject()
|
|
737
|
+
setupTempRuntime(projectRoot)
|
|
738
|
+
|
|
739
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
740
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
741
|
+
state.current_stage = "full_implementation"
|
|
742
|
+
state.status = "in_progress"
|
|
743
|
+
state.current_owner = "FullstackAgent"
|
|
744
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
745
|
+
|
|
746
|
+
const result = runCli(projectRoot, ["doctor"])
|
|
747
|
+
|
|
748
|
+
assert.equal(result.status, 1)
|
|
749
|
+
assert.match(result.stdout, /\[error\] active work item task board is valid/)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test("doctor reports invalid active full task board as an error even when runtime state is invalid", () => {
|
|
753
|
+
const projectRoot = makeTempProject()
|
|
754
|
+
setupTempRuntime(projectRoot)
|
|
755
|
+
|
|
756
|
+
const statePath = path.join(projectRoot, ".opencode", "workflow-state.json")
|
|
757
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"))
|
|
758
|
+
state.current_stage = "full_implementation"
|
|
759
|
+
state.status = "in_progress"
|
|
760
|
+
state.current_owner = "FullstackAgent"
|
|
761
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
762
|
+
|
|
763
|
+
writeTaskBoard(projectRoot, "feature-001", {
|
|
764
|
+
mode: "full",
|
|
765
|
+
current_stage: "full_implementation",
|
|
766
|
+
tasks: [
|
|
767
|
+
{
|
|
768
|
+
task_id: "TASK-BOARD-1",
|
|
769
|
+
title: "Broken diagnostics",
|
|
770
|
+
summary: "Missing primary owner makes the board invalid",
|
|
771
|
+
kind: "implementation",
|
|
772
|
+
status: "in_progress",
|
|
773
|
+
primary_owner: null,
|
|
774
|
+
qa_owner: null,
|
|
775
|
+
depends_on: [],
|
|
776
|
+
blocked_by: [],
|
|
777
|
+
artifact_refs: [],
|
|
778
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
779
|
+
branch_or_worktree: ".worktrees/parallel-agent-rollout/task-board-1",
|
|
780
|
+
created_by: "TechLeadAgent",
|
|
781
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
782
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
issues: [],
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
const result = runCli(projectRoot, ["doctor"])
|
|
789
|
+
|
|
790
|
+
assert.equal(result.status, 1)
|
|
791
|
+
assert.match(result.stdout, /\[error\] workflow state is valid/)
|
|
792
|
+
assert.match(result.stdout, /\[error\] active work item task board is valid/)
|
|
793
|
+
assert.doesNotMatch(result.stdout, /\[ok\] active work item task board is valid/)
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test("doctor reports compatibility mirror divergence as an error", () => {
|
|
797
|
+
const projectRoot = makeTempProject()
|
|
798
|
+
setupTempRuntime(projectRoot)
|
|
799
|
+
|
|
800
|
+
const indexPath = path.join(projectRoot, ".opencode", "work-items", "index.json")
|
|
801
|
+
const workItemStatePath = path.join(projectRoot, ".opencode", "work-items", "feature-001", "state.json")
|
|
802
|
+
fs.mkdirSync(path.dirname(workItemStatePath), { recursive: true })
|
|
803
|
+
fs.writeFileSync(
|
|
804
|
+
indexPath,
|
|
805
|
+
`${JSON.stringify({
|
|
806
|
+
active_work_item_id: "feature-001",
|
|
807
|
+
work_items: [
|
|
808
|
+
{
|
|
809
|
+
work_item_id: "feature-001",
|
|
810
|
+
feature_id: "FEATURE-001",
|
|
811
|
+
feature_slug: "task-intake-dashboard",
|
|
812
|
+
mode: "full",
|
|
813
|
+
status: "done",
|
|
814
|
+
state_path: ".opencode/work-items/feature-001/state.json",
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
}, null, 2)}\n`,
|
|
818
|
+
"utf8",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
const mirrorState = JSON.parse(fs.readFileSync(path.join(projectRoot, ".opencode", "workflow-state.json"), "utf8"))
|
|
822
|
+
const workItemState = { ...mirrorState, current_stage: "full_plan", current_owner: "TechLeadAgent", status: "in_progress", work_item_id: "feature-001" }
|
|
823
|
+
fs.writeFileSync(workItemStatePath, `${JSON.stringify(workItemState, null, 2)}\n`, "utf8")
|
|
824
|
+
|
|
825
|
+
const result = runCli(projectRoot, ["doctor"])
|
|
826
|
+
|
|
827
|
+
assert.equal(result.status, 1)
|
|
828
|
+
assert.match(result.stdout, /\[error\] compatibility mirror matches active work item state/)
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
test("legacy start-feature command creates a full work item and repo-root inspection commands read it", () => {
|
|
832
|
+
const projectRoot = makeTempProject()
|
|
833
|
+
setupTempRuntime(projectRoot)
|
|
834
|
+
|
|
835
|
+
let result = runCli(projectRoot, ["start-feature", "FEATURE-920", "legacy-feature"])
|
|
836
|
+
assert.equal(result.status, 0)
|
|
837
|
+
assert.match(result.stdout, /Started feature FEATURE-920 \(legacy-feature\)/)
|
|
838
|
+
|
|
839
|
+
result = runCli(projectRoot, ["status"])
|
|
840
|
+
assert.equal(result.status, 0)
|
|
841
|
+
assert.match(result.stdout, /mode: full/)
|
|
842
|
+
assert.match(result.stdout, /stage: full_intake/)
|
|
843
|
+
assert.match(result.stdout, /work item: FEATURE-920 \(legacy-feature\)/)
|
|
844
|
+
|
|
845
|
+
result = runCli(projectRoot, ["show"])
|
|
846
|
+
assert.equal(result.status, 0)
|
|
847
|
+
assert.match(result.stdout, /"feature_id": "FEATURE-920"/)
|
|
848
|
+
assert.match(result.stdout, /"work_item_id": "feature-920"/)
|
|
849
|
+
|
|
850
|
+
result = runCli(projectRoot, ["doctor"])
|
|
851
|
+
assert.equal(result.status, 0)
|
|
852
|
+
assert.match(result.stdout, /\[ok\] workflow state is valid/)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
test("legacy start-task quick command creates a quick work item and repo-root inspection commands read it", () => {
|
|
856
|
+
const projectRoot = makeTempProject()
|
|
857
|
+
setupTempRuntime(projectRoot)
|
|
858
|
+
|
|
859
|
+
let result = runCli(projectRoot, ["start-task", "quick", "TASK-920", "legacy-quick", "Legacy quick runtime"])
|
|
860
|
+
assert.equal(result.status, 0)
|
|
861
|
+
assert.match(result.stdout, /Started quick task TASK-920 \(legacy-quick\)/)
|
|
862
|
+
|
|
863
|
+
result = runCli(projectRoot, ["status"])
|
|
864
|
+
assert.equal(result.status, 0)
|
|
865
|
+
assert.match(result.stdout, /mode: quick/)
|
|
866
|
+
assert.match(result.stdout, /stage: quick_intake/)
|
|
867
|
+
assert.match(result.stdout, /work item: TASK-920 \(legacy-quick\)/)
|
|
868
|
+
|
|
869
|
+
result = runCli(projectRoot, ["show"])
|
|
870
|
+
assert.equal(result.status, 0)
|
|
871
|
+
assert.match(result.stdout, /"feature_id": "TASK-920"/)
|
|
872
|
+
assert.match(result.stdout, /"mode": "quick"/)
|
|
873
|
+
assert.match(result.stdout, /"work_item_id": "task-920"/)
|
|
874
|
+
|
|
875
|
+
result = runCli(projectRoot, ["doctor"])
|
|
876
|
+
assert.equal(result.status, 0)
|
|
877
|
+
assert.match(result.stdout, /\[ok\] workflow state is valid/)
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
test("legacy start-task full command creates a full work item and repo-root inspection commands read it", () => {
|
|
881
|
+
const projectRoot = makeTempProject()
|
|
882
|
+
setupTempRuntime(projectRoot)
|
|
883
|
+
|
|
884
|
+
let result = runCli(projectRoot, ["start-task", "full", "FEATURE-921", "legacy-full", "Legacy full runtime"])
|
|
885
|
+
assert.equal(result.status, 0)
|
|
886
|
+
assert.match(result.stdout, /Started full task FEATURE-921 \(legacy-full\)/)
|
|
887
|
+
|
|
888
|
+
result = runCli(projectRoot, ["status"])
|
|
889
|
+
assert.equal(result.status, 0)
|
|
890
|
+
assert.match(result.stdout, /mode: full/)
|
|
891
|
+
assert.match(result.stdout, /stage: full_intake/)
|
|
892
|
+
assert.match(result.stdout, /work item: FEATURE-921 \(legacy-full\)/)
|
|
893
|
+
|
|
894
|
+
result = runCli(projectRoot, ["show"])
|
|
895
|
+
assert.equal(result.status, 0)
|
|
896
|
+
assert.match(result.stdout, /"feature_id": "FEATURE-921"/)
|
|
897
|
+
assert.match(result.stdout, /"mode": "full"/)
|
|
898
|
+
assert.match(result.stdout, /"work_item_id": "feature-921"/)
|
|
899
|
+
|
|
900
|
+
result = runCli(projectRoot, ["doctor"])
|
|
901
|
+
assert.equal(result.status, 0)
|
|
902
|
+
assert.match(result.stdout, /\[ok\] workflow state is valid/)
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
test("legacy start-task migration command creates a migration work item and repo-root inspection commands read it", () => {
|
|
906
|
+
const projectRoot = makeTempProject()
|
|
907
|
+
setupTempRuntime(projectRoot)
|
|
908
|
+
|
|
909
|
+
let result = runCli(projectRoot, [
|
|
910
|
+
"start-task",
|
|
911
|
+
"migration",
|
|
912
|
+
"MIGRATE-921",
|
|
913
|
+
"legacy-migration",
|
|
914
|
+
"Legacy migration runtime",
|
|
915
|
+
])
|
|
916
|
+
assert.equal(result.status, 0)
|
|
917
|
+
assert.match(result.stdout, /Started migration task MIGRATE-921 \(legacy-migration\)/)
|
|
918
|
+
|
|
919
|
+
result = runCli(projectRoot, ["status"])
|
|
920
|
+
assert.equal(result.status, 0)
|
|
921
|
+
assert.match(result.stdout, /mode: migration/)
|
|
922
|
+
assert.match(result.stdout, /stage: migration_intake/)
|
|
923
|
+
assert.match(result.stdout, /work item: MIGRATE-921 \(legacy-migration\)/)
|
|
924
|
+
|
|
925
|
+
result = runCli(projectRoot, ["show"])
|
|
926
|
+
assert.equal(result.status, 0)
|
|
927
|
+
assert.match(result.stdout, /"feature_id": "MIGRATE-921"/)
|
|
928
|
+
assert.match(result.stdout, /"mode": "migration"/)
|
|
929
|
+
assert.match(result.stdout, /"work_item_id": "migrate-921"/)
|
|
930
|
+
|
|
931
|
+
result = runCli(projectRoot, ["doctor"])
|
|
932
|
+
assert.equal(result.status, 0)
|
|
933
|
+
assert.match(result.stdout, /\[ok\] workflow state is valid/)
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
test("CLI work-item and task-board commands manage a full-delivery board", () => {
|
|
937
|
+
const projectRoot = makeTempProject()
|
|
938
|
+
setupTempRuntime(projectRoot)
|
|
939
|
+
|
|
940
|
+
let result = runCli(projectRoot, [
|
|
941
|
+
"create-work-item",
|
|
942
|
+
"full",
|
|
943
|
+
"FEATURE-900",
|
|
944
|
+
"parallel-rollout",
|
|
945
|
+
"Parallel rollout board setup",
|
|
946
|
+
])
|
|
947
|
+
assert.equal(result.status, 0)
|
|
948
|
+
assert.match(result.stdout, /Created full work item FEATURE-900 \(parallel-rollout\)/)
|
|
949
|
+
|
|
950
|
+
moveFullWorkItemToPlan(projectRoot, "feature-900")
|
|
951
|
+
|
|
952
|
+
result = runCli(projectRoot, ["list-work-items"])
|
|
953
|
+
assert.equal(result.status, 0)
|
|
954
|
+
assert.match(result.stdout, /Active work item: feature-900/)
|
|
955
|
+
assert.match(result.stdout, /\* feature-900 \| FEATURE-900 \| full \| in_progress/)
|
|
956
|
+
|
|
957
|
+
result = runCli(projectRoot, ["show-work-item", "feature-900"])
|
|
958
|
+
assert.equal(result.status, 0)
|
|
959
|
+
assert.match(result.stdout, /Work item: feature-900/)
|
|
960
|
+
assert.match(result.stdout, /feature: FEATURE-900 \(parallel-rollout\)/)
|
|
961
|
+
|
|
962
|
+
result = runCli(projectRoot, [
|
|
963
|
+
"create-task",
|
|
964
|
+
"feature-900",
|
|
965
|
+
"TASK-900",
|
|
966
|
+
"Wire CLI",
|
|
967
|
+
"implementation",
|
|
968
|
+
])
|
|
969
|
+
assert.equal(result.status, 0)
|
|
970
|
+
assert.match(result.stdout, /Created task 'TASK-900' on work item 'feature-900'/)
|
|
971
|
+
|
|
972
|
+
result = runCli(projectRoot, ["list-tasks", "feature-900"])
|
|
973
|
+
assert.equal(result.status, 0)
|
|
974
|
+
assert.match(result.stdout, /Tasks for feature-900:/)
|
|
975
|
+
assert.match(result.stdout, /TASK-900 \| ready \| implementation \| Wire CLI/)
|
|
976
|
+
|
|
977
|
+
result = runCli(projectRoot, ["claim-task", "feature-900", "TASK-900", "FullstackAgent"])
|
|
978
|
+
assert.equal(result.status, 1)
|
|
979
|
+
assert.match(result.stderr, /requires <requested_by>/)
|
|
980
|
+
|
|
981
|
+
result = runCli(projectRoot, ["claim-task", "feature-900", "TASK-900", "FullstackAgent", "TechLeadAgent"])
|
|
982
|
+
assert.equal(result.status, 0)
|
|
983
|
+
assert.match(result.stdout, /Claimed task 'TASK-900' for 'FullstackAgent'/)
|
|
984
|
+
|
|
985
|
+
result = runCli(projectRoot, ["claim-task", "feature-900", "TASK-900", "AnotherDev", "TechLeadAgent"])
|
|
986
|
+
assert.equal(result.status, 1)
|
|
987
|
+
assert.match(result.stderr, /Implicit reassignment is not allowed; use reassignTask/)
|
|
988
|
+
|
|
989
|
+
result = runCli(projectRoot, ["reassign-task", "feature-900", "TASK-900", "AnotherDev"])
|
|
990
|
+
assert.equal(result.status, 1)
|
|
991
|
+
assert.match(result.stderr, /requires <requested_by>/)
|
|
992
|
+
|
|
993
|
+
result = runCli(projectRoot, ["reassign-task", "feature-900", "TASK-900", "AnotherDev", "TechLeadAgent"])
|
|
994
|
+
assert.equal(result.status, 0)
|
|
995
|
+
assert.match(result.stdout, /Reassigned task 'TASK-900' to 'AnotherDev'/)
|
|
996
|
+
|
|
997
|
+
result = runCli(projectRoot, ["release-task", "feature-900", "TASK-900"])
|
|
998
|
+
assert.equal(result.status, 1)
|
|
999
|
+
assert.match(result.stderr, /requires <requested_by>/)
|
|
1000
|
+
|
|
1001
|
+
result = runCli(projectRoot, ["release-task", "feature-900", "TASK-900", "TechLeadAgent"])
|
|
1002
|
+
assert.equal(result.status, 0)
|
|
1003
|
+
assert.match(result.stdout, /Released task 'TASK-900'/)
|
|
1004
|
+
|
|
1005
|
+
result = runCli(projectRoot, ["claim-task", "feature-900", "TASK-900", "FullstackAgent", "TechLeadAgent"])
|
|
1006
|
+
assert.equal(result.status, 0)
|
|
1007
|
+
|
|
1008
|
+
result = runCli(projectRoot, ["set-task-status", "feature-900", "TASK-900", "in_progress"])
|
|
1009
|
+
assert.equal(result.status, 0)
|
|
1010
|
+
assert.match(result.stdout, /Updated task 'TASK-900' to 'in_progress'/)
|
|
1011
|
+
|
|
1012
|
+
result = runCli(projectRoot, ["set-task-status", "feature-900", "TASK-900", "dev_done"])
|
|
1013
|
+
assert.equal(result.status, 0)
|
|
1014
|
+
assert.match(result.stdout, /Updated task 'TASK-900' to 'dev_done'/)
|
|
1015
|
+
|
|
1016
|
+
result = runCli(projectRoot, ["assign-qa-owner", "feature-900", "TASK-900", "QAAgent"])
|
|
1017
|
+
assert.equal(result.status, 1)
|
|
1018
|
+
assert.match(result.stderr, /requires <requested_by>/)
|
|
1019
|
+
|
|
1020
|
+
result = runCli(projectRoot, ["assign-qa-owner", "feature-900", "TASK-900", "QAAgent", "TechLeadAgent"])
|
|
1021
|
+
assert.equal(result.status, 0)
|
|
1022
|
+
assert.match(result.stdout, /Assigned QA owner 'QAAgent' to task 'TASK-900'/)
|
|
1023
|
+
|
|
1024
|
+
result = runCli(projectRoot, ["set-task-status", "feature-900", "TASK-900", "qa_ready"])
|
|
1025
|
+
assert.equal(result.status, 0)
|
|
1026
|
+
assert.match(result.stdout, /Updated task 'TASK-900' to 'qa_ready'/)
|
|
1027
|
+
|
|
1028
|
+
result = runCli(projectRoot, ["validate-work-item-board", "feature-900"])
|
|
1029
|
+
assert.equal(result.status, 0)
|
|
1030
|
+
assert.match(result.stdout, /Task board is valid for work item 'feature-900'/)
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
test("CLI rejects quick items carrying task data through managed validation", () => {
|
|
1034
|
+
const projectRoot = makeTempProject()
|
|
1035
|
+
setupTempRuntime(projectRoot)
|
|
1036
|
+
|
|
1037
|
+
let result = runCli(projectRoot, ["start-task", "quick", "TASK-930", "quick-stale-board", "Quick item"])
|
|
1038
|
+
assert.equal(result.status, 0)
|
|
1039
|
+
|
|
1040
|
+
writeTaskBoard(projectRoot, "task-930", {
|
|
1041
|
+
mode: "full",
|
|
1042
|
+
current_stage: "full_plan",
|
|
1043
|
+
tasks: [
|
|
1044
|
+
{
|
|
1045
|
+
task_id: "TASK-930-A",
|
|
1046
|
+
title: "Invalid quick board",
|
|
1047
|
+
summary: "Should fail validation",
|
|
1048
|
+
kind: "implementation",
|
|
1049
|
+
status: "ready",
|
|
1050
|
+
primary_owner: null,
|
|
1051
|
+
qa_owner: null,
|
|
1052
|
+
depends_on: [],
|
|
1053
|
+
blocked_by: [],
|
|
1054
|
+
artifact_refs: [],
|
|
1055
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
1056
|
+
branch_or_worktree: null,
|
|
1057
|
+
created_by: "TechLeadAgent",
|
|
1058
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
1059
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
issues: [],
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
result = runCli(projectRoot, ["show"])
|
|
1066
|
+
assert.equal(result.status, 1)
|
|
1067
|
+
assert.match(result.stderr, /Quick mode cannot carry a task board/)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
test("CLI rejects migration items carrying task data through managed validation", () => {
|
|
1071
|
+
const projectRoot = makeTempProject()
|
|
1072
|
+
setupTempRuntime(projectRoot)
|
|
1073
|
+
|
|
1074
|
+
let result = runCli(projectRoot, [
|
|
1075
|
+
"start-task",
|
|
1076
|
+
"migration",
|
|
1077
|
+
"MIGRATE-930",
|
|
1078
|
+
"migration-stale-board",
|
|
1079
|
+
"Migration item",
|
|
1080
|
+
])
|
|
1081
|
+
assert.equal(result.status, 0)
|
|
1082
|
+
|
|
1083
|
+
writeTaskBoard(projectRoot, "migrate-930", {
|
|
1084
|
+
mode: "full",
|
|
1085
|
+
current_stage: "full_plan",
|
|
1086
|
+
tasks: [
|
|
1087
|
+
{
|
|
1088
|
+
task_id: "TASK-930-A",
|
|
1089
|
+
title: "Invalid migration board",
|
|
1090
|
+
summary: "Should fail validation",
|
|
1091
|
+
kind: "implementation",
|
|
1092
|
+
status: "ready",
|
|
1093
|
+
primary_owner: null,
|
|
1094
|
+
qa_owner: null,
|
|
1095
|
+
depends_on: [],
|
|
1096
|
+
blocked_by: [],
|
|
1097
|
+
artifact_refs: [],
|
|
1098
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
1099
|
+
branch_or_worktree: null,
|
|
1100
|
+
created_by: "TechLeadAgent",
|
|
1101
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
1102
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
issues: [],
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
result = runCli(projectRoot, ["show"])
|
|
1109
|
+
assert.equal(result.status, 1)
|
|
1110
|
+
assert.match(result.stderr, /Migration mode cannot carry a task board/)
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
test("CLI rejects claim-task reassignment from the wrong authority", () => {
|
|
1114
|
+
const projectRoot = makeTempProject()
|
|
1115
|
+
setupTempRuntime(projectRoot)
|
|
1116
|
+
|
|
1117
|
+
let result = runCli(projectRoot, ["create-work-item", "full", "FEATURE-931", "cli-reassign", "Assignment safety"])
|
|
1118
|
+
assert.equal(result.status, 0)
|
|
1119
|
+
|
|
1120
|
+
moveFullWorkItemToPlan(projectRoot, "feature-931")
|
|
1121
|
+
|
|
1122
|
+
result = runCli(projectRoot, ["create-task", "feature-931", "TASK-931", "Implement safety", "implementation"])
|
|
1123
|
+
assert.equal(result.status, 0)
|
|
1124
|
+
|
|
1125
|
+
result = runCli(projectRoot, ["claim-task", "feature-931", "TASK-931", "Dev-A", "TechLeadAgent"])
|
|
1126
|
+
assert.equal(result.status, 0)
|
|
1127
|
+
|
|
1128
|
+
result = runCli(projectRoot, ["claim-task", "feature-931", "TASK-931", "Dev-B", "QAAgent"])
|
|
1129
|
+
assert.equal(result.status, 1)
|
|
1130
|
+
assert.match(result.stderr, /Implicit reassignment is not allowed; use reassignTask/)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
test("CLI reassign-task enforces authority explicitly", () => {
|
|
1134
|
+
const projectRoot = makeTempProject()
|
|
1135
|
+
setupTempRuntime(projectRoot)
|
|
1136
|
+
|
|
1137
|
+
let result = runCli(projectRoot, ["create-work-item", "full", "FEATURE-934", "cli-reassign-explicit", "Assignment safety"])
|
|
1138
|
+
assert.equal(result.status, 0)
|
|
1139
|
+
|
|
1140
|
+
moveFullWorkItemToPlan(projectRoot, "feature-934")
|
|
1141
|
+
|
|
1142
|
+
result = runCli(projectRoot, ["create-task", "feature-934", "TASK-934", "Implement safety", "implementation"])
|
|
1143
|
+
assert.equal(result.status, 0)
|
|
1144
|
+
|
|
1145
|
+
result = runCli(projectRoot, ["claim-task", "feature-934", "TASK-934", "Dev-A", "TechLeadAgent"])
|
|
1146
|
+
assert.equal(result.status, 0)
|
|
1147
|
+
|
|
1148
|
+
result = runCli(projectRoot, ["reassign-task", "feature-934", "TASK-934", "Dev-B", "QAAgent"])
|
|
1149
|
+
assert.equal(result.status, 1)
|
|
1150
|
+
assert.match(result.stderr, /Only MasterOrchestrator or TechLeadAgent can reassign primary_owner/)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
test("CLI rejects QA-fail local rework without task-scoped finding metadata", () => {
|
|
1154
|
+
const projectRoot = makeTempProject()
|
|
1155
|
+
setupTempRuntime(projectRoot)
|
|
1156
|
+
|
|
1157
|
+
let result = runCli(projectRoot, ["create-work-item", "full", "FEATURE-932", "cli-qa-fail", "QA fail safety"])
|
|
1158
|
+
assert.equal(result.status, 0)
|
|
1159
|
+
|
|
1160
|
+
moveFullWorkItemToPlan(projectRoot, "feature-932")
|
|
1161
|
+
|
|
1162
|
+
writeTaskBoard(projectRoot, "feature-932", {
|
|
1163
|
+
mode: "full",
|
|
1164
|
+
current_stage: "full_qa",
|
|
1165
|
+
tasks: [
|
|
1166
|
+
{
|
|
1167
|
+
task_id: "TASK-932",
|
|
1168
|
+
title: "QA local rework",
|
|
1169
|
+
summary: "Require local rework metadata",
|
|
1170
|
+
kind: "implementation",
|
|
1171
|
+
status: "qa_in_progress",
|
|
1172
|
+
primary_owner: "Dev-A",
|
|
1173
|
+
qa_owner: "QA-Agent",
|
|
1174
|
+
depends_on: [],
|
|
1175
|
+
blocked_by: [],
|
|
1176
|
+
artifact_refs: [],
|
|
1177
|
+
plan_refs: ["docs/plans/2026-03-21-feature.md"],
|
|
1178
|
+
branch_or_worktree: null,
|
|
1179
|
+
created_by: "TechLeadAgent",
|
|
1180
|
+
created_at: "2026-03-21T00:00:00.000Z",
|
|
1181
|
+
updated_at: "2026-03-21T00:00:00.000Z",
|
|
1182
|
+
},
|
|
1183
|
+
],
|
|
1184
|
+
issues: [],
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
result = runCli(projectRoot, ["set-task-status", "feature-932", "TASK-932", "claimed"])
|
|
1188
|
+
assert.equal(result.status, 1)
|
|
1189
|
+
assert.match(result.stderr, /task-scoped finding/)
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
test("CLI release-task allows the current owner to release explicitly", () => {
|
|
1193
|
+
const projectRoot = makeTempProject()
|
|
1194
|
+
setupTempRuntime(projectRoot)
|
|
1195
|
+
|
|
1196
|
+
let result = runCli(projectRoot, ["create-work-item", "full", "FEATURE-935", "cli-owner-release", "Owner release"])
|
|
1197
|
+
assert.equal(result.status, 0)
|
|
1198
|
+
|
|
1199
|
+
moveFullWorkItemToPlan(projectRoot, "feature-935")
|
|
1200
|
+
|
|
1201
|
+
result = runCli(projectRoot, ["create-task", "feature-935", "TASK-935", "Owner release", "implementation"])
|
|
1202
|
+
assert.equal(result.status, 0)
|
|
1203
|
+
|
|
1204
|
+
result = runCli(projectRoot, ["claim-task", "feature-935", "TASK-935", "Dev-A", "TechLeadAgent"])
|
|
1205
|
+
assert.equal(result.status, 0)
|
|
1206
|
+
|
|
1207
|
+
result = runCli(projectRoot, ["release-task", "feature-935", "TASK-935", "Dev-A"])
|
|
1208
|
+
assert.equal(result.status, 0)
|
|
1209
|
+
assert.match(result.stdout, /Released task 'TASK-935'/)
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
test("help output includes explicit release and reassign task commands", () => {
|
|
1213
|
+
const projectRoot = makeTempProject()
|
|
1214
|
+
setupTempRuntime(projectRoot)
|
|
1215
|
+
|
|
1216
|
+
const result = runCli(projectRoot, ["help"])
|
|
1217
|
+
|
|
1218
|
+
assert.equal(result.status, 0)
|
|
1219
|
+
assert.match(result.stdout, /release-task <work_item_id> <task_id> <requested_by>/)
|
|
1220
|
+
assert.match(result.stdout, /reassign-task <work_item_id> <task_id> <owner> <requested_by>/)
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
test("CLI rejects invalid worktree metadata when creating a task", () => {
|
|
1224
|
+
const projectRoot = makeTempProject()
|
|
1225
|
+
setupTempRuntime(projectRoot)
|
|
1226
|
+
|
|
1227
|
+
let result = runCli(projectRoot, ["create-work-item", "full", "FEATURE-933", "cli-worktree", "Worktree safety"])
|
|
1228
|
+
assert.equal(result.status, 0)
|
|
1229
|
+
|
|
1230
|
+
moveFullWorkItemToPlan(projectRoot, "feature-933")
|
|
1231
|
+
|
|
1232
|
+
result = runCli(projectRoot, [
|
|
1233
|
+
"create-task",
|
|
1234
|
+
"feature-933",
|
|
1235
|
+
"TASK-933",
|
|
1236
|
+
"Task metadata",
|
|
1237
|
+
"implementation",
|
|
1238
|
+
"main",
|
|
1239
|
+
".worktrees/task-933-parallel",
|
|
1240
|
+
])
|
|
1241
|
+
assert.equal(result.status, 1)
|
|
1242
|
+
assert.match(result.stderr, /must not target main/)
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
test("activate-work-item switches the active selection", () => {
|
|
1246
|
+
const projectRoot = makeTempProject()
|
|
1247
|
+
setupTempRuntime(projectRoot)
|
|
1248
|
+
|
|
1249
|
+
let result = runCli(projectRoot, [
|
|
1250
|
+
"create-work-item",
|
|
1251
|
+
"full",
|
|
1252
|
+
"FEATURE-910",
|
|
1253
|
+
"alpha-item",
|
|
1254
|
+
"First full work item",
|
|
1255
|
+
])
|
|
1256
|
+
assert.equal(result.status, 0)
|
|
1257
|
+
|
|
1258
|
+
result = runCli(projectRoot, [
|
|
1259
|
+
"create-work-item",
|
|
1260
|
+
"quick",
|
|
1261
|
+
"TASK-910",
|
|
1262
|
+
"beta-item",
|
|
1263
|
+
"Second quick work item",
|
|
1264
|
+
])
|
|
1265
|
+
assert.equal(result.status, 0)
|
|
1266
|
+
|
|
1267
|
+
result = runCli(projectRoot, ["activate-work-item", "feature-910"])
|
|
1268
|
+
assert.equal(result.status, 0)
|
|
1269
|
+
assert.match(result.stdout, /Activated work item 'feature-910'/)
|
|
1270
|
+
|
|
1271
|
+
result = runCli(projectRoot, ["list-work-items"])
|
|
1272
|
+
assert.equal(result.status, 0)
|
|
1273
|
+
assert.match(result.stdout, /Active work item: feature-910/)
|
|
1274
|
+
assert.match(result.stdout, /\* feature-910 \| FEATURE-910 \| full \| in_progress/)
|
|
1275
|
+
})
|