ai-delivery-workflow 0.2.2

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/ai-delivery.mjs +138 -0
  4. package/bin/validate-skills.mjs +12 -0
  5. package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +125 -0
  6. package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +75 -0
  7. package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +160 -0
  8. package/docs/FILE-REFERENCE.zh-CN.md +503 -0
  9. package/docs/PROJECT-MANUAL.zh-CN.md +931 -0
  10. package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +135 -0
  11. package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +186 -0
  12. package/docs/VIEWER-MAINTENANCE.zh-CN.md +373 -0
  13. package/docs/VIEWER-USER-GUIDE.zh-CN.md +235 -0
  14. package/lib/delivery-state.mjs +1220 -0
  15. package/lib/evolution.mjs +971 -0
  16. package/lib/project-bootstrap.mjs +927 -0
  17. package/lib/project-installer.mjs +1184 -0
  18. package/lib/skill-validator.mjs +154 -0
  19. package/lib/toml-hooks.mjs +97 -0
  20. package/lib/workspace.mjs +302 -0
  21. package/lib/yaml-runtime.mjs +18 -0
  22. package/package.json +39 -0
  23. package/skills/ai-delivery-assemble-release/SKILL.md +27 -0
  24. package/skills/ai-delivery-assemble-release/agents/openai.yaml +4 -0
  25. package/skills/ai-delivery-assemble-release/assets/release-candidate-manifest.yaml +22 -0
  26. package/skills/ai-delivery-assemble-release/assets/release-git-plan.yaml +9 -0
  27. package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +2 -0
  28. package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +2 -0
  29. package/skills/ai-delivery-assemble-release/references/release-assembly-contract.md +7 -0
  30. package/skills/ai-delivery-assemble-release/scripts/plan-release.mjs +298 -0
  31. package/skills/ai-delivery-bootstrap/SKILL.md +59 -0
  32. package/skills/ai-delivery-bootstrap/agents/openai.yaml +4 -0
  33. package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +70 -0
  34. package/skills/ai-delivery-checkpoint-task/SKILL.md +63 -0
  35. package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +4 -0
  36. package/skills/ai-delivery-checkpoint-task/assets/codex-hook-config.toml +92 -0
  37. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/resume.md +15 -0
  38. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +30 -0
  39. package/skills/ai-delivery-checkpoint-task/assets/runtime-template/version-archive.json +10 -0
  40. package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +95 -0
  41. package/skills/ai-delivery-checkpoint-task/references/checkpoint-recovery.zh-CN.md +111 -0
  42. package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +131 -0
  43. package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1073 -0
  44. package/skills/ai-delivery-close-version/SKILL.md +33 -0
  45. package/skills/ai-delivery-close-version/agents/openai.yaml +4 -0
  46. package/skills/ai-delivery-close-version/assets/line-advance-record.yaml +12 -0
  47. package/skills/ai-delivery-close-version/assets/version-lineage.yaml +12 -0
  48. package/skills/ai-delivery-close-version/assets/version-manifest.yaml +20 -0
  49. package/skills/ai-delivery-close-version/references/version-closeout-contract.md +20 -0
  50. package/skills/ai-delivery-define-product/SKILL.md +76 -0
  51. package/skills/ai-delivery-define-product/agents/openai.yaml +4 -0
  52. package/skills/ai-delivery-define-product/assets/product-template/acceptance-criteria.csv +1 -0
  53. package/skills/ai-delivery-define-product/assets/product-template/discovery-baseline.md +19 -0
  54. package/skills/ai-delivery-define-product/assets/product-template/feature-detail.md +19 -0
  55. package/skills/ai-delivery-define-product/assets/product-template/feature-terminals.csv +1 -0
  56. package/skills/ai-delivery-define-product/assets/product-template/features.csv +1 -0
  57. package/skills/ai-delivery-define-product/assets/product-template/product-baseline.yaml +8 -0
  58. package/skills/ai-delivery-define-product/assets/product-template/product-manual.md +24 -0
  59. package/skills/ai-delivery-define-product/assets/product-template/terminals.csv +1 -0
  60. package/skills/ai-delivery-define-product/assets/product-template/user-stories.csv +1 -0
  61. package/skills/ai-delivery-define-product/references/product-contract.md +57 -0
  62. package/skills/ai-delivery-deploy-production/SKILL.md +45 -0
  63. package/skills/ai-delivery-deploy-production/agents/openai.yaml +4 -0
  64. package/skills/ai-delivery-deploy-production/references/deployment-contract.md +7 -0
  65. package/skills/ai-delivery-design-architecture/SKILL.md +67 -0
  66. package/skills/ai-delivery-design-architecture/agents/openai.yaml +4 -0
  67. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-baseline.md +25 -0
  68. package/skills/ai-delivery-design-architecture/assets/architecture-template/architecture-gate.yaml +15 -0
  69. package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +58 -0
  70. package/skills/ai-delivery-design-architecture/references/architecture-contract.md +32 -0
  71. package/skills/ai-delivery-design-experience/SKILL.md +71 -0
  72. package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -0
  73. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +1 -0
  74. package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +1 -0
  75. package/skills/ai-delivery-design-experience/assets/experience-template/component-state-matrix.csv +1 -0
  76. package/skills/ai-delivery-design-experience/assets/experience-template/design-tokens.json +19 -0
  77. package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +17 -0
  78. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-domains.csv +1 -0
  79. package/skills/ai-delivery-design-experience/assets/experience-template/experience-change-set.csv +1 -0
  80. package/skills/ai-delivery-design-experience/assets/experience-template/feature-screen-coverage.csv +1 -0
  81. package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +33 -0
  82. package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -0
  83. package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +32 -0
  84. package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +1 -0
  85. package/skills/ai-delivery-design-experience/assets/experience-template/page-component-map.csv +1 -0
  86. package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +28 -0
  87. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -0
  88. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +99 -0
  89. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +1 -0
  90. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +1 -0
  91. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +25 -0
  92. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +74 -0
  93. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -0
  94. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -0
  95. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +29 -0
  96. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -0
  97. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +22 -0
  98. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -0
  99. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +22 -0
  100. package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -0
  101. package/skills/ai-delivery-design-experience/assets/experience-template/requirement-change-set.csv +1 -0
  102. package/skills/ai-delivery-design-experience/assets/experience-template/screen-states.csv +1 -0
  103. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-candidates.csv +1 -0
  104. package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +18 -0
  105. package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +44 -0
  106. package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +16 -0
  107. package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +31 -0
  108. package/skills/ai-delivery-design-experience/references/experience-contract.md +82 -0
  109. package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +68 -0
  110. package/skills/ai-delivery-design-tests/SKILL.md +56 -0
  111. package/skills/ai-delivery-design-tests/agents/openai.yaml +4 -0
  112. package/skills/ai-delivery-design-tests/assets/test-design-template/iteration-test-contract.md +19 -0
  113. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -0
  114. package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +73 -0
  115. package/skills/ai-delivery-design-tests/references/test-design-contract.md +31 -0
  116. package/skills/ai-delivery-develop-iteration/SKILL.md +66 -0
  117. package/skills/ai-delivery-develop-iteration/agents/openai.yaml +4 -0
  118. package/skills/ai-delivery-develop-iteration/assets/development-template/task-experience-change-traceability.csv +1 -0
  119. package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -0
  120. package/skills/ai-delivery-develop-iteration/references/development-contract.md +39 -0
  121. package/skills/ai-delivery-evolve-workflow/SKILL.md +57 -0
  122. package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +4 -0
  123. package/skills/ai-delivery-evolve-workflow/references/evolution-contract.md +42 -0
  124. package/skills/ai-delivery-execute-test/SKILL.md +34 -0
  125. package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -0
  126. package/skills/ai-delivery-execute-test/references/test-execution-contract.md +7 -0
  127. package/skills/ai-delivery-execute-work-package/SKILL.md +53 -0
  128. package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -0
  129. package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +20 -0
  130. package/skills/ai-delivery-manage-git/SKILL.md +60 -0
  131. package/skills/ai-delivery-manage-git/agents/openai.yaml +4 -0
  132. package/skills/ai-delivery-manage-git/references/git-policy.md +42 -0
  133. package/skills/ai-delivery-manage-standards/SKILL.md +44 -0
  134. package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -0
  135. package/skills/ai-delivery-manage-standards/assets/standards-template/standard.md +25 -0
  136. package/skills/ai-delivery-manage-standards/references/standards-contract.md +20 -0
  137. package/skills/ai-delivery-orchestrate/SKILL.md +105 -0
  138. package/skills/ai-delivery-orchestrate/agents/openai.yaml +4 -0
  139. package/skills/ai-delivery-orchestrate/assets/project-template/.codex/config.toml +92 -0
  140. package/skills/ai-delivery-orchestrate/assets/project-template/.gitattributes +6 -0
  141. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/artifact-registry.yaml +3 -0
  142. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +6 -0
  143. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/standards-baseline.yaml +8 -0
  144. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +34 -0
  145. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/decisions/.gitkeep +0 -0
  146. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/experiments/.gitkeep +0 -0
  147. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/feedback/.gitkeep +0 -0
  148. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/index.yaml +7 -0
  149. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/promotion/.gitkeep +0 -0
  150. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/evolution/proposals/.gitkeep +0 -0
  151. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/extensions/.gitkeep +0 -0
  152. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-ARTIFACT-PATH-001.md +47 -0
  153. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-DOC-LANG-001.md +62 -0
  154. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/STD-TEST-LOCATION-001.md +23 -0
  155. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/standards/index.yaml +36 -0
  156. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/bootstrap/bootstrap.mjs +33 -0
  157. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/evolution/evolve.mjs +15 -0
  158. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +131 -0
  159. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/state/state.mjs +21 -0
  160. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +546 -0
  161. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +199 -0
  162. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +529 -0
  163. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +780 -0
  164. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +4 -0
  165. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +2 -0
  166. package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.sh +4 -0
  167. package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +44 -0
  168. package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +64 -0
  169. package/skills/ai-delivery-orchestrate/references/workflow-model.md +29 -0
  170. package/skills/ai-delivery-orchestrate-release/SKILL.md +33 -0
  171. package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -0
  172. package/skills/ai-delivery-orchestrate-release/assets/release-request.yaml +9 -0
  173. package/skills/ai-delivery-orchestrate-release/assets/version-selection.yaml +16 -0
  174. package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +11 -0
  175. package/skills/ai-delivery-plan-iteration/SKILL.md +57 -0
  176. package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -0
  177. package/skills/ai-delivery-plan-iteration/assets/planning-template/backlog.csv +1 -0
  178. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-dependencies.csv +1 -0
  179. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-experience-change-traceability.csv +1 -0
  180. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -0
  181. package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-sources.csv +1 -0
  182. package/skills/ai-delivery-plan-iteration/assets/planning-template/vertical-slices.csv +1 -0
  183. package/skills/ai-delivery-plan-iteration/references/planning-contract.md +40 -0
  184. package/skills/ai-delivery-prepare-platform/SKILL.md +49 -0
  185. package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -0
  186. package/skills/ai-delivery-prepare-platform/assets/platform-template/environment-ledger.csv +1 -0
  187. package/skills/ai-delivery-prepare-platform/references/platform-contract.md +18 -0
  188. package/skills/ai-delivery-prepare-release/SKILL.md +47 -0
  189. package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -0
  190. package/skills/ai-delivery-prepare-release/assets/release-template/delivery-items.csv +1 -0
  191. package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +18 -0
  192. package/skills/ai-delivery-prepare-release/references/release-contract.md +21 -0
  193. package/skills/ai-delivery-review-change/SKILL.md +40 -0
  194. package/skills/ai-delivery-review-change/agents/openai.yaml +4 -0
  195. package/skills/ai-delivery-review-change/references/review-contract.md +20 -0
  196. package/skills/ai-delivery-triage-failure/SKILL.md +45 -0
  197. package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -0
  198. package/skills/ai-delivery-triage-failure/references/failure-routing-contract.md +13 -0
  199. package/skills/ai-delivery-validate-artifacts/SKILL.md +37 -0
  200. package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -0
  201. package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +44 -0
  202. package/skills/ai-delivery-verify-candidate/SKILL.md +56 -0
  203. package/skills/ai-delivery-verify-candidate/agents/openai.yaml +4 -0
  204. package/skills/ai-delivery-verify-candidate/assets/verification-template/defect-artifacts.csv +1 -0
  205. package/skills/ai-delivery-verify-candidate/assets/verification-template/defects.csv +1 -0
  206. package/skills/ai-delivery-verify-candidate/assets/verification-template/test-work-packages.csv +1 -0
  207. package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +23 -0
  208. package/skills/ai-delivery-verify-production/SKILL.md +50 -0
  209. package/skills/ai-delivery-verify-production/agents/openai.yaml +4 -0
  210. package/skills/ai-delivery-verify-production/assets/production-verification-template/production-verification.md +15 -0
  211. package/skills/ai-delivery-verify-production/references/production-verification-contract.md +14 -0
