@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,1739 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
resolveKitRoot,
|
|
6
|
+
resolveProjectRoot,
|
|
7
|
+
resolveRuntimeRoot,
|
|
8
|
+
resolveStatePath,
|
|
9
|
+
} = require("./runtime-paths")
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
bootstrapRuntimeStore,
|
|
13
|
+
bootstrapLegacyWorkflowState,
|
|
14
|
+
deriveWorkItemId,
|
|
15
|
+
readWorkItemIndex,
|
|
16
|
+
readWorkItemState,
|
|
17
|
+
resolveWorkItemPaths,
|
|
18
|
+
setActiveWorkItem,
|
|
19
|
+
writeCompatibilityMirror,
|
|
20
|
+
writeWorkItemIndex,
|
|
21
|
+
writeWorkItemState,
|
|
22
|
+
} = require("./work-item-store")
|
|
23
|
+
const {
|
|
24
|
+
ARTIFACT_KINDS,
|
|
25
|
+
ESCALATION_RETRY_THRESHOLD,
|
|
26
|
+
ISSUE_SEVERITIES,
|
|
27
|
+
ISSUE_TYPES,
|
|
28
|
+
MODE_VALUES,
|
|
29
|
+
RECOMMENDED_OWNERS,
|
|
30
|
+
ROOTED_IN_VALUES,
|
|
31
|
+
ROUTING_BEHAVIOR_DELTA_VALUES,
|
|
32
|
+
ROUTING_DOMINANT_UNCERTAINTY_VALUES,
|
|
33
|
+
ROUTING_SCOPE_SHAPE_VALUES,
|
|
34
|
+
ROUTING_WORK_INTENT_VALUES,
|
|
35
|
+
STAGE_OWNERS,
|
|
36
|
+
STAGE_SEQUENCE,
|
|
37
|
+
STATUS_VALUES,
|
|
38
|
+
createDefaultRoutingProfile,
|
|
39
|
+
createEmptyApprovals,
|
|
40
|
+
createEmptyArtifacts,
|
|
41
|
+
getApprovalGatesForMode,
|
|
42
|
+
getInitialStageForMode,
|
|
43
|
+
getModeForStage,
|
|
44
|
+
getNextStage,
|
|
45
|
+
getReworkRoute,
|
|
46
|
+
getTransitionGate,
|
|
47
|
+
} = require("./workflow-state-rules")
|
|
48
|
+
const { getContractConsistencyReport: buildContractConsistencyReport } = require("./contract-consistency")
|
|
49
|
+
const { SUPPORTED_SCAFFOLDS, scaffoldArtifact } = require("./artifact-scaffolder")
|
|
50
|
+
const { captureRevision, guardWrite, planGuardedMirrorRefresh } = require("./state-guard")
|
|
51
|
+
const {
|
|
52
|
+
VALID_ASSIGNMENT_AUTHORITIES,
|
|
53
|
+
decideQaFailLocalRework,
|
|
54
|
+
validateParallelAssignments,
|
|
55
|
+
validateReassignmentAuthority,
|
|
56
|
+
validateWorktreeMetadata,
|
|
57
|
+
} = require("./parallel-execution-rules")
|
|
58
|
+
const { validateTaskBoard, validateTaskShape, validateTaskStatus, validateTaskTransition } = require("./task-board-rules")
|
|
59
|
+
|
|
60
|
+
function fail(message) {
|
|
61
|
+
const error = new Error(message)
|
|
62
|
+
error.isWorkflowStateError = true
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatModeLabel(mode) {
|
|
67
|
+
return typeof mode === "string" && mode.length > 0 ? `${mode.charAt(0).toUpperCase()}${mode.slice(1)}` : "Unknown"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function timestamp() {
|
|
71
|
+
return new Date().toISOString()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readState(customStatePath) {
|
|
75
|
+
const statePath = resolveStatePath(customStatePath)
|
|
76
|
+
|
|
77
|
+
let raw
|
|
78
|
+
try {
|
|
79
|
+
raw = fs.readFileSync(statePath, "utf8")
|
|
80
|
+
} catch (error) {
|
|
81
|
+
fail(`Unable to read workflow state file at '${statePath}': ${error.message}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return {
|
|
86
|
+
statePath,
|
|
87
|
+
state: JSON.parse(raw),
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
fail(`Malformed workflow state JSON at '${statePath}': ${error.message}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeState(statePath, state) {
|
|
95
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createEmptyWorkItemIndex() {
|
|
99
|
+
return {
|
|
100
|
+
active_work_item_id: null,
|
|
101
|
+
work_items: [],
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ensureWorkItemStoreReady(customStatePath) {
|
|
106
|
+
const runtimeRoot = resolveRuntimeRoot(customStatePath)
|
|
107
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
108
|
+
|
|
109
|
+
if (runtimeRoot !== projectRoot) {
|
|
110
|
+
const runtimePaths = resolveWorkItemPaths(runtimeRoot, "__bootstrap__")
|
|
111
|
+
const projectPaths = resolveWorkItemPaths(projectRoot, "__bootstrap__")
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(runtimePaths.indexPath) && fs.existsSync(projectPaths.workflowStatePath)) {
|
|
114
|
+
fs.mkdirSync(path.dirname(runtimePaths.workflowStatePath), { recursive: true })
|
|
115
|
+
fs.copyFileSync(projectPaths.workflowStatePath, runtimePaths.workflowStatePath)
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(projectPaths.workItemsDir) && !fs.existsSync(runtimePaths.workItemsDir)) {
|
|
118
|
+
fs.cpSync(projectPaths.workItemsDir, runtimePaths.workItemsDir, { recursive: true })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
bootstrapRuntimeStore(runtimeRoot)
|
|
124
|
+
|
|
125
|
+
return runtimeRoot
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function upsertWorkItemIndexEntry(index, state, workItemId, relativeStatePath) {
|
|
129
|
+
const nextEntry = {
|
|
130
|
+
work_item_id: workItemId,
|
|
131
|
+
feature_id: state.feature_id,
|
|
132
|
+
feature_slug: state.feature_slug,
|
|
133
|
+
mode: state.mode,
|
|
134
|
+
status: state.status,
|
|
135
|
+
state_path: relativeStatePath,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const existingIndex = index.work_items.findIndex((entry) => entry.work_item_id === workItemId)
|
|
139
|
+
if (existingIndex === -1) {
|
|
140
|
+
index.work_items.push(nextEntry)
|
|
141
|
+
} else {
|
|
142
|
+
index.work_items[existingIndex] = nextEntry
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return nextEntry
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readManagedState(customStatePath, workItemId = null) {
|
|
149
|
+
const statePath = resolveStatePath(customStatePath)
|
|
150
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
151
|
+
const index = readWorkItemIndex(projectRoot)
|
|
152
|
+
const resolvedWorkItemId = workItemId ?? index.active_work_item_id
|
|
153
|
+
|
|
154
|
+
if (!resolvedWorkItemId) {
|
|
155
|
+
fail("Active work item pointer missing")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
statePath,
|
|
160
|
+
projectRoot,
|
|
161
|
+
index,
|
|
162
|
+
workItemId: resolvedWorkItemId,
|
|
163
|
+
state: readWorkItemState(projectRoot, resolvedWorkItemId),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function persistManagedState(customStatePath, state, options = {}) {
|
|
168
|
+
const statePath = resolveStatePath(customStatePath)
|
|
169
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
170
|
+
const workItemId = options.workItemId ?? deriveWorkItemId(state)
|
|
171
|
+
const workItemPaths = resolveWorkItemPaths(projectRoot, workItemId)
|
|
172
|
+
const index = fs.existsSync(workItemPaths.indexPath)
|
|
173
|
+
? readWorkItemIndex(projectRoot)
|
|
174
|
+
: createEmptyWorkItemIndex()
|
|
175
|
+
const hasPersistedState = fs.existsSync(workItemPaths.statePath)
|
|
176
|
+
const currentPersistedState = hasPersistedState ? readWorkItemState(projectRoot, workItemId) : null
|
|
177
|
+
const previousIndexSnapshot = JSON.parse(JSON.stringify(index))
|
|
178
|
+
const hadMirrorState = fs.existsSync(statePath)
|
|
179
|
+
const previousMirrorState = hadMirrorState ? readState(statePath).state : null
|
|
180
|
+
|
|
181
|
+
if (currentPersistedState) {
|
|
182
|
+
guardWrite({
|
|
183
|
+
currentState: currentPersistedState,
|
|
184
|
+
expectedRevision: options.expectedRevision,
|
|
185
|
+
nextState: state,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const persistedState = writeWorkItemState(projectRoot, workItemId, state)
|
|
190
|
+
try {
|
|
191
|
+
validateManagedState(persistedState, projectRoot, workItemId)
|
|
192
|
+
|
|
193
|
+
const nextActiveWorkItemId = options.activateWorkItemId ?? workItemId
|
|
194
|
+
const mirrorRefreshPlan = planGuardedMirrorRefresh({
|
|
195
|
+
activeWorkItemId: nextActiveWorkItemId,
|
|
196
|
+
targetWorkItemId: workItemId,
|
|
197
|
+
nextState: persistedState,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (mirrorRefreshPlan.shouldRefreshMirror) {
|
|
201
|
+
const preflightMirrorState = fs.existsSync(statePath) ? readState(statePath).state : null
|
|
202
|
+
|
|
203
|
+
if (preflightMirrorState && options.expectedMirrorRevision) {
|
|
204
|
+
guardWrite({
|
|
205
|
+
currentState: preflightMirrorState,
|
|
206
|
+
expectedRevision: options.expectedMirrorRevision,
|
|
207
|
+
nextState: persistedState,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
writeCompatibilityMirror(projectRoot, persistedState)
|
|
212
|
+
|
|
213
|
+
const mirrorState = readState(statePath).state
|
|
214
|
+
const mirrorRevision = captureRevision(mirrorState)
|
|
215
|
+
|
|
216
|
+
if (mirrorRevision !== mirrorRefreshPlan.mirrorRevision) {
|
|
217
|
+
fail(
|
|
218
|
+
`Compatibility mirror refresh revision conflict for active work item '${workItemId}'; expected ${mirrorRefreshPlan.mirrorRevision} but found ${mirrorRevision}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
upsertWorkItemIndexEntry(index, persistedState, workItemId, workItemPaths.relativeStatePath)
|
|
224
|
+
index.active_work_item_id = nextActiveWorkItemId
|
|
225
|
+
writeWorkItemIndex(projectRoot, index)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (currentPersistedState) {
|
|
228
|
+
writeWorkItemState(projectRoot, workItemId, currentPersistedState)
|
|
229
|
+
} else if (fs.existsSync(workItemPaths.statePath)) {
|
|
230
|
+
fs.rmSync(workItemPaths.statePath)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (previousMirrorState) {
|
|
234
|
+
writeCompatibilityMirror(projectRoot, previousMirrorState)
|
|
235
|
+
} else if (!hadMirrorState && fs.existsSync(statePath)) {
|
|
236
|
+
fs.rmSync(statePath)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
writeWorkItemIndex(projectRoot, previousIndexSnapshot)
|
|
241
|
+
} catch (_rollbackError) {
|
|
242
|
+
// Preserve the original failure after best-effort rollback.
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
statePath,
|
|
249
|
+
projectRoot,
|
|
250
|
+
index,
|
|
251
|
+
state: persistedState,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getTaskBoardPath(projectRoot, workItemId) {
|
|
256
|
+
return path.join(resolveWorkItemPaths(projectRoot, workItemId).workItemDir, "tasks.json")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readTaskBoardIfExists(projectRoot, workItemId) {
|
|
260
|
+
const taskBoardPath = getTaskBoardPath(projectRoot, workItemId)
|
|
261
|
+
if (!fs.existsSync(taskBoardPath)) {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(fs.readFileSync(taskBoardPath, "utf8"))
|
|
267
|
+
} catch (error) {
|
|
268
|
+
fail(`Malformed JSON at '${taskBoardPath}': ${error.message}`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function writeTaskBoard(projectRoot, workItemId, board) {
|
|
273
|
+
const taskBoardPath = getTaskBoardPath(projectRoot, workItemId)
|
|
274
|
+
fs.mkdirSync(path.dirname(taskBoardPath), { recursive: true })
|
|
275
|
+
fs.writeFileSync(taskBoardPath, `${JSON.stringify(board, null, 2)}\n`, "utf8")
|
|
276
|
+
return board
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildBoardView(state, board) {
|
|
280
|
+
return {
|
|
281
|
+
mode: state.mode,
|
|
282
|
+
current_stage: state.current_stage,
|
|
283
|
+
tasks: board?.tasks ?? [],
|
|
284
|
+
issues: board?.issues ?? [],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readWorkItemContext(workItemId, customStatePath) {
|
|
289
|
+
ensureString(workItemId, "work_item_id")
|
|
290
|
+
const statePath = resolveStatePath(customStatePath)
|
|
291
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
292
|
+
const state = readWorkItemState(projectRoot, workItemId)
|
|
293
|
+
validateStateObject(state)
|
|
294
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
statePath,
|
|
298
|
+
projectRoot,
|
|
299
|
+
workItemId,
|
|
300
|
+
state,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function requireFullModeWorkItem(state, workItemId) {
|
|
305
|
+
if (state.mode !== "full") {
|
|
306
|
+
fail(`Work item '${workItemId}' must be in full mode to use a task board`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isTaskBoardStageAllowed(stage) {
|
|
311
|
+
return ["full_plan", "full_implementation", "full_qa", "full_done"].includes(stage)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function requireTaskBoardStage(state, workItemId, action = "use a task board") {
|
|
315
|
+
if (!isTaskBoardStageAllowed(state.current_stage)) {
|
|
316
|
+
fail(
|
|
317
|
+
`Work item '${workItemId}' must reach 'full_plan' before it can ${action}; current stage is '${state.current_stage}'`,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildBoardForValidation(state, board) {
|
|
323
|
+
validateParallelAssignments(board.tasks ?? [])
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
...board,
|
|
327
|
+
mode: state.mode,
|
|
328
|
+
current_stage: state.current_stage,
|
|
329
|
+
issues: board.issues ?? [],
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function withTaskBoard(workItemId, customStatePath, mutator) {
|
|
334
|
+
const context = readWorkItemContext(workItemId, customStatePath)
|
|
335
|
+
const { state, projectRoot } = context
|
|
336
|
+
requireFullModeWorkItem(state, workItemId)
|
|
337
|
+
requireTaskBoardStage(state, workItemId)
|
|
338
|
+
|
|
339
|
+
const existingBoard = readTaskBoardIfExists(projectRoot, workItemId)
|
|
340
|
+
const baseBoard = buildBoardView(state, existingBoard)
|
|
341
|
+
const nextBoard = mutator(JSON.parse(JSON.stringify(baseBoard)), context)
|
|
342
|
+
const validatedBoard = validateTaskBoard(buildBoardForValidation(state, nextBoard))
|
|
343
|
+
writeTaskBoard(projectRoot, workItemId, validatedBoard)
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
...context,
|
|
347
|
+
board: validatedBoard,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function findTask(board, taskId) {
|
|
352
|
+
ensureString(taskId, "task_id")
|
|
353
|
+
const task = board.tasks.find((entry) => entry.task_id === taskId)
|
|
354
|
+
if (!task) {
|
|
355
|
+
fail(`Unknown task '${taskId}'`)
|
|
356
|
+
}
|
|
357
|
+
return task
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildTaskRecord(taskInput) {
|
|
361
|
+
ensureObject(taskInput, "task")
|
|
362
|
+
ensureString(taskInput.task_id, "task.task_id")
|
|
363
|
+
ensureString(taskInput.title, "task.title")
|
|
364
|
+
ensureString(taskInput.kind, "task.kind")
|
|
365
|
+
|
|
366
|
+
if (taskInput.worktree_metadata !== undefined) {
|
|
367
|
+
validateWorktreeMetadata(taskInput.worktree_metadata)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const now = timestamp()
|
|
371
|
+
|
|
372
|
+
return validateTaskShape({
|
|
373
|
+
task_id: taskInput.task_id,
|
|
374
|
+
title: taskInput.title,
|
|
375
|
+
summary: taskInput.summary ?? taskInput.title,
|
|
376
|
+
kind: taskInput.kind,
|
|
377
|
+
status: taskInput.status ?? "ready",
|
|
378
|
+
primary_owner: taskInput.primary_owner ?? null,
|
|
379
|
+
qa_owner: taskInput.qa_owner ?? null,
|
|
380
|
+
depends_on: taskInput.depends_on ?? [],
|
|
381
|
+
blocked_by: taskInput.blocked_by ?? [],
|
|
382
|
+
artifact_refs: taskInput.artifact_refs ?? [],
|
|
383
|
+
plan_refs: taskInput.plan_refs ?? [],
|
|
384
|
+
branch_or_worktree: taskInput.branch_or_worktree ?? taskInput.worktree_metadata?.worktree_path ?? null,
|
|
385
|
+
created_by: taskInput.created_by ?? "TechLeadAgent",
|
|
386
|
+
created_at: taskInput.created_at ?? now,
|
|
387
|
+
updated_at: taskInput.updated_at ?? now,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function validateManagedState(state, projectRoot, workItemId, options = {}) {
|
|
392
|
+
const taskBoard = readTaskBoardIfExists(projectRoot, workItemId)
|
|
393
|
+
|
|
394
|
+
if (state.mode !== "full") {
|
|
395
|
+
if (taskBoard !== null) {
|
|
396
|
+
fail(`${formatModeLabel(state.mode)} mode cannot carry a task board; task boards are full-delivery only`)
|
|
397
|
+
}
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (taskBoard === null) {
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
requireTaskBoardStage(state, workItemId, "carry a task board")
|
|
406
|
+
|
|
407
|
+
const boardStage = options.boardStage ?? taskBoard.current_stage ?? state.current_stage
|
|
408
|
+
validateTaskBoard({
|
|
409
|
+
...taskBoard,
|
|
410
|
+
mode: state.mode,
|
|
411
|
+
current_stage: boardStage,
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
return taskBoard
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function requireValidTaskBoard(state, projectRoot, workItemId, boardStage, reason) {
|
|
418
|
+
const taskBoard = readTaskBoardIfExists(projectRoot, workItemId)
|
|
419
|
+
|
|
420
|
+
if (state.mode !== "full") {
|
|
421
|
+
if (taskBoard !== null) {
|
|
422
|
+
fail(`${formatModeLabel(state.mode)} mode cannot carry a task board; task boards are full-delivery only`)
|
|
423
|
+
}
|
|
424
|
+
fail(reason)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (taskBoard === null) {
|
|
428
|
+
fail(reason)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
validateTaskBoard({
|
|
432
|
+
...taskBoard,
|
|
433
|
+
mode: state.mode,
|
|
434
|
+
current_stage: boardStage,
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readJsonIfExists(filePath) {
|
|
439
|
+
if (!fs.existsSync(filePath)) {
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"))
|
|
445
|
+
} catch (error) {
|
|
446
|
+
fail(`Malformed JSON at '${filePath}': ${error.message}`)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function getManifestPaths(projectRoot, manifest) {
|
|
451
|
+
const kit = manifest?.kit ?? {}
|
|
452
|
+
const registryPath = kit.registry?.path
|
|
453
|
+
? path.resolve(projectRoot, kit.registry.path)
|
|
454
|
+
: path.join(projectRoot, "registry.json")
|
|
455
|
+
const installManifestPath = kit.installManifest?.path
|
|
456
|
+
? path.resolve(projectRoot, kit.installManifest.path)
|
|
457
|
+
: path.join(projectRoot, ".opencode", "install-manifest.json")
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
registryPath,
|
|
461
|
+
installManifestPath,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function tryReadJson(filePath) {
|
|
466
|
+
if (!fs.existsSync(filePath)) {
|
|
467
|
+
return { exists: false, readable: false, data: null }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
return {
|
|
472
|
+
exists: true,
|
|
473
|
+
readable: true,
|
|
474
|
+
data: JSON.parse(fs.readFileSync(filePath, "utf8")),
|
|
475
|
+
}
|
|
476
|
+
} catch (_error) {
|
|
477
|
+
return {
|
|
478
|
+
exists: true,
|
|
479
|
+
readable: false,
|
|
480
|
+
data: null,
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getRegistry(customStatePath) {
|
|
486
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
487
|
+
const manifestPath = path.join(projectRoot, ".opencode", "opencode.json")
|
|
488
|
+
const manifest = readJsonIfExists(manifestPath)
|
|
489
|
+
const { registryPath } = getManifestPaths(projectRoot, manifest)
|
|
490
|
+
const registry = readJsonIfExists(registryPath)
|
|
491
|
+
|
|
492
|
+
if (!registry) {
|
|
493
|
+
fail(`Unable to read registry metadata at '${registryPath}'`)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
projectRoot,
|
|
498
|
+
manifestPath,
|
|
499
|
+
registryPath,
|
|
500
|
+
manifest,
|
|
501
|
+
registry,
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function getInstallManifest(customStatePath) {
|
|
506
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
507
|
+
const manifestPath = path.join(projectRoot, ".opencode", "opencode.json")
|
|
508
|
+
const manifest = readJsonIfExists(manifestPath)
|
|
509
|
+
const { installManifestPath } = getManifestPaths(projectRoot, manifest)
|
|
510
|
+
const installManifest = readJsonIfExists(installManifestPath)
|
|
511
|
+
|
|
512
|
+
if (!installManifest) {
|
|
513
|
+
fail(`Unable to read install manifest at '${installManifestPath}'`)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
projectRoot,
|
|
518
|
+
manifestPath,
|
|
519
|
+
installManifestPath,
|
|
520
|
+
manifest,
|
|
521
|
+
installManifest,
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function listProfiles(customStatePath) {
|
|
526
|
+
const { registry } = getRegistry(customStatePath)
|
|
527
|
+
return registry.profiles ?? []
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function getProfile(profileName, customStatePath) {
|
|
531
|
+
ensureString(profileName, "profile name")
|
|
532
|
+
const { registry } = getRegistry(customStatePath)
|
|
533
|
+
const profiles = registry.profiles ?? []
|
|
534
|
+
const profile = profiles.find((entry) => entry.name === profileName)
|
|
535
|
+
|
|
536
|
+
if (!profile) {
|
|
537
|
+
fail(`Unknown profile '${profileName}'`)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return profile
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function syncInstallManifest(profileName, customStatePath) {
|
|
544
|
+
const profile = getProfile(profileName, customStatePath)
|
|
545
|
+
const { installManifestPath, installManifest } = getInstallManifest(customStatePath)
|
|
546
|
+
|
|
547
|
+
const nextManifest = JSON.parse(JSON.stringify(installManifest))
|
|
548
|
+
nextManifest.installation = nextManifest.installation ?? {}
|
|
549
|
+
nextManifest.installation.activeProfile = profile.name
|
|
550
|
+
writeState(installManifestPath, nextManifest)
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
installManifestPath,
|
|
554
|
+
installManifest: nextManifest,
|
|
555
|
+
profile,
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function ensureObject(value, label) {
|
|
560
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
561
|
+
fail(`${label} must be an object`)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function ensureArray(value, label) {
|
|
566
|
+
if (!Array.isArray(value)) {
|
|
567
|
+
fail(`${label} must be an array`)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function ensureString(value, label) {
|
|
572
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
573
|
+
fail(`${label} must be a non-empty string`)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function ensureNullableString(value, label) {
|
|
578
|
+
if (value !== null && typeof value !== "string") {
|
|
579
|
+
fail(`${label} must be a string or null`)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function ensureKnown(value, allowedValues, label) {
|
|
584
|
+
if (!allowedValues.includes(value)) {
|
|
585
|
+
fail(`${label} must be one of: ${allowedValues.join(", ")}`)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function ensureGateShape(gateName, gateValue) {
|
|
590
|
+
ensureObject(gateValue, `approvals.${gateName}`)
|
|
591
|
+
for (const key of ["status", "approved_by", "approved_at", "notes"]) {
|
|
592
|
+
if (!(key in gateValue)) {
|
|
593
|
+
fail(`approvals.${gateName}.${key} is required`)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
ensureKnown(gateValue.status, ["pending", "approved", "rejected"], `approvals.${gateName}.status`)
|
|
598
|
+
ensureNullableString(gateValue.approved_by, `approvals.${gateName}.approved_by`)
|
|
599
|
+
ensureNullableString(gateValue.approved_at, `approvals.${gateName}.approved_at`)
|
|
600
|
+
ensureNullableString(gateValue.notes, `approvals.${gateName}.notes`)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function ensureIssueShape(issue, index) {
|
|
604
|
+
ensureObject(issue, `issues[${index}]`)
|
|
605
|
+
|
|
606
|
+
for (const key of [
|
|
607
|
+
"issue_id",
|
|
608
|
+
"title",
|
|
609
|
+
"type",
|
|
610
|
+
"severity",
|
|
611
|
+
"rooted_in",
|
|
612
|
+
"recommended_owner",
|
|
613
|
+
"evidence",
|
|
614
|
+
"artifact_refs",
|
|
615
|
+
]) {
|
|
616
|
+
if (!(key in issue)) {
|
|
617
|
+
fail(`issues[${index}].${key} is required`)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
ensureString(issue.issue_id, `issues[${index}].issue_id`)
|
|
622
|
+
ensureString(issue.title, `issues[${index}].title`)
|
|
623
|
+
ensureKnown(issue.type, ISSUE_TYPES, `issues[${index}].type`)
|
|
624
|
+
ensureKnown(issue.severity, ISSUE_SEVERITIES, `issues[${index}].severity`)
|
|
625
|
+
ensureKnown(issue.rooted_in, ROOTED_IN_VALUES, `issues[${index}].rooted_in`)
|
|
626
|
+
ensureString(issue.recommended_owner, `issues[${index}].recommended_owner`)
|
|
627
|
+
ensureString(issue.evidence, `issues[${index}].evidence`)
|
|
628
|
+
|
|
629
|
+
const allowedOwners = RECOMMENDED_OWNERS[issue.type] ?? []
|
|
630
|
+
if (!allowedOwners.includes(issue.recommended_owner)) {
|
|
631
|
+
fail(`issues[${index}].recommended_owner must be one of: ${allowedOwners.join(", ")}`)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (typeof issue.artifact_refs === "string") {
|
|
635
|
+
issue.artifact_refs = [issue.artifact_refs]
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
ensureArray(issue.artifact_refs, `issues[${index}].artifact_refs`)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function validateArtifacts(artifacts) {
|
|
642
|
+
ensureObject(artifacts, "artifacts")
|
|
643
|
+
for (const key of ["task_card", "brief", "spec", "architecture", "plan", "migration_report", "qa_report", "adr"]) {
|
|
644
|
+
if (!(key in artifacts)) {
|
|
645
|
+
fail(`artifacts.${key} is required`)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
ensureNullableString(artifacts.task_card, "artifacts.task_card")
|
|
650
|
+
ensureNullableString(artifacts.brief, "artifacts.brief")
|
|
651
|
+
ensureNullableString(artifacts.spec, "artifacts.spec")
|
|
652
|
+
ensureNullableString(artifacts.architecture, "artifacts.architecture")
|
|
653
|
+
ensureNullableString(artifacts.plan, "artifacts.plan")
|
|
654
|
+
ensureNullableString(artifacts.migration_report, "artifacts.migration_report")
|
|
655
|
+
ensureNullableString(artifacts.qa_report, "artifacts.qa_report")
|
|
656
|
+
ensureArray(artifacts.adr, "artifacts.adr")
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function validateRoutingProfile(routingProfile) {
|
|
660
|
+
ensureObject(routingProfile, "routing_profile")
|
|
661
|
+
|
|
662
|
+
for (const key of [
|
|
663
|
+
"work_intent",
|
|
664
|
+
"behavior_delta",
|
|
665
|
+
"dominant_uncertainty",
|
|
666
|
+
"scope_shape",
|
|
667
|
+
"selection_reason",
|
|
668
|
+
]) {
|
|
669
|
+
if (!(key in routingProfile)) {
|
|
670
|
+
fail(`routing_profile.${key} is required`)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
ensureKnown(routingProfile.work_intent, ROUTING_WORK_INTENT_VALUES, "routing_profile.work_intent")
|
|
675
|
+
ensureKnown(routingProfile.behavior_delta, ROUTING_BEHAVIOR_DELTA_VALUES, "routing_profile.behavior_delta")
|
|
676
|
+
ensureKnown(
|
|
677
|
+
routingProfile.dominant_uncertainty,
|
|
678
|
+
ROUTING_DOMINANT_UNCERTAINTY_VALUES,
|
|
679
|
+
"routing_profile.dominant_uncertainty",
|
|
680
|
+
)
|
|
681
|
+
ensureKnown(routingProfile.scope_shape, ROUTING_SCOPE_SHAPE_VALUES, "routing_profile.scope_shape")
|
|
682
|
+
ensureString(routingProfile.selection_reason, "routing_profile.selection_reason")
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function validateRoutingProfileForMode(mode, routingProfile) {
|
|
686
|
+
if (mode === "quick") {
|
|
687
|
+
if (routingProfile.work_intent !== "maintenance") {
|
|
688
|
+
fail("routing_profile.work_intent must be 'maintenance' for quick mode")
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (routingProfile.behavior_delta !== "preserve") {
|
|
692
|
+
fail("routing_profile.behavior_delta must be 'preserve' for quick mode")
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (routingProfile.dominant_uncertainty !== "low_local") {
|
|
696
|
+
fail("routing_profile.dominant_uncertainty must be 'low_local' for quick mode")
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (routingProfile.scope_shape === "cross_boundary") {
|
|
700
|
+
fail("routing_profile.scope_shape cannot be 'cross_boundary' for quick mode")
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (mode === "migration") {
|
|
707
|
+
if (routingProfile.work_intent !== "modernization") {
|
|
708
|
+
fail("routing_profile.work_intent must be 'modernization' for migration mode")
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (routingProfile.behavior_delta !== "preserve") {
|
|
712
|
+
fail("routing_profile.behavior_delta must be 'preserve' for migration mode")
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (routingProfile.dominant_uncertainty !== "compatibility") {
|
|
716
|
+
fail("routing_profile.dominant_uncertainty must be 'compatibility' for migration mode")
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const supportsFullMode =
|
|
723
|
+
routingProfile.dominant_uncertainty === "product" ||
|
|
724
|
+
routingProfile.behavior_delta !== "preserve" ||
|
|
725
|
+
routingProfile.work_intent === "feature" ||
|
|
726
|
+
routingProfile.scope_shape === "cross_boundary"
|
|
727
|
+
|
|
728
|
+
if (!supportsFullMode) {
|
|
729
|
+
fail(
|
|
730
|
+
"routing_profile must reflect product uncertainty, changed behavior, feature intent, or cross-boundary scope for full mode",
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function validateArtifactSignatureForMode(mode, artifacts) {
|
|
736
|
+
if (mode === "quick") {
|
|
737
|
+
for (const key of ["brief", "spec", "architecture", "plan", "migration_report", "qa_report"]) {
|
|
738
|
+
if (artifacts[key] !== null) {
|
|
739
|
+
fail(`artifacts.${key} must be null in quick mode`)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (artifacts.adr.length > 0) {
|
|
744
|
+
fail("artifacts.adr must stay empty in quick mode")
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (mode === "migration") {
|
|
751
|
+
for (const key of ["task_card", "brief", "spec", "qa_report"]) {
|
|
752
|
+
if (artifacts[key] !== null) {
|
|
753
|
+
fail(`artifacts.${key} must be null in migration mode`)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
for (const key of ["task_card", "migration_report"]) {
|
|
761
|
+
if (artifacts[key] !== null) {
|
|
762
|
+
fail(`artifacts.${key} must be null in full mode`)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function validateApprovals(mode, approvals) {
|
|
768
|
+
ensureObject(approvals, "approvals")
|
|
769
|
+
|
|
770
|
+
const requiredGates = getApprovalGatesForMode(mode)
|
|
771
|
+
const approvalKeys = Object.keys(approvals)
|
|
772
|
+
|
|
773
|
+
for (const gate of requiredGates) {
|
|
774
|
+
if (!(gate in approvals)) {
|
|
775
|
+
fail(`approvals.${gate} is required for mode '${mode}'`)
|
|
776
|
+
}
|
|
777
|
+
ensureGateShape(gate, approvals[gate])
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
for (const gate of approvalKeys) {
|
|
781
|
+
if (!requiredGates.includes(gate)) {
|
|
782
|
+
fail(`approvals.${gate} is not valid for mode '${mode}'`)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function validateStateObject(state, options = {}) {
|
|
788
|
+
ensureObject(state, "workflow state")
|
|
789
|
+
|
|
790
|
+
for (const key of [
|
|
791
|
+
"feature_id",
|
|
792
|
+
"feature_slug",
|
|
793
|
+
"mode",
|
|
794
|
+
"mode_reason",
|
|
795
|
+
"routing_profile",
|
|
796
|
+
"current_stage",
|
|
797
|
+
"status",
|
|
798
|
+
"current_owner",
|
|
799
|
+
"artifacts",
|
|
800
|
+
"approvals",
|
|
801
|
+
"issues",
|
|
802
|
+
"retry_count",
|
|
803
|
+
"escalated_from",
|
|
804
|
+
"escalation_reason",
|
|
805
|
+
"updated_at",
|
|
806
|
+
]) {
|
|
807
|
+
if (!(key in state)) {
|
|
808
|
+
fail(`${key} is required`)
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (state.feature_id !== null) {
|
|
813
|
+
ensureString(state.feature_id, "feature_id")
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (state.feature_slug !== null) {
|
|
817
|
+
ensureString(state.feature_slug, "feature_slug")
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
ensureKnown(state.mode, MODE_VALUES, "mode")
|
|
821
|
+
ensureString(state.mode_reason, "mode_reason")
|
|
822
|
+
validateRoutingProfile(state.routing_profile)
|
|
823
|
+
validateRoutingProfileForMode(state.mode, state.routing_profile)
|
|
824
|
+
ensureKnown(state.current_stage, STAGE_SEQUENCE, "current_stage")
|
|
825
|
+
ensureKnown(state.status, STATUS_VALUES, "status")
|
|
826
|
+
|
|
827
|
+
const stageMode = getModeForStage(state.current_stage)
|
|
828
|
+
if (stageMode !== state.mode) {
|
|
829
|
+
fail(`current_stage '${state.current_stage}' does not belong to mode '${state.mode}'`)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (options.strictOwner !== false) {
|
|
833
|
+
const expectedOwner = STAGE_OWNERS[state.current_stage]
|
|
834
|
+
if (state.current_owner !== expectedOwner) {
|
|
835
|
+
fail(`current_owner must be '${expectedOwner}' for stage '${state.current_stage}'`)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
validateArtifacts(state.artifacts)
|
|
840
|
+
validateArtifactSignatureForMode(state.mode, state.artifacts)
|
|
841
|
+
validateApprovals(state.mode, state.approvals)
|
|
842
|
+
|
|
843
|
+
ensureArray(state.issues, "issues")
|
|
844
|
+
state.issues.forEach((issue, index) => ensureIssueShape(issue, index))
|
|
845
|
+
|
|
846
|
+
if (typeof state.retry_count !== "number" || Number.isNaN(state.retry_count) || state.retry_count < 0) {
|
|
847
|
+
fail("retry_count must be a non-negative number")
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (state.escalated_from !== null) {
|
|
851
|
+
ensureKnown(state.escalated_from, ["quick", "migration"], "escalated_from")
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
ensureNullableString(state.escalation_reason, "escalation_reason")
|
|
855
|
+
ensureNullableString(state.updated_at, "updated_at")
|
|
856
|
+
|
|
857
|
+
if (state.escalated_from === null && state.escalation_reason !== null) {
|
|
858
|
+
fail("escalation_reason must be null when escalated_from is null")
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if ((state.escalated_from === "quick" || state.escalated_from === "migration") && state.mode !== "full") {
|
|
862
|
+
fail("mode must be 'full' when escalated_from records an escalated lane")
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return state
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function createFreshState({ workItemId, mode, featureId, featureSlug, modeReason, updatedAt }) {
|
|
869
|
+
const initialStage = getInitialStageForMode(mode)
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
work_item_id: workItemId,
|
|
873
|
+
feature_id: featureId,
|
|
874
|
+
feature_slug: featureSlug,
|
|
875
|
+
mode,
|
|
876
|
+
mode_reason: modeReason,
|
|
877
|
+
routing_profile: createDefaultRoutingProfile(mode, modeReason),
|
|
878
|
+
current_stage: initialStage,
|
|
879
|
+
status: "in_progress",
|
|
880
|
+
current_owner: STAGE_OWNERS[initialStage],
|
|
881
|
+
artifacts: createEmptyArtifacts(),
|
|
882
|
+
approvals: createEmptyApprovals(mode),
|
|
883
|
+
issues: [],
|
|
884
|
+
retry_count: 0,
|
|
885
|
+
escalated_from: null,
|
|
886
|
+
escalation_reason: null,
|
|
887
|
+
updated_at: updatedAt,
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function setRoutingProfile(workIntent, behaviorDelta, dominantUncertainty, scopeShape, selectionReason, customStatePath) {
|
|
892
|
+
ensureKnown(workIntent, ROUTING_WORK_INTENT_VALUES, "routing_profile.work_intent")
|
|
893
|
+
ensureKnown(behaviorDelta, ROUTING_BEHAVIOR_DELTA_VALUES, "routing_profile.behavior_delta")
|
|
894
|
+
ensureKnown(
|
|
895
|
+
dominantUncertainty,
|
|
896
|
+
ROUTING_DOMINANT_UNCERTAINTY_VALUES,
|
|
897
|
+
"routing_profile.dominant_uncertainty",
|
|
898
|
+
)
|
|
899
|
+
ensureKnown(scopeShape, ROUTING_SCOPE_SHAPE_VALUES, "routing_profile.scope_shape")
|
|
900
|
+
ensureString(selectionReason, "routing_profile.selection_reason")
|
|
901
|
+
|
|
902
|
+
return mutate(customStatePath, (state) => {
|
|
903
|
+
state.routing_profile = {
|
|
904
|
+
work_intent: workIntent,
|
|
905
|
+
behavior_delta: behaviorDelta,
|
|
906
|
+
dominant_uncertainty: dominantUncertainty,
|
|
907
|
+
scope_shape: scopeShape,
|
|
908
|
+
selection_reason: selectionReason,
|
|
909
|
+
}
|
|
910
|
+
return state
|
|
911
|
+
})
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function mutate(customStatePath, mutator) {
|
|
915
|
+
const context = readManagedState(customStatePath)
|
|
916
|
+
const { state, workItemId, index, projectRoot } = context
|
|
917
|
+
const expectedRevision = captureRevision(state)
|
|
918
|
+
const expectedMirrorRevision = index.active_work_item_id === workItemId ? expectedRevision : null
|
|
919
|
+
validateStateObject(state)
|
|
920
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
921
|
+
const nextState = mutator(JSON.parse(JSON.stringify(state)), context)
|
|
922
|
+
nextState.updated_at = timestamp()
|
|
923
|
+
validateStateObject(nextState)
|
|
924
|
+
return persistManagedState(customStatePath, nextState, {
|
|
925
|
+
expectedRevision,
|
|
926
|
+
expectedMirrorRevision,
|
|
927
|
+
workItemId,
|
|
928
|
+
activateWorkItemId: index.active_work_item_id ?? workItemId,
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function mutateWorkItem(workItemId, customStatePath, mutator) {
|
|
933
|
+
const context = readWorkItemContext(workItemId, customStatePath)
|
|
934
|
+
const { state, projectRoot } = context
|
|
935
|
+
const index = readWorkItemIndex(projectRoot)
|
|
936
|
+
const expectedRevision = captureRevision(state)
|
|
937
|
+
const expectedMirrorRevision = index.active_work_item_id === workItemId ? expectedRevision : null
|
|
938
|
+
|
|
939
|
+
validateStateObject(state)
|
|
940
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
941
|
+
|
|
942
|
+
const nextState = mutator(JSON.parse(JSON.stringify(state)), context)
|
|
943
|
+
nextState.updated_at = timestamp()
|
|
944
|
+
validateStateObject(nextState)
|
|
945
|
+
|
|
946
|
+
return persistManagedState(customStatePath, nextState, {
|
|
947
|
+
expectedRevision,
|
|
948
|
+
expectedMirrorRevision,
|
|
949
|
+
workItemId,
|
|
950
|
+
activateWorkItemId: index.active_work_item_id ?? workItemId,
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function showState(customStatePath) {
|
|
955
|
+
const { statePath, state, projectRoot, workItemId } = readManagedState(customStatePath)
|
|
956
|
+
validateStateObject(state)
|
|
957
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
958
|
+
return { statePath, state }
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function showWorkItemState(workItemId, customStatePath) {
|
|
962
|
+
ensureString(workItemId, "work_item_id")
|
|
963
|
+
const { statePath, projectRoot, state } = readManagedState(customStatePath, workItemId)
|
|
964
|
+
validateStateObject(state)
|
|
965
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
966
|
+
|
|
967
|
+
return {
|
|
968
|
+
statePath,
|
|
969
|
+
workItemStatePath: resolveWorkItemPaths(projectRoot, workItemId).statePath,
|
|
970
|
+
state,
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function createWorkItem(mode, featureId, featureSlug, modeReason, customStatePath) {
|
|
975
|
+
return startTask(mode, featureId, featureSlug, modeReason, customStatePath)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function listWorkItems(customStatePath) {
|
|
979
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
980
|
+
const index = readWorkItemIndex(projectRoot)
|
|
981
|
+
return {
|
|
982
|
+
projectRoot,
|
|
983
|
+
index,
|
|
984
|
+
workItems: index.work_items,
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function listTasks(workItemId, customStatePath) {
|
|
989
|
+
const context = readWorkItemContext(workItemId, customStatePath)
|
|
990
|
+
const { state, projectRoot } = context
|
|
991
|
+
requireFullModeWorkItem(state, workItemId)
|
|
992
|
+
requireTaskBoardStage(state, workItemId, "view a task board")
|
|
993
|
+
|
|
994
|
+
const board = buildBoardView(state, readTaskBoardIfExists(projectRoot, workItemId))
|
|
995
|
+
return {
|
|
996
|
+
...context,
|
|
997
|
+
board,
|
|
998
|
+
tasks: board.tasks,
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function createTask(workItemId, taskInput, customStatePath) {
|
|
1003
|
+
return withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1004
|
+
const task = buildTaskRecord(taskInput)
|
|
1005
|
+
|
|
1006
|
+
if (board.tasks.some((entry) => entry.task_id === task.task_id)) {
|
|
1007
|
+
fail(`Duplicate task_id '${task.task_id}' in task board`)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
board.tasks.push(task)
|
|
1011
|
+
return board
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function claimTask(workItemId, taskId, owner, customStatePath, options = {}) {
|
|
1016
|
+
ensureString(owner, "owner")
|
|
1017
|
+
|
|
1018
|
+
return withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1019
|
+
const task = findTask(board, taskId)
|
|
1020
|
+
|
|
1021
|
+
if (task.primary_owner && task.primary_owner !== owner) {
|
|
1022
|
+
fail("Implicit reassignment is not allowed; use reassignTask")
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
validateReassignmentAuthority({
|
|
1026
|
+
task,
|
|
1027
|
+
ownerField: "primary_owner",
|
|
1028
|
+
requestedBy: options.requestedBy,
|
|
1029
|
+
nextOwner: owner,
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
validateTaskTransition(task, "claimed")
|
|
1033
|
+
task.primary_owner = owner
|
|
1034
|
+
task.status = "claimed"
|
|
1035
|
+
task.updated_at = timestamp()
|
|
1036
|
+
return board
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function assignQaOwner(workItemId, taskId, qaOwner, customStatePath, options = {}) {
|
|
1041
|
+
ensureString(qaOwner, "qa_owner")
|
|
1042
|
+
|
|
1043
|
+
return withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1044
|
+
const task = findTask(board, taskId)
|
|
1045
|
+
|
|
1046
|
+
validateReassignmentAuthority({
|
|
1047
|
+
task,
|
|
1048
|
+
ownerField: "qa_owner",
|
|
1049
|
+
requestedBy: options.requestedBy,
|
|
1050
|
+
nextOwner: qaOwner,
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
task.qa_owner = qaOwner
|
|
1054
|
+
task.updated_at = timestamp()
|
|
1055
|
+
return board
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function reassignTask(workItemId, taskId, owner, customStatePath, options = {}) {
|
|
1060
|
+
ensureString(owner, "owner")
|
|
1061
|
+
|
|
1062
|
+
return withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1063
|
+
const task = findTask(board, taskId)
|
|
1064
|
+
|
|
1065
|
+
validateReassignmentAuthority({
|
|
1066
|
+
task,
|
|
1067
|
+
ownerField: "primary_owner",
|
|
1068
|
+
requestedBy: options.requestedBy,
|
|
1069
|
+
nextOwner: owner,
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
task.primary_owner = owner
|
|
1073
|
+
if (task.status === "ready") {
|
|
1074
|
+
task.status = "claimed"
|
|
1075
|
+
}
|
|
1076
|
+
task.updated_at = timestamp()
|
|
1077
|
+
return board
|
|
1078
|
+
})
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function releaseTask(workItemId, taskId, customStatePath, options = {}) {
|
|
1082
|
+
ensureString(options.requestedBy, "requestedBy")
|
|
1083
|
+
|
|
1084
|
+
return withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1085
|
+
const task = findTask(board, taskId)
|
|
1086
|
+
const allowedAuthorities = VALID_ASSIGNMENT_AUTHORITIES.primary_owner
|
|
1087
|
+
const isCurrentOwner = task.primary_owner === options.requestedBy
|
|
1088
|
+
|
|
1089
|
+
if (!allowedAuthorities.includes(options.requestedBy) && !isCurrentOwner) {
|
|
1090
|
+
fail(`Only ${allowedAuthorities.join(" or ")} can release primary_owner`)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
task.primary_owner = null
|
|
1094
|
+
if (task.status === "claimed") {
|
|
1095
|
+
task.status = "ready"
|
|
1096
|
+
}
|
|
1097
|
+
task.updated_at = timestamp()
|
|
1098
|
+
return board
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function setTaskStatus(workItemId, taskId, nextStatus, customStatePath, options = {}) {
|
|
1103
|
+
validateTaskStatus(nextStatus)
|
|
1104
|
+
|
|
1105
|
+
const targetContext = readWorkItemContext(workItemId, customStatePath)
|
|
1106
|
+
const { state: targetState, projectRoot } = targetContext
|
|
1107
|
+
validateManagedState(targetState, projectRoot, workItemId)
|
|
1108
|
+
const targetBoard = buildBoardView(targetState, readTaskBoardIfExists(projectRoot, workItemId))
|
|
1109
|
+
const targetTask = findTask(targetBoard, taskId)
|
|
1110
|
+
|
|
1111
|
+
if (targetTask.status === "qa_in_progress" && (nextStatus === "claimed" || nextStatus === "in_progress")) {
|
|
1112
|
+
decideQaFailLocalRework({
|
|
1113
|
+
mode: "full",
|
|
1114
|
+
task: targetTask,
|
|
1115
|
+
finding: options.finding,
|
|
1116
|
+
rerouteDecision: options.rerouteDecision,
|
|
1117
|
+
})
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const shouldApplyQaFailReroute = nextStatus === "claimed" || nextStatus === "in_progress"
|
|
1121
|
+
let rerouteDecision = null
|
|
1122
|
+
|
|
1123
|
+
const result = withTaskBoard(workItemId, customStatePath, (board) => {
|
|
1124
|
+
const task = findTask(board, taskId)
|
|
1125
|
+
|
|
1126
|
+
if (task.status === "qa_in_progress" && (nextStatus === "claimed" || nextStatus === "in_progress")) {
|
|
1127
|
+
const decision = decideQaFailLocalRework({
|
|
1128
|
+
mode: "full",
|
|
1129
|
+
task,
|
|
1130
|
+
finding: options.finding,
|
|
1131
|
+
rerouteDecision: options.rerouteDecision,
|
|
1132
|
+
})
|
|
1133
|
+
rerouteDecision = decision.route
|
|
1134
|
+
|
|
1135
|
+
validateTaskTransition(task, nextStatus, {
|
|
1136
|
+
allowQaFailRework: true,
|
|
1137
|
+
finding: options.finding,
|
|
1138
|
+
})
|
|
1139
|
+
} else {
|
|
1140
|
+
validateTaskTransition(task, nextStatus)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
task.status = nextStatus
|
|
1144
|
+
task.updated_at = timestamp()
|
|
1145
|
+
return board
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
if (shouldApplyQaFailReroute && rerouteDecision) {
|
|
1149
|
+
const rerouted = mutateWorkItem(workItemId, customStatePath, (state) => {
|
|
1150
|
+
state.current_stage = rerouteDecision.stage
|
|
1151
|
+
state.current_owner = rerouteDecision.owner
|
|
1152
|
+
state.status = "in_progress"
|
|
1153
|
+
return state
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
...rerouted,
|
|
1158
|
+
board: result.board,
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return result
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function validateWorkItemBoard(workItemId, customStatePath) {
|
|
1166
|
+
const context = readWorkItemContext(workItemId, customStatePath)
|
|
1167
|
+
const { state, projectRoot } = context
|
|
1168
|
+
requireFullModeWorkItem(state, workItemId)
|
|
1169
|
+
const board = readTaskBoardIfExists(projectRoot, workItemId)
|
|
1170
|
+
|
|
1171
|
+
if (!board) {
|
|
1172
|
+
fail(`Task board missing for work item '${workItemId}'`)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return {
|
|
1176
|
+
...context,
|
|
1177
|
+
board: validateTaskBoard(buildBoardForValidation(state, board)),
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function selectActiveWorkItem(workItemId, customStatePath) {
|
|
1182
|
+
ensureString(workItemId, "work_item_id")
|
|
1183
|
+
const statePath = resolveStatePath(customStatePath)
|
|
1184
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
1185
|
+
const previousIndex = readWorkItemIndex(projectRoot)
|
|
1186
|
+
const previousActiveWorkItemId = previousIndex.active_work_item_id
|
|
1187
|
+
|
|
1188
|
+
try {
|
|
1189
|
+
setActiveWorkItem(projectRoot, workItemId)
|
|
1190
|
+
const nextActiveState = readWorkItemState(projectRoot, workItemId)
|
|
1191
|
+
const mirrorRefreshPlan = planGuardedMirrorRefresh({
|
|
1192
|
+
activeWorkItemId: workItemId,
|
|
1193
|
+
targetWorkItemId: workItemId,
|
|
1194
|
+
nextState: nextActiveState,
|
|
1195
|
+
})
|
|
1196
|
+
const preflightMirrorState = fs.existsSync(statePath) ? readState(statePath).state : null
|
|
1197
|
+
|
|
1198
|
+
if (preflightMirrorState) {
|
|
1199
|
+
guardWrite({
|
|
1200
|
+
currentState: preflightMirrorState,
|
|
1201
|
+
expectedRevision: captureRevision(preflightMirrorState),
|
|
1202
|
+
nextState: nextActiveState,
|
|
1203
|
+
})
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
writeCompatibilityMirror(projectRoot, nextActiveState)
|
|
1207
|
+
const state = readState(statePath).state
|
|
1208
|
+
const mirrorRevision = captureRevision(state)
|
|
1209
|
+
|
|
1210
|
+
if (mirrorRevision !== mirrorRefreshPlan.mirrorRevision) {
|
|
1211
|
+
fail(
|
|
1212
|
+
`Compatibility mirror refresh revision conflict for active work item '${workItemId}'; expected ${mirrorRefreshPlan.mirrorRevision} but found ${mirrorRevision}`,
|
|
1213
|
+
)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
validateStateObject(state)
|
|
1217
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
statePath,
|
|
1221
|
+
state,
|
|
1222
|
+
}
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (previousActiveWorkItemId !== null && previousActiveWorkItemId !== workItemId) {
|
|
1225
|
+
setActiveWorkItem(projectRoot, previousActiveWorkItemId)
|
|
1226
|
+
}
|
|
1227
|
+
throw error
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function getRuntimeStatus(customStatePath) {
|
|
1232
|
+
const { statePath, state } = showState(customStatePath)
|
|
1233
|
+
|
|
1234
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
1235
|
+
const runtimeRoot = resolveRuntimeRoot(customStatePath)
|
|
1236
|
+
const kitRoot = resolveKitRoot(projectRoot)
|
|
1237
|
+
const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
|
|
1238
|
+
const manifest = readJsonIfExists(manifestPath)
|
|
1239
|
+
const { registryPath, installManifestPath } = getManifestPaths(kitRoot, manifest)
|
|
1240
|
+
const installManifest = readJsonIfExists(installManifestPath)
|
|
1241
|
+
const hooksConfigPath = path.join(kitRoot, "hooks", "hooks.json")
|
|
1242
|
+
const sessionStartPath = path.join(kitRoot, "hooks", "session-start")
|
|
1243
|
+
const metaSkillPath = path.join(kitRoot, "skills", "using-skills", "SKILL.md")
|
|
1244
|
+
const kit = manifest?.kit ?? {}
|
|
1245
|
+
|
|
1246
|
+
return {
|
|
1247
|
+
projectRoot,
|
|
1248
|
+
runtimeRoot,
|
|
1249
|
+
kitRoot,
|
|
1250
|
+
statePath,
|
|
1251
|
+
manifestPath,
|
|
1252
|
+
registryPath,
|
|
1253
|
+
installManifestPath,
|
|
1254
|
+
hooksConfigPath,
|
|
1255
|
+
sessionStartPath,
|
|
1256
|
+
metaSkillPath,
|
|
1257
|
+
kitName: kit.name ?? "Unknown kit",
|
|
1258
|
+
kitVersion: kit.version ?? "unknown",
|
|
1259
|
+
entryAgent: kit.entryAgent ?? "unknown",
|
|
1260
|
+
activeProfile: installManifest?.installation?.activeProfile ?? kit.activeProfile ?? "unknown",
|
|
1261
|
+
installManifest,
|
|
1262
|
+
state,
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function getVersionInfo(customStatePath) {
|
|
1267
|
+
const runtime = getRuntimeStatus(customStatePath)
|
|
1268
|
+
return {
|
|
1269
|
+
kitName: runtime.kitName,
|
|
1270
|
+
kitVersion: runtime.kitVersion,
|
|
1271
|
+
activeProfile: runtime.activeProfile,
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function runDoctor(customStatePath) {
|
|
1276
|
+
const statePath = resolveStatePath(customStatePath)
|
|
1277
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
1278
|
+
const runtimeRoot = resolveRuntimeRoot(customStatePath)
|
|
1279
|
+
const kitRoot = resolveKitRoot(projectRoot)
|
|
1280
|
+
const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
|
|
1281
|
+
const manifestInfo = tryReadJson(manifestPath)
|
|
1282
|
+
const manifest = manifestInfo.data
|
|
1283
|
+
const { registryPath, installManifestPath } = getManifestPaths(kitRoot, manifest)
|
|
1284
|
+
const registryInfo = tryReadJson(registryPath)
|
|
1285
|
+
const installManifestInfo = tryReadJson(installManifestPath)
|
|
1286
|
+
const installManifest = installManifestInfo.data
|
|
1287
|
+
const hooksConfigPath = path.join(kitRoot, "hooks", "hooks.json")
|
|
1288
|
+
const sessionStartPath = path.join(kitRoot, "hooks", "session-start")
|
|
1289
|
+
const metaSkillPath = path.join(kitRoot, "skills", "using-skills", "SKILL.md")
|
|
1290
|
+
const workflowStateCliPath = path.join(kitRoot, ".opencode", "workflow-state.js")
|
|
1291
|
+
|
|
1292
|
+
let stateValid = false
|
|
1293
|
+
let state = null
|
|
1294
|
+
let kitName = "Unknown kit"
|
|
1295
|
+
let kitVersion = "unknown"
|
|
1296
|
+
let entryAgent = "unknown"
|
|
1297
|
+
|
|
1298
|
+
if (manifest?.kit) {
|
|
1299
|
+
kitName = manifest.kit.name ?? kitName
|
|
1300
|
+
kitVersion = manifest.kit.version ?? kitVersion
|
|
1301
|
+
entryAgent = manifest.kit.entryAgent ?? entryAgent
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (fs.existsSync(statePath)) {
|
|
1305
|
+
try {
|
|
1306
|
+
const result = showState(customStatePath)
|
|
1307
|
+
state = result.state
|
|
1308
|
+
stateValid = true
|
|
1309
|
+
} catch (_error) {
|
|
1310
|
+
stateValid = false
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const runtime = {
|
|
1315
|
+
projectRoot,
|
|
1316
|
+
runtimeRoot,
|
|
1317
|
+
kitRoot,
|
|
1318
|
+
statePath,
|
|
1319
|
+
manifestPath,
|
|
1320
|
+
registryPath,
|
|
1321
|
+
installManifestPath,
|
|
1322
|
+
workflowStateCliPath,
|
|
1323
|
+
hooksConfigPath,
|
|
1324
|
+
sessionStartPath,
|
|
1325
|
+
metaSkillPath,
|
|
1326
|
+
kitName,
|
|
1327
|
+
kitVersion,
|
|
1328
|
+
entryAgent,
|
|
1329
|
+
activeProfile: installManifest?.installation?.activeProfile ?? manifest?.kit?.activeProfile ?? "unknown",
|
|
1330
|
+
installManifest,
|
|
1331
|
+
state,
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const checks = [
|
|
1335
|
+
{ label: "manifest file found", ok: fs.existsSync(manifestPath) },
|
|
1336
|
+
{ label: "workflow state file found", ok: fs.existsSync(statePath) },
|
|
1337
|
+
{ label: "workflow state is valid", ok: stateValid },
|
|
1338
|
+
{ label: "registry file found", ok: fs.existsSync(registryPath) },
|
|
1339
|
+
{ label: "registry metadata is readable", ok: !registryInfo.exists || registryInfo.readable },
|
|
1340
|
+
{ label: "install manifest found", ok: fs.existsSync(installManifestPath) },
|
|
1341
|
+
{
|
|
1342
|
+
label: "install manifest is readable",
|
|
1343
|
+
ok: !installManifestInfo.exists || installManifestInfo.readable,
|
|
1344
|
+
},
|
|
1345
|
+
{ label: "workflow state CLI found", ok: fs.existsSync(workflowStateCliPath) },
|
|
1346
|
+
{ label: "hooks config found", ok: fs.existsSync(hooksConfigPath) },
|
|
1347
|
+
{ label: "session-start hook found", ok: fs.existsSync(sessionStartPath) },
|
|
1348
|
+
{ label: "meta-skill found", ok: fs.existsSync(metaSkillPath) },
|
|
1349
|
+
{
|
|
1350
|
+
label: "active profile exists in registry",
|
|
1351
|
+
ok:
|
|
1352
|
+
!installManifestInfo.readable ||
|
|
1353
|
+
!registryInfo.readable ||
|
|
1354
|
+
(registryInfo.data?.profiles ?? []).some(
|
|
1355
|
+
(profile) => profile.name === (installManifest?.installation?.activeProfile ?? manifest?.kit?.activeProfile),
|
|
1356
|
+
),
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
label: "manifest and install manifest profiles agree",
|
|
1360
|
+
ok:
|
|
1361
|
+
!installManifestInfo.readable ||
|
|
1362
|
+
!manifest?.kit?.activeProfile ||
|
|
1363
|
+
manifest.kit.activeProfile === installManifest?.installation?.activeProfile,
|
|
1364
|
+
},
|
|
1365
|
+
]
|
|
1366
|
+
|
|
1367
|
+
const contractReport = buildContractConsistencyReport({ projectRoot: kitRoot, manifest })
|
|
1368
|
+
checks.push(...contractReport.checks)
|
|
1369
|
+
|
|
1370
|
+
const summary = checks.reduce(
|
|
1371
|
+
(counts, check) => {
|
|
1372
|
+
if (check.ok) {
|
|
1373
|
+
counts.ok += 1
|
|
1374
|
+
} else {
|
|
1375
|
+
counts.error += 1
|
|
1376
|
+
}
|
|
1377
|
+
return counts
|
|
1378
|
+
},
|
|
1379
|
+
{ ok: 0, warn: 0, error: 0 },
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
runtime,
|
|
1384
|
+
checks,
|
|
1385
|
+
summary,
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function getContractConsistencyReport(customStatePath) {
|
|
1390
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
1391
|
+
const kitRoot = resolveKitRoot(projectRoot)
|
|
1392
|
+
const manifestPath = path.join(kitRoot, ".opencode", "opencode.json")
|
|
1393
|
+
const manifest = readJsonIfExists(manifestPath)
|
|
1394
|
+
|
|
1395
|
+
return buildContractConsistencyReport({ projectRoot: kitRoot, manifest })
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function validateState(customStatePath) {
|
|
1399
|
+
const { statePath, state, projectRoot, workItemId } = readManagedState(customStatePath)
|
|
1400
|
+
validateStateObject(state)
|
|
1401
|
+
validateManagedState(state, projectRoot, workItemId)
|
|
1402
|
+
return { statePath, state }
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function startTask(mode, featureId, featureSlug, modeReason, customStatePath) {
|
|
1406
|
+
ensureKnown(mode, MODE_VALUES, "mode")
|
|
1407
|
+
ensureString(featureId, "feature_id")
|
|
1408
|
+
ensureString(featureSlug, "feature_slug")
|
|
1409
|
+
ensureString(modeReason, "mode_reason")
|
|
1410
|
+
|
|
1411
|
+
const workItemId = deriveWorkItemId({ feature_id: featureId })
|
|
1412
|
+
const projectRoot = ensureWorkItemStoreReady(customStatePath)
|
|
1413
|
+
const workItemPaths = resolveWorkItemPaths(projectRoot, workItemId)
|
|
1414
|
+
const index = readWorkItemIndex(projectRoot)
|
|
1415
|
+
const existingState = fs.existsSync(workItemPaths.statePath) ? readWorkItemState(projectRoot, workItemId) : null
|
|
1416
|
+
const expectedRevision = existingState ? captureRevision(existingState) : null
|
|
1417
|
+
const expectedMirrorRevision = index.active_work_item_id === workItemId && existingState ? expectedRevision : null
|
|
1418
|
+
const nextState = createFreshState({
|
|
1419
|
+
workItemId,
|
|
1420
|
+
mode,
|
|
1421
|
+
featureId,
|
|
1422
|
+
featureSlug,
|
|
1423
|
+
modeReason,
|
|
1424
|
+
updatedAt: timestamp(),
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
validateStateObject(nextState)
|
|
1428
|
+
return persistManagedState(customStatePath, nextState, {
|
|
1429
|
+
expectedRevision,
|
|
1430
|
+
expectedMirrorRevision,
|
|
1431
|
+
workItemId,
|
|
1432
|
+
activateWorkItemId: workItemId,
|
|
1433
|
+
})
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function startFeature(featureId, featureSlug, customStatePath) {
|
|
1437
|
+
return startTask(
|
|
1438
|
+
"full",
|
|
1439
|
+
featureId,
|
|
1440
|
+
featureSlug,
|
|
1441
|
+
"Started with legacy start-feature command; defaulting to Full Delivery mode",
|
|
1442
|
+
customStatePath,
|
|
1443
|
+
)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function setApproval(gate, status, approvedBy, approvedAt, notes, customStatePath) {
|
|
1447
|
+
ensureKnown(status, ["pending", "approved", "rejected"], "status")
|
|
1448
|
+
|
|
1449
|
+
return mutate(customStatePath, (state, context) => {
|
|
1450
|
+
const allowedGates = getApprovalGatesForMode(state.mode)
|
|
1451
|
+
ensureKnown(gate, allowedGates, `gate for mode '${state.mode}'`)
|
|
1452
|
+
|
|
1453
|
+
const { projectRoot, workItemId } = context
|
|
1454
|
+
if (status === "approved" && gate === "tech_lead_to_fullstack") {
|
|
1455
|
+
requireValidTaskBoard(
|
|
1456
|
+
state,
|
|
1457
|
+
projectRoot,
|
|
1458
|
+
workItemId,
|
|
1459
|
+
"full_plan",
|
|
1460
|
+
"A valid task board is required before approving 'tech_lead_to_fullstack' or entering 'full_implementation'",
|
|
1461
|
+
)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
state.approvals[gate] = {
|
|
1465
|
+
status,
|
|
1466
|
+
approved_by: approvedBy ?? null,
|
|
1467
|
+
approved_at: approvedAt ?? null,
|
|
1468
|
+
notes: notes ?? null,
|
|
1469
|
+
}
|
|
1470
|
+
return state
|
|
1471
|
+
})
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function advanceStage(targetStage, customStatePath) {
|
|
1475
|
+
ensureKnown(targetStage, STAGE_SEQUENCE, "target stage")
|
|
1476
|
+
|
|
1477
|
+
return mutate(customStatePath, (state, context) => {
|
|
1478
|
+
const { projectRoot, workItemId } = context
|
|
1479
|
+
if (getModeForStage(targetStage) !== state.mode) {
|
|
1480
|
+
fail(`target stage '${targetStage}' does not belong to mode '${state.mode}'`)
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const nextStage = getNextStage(state.mode, state.current_stage)
|
|
1484
|
+
if (!nextStage) {
|
|
1485
|
+
fail(`Stage '${state.current_stage}' cannot advance further`)
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (targetStage !== nextStage) {
|
|
1489
|
+
fail(`advance-stage only allows the immediate next stage '${nextStage}', not '${targetStage}'`)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (targetStage === "full_implementation") {
|
|
1493
|
+
requireValidTaskBoard(
|
|
1494
|
+
state,
|
|
1495
|
+
projectRoot,
|
|
1496
|
+
workItemId,
|
|
1497
|
+
"full_implementation",
|
|
1498
|
+
"A valid task board is required before entering 'full_implementation'",
|
|
1499
|
+
)
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (targetStage === "full_qa") {
|
|
1503
|
+
requireValidTaskBoard(
|
|
1504
|
+
state,
|
|
1505
|
+
projectRoot,
|
|
1506
|
+
workItemId,
|
|
1507
|
+
"full_qa",
|
|
1508
|
+
"A valid task board is required before entering 'full_qa'",
|
|
1509
|
+
)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const requiredGate = getTransitionGate(state.mode, state.current_stage, targetStage)
|
|
1513
|
+
if (requiredGate && state.approvals[requiredGate].status !== "approved") {
|
|
1514
|
+
fail(`Cannot advance from '${state.current_stage}' to '${targetStage}' until gate '${requiredGate}' is approved`)
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
state.current_stage = targetStage
|
|
1518
|
+
state.current_owner = STAGE_OWNERS[targetStage]
|
|
1519
|
+
state.status = targetStage.endsWith("_done") ? "done" : "in_progress"
|
|
1520
|
+
return state
|
|
1521
|
+
})
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function linkArtifact(kind, artifactPath, customStatePath) {
|
|
1525
|
+
ensureKnown(kind, ARTIFACT_KINDS, "artifact kind")
|
|
1526
|
+
ensureString(artifactPath, "artifact path")
|
|
1527
|
+
|
|
1528
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
1529
|
+
const resolvedArtifactPath = path.isAbsolute(artifactPath)
|
|
1530
|
+
? artifactPath
|
|
1531
|
+
: path.resolve(projectRoot, artifactPath)
|
|
1532
|
+
if (!fs.existsSync(resolvedArtifactPath)) {
|
|
1533
|
+
fail(`Artifact path does not exist: '${artifactPath}'`)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
return mutate(customStatePath, (state) => {
|
|
1537
|
+
if (kind === "adr") {
|
|
1538
|
+
if (!state.artifacts.adr.includes(artifactPath)) {
|
|
1539
|
+
state.artifacts.adr.push(artifactPath)
|
|
1540
|
+
}
|
|
1541
|
+
return state
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
state.artifacts[kind] = artifactPath
|
|
1545
|
+
return state
|
|
1546
|
+
})
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function scaffoldAndLinkArtifact(kind, slug, customStatePath, options = {}) {
|
|
1550
|
+
ensureString(kind, "artifact kind")
|
|
1551
|
+
ensureString(slug, "artifact slug")
|
|
1552
|
+
|
|
1553
|
+
if (!SUPPORTED_SCAFFOLDS[kind]) {
|
|
1554
|
+
fail(`Unsupported scaffold kind '${kind}'`)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const { statePath, state } = readState(customStatePath)
|
|
1558
|
+
validateStateObject(state)
|
|
1559
|
+
|
|
1560
|
+
if (kind !== "adr" && state.artifacts[kind] !== null) {
|
|
1561
|
+
fail(`Artifact already linked for artifact kind '${kind}'`)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (kind === "task_card" && state.mode !== "quick") {
|
|
1565
|
+
fail(`Artifact scaffold kind 'task_card' requires quick mode`)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (kind === "plan") {
|
|
1569
|
+
if (state.mode !== "full" && state.mode !== "migration") {
|
|
1570
|
+
fail(`Artifact scaffold kind 'plan' requires full or migration mode`)
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (state.mode === "full" && state.current_stage !== "full_plan") {
|
|
1574
|
+
fail(`Artifact scaffold kind 'plan' requires current stage 'full_plan'`)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
if (state.mode === "migration" && state.current_stage !== "migration_strategy") {
|
|
1578
|
+
fail(`Artifact scaffold kind 'plan' requires current stage 'migration_strategy'`)
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (typeof state.artifacts.architecture !== "string" || state.artifacts.architecture.length === 0) {
|
|
1582
|
+
fail(`Artifact scaffold kind 'plan' requires a linked architecture artifact`)
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (kind === "migration_report") {
|
|
1587
|
+
if (state.mode !== "migration") {
|
|
1588
|
+
fail(`Artifact scaffold kind 'migration_report' requires migration mode`)
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (state.current_stage !== "migration_baseline" && state.current_stage !== "migration_strategy") {
|
|
1592
|
+
fail(`Artifact scaffold kind 'migration_report' requires current stage 'migration_baseline' or 'migration_strategy'`)
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const projectRoot = resolveProjectRoot(customStatePath)
|
|
1597
|
+
const featureId = state.feature_id
|
|
1598
|
+
const featureSlug = state.feature_slug
|
|
1599
|
+
|
|
1600
|
+
if (typeof featureId !== "string" || featureId.length === 0) {
|
|
1601
|
+
fail("feature_id must be set before scaffolding an artifact")
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (typeof featureSlug !== "string" || featureSlug.length === 0) {
|
|
1605
|
+
fail("feature_slug must be set before scaffolding an artifact")
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const scaffoldResult = scaffoldArtifact({
|
|
1609
|
+
projectRoot,
|
|
1610
|
+
kind,
|
|
1611
|
+
mode: state.mode,
|
|
1612
|
+
slug,
|
|
1613
|
+
featureId,
|
|
1614
|
+
featureSlug,
|
|
1615
|
+
sourceArchitecture:
|
|
1616
|
+
kind === "plan" || kind === "migration_report" ? state.artifacts.architecture : null,
|
|
1617
|
+
sourcePlan: kind === "migration_report" ? state.artifacts.plan : null,
|
|
1618
|
+
})
|
|
1619
|
+
|
|
1620
|
+
try {
|
|
1621
|
+
if (typeof options.beforeLink === "function") {
|
|
1622
|
+
options.beforeLink(scaffoldResult)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const next = linkArtifact(kind, scaffoldResult.artifactPath, statePath)
|
|
1626
|
+
|
|
1627
|
+
return {
|
|
1628
|
+
...next,
|
|
1629
|
+
artifactPath: scaffoldResult.artifactPath,
|
|
1630
|
+
}
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
const createdPath = path.join(projectRoot, scaffoldResult.artifactPath)
|
|
1633
|
+
if (fs.existsSync(createdPath)) {
|
|
1634
|
+
fs.rmSync(createdPath)
|
|
1635
|
+
}
|
|
1636
|
+
throw error
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function recordIssue(issue, customStatePath) {
|
|
1641
|
+
return mutate(customStatePath, (state) => {
|
|
1642
|
+
const nextIssue = { ...issue }
|
|
1643
|
+
if (typeof nextIssue.artifact_refs === "string") {
|
|
1644
|
+
nextIssue.artifact_refs = [nextIssue.artifact_refs]
|
|
1645
|
+
}
|
|
1646
|
+
ensureIssueShape(nextIssue, state.issues.length)
|
|
1647
|
+
state.issues.push(nextIssue)
|
|
1648
|
+
state.status = "blocked"
|
|
1649
|
+
return state
|
|
1650
|
+
})
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function clearIssues(customStatePath) {
|
|
1654
|
+
return mutate(customStatePath, (state) => {
|
|
1655
|
+
state.issues = []
|
|
1656
|
+
if (!state.current_stage.endsWith("_done")) {
|
|
1657
|
+
state.status = "in_progress"
|
|
1658
|
+
}
|
|
1659
|
+
return state
|
|
1660
|
+
})
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function routeRework(issueType, repeatFailedFix, customStatePath) {
|
|
1664
|
+
ensureKnown(issueType, ISSUE_TYPES, "issue_type")
|
|
1665
|
+
|
|
1666
|
+
return mutate(customStatePath, (state) => {
|
|
1667
|
+
const previousMode = state.mode
|
|
1668
|
+
const route = getReworkRoute(state.mode, issueType)
|
|
1669
|
+
if (!route) {
|
|
1670
|
+
fail(`No rework route exists for issue type '${issueType}' in mode '${state.mode}'`)
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (route.escalate) {
|
|
1674
|
+
state.mode = route.mode
|
|
1675
|
+
state.mode_reason = `Promoted from ${previousMode} mode after '${issueType}' QA finding`
|
|
1676
|
+
state.routing_profile = createDefaultRoutingProfile(route.mode, state.mode_reason)
|
|
1677
|
+
state.current_stage = route.stage
|
|
1678
|
+
state.current_owner = route.owner
|
|
1679
|
+
state.status = "in_progress"
|
|
1680
|
+
state.approvals = createEmptyApprovals(route.mode)
|
|
1681
|
+
state.escalated_from = previousMode
|
|
1682
|
+
state.escalation_reason = `${previousMode} work escalated to ${route.mode === "full" ? "Full Delivery" : route.mode} because QA reported '${issueType}'`
|
|
1683
|
+
} else {
|
|
1684
|
+
state.current_stage = route.stage
|
|
1685
|
+
state.current_owner = route.owner
|
|
1686
|
+
state.status = "in_progress"
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (repeatFailedFix) {
|
|
1690
|
+
state.retry_count += 1
|
|
1691
|
+
if (state.retry_count >= ESCALATION_RETRY_THRESHOLD) {
|
|
1692
|
+
state.status = "blocked"
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return state
|
|
1697
|
+
})
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
module.exports = {
|
|
1701
|
+
advanceStage,
|
|
1702
|
+
assignQaOwner,
|
|
1703
|
+
clearIssues,
|
|
1704
|
+
claimTask,
|
|
1705
|
+
createTask,
|
|
1706
|
+
createWorkItem,
|
|
1707
|
+
ESCALATION_RETRY_THRESHOLD,
|
|
1708
|
+
getContractConsistencyReport,
|
|
1709
|
+
getInstallManifest,
|
|
1710
|
+
getProfile,
|
|
1711
|
+
getRegistry,
|
|
1712
|
+
getRuntimeStatus,
|
|
1713
|
+
getVersionInfo,
|
|
1714
|
+
linkArtifact,
|
|
1715
|
+
listTasks,
|
|
1716
|
+
listWorkItems,
|
|
1717
|
+
listProfiles,
|
|
1718
|
+
readState,
|
|
1719
|
+
recordIssue,
|
|
1720
|
+
reassignTask,
|
|
1721
|
+
releaseTask,
|
|
1722
|
+
resolveStatePath,
|
|
1723
|
+
routeRework,
|
|
1724
|
+
runDoctor,
|
|
1725
|
+
scaffoldAndLinkArtifact,
|
|
1726
|
+
selectActiveWorkItem,
|
|
1727
|
+
setRoutingProfile,
|
|
1728
|
+
setTaskStatus,
|
|
1729
|
+
setApproval,
|
|
1730
|
+
showState,
|
|
1731
|
+
showWorkItemState,
|
|
1732
|
+
syncInstallManifest,
|
|
1733
|
+
startFeature,
|
|
1734
|
+
startTask,
|
|
1735
|
+
validateWorkItemBoard,
|
|
1736
|
+
validateState,
|
|
1737
|
+
validateStateObject,
|
|
1738
|
+
writeState,
|
|
1739
|
+
}
|