@duypham93/openkit 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/.opencode/README.md +47 -0
  2. package/.opencode/install-manifest.json +41 -0
  3. package/.opencode/lib/artifact-scaffolder.js +111 -0
  4. package/.opencode/lib/contract-consistency.js +218 -0
  5. package/.opencode/lib/parallel-execution-rules.js +261 -0
  6. package/.opencode/lib/runtime-paths.js +95 -0
  7. package/.opencode/lib/runtime-summary.js +82 -0
  8. package/.opencode/lib/state-guard.js +99 -0
  9. package/.opencode/lib/task-board-rules.js +375 -0
  10. package/.opencode/lib/work-item-store.js +280 -0
  11. package/.opencode/lib/workflow-state-controller.js +1739 -0
  12. package/.opencode/lib/workflow-state-rules.js +331 -0
  13. package/.opencode/opencode.json +93 -0
  14. package/.opencode/package.json +3 -0
  15. package/.opencode/tests/artifact-scaffolder.test.js +733 -0
  16. package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
  17. package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
  18. package/.opencode/tests/session-start-hook.test.js +357 -0
  19. package/.opencode/tests/state-guard.test.js +124 -0
  20. package/.opencode/tests/task-board-rules.test.js +204 -0
  21. package/.opencode/tests/work-item-store.test.js +380 -0
  22. package/.opencode/tests/workflow-behavior.test.js +149 -0
  23. package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
  24. package/.opencode/tests/workflow-state-cli.test.js +1275 -0
  25. package/.opencode/tests/workflow-state-controller.test.js +1038 -0
  26. package/.opencode/work-items/feature-001/state.json +70 -0
  27. package/.opencode/work-items/index.json +13 -0
  28. package/.opencode/workflow-state.js +489 -0
  29. package/.opencode/workflow-state.json +70 -0
  30. package/AGENTS.md +265 -0
  31. package/README.md +401 -0
  32. package/agents/architect-agent.md +63 -0
  33. package/agents/ba-agent.md +56 -0
  34. package/agents/code-reviewer.md +77 -0
  35. package/agents/fullstack-agent.md +115 -0
  36. package/agents/master-orchestrator.md +60 -0
  37. package/agents/pm-agent.md +56 -0
  38. package/agents/qa-agent.md +124 -0
  39. package/agents/tech-lead-agent.md +60 -0
  40. package/assets/install-bundle/README.md +7 -0
  41. package/assets/install-bundle/opencode/README.md +11 -0
  42. package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
  43. package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
  44. package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
  45. package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
  46. package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
  47. package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
  48. package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
  49. package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
  50. package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
  51. package/assets/install-bundle/opencode/commands/delivery.md +45 -0
  52. package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
  53. package/assets/install-bundle/opencode/commands/migrate.md +61 -0
  54. package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
  55. package/assets/install-bundle/opencode/commands/task.md +46 -0
  56. package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
  57. package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
  58. package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
  59. package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
  60. package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
  61. package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
  62. package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
  63. package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
  64. package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
  65. package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
  66. package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
  67. package/assets/opencode.json.template +11 -0
  68. package/assets/openkit-install.json.template +19 -0
  69. package/bin/openkit.js +9 -0
  70. package/commands/brainstorm.md +44 -0
  71. package/commands/delivery.md +45 -0
  72. package/commands/execute-plan.md +44 -0
  73. package/commands/migrate.md +61 -0
  74. package/commands/quick-task.md +45 -0
  75. package/commands/task.md +46 -0
  76. package/commands/write-plan.md +50 -0
  77. package/context/core/approval-gates.md +146 -0
  78. package/context/core/code-quality.md +42 -0
  79. package/context/core/issue-routing.md +85 -0
  80. package/context/core/lane-selection.md +54 -0
  81. package/context/core/project-config.md +143 -0
  82. package/context/core/session-resume.md +85 -0
  83. package/context/core/workflow-state-schema.md +224 -0
  84. package/context/core/workflow.md +442 -0
  85. package/context/navigation.md +94 -0
  86. package/docs/adr/README.md +6 -0
  87. package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
  88. package/docs/architecture/README.md +7 -0
  89. package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
  90. package/docs/briefs/README.md +7 -0
  91. package/docs/governance/README.md +25 -0
  92. package/docs/governance/adr-policy.md +27 -0
  93. package/docs/governance/definition-of-done.md +17 -0
  94. package/docs/governance/naming-conventions.md +21 -0
  95. package/docs/governance/severity-levels.md +12 -0
  96. package/docs/maintainer/README.md +51 -0
  97. package/docs/operations/README.md +79 -0
  98. package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
  99. package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
  100. package/docs/operations/internal-records/README.md +18 -0
  101. package/docs/operations/runbooks/README.md +23 -0
  102. package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
  103. package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
  104. package/docs/operator/README.md +50 -0
  105. package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
  106. package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
  107. package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
  108. package/docs/plans/README.md +7 -0
  109. package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
  110. package/docs/qa/README.md +7 -0
  111. package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
  112. package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
  113. package/docs/specs/README.md +7 -0
  114. package/docs/templates/README.md +36 -0
  115. package/docs/templates/adr-template.md +18 -0
  116. package/docs/templates/architecture-template.md +31 -0
  117. package/docs/templates/implementation-plan-template.md +32 -0
  118. package/docs/templates/migration-baseline-checklist.md +48 -0
  119. package/docs/templates/migration-plan-template.md +52 -0
  120. package/docs/templates/migration-report-template.md +74 -0
  121. package/docs/templates/migration-verify-checklist.md +39 -0
  122. package/docs/templates/product-brief-template.md +32 -0
  123. package/docs/templates/qa-report-template.md +37 -0
  124. package/docs/templates/quick-task-template.md +36 -0
  125. package/docs/templates/spec-template.md +31 -0
  126. package/hooks/hooks.json +16 -0
  127. package/hooks/session-start +162 -0
  128. package/package.json +24 -0
  129. package/registry.json +328 -0
  130. package/skills/brainstorming/SKILL.md +51 -0
  131. package/skills/code-review/SKILL.md +48 -0
  132. package/skills/subagent-driven-development/SKILL.md +79 -0
  133. package/skills/systematic-debugging/SKILL.md +61 -0
  134. package/skills/test-driven-development/SKILL.md +48 -0
  135. package/skills/using-skills/SKILL.md +39 -0
  136. package/skills/verification-before-completion/SKILL.md +137 -0
  137. package/skills/writing-plans/SKILL.md +68 -0
  138. package/skills/writing-specs/SKILL.md +47 -0
  139. package/src/audit/vietnamese-detection.js +259 -0
  140. package/src/cli/commands/detect-vietnamese.js +24 -0
  141. package/src/cli/commands/doctor.js +33 -0
  142. package/src/cli/commands/help.js +33 -0
  143. package/src/cli/commands/init.js +25 -0
  144. package/src/cli/commands/install-global.js +26 -0
  145. package/src/cli/commands/install.js +25 -0
  146. package/src/cli/commands/run.js +63 -0
  147. package/src/cli/commands/uninstall.js +32 -0
  148. package/src/cli/commands/upgrade.js +25 -0
  149. package/src/cli/conflict-output.js +19 -0
  150. package/src/cli/index.js +56 -0
  151. package/src/global/doctor.js +101 -0
  152. package/src/global/ensure-install.js +32 -0
  153. package/src/global/install-state.js +73 -0
  154. package/src/global/launcher.js +51 -0
  155. package/src/global/materialize.js +123 -0
  156. package/src/global/paths.js +85 -0
  157. package/src/global/uninstall.js +25 -0
  158. package/src/global/workspace-state.js +63 -0
  159. package/src/install/asset-manifest.js +284 -0
  160. package/src/install/conflicts.js +43 -0
  161. package/src/install/discovery.js +138 -0
  162. package/src/install/install-state.js +136 -0
  163. package/src/install/materialize.js +158 -0
  164. package/src/install/merge-policy.js +145 -0
  165. package/src/runtime/doctor.js +281 -0
  166. package/src/runtime/launcher.js +49 -0
  167. package/src/runtime/opencode-layering.js +135 -0
  168. package/src/runtime/openkit-managed-summary.js +27 -0
  169. package/tests/cli/openkit-cli.test.js +417 -0
  170. package/tests/global/doctor.test.js +130 -0
  171. package/tests/global/ensure-install.test.js +105 -0
  172. package/tests/install/discovery.test.js +124 -0
  173. package/tests/install/install-state.test.js +346 -0
  174. package/tests/install/materialize.test.js +244 -0
  175. package/tests/install/merge-policy.test.js +177 -0
  176. package/tests/runtime/doctor.test.js +430 -0
  177. package/tests/runtime/launcher.test.js +230 -0
  178. package/tests/runtime/module-boundary.test.js +16 -0