@@ -0,0 +1,1073 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
6
+
7
+ const moduleRequire = createRequire(import.meta.url);
8
+
9
+ const VALID_STATUS = new Set([
10
+ "queued", "preparing", "running", "checkpointed", "pending-review",
11
+ "blocked", "interrupted", "completed", "failed", "abandoned",
12
+ ]);
13
+ const FINISH_STATUS = new Set(["completed", "blocked", "interrupted", "failed", "abandoned"]);
14
+ const CLOSED_STATUS = new Set(["completed", "failed", "abandoned"]);
15
+ const RESUMABLE_STATUS = new Set([
16
+ "preparing", "running", "checkpointed", "pending-review", "blocked", "interrupted",
17
+ ]);
18
+ const VALID_NEXT_ACTION_OWNER = new Set(["agent", "user", "external"]);
19
+ const REPEATABLE_ARGS = new Set([
20
+ "scope", "input-artifact", "standard", "planned-output", "actual-output",
21
+ "completed-step", "blocker", "evidence", "depends-on",
22
+ ]);
23
+
24
+ function parseArgs(argv) {
25
+ const result = { _: [] };
26
+ for (let index = 0; index < argv.length; index += 1) {
27
+ const value = argv[index];
28
+ if (!value.startsWith("--")) {
29
+ result._.push(value);
30
+ continue;
31
+ }
32
+ const key = value.slice(2);
33
+ const next = argv[index + 1];
34
+ const parsed = next && !next.startsWith("--") ? (index += 1, next) : true;
35
+ if (REPEATABLE_ARGS.has(key)) {
36
+ result[key] = [...(result[key] || []), parsed];
37
+ } else {
38
+ result[key] = parsed;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function requireValue(args, key) {
45
+ const value = args[key];
46
+ if (!value || value === true) throw new Error(`Missing --${key}`);
47
+ return value;
48
+ }
49
+
50
+ function now() {
51
+ return new Date().toISOString();
52
+ }
53
+
54
+ function sanitizeId(value) {
55
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
56
+ throw new Error(`Invalid identifier: ${value}`);
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function parseSequence(value) {
62
+ if (value === undefined || value === null || value === "") return null;
63
+ const sequence = Number(value);
64
+ if (!Number.isInteger(sequence) || sequence < 1) {
65
+ throw new Error(`Invalid positive integer sequence: ${value}`);
66
+ }
67
+ return sequence;
68
+ }
69
+
70
+ function parseNextActionOwner(value, fallback = "agent") {
71
+ const owner = value === undefined ? fallback : value;
72
+ if (!VALID_NEXT_ACTION_OWNER.has(owner)) {
73
+ throw new Error(`Invalid next action owner: ${owner}; use agent, user, or external.`);
74
+ }
75
+ return owner;
76
+ }
77
+
78
+ function requestedNextActionOwner(args) {
79
+ if (args["next-action-owner"] === undefined) return null;
80
+ return parseNextActionOwner(args["next-action-owner"]);
81
+ }
82
+
83
+ function assertWaitStateTransfer(task, args, command) {
84
+ const requestedOwner = requestedNextActionOwner(args);
85
+ if (task.next_action_owner === "agent") return requestedOwner;
86
+ if (requestedOwner !== "agent") {
87
+ throw new Error(
88
+ `Task ${task.task_id} is waiting for ${task.next_action_owner}; ${command} requires an explicit --next-action-owner agent after that input exists.`,
89
+ );
90
+ }
91
+ if (!args["next-action"] || args["next-action"] === true) {
92
+ throw new Error(`Task ${task.task_id} requires a new --next-action when ownership transfers to agent.`);
93
+ }
94
+ return requestedOwner;
95
+ }
96
+
97
+ function findProjectRoot(start = process.cwd()) {
98
+ let current = path.resolve(start);
99
+ while (true) {
100
+ if (fs.existsSync(path.join(current, ".workflow", "delivery", "project.yaml"))) return current;
101
+ if (fs.existsSync(path.join(current, "delivery", "project.yaml"))) return current;
102
+ const parent = path.dirname(current);
103
+ if (parent === current) return path.resolve(start);
104
+ current = parent;
105
+ }
106
+ }
107
+
108
+ function workflowRootFor(projectRoot) {
109
+ if (fs.existsSync(path.join(projectRoot, ".workflow", "delivery", "project.yaml"))) {
110
+ return path.join(projectRoot, ".workflow");
111
+ }
112
+ if (fs.existsSync(path.join(projectRoot, "delivery", "project.yaml"))) return projectRoot;
113
+ return path.join(projectRoot, ".workflow");
114
+ }
115
+
116
+ function taskMarkerFile(stateRoot) {
117
+ return path.join(workflowRootFor(stateRoot), "delivery", "runtime", "active-task.json");
118
+ }
119
+
120
+ function readTaskMarker(start = process.cwd()) {
121
+ const markerFile = taskMarkerFile(findProjectRoot(start));
122
+ if (!markerFile) return null;
123
+ if (!fs.existsSync(markerFile)) return null;
124
+ const marker = JSON.parse(fs.readFileSync(markerFile, "utf8"));
125
+ if (!marker.task_id || !marker.state_root) {
126
+ throw new Error(`Invalid task marker: ${markerFile}`);
127
+ }
128
+ return { ...marker, marker_file: markerFile };
129
+ }
130
+
131
+ function statePaths(args) {
132
+ const projectRoot = path.resolve(args.root || findProjectRoot());
133
+ const marker = readTaskMarker(projectRoot);
134
+ const stateRoot = path.resolve(args["state-root"] || marker?.state_root || projectRoot);
135
+ const workflowRoot = workflowRootFor(stateRoot);
136
+ const runtime = path.join(workflowRoot, "delivery", "runtime");
137
+ return { projectRoot, stateRoot, workflowRoot, runtime, marker };
138
+ }
139
+
140
+ function atomicWrite(file, value) {
141
+ fs.mkdirSync(path.dirname(file), { recursive: true });
142
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
143
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
144
+ fs.renameSync(temp, file);
145
+ }
146
+
147
+ function sleep(ms) {
148
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
149
+ }
150
+
151
+ function withTaskLock(runtime, taskId, action) {
152
+ const lockDir = path.join(runtime, "locks");
153
+ const lockFile = path.join(lockDir, `${taskId}.lock`);
154
+ fs.mkdirSync(lockDir, { recursive: true });
155
+ for (let attempt = 0; attempt < 50; attempt += 1) {
156
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
157
+ let fd;
158
+ try {
159
+ fd = fs.openSync(lockFile, "wx");
160
+ } catch (error) {
161
+ if (error.code !== "EEXIST") throw error;
162
+ let age;
163
+ try {
164
+ age = Date.now() - fs.statSync(lockFile).mtimeMs;
165
+ } catch (statError) {
166
+ if (statError.code === "ENOENT") continue;
167
+ throw statError;
168
+ }
169
+ if (age > 60_000) {
170
+ const staleFile = `${lockFile}.${process.pid}.${Date.now()}.stale`;
171
+ try {
172
+ fs.renameSync(lockFile, staleFile);
173
+ fs.unlinkSync(staleFile);
174
+ } catch (staleError) {
175
+ if (staleError.code !== "ENOENT" && staleError.code !== "EACCES" && staleError.code !== "EPERM") {
176
+ throw staleError;
177
+ }
178
+ }
179
+ continue;
180
+ }
181
+ sleep(20);
182
+ continue;
183
+ }
184
+ try {
185
+ fs.writeFileSync(fd, `${token} ${now()}\n`, "utf8");
186
+ return action();
187
+ } finally {
188
+ fs.closeSync(fd);
189
+ try {
190
+ const owner = fs.readFileSync(lockFile, "utf8");
191
+ if (owner.startsWith(`${token} `)) fs.unlinkSync(lockFile);
192
+ } catch (error) {
193
+ if (error.code !== "ENOENT") throw error;
194
+ }
195
+ }
196
+ }
197
+ throw new Error(`Timed out acquiring task lock: ${taskId}`);
198
+ }
199
+
200
+ function taskFile(runtime, taskId) {
201
+ return path.join(runtime, "tasks", `${taskId}.json`);
202
+ }
203
+
204
+ function sharedTaskFile(workflowRoot, taskId) {
205
+ return path.join(workflowRoot, "control", "tasks", `${taskId}.yaml`);
206
+ }
207
+
208
+ function portablePath(root, target) {
209
+ const relative = path.relative(root, target);
210
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
211
+ ? relative.replaceAll("\\", "/")
212
+ : target;
213
+ }
214
+
215
+ function parseWorkspaceConfiguration(workflowRoot) {
216
+ const workspaceFile = path.join(workflowRoot, "config", "workspace.yaml");
217
+ if (!fs.existsSync(workspaceFile)) return null;
218
+ let yaml;
219
+ try {
220
+ yaml = moduleRequire("yaml");
221
+ } catch {
222
+ const vendorRoot = path.join(workflowRoot, "tools", "state", "vendor", "yaml");
223
+ const vendorRequire = createRequire(path.join(vendorRoot, "package.json"));
224
+ yaml = vendorRequire("./");
225
+ }
226
+ return { file: workspaceFile, configuration: yaml.parse(fs.readFileSync(workspaceFile, "utf8")) };
227
+ }
228
+
229
+ function configuredCodeState(workflowRoot) {
230
+ const workspace = parseWorkspaceConfiguration(workflowRoot);
231
+ if (!workspace) {
232
+ throw new Error(`Authoritative workspace configuration is missing: ${path.join(workflowRoot, "config", "workspace.yaml")}`);
233
+ }
234
+ const codePath = workspace.configuration?.code_repository?.path;
235
+ const normalized = codePath && !path.isAbsolute(codePath)
236
+ ? path.normalize(codePath).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "")
237
+ : null;
238
+ if (!normalized || normalized.includes("/") || normalized === "." || normalized === "..") {
239
+ throw new Error(`Code repository path must be a relative direct child in ${workspace.file}`);
240
+ }
241
+ const projectRoot = path.basename(workflowRoot) === ".workflow"
242
+ ? path.dirname(workflowRoot)
243
+ : workflowRoot;
244
+ const codeRoot = path.resolve(projectRoot, normalized);
245
+ const relative = path.relative(projectRoot, codeRoot);
246
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
247
+ throw new Error(`Code repository path escapes the project: ${codePath}`);
248
+ }
249
+ const code = gitState(codeRoot);
250
+ if (!code.repository || path.resolve(code.root).toLowerCase() !== codeRoot.toLowerCase()) {
251
+ throw new Error(`Configured code repository is not an independent Git root: ${codeRoot}`);
252
+ }
253
+ return code;
254
+ }
255
+
256
+ function writeSharedTask(runtime, workflowRoot, task) {
257
+ const sharedFile = sharedTaskFile(workflowRoot, task.task_id);
258
+ let previousRevision = 0;
259
+ if (fs.existsSync(sharedFile)) {
260
+ previousRevision = Number(JSON.parse(fs.readFileSync(sharedFile, "utf8")).shared_revision || 0);
261
+ const localRevision = Number(task.shared_revision || 0);
262
+ if (localRevision !== previousRevision) {
263
+ throw new Error(
264
+ `Local and shared task state conflict for ${task.task_id}: local revision ${localRevision}, shared revision ${previousRevision}; recover before updating.`,
265
+ );
266
+ }
267
+ }
268
+ task.shared_revision = previousRevision + 1;
269
+ const code = configuredCodeState(workflowRoot);
270
+ const shared = {
271
+ ...task,
272
+ state_root: ".",
273
+ worktree: portablePath(task.state_root, task.worktree),
274
+ code_branch: code.branch || task.branch || null,
275
+ code_commit: code.head || null,
276
+ };
277
+ atomicWrite(taskFile(runtime, task.task_id), task);
278
+ writeCheckpoint(runtime, task);
279
+ atomicWrite(sharedFile, shared);
280
+ const checkpointId = `${task.shared_revision}-${task.updated_at.replaceAll(":", "-")}`;
281
+ atomicWrite(path.join(workflowRoot, "control", "checkpoints", task.task_id, `${checkpointId}.yaml`), shared);
282
+ }
283
+
284
+ function readSharedTasks(workflowRoot, stateRoot) {
285
+ const directory = path.join(workflowRoot, "control", "tasks");
286
+ if (!fs.existsSync(directory)) return [];
287
+ return fs.readdirSync(directory)
288
+ .filter((name) => name.endsWith(".yaml"))
289
+ .map((name) => {
290
+ const shared = normalizeTask(JSON.parse(fs.readFileSync(path.join(directory, name), "utf8")));
291
+ shared.state_root = stateRoot;
292
+ shared.worktree = path.isAbsolute(shared.worktree)
293
+ ? shared.worktree
294
+ : path.resolve(stateRoot, shared.worktree || "code");
295
+ return shared;
296
+ });
297
+ }
298
+
299
+ function archivedTaskIndexFile(runtime, taskId) {
300
+ return path.join(runtime, "archive", "index", "tasks", `${taskId}.json`);
301
+ }
302
+
303
+ function archivedVersionIndexFile(runtime, iterationId) {
304
+ return path.join(runtime, "archive", "index", "versions", `${iterationId}.json`);
305
+ }
306
+
307
+ function readTask(runtime, taskId) {
308
+ const file = taskFile(runtime, taskId);
309
+ if (!fs.existsSync(file)) throw new Error(`Task not found: ${taskId}`);
310
+ return normalizeTask(JSON.parse(fs.readFileSync(file, "utf8")));
311
+ }
312
+
313
+ function normalizeTask(task) {
314
+ for (const key of [
315
+ "scope", "input_artifacts", "standards", "planned_outputs", "actual_outputs",
316
+ "completed_steps", "blockers", "evidence", "depends_on",
317
+ ]) {
318
+ if (!Array.isArray(task[key])) task[key] = [];
319
+ task[key] = [...new Set(task[key])];
320
+ }
321
+ task.sequence = parseSequence(task.sequence);
322
+ task.next_action_owner = parseNextActionOwner(task.next_action_owner);
323
+ return task;
324
+ }
325
+
326
+ function stableValue(value) {
327
+ if (Array.isArray(value)) return value.map(stableValue);
328
+ if (!value || typeof value !== "object") return value;
329
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
330
+ }
331
+
332
+ function comparableTaskState(task) {
333
+ const comparable = { ...task };
334
+ const stateRoot = path.resolve(comparable.state_root || ".");
335
+ comparable.state_root = process.platform === "win32" ? stateRoot.toLowerCase() : stateRoot;
336
+ if (comparable.worktree) {
337
+ const worktree = path.resolve(stateRoot, comparable.worktree);
338
+ comparable.worktree = process.platform === "win32" ? worktree.toLowerCase() : worktree;
339
+ }
340
+ for (const key of ["code_branch", "code_commit"]) delete comparable[key];
341
+ return comparable;
342
+ }
343
+
344
+ function taskStateChecksum(task) {
345
+ return crypto.createHash("sha256")
346
+ .update(JSON.stringify(stableValue(comparableTaskState(task))))
347
+ .digest("hex");
348
+ }
349
+
350
+ function assertTaskSourcesMatch(local, shared) {
351
+ if (Number(local.shared_revision || 0) !== Number(shared.shared_revision || 0)) {
352
+ throw new Error(
353
+ `Local and shared task state conflict for ${local.task_id}: local revision ${local.shared_revision || 0}, shared revision ${shared.shared_revision || 0}; reconcile before recovery.`,
354
+ );
355
+ }
356
+ if (taskStateChecksum(local) !== taskStateChecksum(shared)) {
357
+ throw new Error(
358
+ `Local and shared task state conflict for ${local.task_id}: revision ${local.shared_revision || 0} has different content; reconcile before recovery.`,
359
+ );
360
+ }
361
+ }
362
+
363
+ function taskSources(runtime, workflowRoot, stateRoot, taskId) {
364
+ const localFile = taskFile(runtime, taskId);
365
+ const local = fs.existsSync(localFile) ? readTask(runtime, taskId) : null;
366
+ const shared = readSharedTasks(workflowRoot, stateRoot).find((task) => task.task_id === taskId) || null;
367
+ if (local && shared) assertTaskSourcesMatch(local, shared);
368
+ return { local, shared, task: local || shared };
369
+ }
370
+
371
+ function appendEvent(runtime, taskId, type, details = {}) {
372
+ const file = path.join(runtime, "events", `${taskId}.jsonl`);
373
+ fs.mkdirSync(path.dirname(file), { recursive: true });
374
+ fs.appendFileSync(file, `${JSON.stringify({ at: now(), type, task_id: taskId, ...details })}\n`, "utf8");
375
+ }
376
+
377
+ function copyIfExists(source, destination) {
378
+ if (!fs.existsSync(source)) return;
379
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
380
+ if (fs.statSync(source).isDirectory()) fs.cpSync(source, destination, { recursive: true, force: true });
381
+ else fs.copyFileSync(source, destination);
382
+ }
383
+
384
+ function removeIfExists(target) {
385
+ if (!fs.existsSync(target)) return;
386
+ if (fs.statSync(target).isDirectory()) fs.rmSync(target, { recursive: true, force: true });
387
+ else fs.unlinkSync(target);
388
+ }
389
+
390
+ function clearTaskMarker(task) {
391
+ if (!task.state_root) return;
392
+ const markerFile = taskMarkerFile(task.state_root);
393
+ if (!fs.existsSync(markerFile)) return;
394
+ try {
395
+ const marker = JSON.parse(fs.readFileSync(markerFile, "utf8"));
396
+ if (marker.task_id === task.task_id) fs.unlinkSync(markerFile);
397
+ } catch {
398
+ // Preserve an invalid marker for explicit reconciliation.
399
+ }
400
+ }
401
+
402
+ function removeActiveTaskBundle(runtime, task) {
403
+ removeIfExists(taskFile(runtime, task.task_id));
404
+ removeIfExists(path.join(runtime, "checkpoints", task.task_id));
405
+ removeIfExists(path.join(runtime, "events", `${task.task_id}.jsonl`));
406
+ removeIfExists(path.join(runtime, "heartbeats", `${task.task_id}.json`));
407
+ removeIfExists(path.join(runtime, "resumes", `${task.task_id}.md`));
408
+ clearTaskMarker(task);
409
+ }
410
+
411
+ function removeSharedTaskBundle(workflowRoot, task) {
412
+ removeIfExists(sharedTaskFile(workflowRoot, task.task_id));
413
+ removeIfExists(path.join(workflowRoot, "control", "checkpoints", task.task_id));
414
+ }
415
+
416
+ function copyTaskToVersionArchive(runtime, task, archiveDirectory, archivedAt) {
417
+ const taskDirectory = path.join(archiveDirectory, "tasks", task.task_id);
418
+ const archivedTask = {
419
+ ...task,
420
+ archived_at: archivedAt,
421
+ archive_path: path.relative(task.state_root, taskDirectory).replaceAll("\\", "/"),
422
+ };
423
+ fs.mkdirSync(taskDirectory, { recursive: true });
424
+ copyIfExists(path.join(runtime, "checkpoints", task.task_id), path.join(taskDirectory, "checkpoints"));
425
+ copyIfExists(path.join(runtime, "events", `${task.task_id}.jsonl`), path.join(taskDirectory, "events.jsonl"));
426
+ copyIfExists(path.join(runtime, "heartbeats", `${task.task_id}.json`), path.join(taskDirectory, "heartbeat.json"));
427
+ copyIfExists(path.join(runtime, "resumes", `${task.task_id}.md`), path.join(taskDirectory, "resume.md"));
428
+ atomicWrite(path.join(taskDirectory, "task.json"), archivedTask);
429
+ return archivedTask;
430
+ }
431
+
432
+ function copyTaskToPortableArchive(stateRoot, workflowRoot, task, archiveDirectory, archivedAt) {
433
+ const taskDirectory = path.join(archiveDirectory, "tasks", task.task_id);
434
+ const sharedFile = sharedTaskFile(workflowRoot, task.task_id);
435
+ const portableTask = fs.existsSync(sharedFile)
436
+ ? normalizeTask(JSON.parse(fs.readFileSync(sharedFile, "utf8")))
437
+ : normalizeTask({ ...task, state_root: ".", worktree: portablePath(task.state_root, task.worktree) });
438
+ portableTask.archived_at = archivedAt;
439
+ portableTask.archive_path = path.relative(stateRoot, taskDirectory).replaceAll("\\", "/");
440
+ fs.mkdirSync(taskDirectory, { recursive: true });
441
+ copyIfExists(
442
+ path.join(workflowRoot, "control", "checkpoints", task.task_id),
443
+ path.join(taskDirectory, "checkpoints"),
444
+ );
445
+ atomicWrite(path.join(taskDirectory, "task.json"), portableTask);
446
+ return portableTask;
447
+ }
448
+
449
+ function writeCheckpoint(runtime, task) {
450
+ const stamp = now().replaceAll(":", "-");
451
+ atomicWrite(path.join(runtime, "checkpoints", task.task_id, `${stamp}.json`), task);
452
+ }
453
+
454
+ function writeResume(runtime, task, reconciliation = null) {
455
+ const lines = [
456
+ `# Task Recovery: ${task.task_id}`,
457
+ "",
458
+ "## Objective", "", task.goal || "Not recorded.", "",
459
+ "## Recorded State", "", `- Status: ${task.status}`, `- Node: ${task.node}`, `- Skill: ${task.skill}`,
460
+ `- Sequence: ${task.sequence || "not recorded"}`,
461
+ `- Dependencies: ${task.depends_on.length ? task.depends_on.join(", ") : "none"}`,
462
+ `- Next action owner: ${task.next_action_owner}`,
463
+ `- Branch: ${task.branch || "not recorded"}`, `- Worktree: ${task.worktree || "not recorded"}`, "",
464
+ "## Completed Work And Evidence", "",
465
+ ...(task.completed_steps.length ? task.completed_steps.map((step) => `- ${step}`) : ["- None recorded."]), "",
466
+ ...(task.actual_outputs?.length ? ["### Actual Outputs", "", ...task.actual_outputs.map((item) => `- ${item}`), ""] : []),
467
+ ...(task.evidence?.length ? ["### Evidence", "", ...task.evidence.map((item) => `- ${item}`), ""] : []),
468
+ "## Blockers", "",
469
+ ...(task.blockers.length ? task.blockers.map((item) => `- ${item}`) : ["- None recorded."]), "",
470
+ "## Next Idempotent Action", "", task.next_action || "Reconcile state before continuing.", "",
471
+ ];
472
+ if (reconciliation) {
473
+ lines.push("## Reconciled Workspace And Git State", "", "```json", JSON.stringify(reconciliation, null, 2), "```", "");
474
+ }
475
+ const file = path.join(runtime, "resumes", `${task.task_id}.md`);
476
+ fs.mkdirSync(path.dirname(file), { recursive: true });
477
+ fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
478
+ }
479
+
480
+ function gitState(cwd) {
481
+ const run = (args) => spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
482
+ const root = run(["rev-parse", "--show-toplevel"]);
483
+ if (root.status !== 0) return { repository: false };
484
+ const branch = run(["branch", "--show-current"]);
485
+ const head = run(["rev-parse", "HEAD"]);
486
+ const status = run(["status", "--porcelain=v1"]);
487
+ return {
488
+ repository: true,
489
+ root: root.stdout.trim(),
490
+ branch: branch.stdout.trim(),
491
+ head: head.status === 0 ? head.stdout.trim() : null,
492
+ dirty_paths: status.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.slice(3)),
493
+ };
494
+ }
495
+
496
+ function compareTasks(left, right) {
497
+ const leftSequence = left.sequence ?? Number.MAX_SAFE_INTEGER;
498
+ const rightSequence = right.sequence ?? Number.MAX_SAFE_INTEGER;
499
+ return leftSequence - rightSequence || left.task_id.localeCompare(right.task_id);
500
+ }
501
+
502
+ function readRuntimeTasks(runtime) {
503
+ const tasksDir = path.join(runtime, "tasks");
504
+ if (!fs.existsSync(tasksDir)) return [];
505
+ return fs.readdirSync(tasksDir)
506
+ .filter((name) => name.endsWith(".json"))
507
+ .map((name) => normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8"))));
508
+ }
509
+
510
+ function dependencyState(task, tasksById) {
511
+ const missing = [];
512
+ const incomplete = [];
513
+ for (const dependencyId of task.depends_on) {
514
+ const dependency = tasksById.get(dependencyId);
515
+ if (!dependency) missing.push(dependencyId);
516
+ else if (dependency.status !== "completed") {
517
+ incomplete.push({ task_id: dependencyId, status: dependency.status });
518
+ }
519
+ }
520
+ return { missing, incomplete, satisfied: !missing.length && !incomplete.length };
521
+ }
522
+
523
+ function summarizeIteration(tasks, iterationId) {
524
+ const iterationTasks = tasks.filter((task) => task.iteration_id === iterationId).sort(compareTasks);
525
+ const tasksById = new Map(iterationTasks.map((task) => [task.task_id, task]));
526
+ const completed = iterationTasks.filter((task) => task.status === "completed");
527
+ const closed = iterationTasks.filter((task) => CLOSED_STATUS.has(task.status));
528
+ const active = iterationTasks.filter((task) => RESUMABLE_STATUS.has(task.status));
529
+ const queued = iterationTasks.filter((task) => task.status === "queued");
530
+ const ready = queued.filter((task) => dependencyState(task, tasksById).satisfied);
531
+ const waiting = queued
532
+ .filter((task) => !ready.includes(task))
533
+ .map((task) => ({ task_id: task.task_id, ...dependencyState(task, tasksById) }));
534
+ const recommended = [...active, ...ready].sort(compareTasks)[0] || null;
535
+ return {
536
+ iteration_id: iterationId,
537
+ total_tasks: iterationTasks.length,
538
+ completed_tasks: completed.length,
539
+ closed_tasks: closed.length,
540
+ remaining_tasks: iterationTasks.length - closed.length,
541
+ active_task_ids: active.map((task) => task.task_id),
542
+ queued_task_ids: queued.map((task) => task.task_id),
543
+ ready_task_ids: ready.map((task) => task.task_id),
544
+ waiting_tasks: waiting,
545
+ recommended_task_id: recommended?.task_id || null,
546
+ recommended_status: recommended?.status || null,
547
+ next_action: recommended?.next_action || null,
548
+ next_action_owner: recommended?.next_action_owner || null,
549
+ };
550
+ }
551
+
552
+ function commandInit(args) {
553
+ const taskId = sanitizeId(requireValue(args, "task-id"));
554
+ const iterationId = sanitizeId(requireValue(args, "iteration-id"));
555
+ const { projectRoot, stateRoot, workflowRoot, runtime } = statePaths(args);
556
+ return withTaskLock(runtime, `version.${iterationId}`, () => withTaskLock(runtime, taskId, () => {
557
+ if (fs.existsSync(archivedTaskIndexFile(runtime, taskId))) {
558
+ throw new Error(`Task ID is already archived and cannot be reused: ${taskId}`);
559
+ }
560
+ if (fs.existsSync(archivedVersionIndexFile(runtime, iterationId))) {
561
+ throw new Error(`Version is already archived and cannot accept new tasks: ${iterationId}`);
562
+ }
563
+ const file = taskFile(runtime, taskId);
564
+ const existing = fs.existsSync(file)
565
+ ? normalizeTask(JSON.parse(fs.readFileSync(file, "utf8")))
566
+ : null;
567
+ if (existing && existing.iteration_id !== iterationId) {
568
+ throw new Error(`Task ${taskId} already belongs to iteration ${existing.iteration_id}`);
569
+ }
570
+ const shared = readSharedTasks(workflowRoot, stateRoot).find((item) => item.task_id === taskId);
571
+ if (existing && shared) assertTaskSourcesMatch(existing, shared);
572
+ if (existing) assertWaitStateTransfer(existing, args, "init");
573
+ const status = args.status || "preparing";
574
+ const dependsOn = (args["depends-on"] || existing?.depends_on || []).map(sanitizeId);
575
+ if (dependsOn.includes(taskId)) throw new Error(`Task cannot depend on itself: ${taskId}`);
576
+ const timestamp = now();
577
+ const task = {
578
+ task_id: taskId,
579
+ iteration_id: iterationId,
580
+ node: requireValue(args, "node"),
581
+ skill: requireValue(args, "skill"),
582
+ status,
583
+ sequence: parseSequence(args.sequence ?? existing?.sequence),
584
+ depends_on: dependsOn,
585
+ attempt: existing ? Number(existing.attempt || 0) + 1 : 1,
586
+ goal: requireValue(args, "goal"),
587
+ scope: args.scope || existing?.scope || [],
588
+ input_artifacts: args["input-artifact"] || existing?.input_artifacts || [],
589
+ standards: args.standard || existing?.standards || [],
590
+ branch: args.branch || existing?.branch || null,
591
+ worktree: path.resolve(args.worktree || projectRoot),
592
+ state_root: stateRoot,
593
+ planned_outputs: args["planned-output"] || existing?.planned_outputs || [],
594
+ actual_outputs: existing?.actual_outputs || [],
595
+ completed_steps: existing?.completed_steps || [],
596
+ blockers: [],
597
+ next_action: requireValue(args, "next-action"),
598
+ next_action_owner: parseNextActionOwner(args["next-action-owner"], existing?.next_action_owner),
599
+ evidence: existing?.evidence || [],
600
+ created_at: existing?.created_at || timestamp,
601
+ started_at: status === "queued" ? existing?.started_at || null : existing?.started_at || timestamp,
602
+ updated_at: timestamp,
603
+ finished_at: CLOSED_STATUS.has(status) ? existing?.finished_at || timestamp : null,
604
+ };
605
+ if (!VALID_STATUS.has(task.status)) throw new Error(`Invalid status: ${task.status}`);
606
+ writeSharedTask(runtime, workflowRoot, task);
607
+ writeResume(runtime, task);
608
+ if (RESUMABLE_STATUS.has(status)) {
609
+ atomicWrite(taskMarkerFile(stateRoot), { task_id: taskId, state_root: stateRoot });
610
+ } else {
611
+ clearTaskMarker(task);
612
+ }
613
+ appendEvent(runtime, taskId, "task-initialized", { status: task.status, attempt: task.attempt });
614
+ return task;
615
+ }));
616
+ }
617
+
618
+ function commandActivate(args) {
619
+ const taskId = sanitizeId(requireValue(args, "task-id"));
620
+ const { stateRoot, workflowRoot, runtime, marker } = statePaths(args);
621
+ if (marker && marker.task_id !== taskId) {
622
+ throw new Error(`Task marker points to active task ${marker.task_id}, not ${taskId}`);
623
+ }
624
+ const initialSources = taskSources(runtime, workflowRoot, stateRoot, taskId);
625
+ if (!initialSources.task) throw new Error(`Task not found: ${taskId}`);
626
+ if (!initialSources.local) {
627
+ if (initialSources.task.next_action_owner === "agent") {
628
+ throw new Error(`Task ${taskId} has no local runtime state; run recover before activation.`);
629
+ }
630
+ assertWaitStateTransfer(initialSources.task, args, "activate");
631
+ }
632
+ const initial = initialSources.task;
633
+ return withTaskLock(runtime, `version.${initial.iteration_id}`, () => withTaskLock(runtime, taskId, () => {
634
+ const currentSources = taskSources(runtime, workflowRoot, stateRoot, taskId);
635
+ const task = currentSources.task;
636
+ if (!task) throw new Error(`Task not found: ${taskId}`);
637
+ if (!new Set(["queued", "preparing", "checkpointed", "blocked", "interrupted"]).has(task.status)) {
638
+ throw new Error(`Task ${taskId} cannot be activated from status ${task.status}`);
639
+ }
640
+ const sharedTasks = readSharedTasks(workflowRoot, stateRoot);
641
+ const tasksById = new Map(sharedTasks.map((item) => [item.task_id, item]));
642
+ for (const item of readRuntimeTasks(runtime)) {
643
+ const shared = tasksById.get(item.task_id);
644
+ if (shared) assertTaskSourcesMatch(item, shared);
645
+ tasksById.set(item.task_id, item);
646
+ }
647
+ const dependencies = dependencyState(task, tasksById);
648
+ if (!dependencies.satisfied) {
649
+ const details = [
650
+ ...dependencies.missing.map((id) => `${id}:missing`),
651
+ ...dependencies.incomplete.map((item) => `${item.task_id}:${item.status}`),
652
+ ];
653
+ throw new Error(`Task ${taskId} has unsatisfied dependencies: ${details.join(", ")}`);
654
+ }
655
+ const status = args.status || "running";
656
+ if (!new Set(["preparing", "running"]).has(status)) {
657
+ throw new Error("activate --status must be preparing or running");
658
+ }
659
+ const requestedOwner = assertWaitStateTransfer(task, args, "activate");
660
+ if (task.next_action_owner === "agent" && requestedOwner && requestedOwner !== "agent") {
661
+ throw new Error("activate --next-action-owner may only be agent; use checkpoint or finish to enter a wait state.");
662
+ }
663
+ const firstStart = !task.started_at;
664
+ task.status = status;
665
+ task.attempt = firstStart ? Number(task.attempt || 1) : Number(task.attempt || 0) + 1;
666
+ task.started_at ||= now();
667
+ task.updated_at = now();
668
+ task.finished_at = null;
669
+ task.blockers = [];
670
+ if (args["next-action"] && args["next-action"] !== true) task.next_action = args["next-action"];
671
+ if (requestedOwner) task.next_action_owner = requestedOwner;
672
+ writeSharedTask(runtime, workflowRoot, task);
673
+ writeResume(runtime, task);
674
+ atomicWrite(taskMarkerFile(task.state_root), {
675
+ task_id: taskId,
676
+ iteration_id: task.iteration_id,
677
+ state_root: task.state_root,
678
+ });
679
+ appendEvent(runtime, taskId, "task-activated", { status, attempt: task.attempt });
680
+ return task;
681
+ }));
682
+ }
683
+
684
+ function commandUpdate(args, finish = false) {
685
+ const taskId = sanitizeId(requireValue(args, "task-id"));
686
+ const { stateRoot, workflowRoot, runtime, marker } = statePaths(args);
687
+ if (marker && marker.task_id !== taskId) {
688
+ throw new Error(`Task marker points to ${marker.task_id}, not ${taskId}`);
689
+ }
690
+ return withTaskLock(runtime, taskId, () => {
691
+ const task = readTask(runtime, taskId);
692
+ const shared = readSharedTasks(workflowRoot, stateRoot).find((item) => item.task_id === taskId);
693
+ if (shared) assertTaskSourcesMatch(task, shared);
694
+ const requestedOwner = assertWaitStateTransfer(task, args, finish ? "finish" : "checkpoint");
695
+ const status = requireValue(args, "status");
696
+ if (!VALID_STATUS.has(status)) throw new Error(`Invalid status: ${status}`);
697
+ if (finish && !FINISH_STATUS.has(status)) {
698
+ throw new Error(`finish requires one of: ${[...FINISH_STATUS].join(", ")}`);
699
+ }
700
+ task.status = status;
701
+ task.next_action = requireValue(args, "next-action");
702
+ task.next_action_owner = requestedOwner || task.next_action_owner;
703
+ for (const step of args["completed-step"] || []) {
704
+ if (!task.completed_steps.includes(step)) task.completed_steps.push(step);
705
+ }
706
+ task.blockers = status === "blocked" ? [...(args.blocker || [])] : [];
707
+ task.evidence = [...new Set([...task.evidence, ...(args.evidence || [])])];
708
+ for (const output of args["actual-output"] || []) {
709
+ if (!task.actual_outputs.includes(output)) task.actual_outputs.push(output);
710
+ }
711
+ task.updated_at = now();
712
+ task.finished_at = finish ? now() : null;
713
+ writeSharedTask(runtime, workflowRoot, task);
714
+ writeResume(runtime, task);
715
+ appendEvent(runtime, taskId, finish ? "task-finished" : "task-checkpointed", { status });
716
+ if (finish && CLOSED_STATUS.has(status)) clearTaskMarker(task);
717
+ return task;
718
+ });
719
+ }
720
+
721
+ function commandArchiveVersion(args) {
722
+ const { runtime, stateRoot, workflowRoot } = statePaths(args);
723
+ const iterationId = sanitizeId(requireValue(args, "iteration-id"));
724
+ const versionStatus = requireValue(args, "version-status");
725
+ if (!new Set(["release-ready", "released", "abandoned"]).has(versionStatus)) {
726
+ throw new Error("--version-status must be release-ready, released, or abandoned");
727
+ }
728
+ const releaseId = args["release-id"] && args["release-id"] !== true ? sanitizeId(args["release-id"]) : null;
729
+ const candidateId = args["candidate-id"] && args["candidate-id"] !== true
730
+ ? sanitizeId(args["candidate-id"])
731
+ : null;
732
+ if (versionStatus === "released" && !releaseId) throw new Error("released version archive requires --release-id");
733
+ if (versionStatus === "release-ready" && !candidateId) {
734
+ throw new Error("release-ready version archive requires --candidate-id");
735
+ }
736
+ const evidence = args.evidence || [];
737
+ if (!evidence.length) throw new Error("version archive requires at least one --evidence");
738
+ const tasksDir = path.join(runtime, "tasks");
739
+ if (!fs.existsSync(tasksDir)) throw new Error(`No active task store found for version: ${iterationId}`);
740
+ return withTaskLock(runtime, `version.${iterationId}`, () => {
741
+ const portableArchiveDirectory = path.join(workflowRoot, "control", "archives", "versions", iterationId);
742
+ if (fs.existsSync(archivedVersionIndexFile(runtime, iterationId))
743
+ || fs.existsSync(path.join(portableArchiveDirectory, "version-manifest.json"))) {
744
+ throw new Error(`Version is already archived: ${iterationId}`);
745
+ }
746
+ const tasks = fs.readdirSync(tasksDir)
747
+ .filter((name) => name.endsWith(".json"))
748
+ .map((name) => normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8"))))
749
+ .filter((task) => task.iteration_id === iterationId);
750
+ if (!tasks.length) throw new Error(`No tasks found for version: ${iterationId}`);
751
+ const sharedById = new Map(readSharedTasks(workflowRoot, stateRoot).map((task) => [task.task_id, task]));
752
+ for (const task of tasks) {
753
+ const shared = sharedById.get(task.task_id);
754
+ if (!shared) throw new Error(`Shared task state not found: ${task.task_id}`);
755
+ assertTaskSourcesMatch(task, shared);
756
+ }
757
+ const notClosed = tasks.filter((task) => !CLOSED_STATUS.has(task.status));
758
+ if (notClosed.length) {
759
+ throw new Error(`Version ${iterationId} is not closed; active tasks: ${notClosed.map((task) => `${task.task_id}:${task.status}`).join(", ")}`);
760
+ }
761
+
762
+ const archivedAt = now();
763
+ const writeRuntimeArchive = !Boolean(args["portable-only"]);
764
+ const archiveDirectory = path.join(runtime, "archive", "versions", iterationId);
765
+ if (writeRuntimeArchive) {
766
+ tasks.map((task) => copyTaskToVersionArchive(runtime, task, archiveDirectory, archivedAt));
767
+ }
768
+ const archivedTasks = tasks.map((task) =>
769
+ copyTaskToPortableArchive(stateRoot, workflowRoot, task, portableArchiveDirectory, archivedAt));
770
+ const relativeArchive = path.relative(stateRoot, portableArchiveDirectory).replaceAll("\\", "/");
771
+ const manifest = {
772
+ iteration_id: iterationId,
773
+ version_status: versionStatus,
774
+ release_id: releaseId,
775
+ candidate_id: candidateId,
776
+ evidence,
777
+ task_count: archivedTasks.length,
778
+ archived_at: archivedAt,
779
+ archive_path: relativeArchive,
780
+ tasks: archivedTasks.map((task) => ({
781
+ task_id: task.task_id,
782
+ node: task.node,
783
+ skill: task.skill,
784
+ status: task.status,
785
+ finished_at: task.finished_at,
786
+ archive_path: task.archive_path,
787
+ })),
788
+ };
789
+ if (writeRuntimeArchive) atomicWrite(path.join(archiveDirectory, "version-manifest.json"), manifest);
790
+ atomicWrite(path.join(portableArchiveDirectory, "version-manifest.json"), manifest);
791
+ if (writeRuntimeArchive) {
792
+ atomicWrite(archivedVersionIndexFile(runtime, iterationId), manifest);
793
+ for (const task of archivedTasks) {
794
+ atomicWrite(archivedTaskIndexFile(runtime, task.task_id), {
795
+ task_id: task.task_id,
796
+ iteration_id: iterationId,
797
+ status: task.status,
798
+ archive_path: task.archive_path,
799
+ });
800
+ }
801
+ }
802
+ for (const task of tasks) {
803
+ removeActiveTaskBundle(runtime, task);
804
+ removeSharedTaskBundle(workflowRoot, task);
805
+ }
806
+ return { ...manifest, runtime_archive_created: writeRuntimeArchive, archived_tasks: manifest.tasks };
807
+ });
808
+ }
809
+
810
+ function commandRecover(args) {
811
+ const { runtime, workflowRoot, stateRoot } = statePaths(args);
812
+ const tasksDir = path.join(runtime, "tasks");
813
+ const staleMs = Number(args["stale-minutes"] || 15) * 60_000;
814
+ const requested = args["task-id"] ? sanitizeId(args["task-id"]) : null;
815
+ const requestedIteration = args["iteration-id"] ? sanitizeId(args["iteration-id"]) : null;
816
+ const includeClosed = Boolean(args["include-closed"]);
817
+ const localTasks = readRuntimeTasks(runtime);
818
+ const localById = new Map(localTasks.map((task) => [task.task_id, task]));
819
+ for (const shared of readSharedTasks(workflowRoot, stateRoot)) {
820
+ const local = localById.get(shared.task_id);
821
+ if (!local) {
822
+ localById.set(shared.task_id, shared);
823
+ if (shared.next_action_owner === "agent") atomicWrite(taskFile(runtime, shared.task_id), shared);
824
+ } else {
825
+ assertTaskSourcesMatch(local, shared);
826
+ }
827
+ }
828
+ const allTasks = [...localById.values()];
829
+ if (!allTasks.length) return { tasks: [], iterations: [], message: "No local or shared tasks found." };
830
+ const selectedTasks = requestedIteration
831
+ ? allTasks.filter((task) => task.iteration_id === requestedIteration)
832
+ : allTasks;
833
+ const iterationIds = [...new Set(selectedTasks.map((task) => task.iteration_id))].sort();
834
+ const reports = [];
835
+ for (const task of selectedTasks.sort(compareTasks)) {
836
+ if (requested && task.task_id !== requested) continue;
837
+ if (!includeClosed && CLOSED_STATUS.has(task.status)) continue;
838
+ const heartbeatFile = path.join(runtime, "heartbeats", `${task.task_id}.json`);
839
+ const heartbeat = fs.existsSync(heartbeatFile) ? JSON.parse(fs.readFileSync(heartbeatFile, "utf8")) : null;
840
+ const latest = new Date(heartbeat?.updated_at || task.updated_at).getTime();
841
+ const stale = task.status === "running" && Date.now() - latest > staleMs;
842
+ const worktreeExists = Boolean(task.worktree && fs.existsSync(task.worktree));
843
+ const git = worktreeExists ? gitState(task.worktree) : { repository: false };
844
+ const branchMatches = !task.branch || (git.repository && task.branch === git.branch);
845
+ const warnings = [];
846
+ if (!worktreeExists) warnings.push("Recorded worktree does not exist; locate or recreate it before resuming.");
847
+ if (task.branch && !git.repository) warnings.push("Recorded worktree is not a Git repository; reconcile it before resuming.");
848
+ else if (!branchMatches) warnings.push(`Recorded branch ${task.branch} differs from current branch ${git.branch || "(detached)"}.`);
849
+ const reconciliation = {
850
+ heartbeat,
851
+ stale_running_task: stale,
852
+ suggested_status: stale ? "interrupted" : task.status,
853
+ worktree_exists: worktreeExists,
854
+ branch_matches: branchMatches,
855
+ warnings,
856
+ git,
857
+ };
858
+ if (stale && args["mark-interrupted"]) {
859
+ withTaskLock(runtime, task.task_id, () => {
860
+ const current = readTask(runtime, task.task_id);
861
+ if (current.next_action_owner !== "agent") {
862
+ throw new Error(
863
+ `Task ${current.task_id} is waiting for ${current.next_action_owner}; recover --mark-interrupted cannot mutate a wait state.`,
864
+ );
865
+ }
866
+ current.status = "interrupted";
867
+ current.updated_at = now();
868
+ current.next_action ||= "Reconcile workspace and resume the last idempotent action.";
869
+ writeSharedTask(runtime, workflowRoot, current);
870
+ appendEvent(runtime, current.task_id, "task-interrupted", { reason: "stale-heartbeat" });
871
+ Object.assign(task, current);
872
+ });
873
+ }
874
+ if (task.next_action_owner === "agent") writeResume(runtime, task, reconciliation);
875
+ reports.push({ task, reconciliation });
876
+ }
877
+ return {
878
+ tasks: reports,
879
+ iterations: iterationIds.map((iterationId) => summarizeIteration(allTasks, iterationId)),
880
+ };
881
+ }
882
+
883
+ function commandPublish(args) {
884
+ const taskId = sanitizeId(requireValue(args, "task-id"));
885
+ const { stateRoot, workflowRoot } = statePaths(args);
886
+ const sharedFile = sharedTaskFile(workflowRoot, taskId);
887
+ const checkpointDirectory = path.join(workflowRoot, "control", "checkpoints", taskId);
888
+ if (!fs.existsSync(sharedFile)) throw new Error(`Shared task state not found: ${taskId}`);
889
+
890
+ const repository = gitState(stateRoot);
891
+ if (!repository.repository || path.resolve(repository.root).toLowerCase() !== path.resolve(stateRoot).toLowerCase()) {
892
+ throw new Error("Shared task state must be published from the workflow Git repository root.");
893
+ }
894
+ const workspaceFile = path.join(stateRoot, ".workflow", "config", "workspace.yaml");
895
+ if (!fs.existsSync(workspaceFile)) throw new Error(`Workspace configuration is missing: ${workspaceFile}`);
896
+ const workspaceText = fs.readFileSync(workspaceFile, "utf8");
897
+ const workflowSection = workspaceText.match(/^workflow_repository:\s*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m)?.[1] || "";
898
+ const scalar = (text, key) => {
899
+ const raw = text.match(new RegExp(`^\\s+${key}:\\s*(.+?)\\s*$`, "m"))?.[1];
900
+ if (!raw || raw === "null" || raw === "~") return null;
901
+ return raw.replace(/^['"]|['"]$/g, "");
902
+ };
903
+ const expectedBranch = scalar(workflowSection, "branch");
904
+ const configuredRemote = scalar(workflowSection, "remote");
905
+ if (!expectedBranch || repository.branch !== expectedBranch) {
906
+ throw new Error(`Workflow state can only be published from ${expectedBranch || "the configured workflow branch"}; current branch is ${repository.branch || "detached"}.`);
907
+ }
908
+ const origin = spawnSync("git", ["remote", "get-url", "origin"], {
909
+ cwd: stateRoot, encoding: "utf8", windowsHide: true,
910
+ });
911
+ const normalizeRemote = (value) => {
912
+ if (!value) return null;
913
+ const candidate = value.trim();
914
+ return path.isAbsolute(candidate)
915
+ ? path.resolve(candidate).replaceAll("\\", "/").toLowerCase()
916
+ : candidate.replaceAll("\\", "/").replace(/\/$/, "");
917
+ };
918
+ if (configuredRemote && (origin.status !== 0
919
+ || normalizeRemote(origin.stdout) !== normalizeRemote(configuredRemote))) {
920
+ throw new Error("Workflow origin does not match .workflow/config/workspace.yaml.");
921
+ }
922
+ if (args.push && !configuredRemote) throw new Error("--push requires a configured workflow remote.");
923
+ const upstream = spawnSync("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], {
924
+ cwd: stateRoot, encoding: "utf8", windowsHide: true,
925
+ });
926
+ if (upstream.status === 0 && upstream.stdout.trim() !== `origin/${expectedBranch}`) {
927
+ throw new Error(`Workflow upstream must be origin/${expectedBranch}, found ${upstream.stdout.trim()}.`);
928
+ }
929
+ const shared = JSON.parse(fs.readFileSync(sharedFile, "utf8"));
930
+ const workflowDocumentRoot = path.join(stateRoot, ".workflow");
931
+ const taskOwnedArtifacts = [...(shared.actual_outputs || []), ...(shared.evidence || [])]
932
+ .map((value) => String(value).replaceAll("\\", "/"))
933
+ .filter((value) => value.startsWith(".workflow/"))
934
+ .filter((value) => {
935
+ const absolute = path.resolve(stateRoot, value);
936
+ const relative = path.relative(workflowDocumentRoot, absolute);
937
+ return !relative.startsWith("..") && !path.isAbsolute(relative) && fs.existsSync(absolute);
938
+ });
939
+ const allowed = [
940
+ path.relative(stateRoot, sharedFile).replaceAll("\\", "/"),
941
+ `${path.relative(stateRoot, checkpointDirectory).replaceAll("\\", "/")}/`,
942
+ ...taskOwnedArtifacts,
943
+ ];
944
+ const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
945
+ cwd: stateRoot,
946
+ encoding: "utf8",
947
+ windowsHide: true,
948
+ });
949
+ if (status.status !== 0) throw new Error(`Cannot inspect workflow Git state: ${status.stderr.trim()}`);
950
+ const dirty = status.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replaceAll("\\", "/"));
951
+ const unrelated = dirty.filter((file) => !allowed.some((entry) => entry.endsWith("/") ? file.startsWith(entry) : file === entry));
952
+ if (unrelated.length) throw new Error(`Workflow repository has unrelated dirty paths: ${unrelated.join(", ")}`);
953
+ if (!dirty.length) return { task_id: taskId, published: false, pushed: false, message: "Shared task state is already committed." };
954
+
955
+ const addPaths = allowed.map((entry) => entry.endsWith("/") ? entry.slice(0, -1) : entry);
956
+ const add = spawnSync("git", ["add", "--", ...addPaths], {
957
+ cwd: stateRoot,
958
+ encoding: "utf8",
959
+ windowsHide: true,
960
+ });
961
+ if (add.status !== 0) throw new Error(`Cannot stage shared task state: ${add.stderr.trim()}`);
962
+ const commit = spawnSync("git", ["commit", "-m", `workflow(task): publish ${taskId} r${shared.shared_revision}`], {
963
+ cwd: stateRoot,
964
+ encoding: "utf8",
965
+ windowsHide: true,
966
+ });
967
+ if (commit.status !== 0) throw new Error(`Cannot commit shared task state: ${commit.stderr.trim()}`);
968
+ const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: stateRoot, encoding: "utf8", windowsHide: true });
969
+ let pushed = false;
970
+ if (args.push) {
971
+ const pushArgs = upstream.status === 0
972
+ ? ["push", "origin", `HEAD:refs/heads/${expectedBranch}`]
973
+ : ["push", "--set-upstream", "origin", `HEAD:refs/heads/${expectedBranch}`];
974
+ const push = spawnSync("git", pushArgs, { cwd: stateRoot, encoding: "utf8", windowsHide: true });
975
+ if (push.status !== 0) throw new Error(`Shared state was committed but push failed: ${push.stderr.trim()}`);
976
+ pushed = true;
977
+ }
978
+ return { task_id: taskId, published: true, pushed, workflow_commit: head.stdout.trim() };
979
+ }
980
+
981
+ function taskEvolutionContext(args, command) {
982
+ if (!new Set(["init", "activate", "checkpoint", "finish", "archive-version"]).has(command)) return null;
983
+ const projectRoot = path.resolve(args.root || findProjectRoot());
984
+ const evolutionCli = path.join(projectRoot, ".workflow", "tools", "evolution", "evolve.mjs");
985
+ if (!fs.existsSync(evolutionCli)) return null;
986
+ const taskId = args["task-id"] && args["task-id"] !== true ? String(args["task-id"]) : null;
987
+ let revision = 0;
988
+ if (taskId) {
989
+ try {
990
+ const { runtime, workflowRoot } = statePaths(args);
991
+ const localFile = taskFile(runtime, taskId);
992
+ const sharedFile = sharedTaskFile(workflowRoot, taskId);
993
+ const sourceFile = fs.existsSync(localFile) ? localFile : sharedFile;
994
+ const task = JSON.parse(fs.readFileSync(sourceFile, "utf8"));
995
+ revision = Number(task.shared_revision || 0);
996
+ } catch {
997
+ revision = 0;
998
+ }
999
+ }
1000
+ return {
1001
+ projectRoot,
1002
+ evolutionCli,
1003
+ command,
1004
+ taskId,
1005
+ iterationId: args["iteration-id"] && args["iteration-id"] !== true ? String(args["iteration-id"]) : null,
1006
+ revision,
1007
+ };
1008
+ }
1009
+
1010
+ function dispatchTaskEvolution(context, phase, result = null, error = null) {
1011
+ if (!context) return { decision: "pass" };
1012
+ const identity = context.taskId || context.iterationId || "unspecified";
1013
+ const idempotencyKey = `task-state-task-${phase}-${context.command}-${identity}-r${context.revision}`;
1014
+ const event = {
1015
+ schema_version: 1,
1016
+ event_id: `EVT-${crypto.createHash("sha256").update(idempotencyKey).digest("hex").slice(0, 24)}`,
1017
+ idempotency_key: idempotencyKey,
1018
+ event_type: `task.${phase}`,
1019
+ occurred_at: now(),
1020
+ sequence: context.revision,
1021
+ scope: {
1022
+ task_id: context.taskId,
1023
+ iteration_id: context.iterationId || result?.iteration_id || null,
1024
+ action: context.command,
1025
+ },
1026
+ source_ref: { command: context.command, shared_revision: context.revision },
1027
+ payload: error
1028
+ ? { outcome: "failed", error: error.message }
1029
+ : { outcome: phase === "before" ? "pending" : "succeeded", status: result?.status || null },
1030
+ };
1031
+ const dispatched = spawnSync(process.execPath, [
1032
+ context.evolutionCli, "hook", "dispatch", "--input", "-", "--project", context.projectRoot,
1033
+ ], {
1034
+ cwd: context.projectRoot,
1035
+ input: JSON.stringify(event),
1036
+ encoding: "utf8",
1037
+ timeout: 15_000,
1038
+ windowsHide: true,
1039
+ });
1040
+ if (dispatched.error || dispatched.status !== 0) {
1041
+ return { decision: "warn", messages: [dispatched.error?.message || dispatched.stderr || "Evolution Hook dispatch failed."], evidence_refs: [] };
1042
+ }
1043
+ try {
1044
+ return JSON.parse(dispatched.stdout || "{}");
1045
+ } catch {
1046
+ return { decision: "warn", messages: ["Evolution Hook returned invalid JSON."], evidence_refs: [] };
1047
+ }
1048
+ }
1049
+
1050
+ const args = parseArgs(process.argv.slice(2));
1051
+ const command = args._[0];
1052
+ const evolutionContext = taskEvolutionContext(args, command);
1053
+ try {
1054
+ const before = dispatchTaskEvolution(evolutionContext, "before");
1055
+ if (before.decision === "block") {
1056
+ throw new Error(`Evolution Hook blocked ${command}: ${(before.messages || []).join("; ") || "no rationale"}`);
1057
+ }
1058
+ let result;
1059
+ if (command === "init") result = commandInit(args);
1060
+ else if (command === "activate") result = commandActivate(args);
1061
+ else if (command === "checkpoint") result = commandUpdate(args, false);
1062
+ else if (command === "finish") result = commandUpdate(args, true);
1063
+ else if (command === "archive-version") result = commandArchiveVersion(args);
1064
+ else if (command === "recover") result = commandRecover(args);
1065
+ else if (command === "publish") result = commandPublish(args);
1066
+ else throw new Error("Use init, activate, checkpoint, finish, archive-version, recover, or publish.");
1067
+ dispatchTaskEvolution(evolutionContext, "after", result);
1068
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1069
+ } catch (error) {
1070
+ dispatchTaskEvolution(evolutionContext, "failed", null, error);
1071
+ process.stderr.write(`${error.message}\n`);
1072
+ process.exitCode = 1;
1073
+ }