@@ -0,0 +1,95 @@
1
+ const crypto = require("crypto")
2
+ const fs = require("fs")
3
+ const os = require("os")
4
+ const path = require("path")
5
+
6
+ function getOpenCodeHome(env = process.env) {
7
+ if (env.OPENCODE_HOME) {
8
+ return path.resolve(env.OPENCODE_HOME)
9
+ }
10
+
11
+ if (process.platform === "win32") {
12
+ const base = env.APPDATA || path.join(os.homedir(), "AppData", "Roaming")
13
+ return path.join(base, "opencode")
14
+ }
15
+
16
+ const base = env.XDG_CONFIG_HOME ? path.resolve(env.XDG_CONFIG_HOME) : path.join(os.homedir(), ".config")
17
+ return path.join(base, "opencode")
18
+ }
19
+
20
+ function detectProjectRoot(startDir) {
21
+ let current = path.resolve(startDir || process.cwd())
22
+
23
+ while (true) {
24
+ if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"))) {
25
+ return current
26
+ }
27
+
28
+ const parent = path.dirname(current)
29
+ if (parent === current) {
30
+ return path.resolve(startDir || process.cwd())
31
+ }
32
+ current = parent
33
+ }
34
+ }
35
+
36
+ function createWorkspaceId(projectRoot) {
37
+ const seed = process.platform === "win32" ? projectRoot.toLowerCase() : projectRoot
38
+ return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16)
39
+ }
40
+
41
+ function resolveProjectRoot(customStatePath) {
42
+ if (process.env.OPENKIT_PROJECT_ROOT) {
43
+ return path.resolve(process.env.OPENKIT_PROJECT_ROOT)
44
+ }
45
+
46
+ if (customStatePath) {
47
+ if (process.env.OPENKIT_GLOBAL_MODE === "1" || process.env.OPENKIT_KIT_ROOT || process.env.OPENCODE_HOME) {
48
+ return detectProjectRoot(process.cwd())
49
+ }
50
+
51
+ return path.dirname(path.dirname(path.resolve(customStatePath)))
52
+ }
53
+
54
+ return detectProjectRoot(process.cwd())
55
+ }
56
+
57
+ function resolveKitRoot(projectRoot) {
58
+ if (process.env.OPENKIT_KIT_ROOT) {
59
+ return path.resolve(process.env.OPENKIT_KIT_ROOT)
60
+ }
61
+
62
+ return projectRoot
63
+ }
64
+
65
+ function resolveStatePath(customStatePath) {
66
+ if (customStatePath) {
67
+ return path.resolve(customStatePath)
68
+ }
69
+
70
+ if (process.env.OPENKIT_WORKFLOW_STATE) {
71
+ return path.resolve(process.env.OPENKIT_WORKFLOW_STATE)
72
+ }
73
+
74
+ if (process.env.OPENKIT_GLOBAL_MODE === "1" || process.env.OPENKIT_KIT_ROOT || process.env.OPENCODE_HOME) {
75
+ const projectRoot = resolveProjectRoot(customStatePath)
76
+ const workspaceId = createWorkspaceId(projectRoot)
77
+ return path.join(getOpenCodeHome(), "workspaces", workspaceId, "openkit", ".opencode", "workflow-state.json")
78
+ }
79
+
80
+ return path.resolve(process.cwd(), ".opencode", "workflow-state.json")
81
+ }
82
+
83
+ function resolveRuntimeRoot(customStatePath) {
84
+ return path.dirname(path.dirname(resolveStatePath(customStatePath)))
85
+ }
86
+
87
+ module.exports = {
88
+ createWorkspaceId,
89
+ detectProjectRoot,
90
+ getOpenCodeHome,
91
+ resolveKitRoot,
92
+ resolveProjectRoot,
93
+ resolveRuntimeRoot,
94
+ resolveStatePath,
95
+ }
@@ -0,0 +1,82 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+
4
+ const { readWorkItemIndex, resolveWorkItemPaths } = require("./work-item-store")
5
+
6
+ const ACTIVE_TASK_STATUSES = new Set(["claimed", "in_progress", "qa_in_progress"])
7
+
8
+ function readJsonIfExists(filePath) {
9
+ if (!fs.existsSync(filePath)) {
10
+ return null
11
+ }
12
+
13
+ return JSON.parse(fs.readFileSync(filePath, "utf8"))
14
+ }
15
+
16
+ function getWorkItemIndexIfExists(projectRoot) {
17
+ const indexPath = path.join(projectRoot, ".opencode", "work-items", "index.json")
18
+ if (!fs.existsSync(indexPath)) {
19
+ return null
20
+ }
21
+
22
+ return readWorkItemIndex(projectRoot)
23
+ }
24
+
25
+ function formatActiveTask(task) {
26
+ if (task.status === "qa_in_progress" && task.qa_owner) {
27
+ return `${task.task_id} (${task.status}, qa: ${task.qa_owner})`
28
+ }
29
+
30
+ if (task.primary_owner) {
31
+ return `${task.task_id} (${task.status}, primary: ${task.primary_owner})`
32
+ }
33
+
34
+ return `${task.task_id} (${task.status})`
35
+ }
36
+
37
+ function getTaskBoardDetails(projectRoot, state) {
38
+ if (!state || state.mode !== "full" || !state.work_item_id) {
39
+ return {
40
+ present: false,
41
+ summary: null,
42
+ }
43
+ }
44
+
45
+ const boardPath = path.join(resolveWorkItemPaths(projectRoot, state.work_item_id).workItemDir, "tasks.json")
46
+ const board = readJsonIfExists(boardPath)
47
+ if (!board) {
48
+ return {
49
+ present: false,
50
+ summary: null,
51
+ }
52
+ }
53
+
54
+ const tasks = Array.isArray(board.tasks) ? board.tasks : []
55
+ const activeTasks = tasks.filter((task) => ACTIVE_TASK_STATUSES.has(task.status))
56
+
57
+ return {
58
+ present: true,
59
+ summary: {
60
+ total: tasks.length,
61
+ ready: tasks.filter((task) => task.status === "ready").length,
62
+ active: activeTasks.length,
63
+ activeTasks: activeTasks.map(formatActiveTask),
64
+ },
65
+ }
66
+ }
67
+
68
+ function getRuntimeContext(projectRoot, state) {
69
+ const index = getWorkItemIndexIfExists(projectRoot)
70
+ const taskBoard = getTaskBoardDetails(projectRoot, state)
71
+
72
+ return {
73
+ activeWorkItemId: index?.active_work_item_id ?? state?.work_item_id ?? null,
74
+ workItemCount: index?.work_items?.length ?? null,
75
+ taskBoardPresent: taskBoard.present,
76
+ taskBoardSummary: taskBoard.summary,
77
+ }
78
+ }
79
+
80
+ module.exports = {
81
+ getRuntimeContext,
82
+ }
@@ -0,0 +1,99 @@
1
+ const crypto = require("crypto")
2
+
3
+ function fail(message, details = {}) {
4
+ const error = new Error(message)
5
+ error.isStateGuardError = true
6
+ Object.assign(error, details)
7
+ throw error
8
+ }
9
+
10
+ function sortJsonValue(value) {
11
+ if (Array.isArray(value)) {
12
+ return value.map(sortJsonValue)
13
+ }
14
+
15
+ if (value && typeof value === "object") {
16
+ return Object.keys(value)
17
+ .sort()
18
+ .reduce((sorted, key) => {
19
+ sorted[key] = sortJsonValue(value[key])
20
+ return sorted
21
+ }, {})
22
+ }
23
+
24
+ return value
25
+ }
26
+
27
+ function captureRevision(state) {
28
+ return crypto.createHash("sha256").update(JSON.stringify(sortJsonValue(state))).digest("hex")
29
+ }
30
+
31
+ function guardWrite({ currentState, expectedRevision, nextState }) {
32
+ const currentRevision = captureRevision(currentState)
33
+
34
+ if (expectedRevision && currentRevision !== expectedRevision) {
35
+ fail("Rejected stale write because the expected revision no longer matches persisted state", {
36
+ code: "STALE_WRITE",
37
+ currentRevision,
38
+ expectedRevision,
39
+ })
40
+ }
41
+
42
+ return {
43
+ currentRevision,
44
+ nextRevision: captureRevision(nextState),
45
+ nextState,
46
+ }
47
+ }
48
+
49
+ function planGuardedMirrorRefresh({ activeWorkItemId, targetWorkItemId, nextState }) {
50
+ const shouldRefreshMirror = activeWorkItemId === targetWorkItemId
51
+
52
+ return {
53
+ shouldRefreshMirror,
54
+ phases: shouldRefreshMirror ? ["primary", "replica"] : ["primary"],
55
+ stateRevision: captureRevision(nextState),
56
+ mirrorRevision: shouldRefreshMirror ? captureRevision(nextState) : null,
57
+ }
58
+ }
59
+
60
+ function detectMirrorDivergence({ activeWorkItemId, activeState, mirrorState }) {
61
+ const activeRevision = captureRevision(activeState)
62
+
63
+ if (!mirrorState) {
64
+ return {
65
+ activeWorkItemId,
66
+ activeRevision,
67
+ mirrorRevision: null,
68
+ isDiverged: true,
69
+ reason: "mirror_missing",
70
+ }
71
+ }
72
+
73
+ const mirrorRevision = captureRevision(mirrorState)
74
+
75
+ if (mirrorRevision !== activeRevision) {
76
+ return {
77
+ activeWorkItemId,
78
+ activeRevision,
79
+ mirrorRevision,
80
+ isDiverged: true,
81
+ reason: "revision_mismatch",
82
+ }
83
+ }
84
+
85
+ return {
86
+ activeWorkItemId,
87
+ activeRevision,
88
+ mirrorRevision,
89
+ isDiverged: false,
90
+ reason: null,
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ captureRevision,
96
+ detectMirrorDivergence,
97
+ guardWrite,
98
+ planGuardedMirrorRefresh,
99
+ }
@@ -0,0 +1,375 @@
1
+ const TASK_STATUS_VALUES = [
2
+ "queued",
3
+ "ready",
4
+ "claimed",
5
+ "in_progress",
6
+ "dev_done",
7
+ "qa_ready",
8
+ "qa_in_progress",
9
+ "done",
10
+ "blocked",
11
+ "cancelled",
12
+ ]
13
+
14
+ const REQUIRED_TASK_FIELDS = {
15
+ task_id: "string",
16
+ title: "string",
17
+ summary: "string",
18
+ kind: "string",
19
+ status: "string",
20
+ depends_on: "array",
21
+ blocked_by: "array",
22
+ artifact_refs: "array",
23
+ plan_refs: "array",
24
+ branch_or_worktree: "nullable_string",
25
+ created_by: "string",
26
+ created_at: "string",
27
+ updated_at: "string",
28
+ }
29
+
30
+ const IMPLEMENTATION_READY_STATUSES = new Set([
31
+ "ready",
32
+ "claimed",
33
+ "in_progress",
34
+ "dev_done",
35
+ "qa_ready",
36
+ "qa_in_progress",
37
+ ])
38
+
39
+ const DEPENDENCY_SATISFIED_STATUSES = new Set([
40
+ "dev_done",
41
+ "qa_ready",
42
+ "qa_in_progress",
43
+ "done",
44
+ "cancelled",
45
+ ])
46
+
47
+ const FULL_QA_ALLOWED_STATUSES = new Set([
48
+ "dev_done",
49
+ "qa_ready",
50
+ "qa_in_progress",
51
+ "done",
52
+ "cancelled",
53
+ ])
54
+
55
+ const TRANSITIONS = new Map([
56
+ ["queued", new Set(["ready", "cancelled"])],
57
+ ["ready", new Set(["claimed", "cancelled"])],
58
+ ["claimed", new Set(["in_progress", "cancelled"])],
59
+ ["in_progress", new Set(["dev_done", "cancelled"])],
60
+ ["dev_done", new Set(["qa_ready", "cancelled"])],
61
+ ["qa_ready", new Set(["qa_in_progress", "cancelled"])],
62
+ ["qa_in_progress", new Set(["done", "blocked", "claimed", "in_progress", "cancelled"])],
63
+ ["blocked", new Set(["claimed", "in_progress", "cancelled"])],
64
+ ["done", new Set(["cancelled"])],
65
+ ["cancelled", new Set()],
66
+ ])
67
+
68
+ function fail(message) {
69
+ throw new Error(message)
70
+ }
71
+
72
+ function formatModeLabel(mode) {
73
+ return typeof mode === "string" && mode.length > 0 ? `${mode.charAt(0).toUpperCase()}${mode.slice(1)}` : "Unknown"
74
+ }
75
+
76
+ function isNonEmptyString(value) {
77
+ return typeof value === "string" && value.trim().length > 0
78
+ }
79
+
80
+ function validateStringArrayEntries(field, values) {
81
+ for (const value of values) {
82
+ if (!isNonEmptyString(value)) {
83
+ fail(`Task field '${field}' must contain only non-empty strings`)
84
+ }
85
+ }
86
+ }
87
+
88
+ function validateTaskStatus(status) {
89
+ if (!TASK_STATUS_VALUES.includes(status)) {
90
+ fail(`Unknown task status '${status}'`)
91
+ }
92
+
93
+ return status
94
+ }
95
+
96
+ function validateTaskShape(task) {
97
+ if (!task || typeof task !== "object" || Array.isArray(task)) {
98
+ fail("Task must be an object")
99
+ }
100
+
101
+ for (const [field, kind] of Object.entries(REQUIRED_TASK_FIELDS)) {
102
+ if (!(field in task) || task[field] === undefined) {
103
+ fail(`Task is missing required field '${field}'`)
104
+ }
105
+
106
+ if (kind === "string" && !isNonEmptyString(task[field])) {
107
+ fail(`Task field '${field}' must be a non-empty string`)
108
+ }
109
+
110
+ if (kind === "array" && !Array.isArray(task[field])) {
111
+ fail(`Task field '${field}' must be an array`)
112
+ }
113
+
114
+ if (kind === "array" && Array.isArray(task[field])) {
115
+ validateStringArrayEntries(field, task[field])
116
+ }
117
+
118
+ if (
119
+ kind === "nullable_string" &&
120
+ task[field] !== null &&
121
+ task[field] !== undefined &&
122
+ !isNonEmptyString(task[field])
123
+ ) {
124
+ fail(`Task field '${field}' must be null or a non-empty string`)
125
+ }
126
+ }
127
+
128
+ if (task.primary_owner !== null && task.primary_owner !== undefined && !isNonEmptyString(task.primary_owner)) {
129
+ fail("Task field 'primary_owner' must be null or a non-empty string")
130
+ }
131
+
132
+ if (task.qa_owner !== null && task.qa_owner !== undefined && !isNonEmptyString(task.qa_owner)) {
133
+ fail("Task field 'qa_owner' must be null or a non-empty string")
134
+ }
135
+
136
+ validateTaskStatus(task.status)
137
+ return task
138
+ }
139
+
140
+ function hasBlockedDependencies(task) {
141
+ return Array.isArray(task.blocked_by) && task.blocked_by.length > 0
142
+ }
143
+
144
+ function validateTaskTransition(task, nextStatus, options = {}) {
145
+ validateTaskShape(task)
146
+ validateTaskStatus(nextStatus)
147
+
148
+ const allowedTargets = TRANSITIONS.get(task.status)
149
+ if (!allowedTargets || !allowedTargets.has(nextStatus)) {
150
+ fail(`Invalid task transition '${task.status} -> ${nextStatus}'`)
151
+ }
152
+
153
+ if ((task.status === "queued" && nextStatus === "ready") || (task.status === "ready" && nextStatus === "claimed")) {
154
+ if (hasBlockedDependencies(task)) {
155
+ fail(`Task '${task.task_id}' cannot move to '${nextStatus}' with a blocked dependency`)
156
+ }
157
+ }
158
+
159
+ if (task.status === "claimed" && nextStatus === "in_progress" && !isNonEmptyString(task.primary_owner)) {
160
+ fail(`Task '${task.task_id}' requires a primary_owner before entering 'in_progress'`)
161
+ }
162
+
163
+ if (task.status === "in_progress" && nextStatus === "dev_done" && !isNonEmptyString(task.primary_owner)) {
164
+ fail(`Task '${task.task_id}' requires a primary_owner before entering 'dev_done'`)
165
+ }
166
+
167
+ if (
168
+ task.status === "dev_done" &&
169
+ nextStatus === "qa_ready" &&
170
+ !isNonEmptyString(task.qa_owner) &&
171
+ options.allowQaAssignment !== true
172
+ ) {
173
+ fail(`Task '${task.task_id}' requires a qa_owner or explicit QA assignment availability before entering 'qa_ready'`)
174
+ }
175
+
176
+ if (
177
+ task.status === "qa_ready" &&
178
+ nextStatus === "qa_in_progress" &&
179
+ !isNonEmptyString(task.qa_owner) &&
180
+ options.allowQaClaim !== true
181
+ ) {
182
+ fail(`Task '${task.task_id}' requires a qa_owner or explicit QA claim availability before entering 'qa_in_progress'`)
183
+ }
184
+
185
+ if (
186
+ task.status === "qa_in_progress" &&
187
+ (nextStatus === "done" || nextStatus === "blocked") &&
188
+ !isNonEmptyString(task.qa_owner)
189
+ ) {
190
+ fail(`Task '${task.task_id}' requires a qa_owner before entering '${nextStatus}'`)
191
+ }
192
+
193
+ if (task.status === "qa_in_progress" && (nextStatus === "claimed" || nextStatus === "in_progress")) {
194
+ if (!options.allowQaFailRework) {
195
+ fail(`QA fail rework transitions require explicit allowQaFailRework for task '${task.task_id}'`)
196
+ }
197
+
198
+ if (!options.finding || typeof options.finding !== "object") {
199
+ fail(`QA fail rework transitions require a task-scoped finding for task '${task.task_id}'`)
200
+ }
201
+
202
+ if (options.finding.task_id !== task.task_id || !isNonEmptyString(options.finding.summary)) {
203
+ fail(`QA fail rework transitions require a task-scoped finding for task '${task.task_id}'`)
204
+ }
205
+ }
206
+
207
+ return nextStatus
208
+ }
209
+
210
+ function buildTaskIndex(tasks) {
211
+ const index = new Map()
212
+
213
+ for (const task of tasks) {
214
+ if (index.has(task.task_id)) {
215
+ fail(`Duplicate task_id '${task.task_id}' in task board`)
216
+ }
217
+
218
+ index.set(task.task_id, task)
219
+ }
220
+
221
+ return index
222
+ }
223
+
224
+ function validateDependencyReferences(tasks, taskIndex) {
225
+ for (const task of tasks) {
226
+ for (const dependencyId of task.depends_on) {
227
+ if (!taskIndex.has(dependencyId)) {
228
+ fail(`Task '${task.task_id}' depends on unknown task '${dependencyId}'`)
229
+ }
230
+ }
231
+
232
+ for (const blockedId of task.blocked_by) {
233
+ if (!taskIndex.has(blockedId)) {
234
+ fail(`Task '${task.task_id}' is blocked by unknown task '${blockedId}'`)
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ function detectDependencyCycle(tasks) {
241
+ const visiting = new Set()
242
+ const visited = new Set()
243
+ const taskIndex = buildTaskIndex(tasks)
244
+
245
+ function visit(taskId, trail) {
246
+ if (visiting.has(taskId)) {
247
+ fail(`Dependency cycle detected: ${[...trail, taskId].join(" -> ")}`)
248
+ }
249
+
250
+ if (visited.has(taskId)) {
251
+ return
252
+ }
253
+
254
+ visiting.add(taskId)
255
+ const task = taskIndex.get(taskId)
256
+ for (const dependencyId of task.depends_on) {
257
+ visit(dependencyId, [...trail, taskId])
258
+ }
259
+ visiting.delete(taskId)
260
+ visited.add(taskId)
261
+ }
262
+
263
+ for (const task of tasks) {
264
+ visit(task.task_id, [])
265
+ }
266
+ }
267
+
268
+ function validateDependencyState(tasks, taskIndex) {
269
+ for (const task of tasks) {
270
+ const unresolvedDependencies = task.depends_on.filter((dependencyId) => {
271
+ const dependency = taskIndex.get(dependencyId)
272
+ return !DEPENDENCY_SATISFIED_STATUSES.has(dependency.status)
273
+ })
274
+
275
+ const activeWhileBlocked = ["ready", "claimed", "in_progress"].includes(task.status)
276
+ if (activeWhileBlocked && unresolvedDependencies.length > 0) {
277
+ fail(`Task '${task.task_id}' cannot be '${task.status}' while blocked by unresolved dependencies: ${unresolvedDependencies.join(", ")}`)
278
+ }
279
+
280
+ if (task.status === "claimed" && !isNonEmptyString(task.primary_owner)) {
281
+ fail(`Task '${task.task_id}' in 'claimed' status requires a primary_owner`)
282
+ }
283
+
284
+ if (task.status === "in_progress" && !isNonEmptyString(task.primary_owner)) {
285
+ fail(`Task '${task.task_id}' in 'in_progress' status requires a primary_owner`)
286
+ }
287
+
288
+ if (task.status === "dev_done" && !isNonEmptyString(task.primary_owner)) {
289
+ fail(`Task '${task.task_id}' in 'dev_done' status requires a primary_owner`)
290
+ }
291
+
292
+ if ((task.status === "qa_in_progress" || task.status === "done" || task.status === "blocked") && !isNonEmptyString(task.qa_owner)) {
293
+ fail(`Task '${task.task_id}' in '${task.status}' status requires a qa_owner`)
294
+ }
295
+ }
296
+ }
297
+
298
+ function validateBoardShape(board) {
299
+ if (!board || typeof board !== "object" || Array.isArray(board)) {
300
+ fail("Task board must be an object")
301
+ }
302
+
303
+ if (board.mode !== "full") {
304
+ fail(`${formatModeLabel(board.mode)} mode cannot carry a task board; task boards are full-delivery only`)
305
+ }
306
+
307
+ if (!Array.isArray(board.tasks)) {
308
+ fail("Task board must include a tasks array")
309
+ }
310
+
311
+ if (board.tasks.length === 0) {
312
+ fail("Full-delivery task board must include at least one task")
313
+ }
314
+
315
+ if (board.issues !== undefined && !Array.isArray(board.issues)) {
316
+ fail("Task board issues must be an array when provided")
317
+ }
318
+ }
319
+
320
+ function validateAggregateRules(board, tasks) {
321
+ if (board.current_stage === "full_implementation") {
322
+ const hasImplementationReadyTask = tasks.some((task) => IMPLEMENTATION_READY_STATUSES.has(task.status))
323
+ if (!hasImplementationReadyTask) {
324
+ fail("A full_implementation board must include at least one implementation-active task")
325
+ }
326
+ }
327
+
328
+ if (board.current_stage === "full_qa") {
329
+ const hasIncompleteImplementationTask = tasks.some((task) => !FULL_QA_ALLOWED_STATUSES.has(task.status))
330
+ if (hasIncompleteImplementationTask) {
331
+ fail("A full_qa board cannot include tasks with incomplete implementation work")
332
+ }
333
+ }
334
+
335
+ if (board.current_stage === "full_done") {
336
+ const hasRemainingTask = tasks.some((task) => !["done", "cancelled"].includes(task.status))
337
+ if (hasRemainingTask) {
338
+ fail("A full_done board requires every required task to be done or cancelled")
339
+ }
340
+
341
+ const hasBlockingIssue = (board.issues ?? []).some((issue) => {
342
+ if (!issue || typeof issue !== "object") {
343
+ return false
344
+ }
345
+
346
+ return issue.blocks_completion === true && issue.status !== "resolved" && issue.status !== "closed"
347
+ })
348
+
349
+ if (hasBlockingIssue) {
350
+ fail("A full_done board cannot close with an open blocking issue")
351
+ }
352
+ }
353
+ }
354
+
355
+ function validateTaskBoard(board) {
356
+ validateBoardShape(board)
357
+
358
+ const tasks = board.tasks.map((task) => validateTaskShape(task))
359
+ const taskIndex = buildTaskIndex(tasks)
360
+
361
+ validateDependencyReferences(tasks, taskIndex)
362
+ detectDependencyCycle(tasks)
363
+ validateDependencyState(tasks, taskIndex)
364
+ validateAggregateRules(board, tasks)
365
+
366
+ return board
367
+ }
368
+
369
+ module.exports = {
370
+ TASK_STATUS_VALUES,
371
+ validateTaskBoard,
372
+ validateTaskShape,
373
+ validateTaskStatus,
374
+ validateTaskTransition,
375
+ }