ultra-builder-pro-cli 0.25.1 → 0.26.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 (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +40 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,794 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 8A.4b — Persist / load / section-slice the execution-plan artifact.
4
- //
5
- // The plan itself is computed by orchestrator/planner/plan-builder.cjs
6
- // (Functional Core). This module handles IO: atomic write via the shared
7
- // file-ops helper for the retired global artifact, plus journaled publication,
8
- // read-back, md rendering, and section projection for change-scoped plans.
9
-
10
- const fs = require('node:fs');
11
- const crypto = require('node:crypto');
12
- const path = require('node:path');
13
- const { spawnSync } = require('node:child_process');
14
- const { writeAtomic } = require('../../adapters/_shared/file-ops.cjs');
15
- const { buildPlan } = require('../../orchestrator/planner/plan-builder.cjs');
16
-
17
- const DEFAULT_ARTIFACT_RELPATH = '.ultra/execution-plan.json';
18
- const PLAN_PUBLICATION_WORKER = path.join(__dirname, 'plan-publication-worker.cjs');
19
-
20
- function planPathError(message, cause) {
21
- const error = new Error(message);
22
- error.code = 'PLAN_ARTIFACT_PATH_UNSAFE';
23
- if (cause) error.cause = cause;
24
- return error;
25
- }
26
-
27
- function containedPath(root, candidate) {
28
- return candidate === root || candidate.startsWith(`${root}${path.sep}`);
29
- }
30
-
31
- function entryExists(candidate) {
32
- try {
33
- fs.lstatSync(candidate);
34
- return true;
35
- } catch (error) {
36
- if (error?.code === 'ENOENT') return false;
37
- throw error;
38
- }
39
- }
40
-
41
- function assertRegularTarget(target, physicalRoot) {
42
- if (!entryExists(target)) return;
43
- const stat = fs.lstatSync(target);
44
- if (!stat.isFile() || stat.isSymbolicLink()) {
45
- throw planPathError(`plan artifact target must be a regular file: ${target}`);
46
- }
47
- const physicalTarget = fs.realpathSync(target);
48
- if (!containedPath(physicalRoot, physicalTarget)) {
49
- throw planPathError(`plan artifact target escapes the physical project root: ${target}`);
50
- }
51
- }
52
-
53
- function assertArtifactDirectoryCurrent(paths) {
54
- let current = paths.root;
55
- const relativeDirectory = path.relative(paths.root, paths.directory);
56
- for (const segment of relativeDirectory.split(path.sep).filter(Boolean)) {
57
- current = path.join(current, segment);
58
- let stat;
59
- try { stat = fs.lstatSync(current); } catch (cause) {
60
- throw planPathError(`plan artifact ancestor disappeared: ${current}`, cause);
61
- }
62
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
63
- throw planPathError(`plan artifact ancestor must be a real directory: ${current}`);
64
- }
65
- }
66
- let physicalDirectory;
67
- try { physicalDirectory = fs.realpathSync(paths.directory); } catch (cause) {
68
- throw planPathError(`plan artifact directory cannot be resolved: ${paths.directory}`, cause);
69
- }
70
- if (physicalDirectory !== paths.physicalDirectory
71
- || !containedPath(paths.physicalRoot, physicalDirectory)) {
72
- throw planPathError(
73
- `plan artifact directory changed after staging: ${paths.directory}`,
74
- );
75
- }
76
- }
77
-
78
- function verifyArtifactDirectory(projectRoot, change) {
79
- const root = path.resolve(projectRoot);
80
- let rootStat;
81
- try { rootStat = fs.lstatSync(root); } catch (cause) {
82
- throw planPathError(`project root does not exist: ${root}`, cause);
83
- }
84
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
85
- throw planPathError(`project root must be a real directory: ${root}`);
86
- }
87
- const physicalRoot = fs.realpathSync(root);
88
- const paths = changePlanPaths(root, change);
89
- const directory = path.dirname(paths.json);
90
- const relativeDirectory = path.relative(root, directory);
91
- let current = root;
92
- for (const segment of relativeDirectory.split(path.sep).filter(Boolean)) {
93
- current = path.join(current, segment);
94
- if (!fs.existsSync(current)) {
95
- try { fs.mkdirSync(current); } catch (cause) {
96
- throw planPathError(`cannot create plan artifact directory: ${current}`, cause);
97
- }
98
- }
99
- const stat = fs.lstatSync(current);
100
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
101
- throw planPathError(`plan artifact ancestor must be a real directory: ${current}`);
102
- }
103
- }
104
- const physicalDirectory = fs.realpathSync(directory);
105
- if (!containedPath(physicalRoot, physicalDirectory)) {
106
- throw planPathError(`plan artifact directory escapes the physical project root: ${directory}`);
107
- }
108
- assertRegularTarget(paths.json, physicalRoot);
109
- assertRegularTarget(paths.md, physicalRoot);
110
- return {
111
- ...paths,
112
- root,
113
- directory,
114
- physicalRoot,
115
- physicalDirectory,
116
- };
117
- }
118
-
119
- function writeExclusive(file, content) {
120
- const flags = fs.constants.O_WRONLY
121
- | fs.constants.O_CREAT
122
- | fs.constants.O_EXCL
123
- | (fs.constants.O_NOFOLLOW || 0);
124
- const fd = fs.openSync(file, flags, 0o600);
125
- try {
126
- fs.writeFileSync(fd, content);
127
- fs.fsyncSync(fd);
128
- } finally {
129
- fs.closeSync(fd);
130
- }
131
- }
132
-
133
- function digestContent(content) {
134
- return crypto.createHash('sha256').update(content).digest('hex');
135
- }
136
-
137
- function directoryIdentity(directory) {
138
- const stat = fs.statSync(directory, { bigint: true });
139
- if (!stat.isDirectory()) {
140
- throw planPathError(`plan artifact directory is not a directory: ${directory}`);
141
- }
142
- return { dev: String(stat.dev), ino: String(stat.ino) };
143
- }
144
-
145
- function workerError(payload, fallback) {
146
- const error = new Error(payload?.message || fallback);
147
- error.code = payload?.code || 'PLAN_WORKER_FAILED';
148
- if (payload?.details !== undefined) error.details = payload.details;
149
- return error;
150
- }
151
-
152
- function runDirectoryWorker(paths, action, payload, {
153
- verifyCurrent = true,
154
- } = {}) {
155
- if (verifyCurrent) assertArtifactDirectoryCurrent(paths);
156
- const result = spawnSync(
157
- process.execPath,
158
- [PLAN_PUBLICATION_WORKER, action],
159
- {
160
- cwd: paths.directory,
161
- input: JSON.stringify(payload),
162
- encoding: 'utf8',
163
- stdio: ['pipe', 'pipe', 'pipe'],
164
- maxBuffer: 64 * 1024 * 1024,
165
- },
166
- );
167
- if (result.error) {
168
- throw planPathError(
169
- `cannot start plan publication worker in ${paths.directory}: ${result.error.message}`,
170
- result.error,
171
- );
172
- }
173
- let response;
174
- try { response = JSON.parse(String(result.stdout || '').trim()); } catch (cause) {
175
- throw workerError(
176
- null,
177
- `plan publication worker returned invalid output: ${String(result.stderr || '').trim()}`,
178
- );
179
- }
180
- if (!response?.ok) throw workerError(response?.error, 'plan publication worker failed');
181
- if (result.status !== 0) {
182
- throw workerError(
183
- response?.error,
184
- `plan publication worker exited ${result.status}: ${String(result.stderr || '').trim()}`,
185
- );
186
- }
187
- return response.result;
188
- }
189
-
190
- function inspectExistingArtifactDirectory(projectRoot, change) {
191
- const root = path.resolve(projectRoot);
192
- const paths = changePlanPaths(root, change);
193
- const directory = path.dirname(paths.json);
194
- let rootStat;
195
- try { rootStat = fs.lstatSync(root); } catch (cause) {
196
- throw planPathError(`project root does not exist: ${root}`, cause);
197
- }
198
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
199
- throw planPathError(`project root must be a real directory: ${root}`);
200
- }
201
- const physicalRoot = fs.realpathSync(root);
202
- let current = root;
203
- for (const segment of path.relative(root, directory).split(path.sep).filter(Boolean)) {
204
- current = path.join(current, segment);
205
- let stat;
206
- try { stat = fs.lstatSync(current); } catch (cause) {
207
- if (cause?.code === 'ENOENT') return null;
208
- throw planPathError(`plan artifact ancestor cannot be inspected: ${current}`, cause);
209
- }
210
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
211
- throw planPathError(`plan artifact ancestor must be a real directory: ${current}`);
212
- }
213
- }
214
- const physicalDirectory = fs.realpathSync(directory);
215
- if (!containedPath(physicalRoot, physicalDirectory)) {
216
- throw planPathError(`plan artifact directory escapes the physical project root: ${directory}`);
217
- }
218
- return {
219
- ...paths,
220
- root,
221
- directory,
222
- physicalRoot,
223
- physicalDirectory,
224
- };
225
- }
226
-
227
- function readChangePlanBytes(projectRoot, change) {
228
- const paths = inspectExistingArtifactDirectory(projectRoot, change);
229
- if (!paths) return null;
230
- const identity = directoryIdentity(paths.directory);
231
- const result = runDirectoryWorker(paths, 'read', {
232
- identity,
233
- target: 'plan.json',
234
- });
235
- if (!result.exists) return null;
236
- const bytes = Buffer.from(result.content, 'base64');
237
- if (digestContent(bytes) !== result.digest) {
238
- throw planPathError('plan publication reader returned inconsistent bytes');
239
- }
240
- return bytes;
241
- }
242
-
243
- function renderPlanMd(plan, { tasks = [] } = {}) {
244
- const lines = [];
245
- lines.push('# Execution Plan');
246
- lines.push('');
247
- if (plan.change_id) lines.push(`- Change: ${plan.change_id}`);
248
- if (plan.context?.snapshot_id) lines.push(`- Context snapshot: ${plan.context.snapshot_id}`);
249
- if (plan.context?.manifest_digest) lines.push(`- Context digest: ${plan.context.manifest_digest}`);
250
- lines.push(`- Waves: ${plan.waves.length}`);
251
- lines.push(plan.estimated_cost_usd === null
252
- ? '- Estimated cost: unavailable (exact runtime model was not recorded)'
253
- : `- Estimated cost: $${plan.estimated_cost_usd}`);
254
- lines.push(`- Estimated duration: ${plan.estimated_duration_min} min`);
255
- if (plan.conflict_surface.length > 0) {
256
- lines.push(`- Conflicts: ${plan.conflict_surface.length}`);
257
- }
258
- if (plan.cycles && plan.cycles.length > 0) {
259
- lines.push(`- Cycles: ${plan.cycles.length}`);
260
- }
261
- lines.push('');
262
- lines.push('## Waves');
263
- lines.push('');
264
- for (const w of plan.waves) {
265
- lines.push(`### Wave ${w.id} ${w.parallel ? '(parallel)' : '(serial)'}`);
266
- for (const id of w.tasks) lines.push(`- ${id}`);
267
- if (w.reason) lines.push(`_reason_: ${w.reason}`);
268
- lines.push('');
269
- }
270
- if (plan.conflict_surface.length > 0) {
271
- lines.push('## Conflict Surface');
272
- lines.push('');
273
- for (const c of plan.conflict_surface) {
274
- lines.push(`- **files**: ${c.files.join(', ')}; **tasks**: ${c.tasks.join(', ')}; **recommend**: ${c.recommend}`);
275
- }
276
- }
277
- if (tasks.length > 0) {
278
- lines.push('');
279
- lines.push('## Task Contracts');
280
- lines.push('');
281
- for (const task of [...tasks].sort((left, right) => String(left.id).localeCompare(String(right.id)))) {
282
- lines.push(`## Task ${task.id}`);
283
- lines.push('');
284
- lines.push(`- Purpose: ${task.outcome || 'unresolved'}`);
285
- lines.push(`- Why: ${task.trace_to || 'unresolved'}`);
286
- lines.push(`- Slice: ${task.slice_kind || 'unresolved'}`);
287
- lines.push(`- Public seam: ${task.public_seam || 'unresolved'}`);
288
- lines.push(`- Target files: ${(task.files_modified || []).join(', ') || 'resolved at implementation time'}`);
289
- lines.push(`- Verification: ${task.verification_command || 'unresolved'}`);
290
- lines.push(`- Documentation: ${task.docs_impact?.status || 'unknown'} — ${task.docs_impact?.rationale || 'unresolved'}`);
291
- lines.push('');
292
- lines.push('### Acceptance');
293
- lines.push('');
294
- for (const item of task.acceptance || []) {
295
- lines.push(`- ${item.id}: ${item.criterion} — \`${item.verification}\``);
296
- }
297
- if ((task.acceptance || []).length === 0) lines.push('- unresolved');
298
- lines.push('');
299
- lines.push('### Pattern and context references');
300
- lines.push('');
301
- for (const item of task.context_refs || []) {
302
- const locator = [item.ref, item.anchor].filter(Boolean).join('#');
303
- const scope = item.scope ? `; scope: ${item.scope}` : '';
304
- lines.push(`- \`${locator}\` — ${item.reason}${scope}`);
305
- }
306
- if ((task.context_refs || []).length === 0) lines.push('- unresolved');
307
- lines.push('');
308
- lines.push('### Recovery and drift');
309
- lines.push('');
310
- lines.push('- Recovery: follow the owning Change Contract recovery strategy and verification.');
311
- lines.push('- Drift: recompile when the Change contract, task contract, or a digest-bound context reference changes.');
312
- lines.push('');
313
- }
314
- }
315
- return lines.join('\n') + '\n';
316
- }
317
-
318
- function savePlanArtifact(plan, outPath, format = 'json', renderOptions = {}) {
319
- if (!outPath) {
320
- const err = new Error('out_path required');
321
- err.code = 'WRITE_FAILED';
322
- throw err;
323
- }
324
- const abs = path.resolve(outPath);
325
- const content = format === 'md'
326
- ? renderPlanMd(plan, renderOptions)
327
- : `${JSON.stringify(plan, null, 2)}\n`;
328
- try {
329
- fs.mkdirSync(path.dirname(abs), { recursive: true });
330
- writeAtomic(abs, content);
331
- } catch (err) {
332
- const wrap = new Error(`cannot write plan: ${err.message}`);
333
- wrap.code = 'WRITE_FAILED';
334
- wrap.cause = err;
335
- throw wrap;
336
- }
337
- return { plan_path: abs };
338
- }
339
-
340
- function loadPlanArtifact(projectRoot) {
341
- const abs = path.resolve(projectRoot, DEFAULT_ARTIFACT_RELPATH);
342
- if (!fs.existsSync(abs)) return null;
343
- try {
344
- return JSON.parse(fs.readFileSync(abs, 'utf8'));
345
- } catch {
346
- return null;
347
- }
348
- }
349
-
350
- function changePlanPaths(projectRoot, change) {
351
- const artifactRoot = String(change?.artifact_root || '').trim();
352
- if (!artifactRoot || path.isAbsolute(artifactRoot)) {
353
- const error = new Error('change artifact_root must be project-relative');
354
- error.code = 'WRITE_FAILED';
355
- throw error;
356
- }
357
- const root = path.resolve(projectRoot);
358
- const directory = path.resolve(root, artifactRoot);
359
- if (directory !== root && !directory.startsWith(`${root}${path.sep}`)) {
360
- const error = new Error('change artifact_root escapes project root');
361
- error.code = 'WRITE_FAILED';
362
- throw error;
363
- }
364
- return {
365
- json: path.join(directory, 'plan.json'),
366
- md: path.join(directory, 'plan.md'),
367
- };
368
- }
369
-
370
- function saveChangePlanArtifacts(plan, {
371
- rootDir,
372
- change,
373
- tasks = [],
374
- context,
375
- } = {}) {
376
- const publication = prepareChangePlanPublication(plan, {
377
- rootDir,
378
- change,
379
- tasks,
380
- context,
381
- });
382
- try {
383
- publication.publish();
384
- } catch (error) {
385
- const rollback = publication.rollback();
386
- if (rollback?.rolled_back === false) {
387
- throw planRecoveryRequiredError(error, rollback, publication.transaction_id);
388
- }
389
- throw error;
390
- }
391
- publication.commit();
392
- return {
393
- plan_path: publication.plan_path,
394
- plan_md_path: publication.plan_md_path,
395
- plan: publication.plan,
396
- };
397
- }
398
-
399
- function errorEvidence(error) {
400
- return {
401
- code: error?.code || 'PLAN_PUBLISH_FAILED',
402
- message: error?.message || 'plan publication failed',
403
- ...(error?.details !== undefined ? { details: error.details } : {}),
404
- };
405
- }
406
-
407
- function planRecoveryRequiredError(originalError, rollback, transactionId) {
408
- const original = originalError?.details?.original_error || errorEvidence(originalError);
409
- const rollbackIssue = rollback?.issue
410
- || originalError?.details?.rollback_issue
411
- || {
412
- code: 'PLAN_RECOVERY_FAILED',
413
- message: 'plan publication rollback did not complete',
414
- };
415
- const error = new Error(
416
- `plan publication failed and rollback recovery is required: ${original.message}; `
417
- + `${rollbackIssue.message}`,
418
- );
419
- error.code = 'PLAN_RECOVERY_REQUIRED';
420
- error.cause = originalError;
421
- error.details = {
422
- transaction_id: transactionId,
423
- original_error: original,
424
- rollback_issue: rollbackIssue,
425
- issue: rollbackIssue,
426
- };
427
- return error;
428
- }
429
-
430
- function prepareChangePlanPublication(plan, {
431
- rootDir,
432
- change,
433
- tasks = [],
434
- context,
435
- } = {}) {
436
- if (!change || plan?.change_id !== change.id) {
437
- const error = new Error('plan must be bound to its owning change');
438
- error.code = 'PLAN_CHANGE_MISMATCH';
439
- throw error;
440
- }
441
- if (!context?.snapshot_id || !/^[0-9a-f]{64}$/.test(context.manifest_digest || '')) {
442
- const error = new Error('plan export requires a digest-bound planning context');
443
- error.code = 'PLAN_CONTEXT_REQUIRED';
444
- throw error;
445
- }
446
- const boundPlan = {
447
- ...plan,
448
- context: {
449
- snapshot_id: context.snapshot_id,
450
- manifest_path: context.manifest_path,
451
- manifest_digest: context.manifest_digest,
452
- },
453
- };
454
- const paths = verifyArtifactDirectory(rootDir, change);
455
- const jsonContent = `${JSON.stringify(boundPlan, null, 2)}\n`;
456
- const mdContent = renderPlanMd(boundPlan, { tasks });
457
- const canonicalPath = (file) => path.relative(path.resolve(rootDir), file)
458
- .split(path.sep).join('/');
459
- return beginPlanPublication({
460
- paths,
461
- boundPlan,
462
- jsonContent,
463
- mdContent,
464
- authority: {
465
- owner_type: 'change',
466
- owner_id: change.id,
467
- paths: {
468
- execution_plan: canonicalPath(paths.json),
469
- execution_plan_markdown: canonicalPath(paths.md),
470
- },
471
- },
472
- });
473
- }
474
-
475
- function beginPlanPublication({
476
- paths,
477
- boundPlan,
478
- jsonContent,
479
- mdContent,
480
- authority,
481
- }) {
482
- const transactionId = crypto.randomUUID();
483
- const identity = directoryIdentity(paths.directory);
484
- let prepared;
485
- try {
486
- prepared = runDirectoryWorker(paths, 'prepare', {
487
- identity,
488
- transaction_id: transactionId,
489
- contents: {
490
- execution_plan: jsonContent,
491
- execution_plan_markdown: mdContent,
492
- },
493
- authority,
494
- });
495
- } catch (cause) {
496
- if (cause?.code === 'PLAN_ARTIFACT_PATH_UNSAFE') throw cause;
497
- const error = new Error(`cannot stage plan publication: ${cause.message}`);
498
- error.code = cause?.code || 'PLAN_PUBLISH_FAILED';
499
- error.cause = cause;
500
- throw error;
501
- }
502
- let published = false;
503
- let finalized = false;
504
-
505
- const rollback = () => {
506
- if (finalized) return;
507
- try {
508
- runDirectoryWorker(paths, 'rollback', {
509
- identity,
510
- transaction_id: transactionId,
511
- });
512
- finalized = true;
513
- } catch (error) {
514
- return {
515
- rolled_back: false,
516
- issue: {
517
- code: error.code || 'PLAN_RECOVERY_FAILED',
518
- message: error.message,
519
- ...(error.details !== undefined ? { details: error.details } : {}),
520
- },
521
- };
522
- }
523
- return { rolled_back: true };
524
- };
525
-
526
- const publish = () => {
527
- if (published) return;
528
- try {
529
- runDirectoryWorker(paths, 'publish', {
530
- identity,
531
- transaction_id: transactionId,
532
- });
533
- published = true;
534
- } catch (cause) {
535
- const rollbackResult = rollback();
536
- if (rollbackResult?.rolled_back === false) {
537
- throw planRecoveryRequiredError(cause, rollbackResult, transactionId);
538
- }
539
- if (['PLAN_ARTIFACT_PATH_UNSAFE', 'PLAN_PUBLISH_CONFLICT'].includes(cause?.code)) {
540
- throw cause;
541
- }
542
- const error = new Error(`cannot publish plan artifacts: ${cause.message}`);
543
- error.code = cause?.code || 'PLAN_PUBLISH_FAILED';
544
- error.cause = cause;
545
- throw error;
546
- }
547
- };
548
-
549
- const commit = () => {
550
- if (finalized) return;
551
- if (!published) {
552
- const error = new Error('plan publication was not published');
553
- error.code = 'PLAN_PUBLISH_FAILED';
554
- throw error;
555
- }
556
- try {
557
- runDirectoryWorker(paths, 'commit', {
558
- identity,
559
- transaction_id: transactionId,
560
- });
561
- finalized = true;
562
- } catch (cause) {
563
- const error = new Error(
564
- `plan publication cleanup is incomplete; recovery is required: ${cause.message}`,
565
- );
566
- error.code = 'PLAN_RECOVERY_REQUIRED';
567
- error.cause = cause;
568
- error.details = {
569
- transaction_id: transactionId,
570
- issue: {
571
- code: cause.code || 'PLAN_RECOVERY_FAILED',
572
- message: cause.message,
573
- details: cause.details,
574
- },
575
- };
576
- throw error;
577
- }
578
- };
579
-
580
- return {
581
- transaction_id: transactionId,
582
- plan_path: paths.json,
583
- plan_md_path: paths.md,
584
- plan: boundPlan,
585
- artifacts: prepared.entries.map((entry) => ({
586
- kind: entry.kind,
587
- path: path.join(paths.directory, entry.target),
588
- digest: entry.digest,
589
- })),
590
- publish,
591
- rollback,
592
- commit,
593
- };
594
- }
595
-
596
- function canonicalChangeRows(db) {
597
- return db.prepare(
598
- `SELECT id, artifact_root
599
- FROM changes
600
- WHERE artifact_root IS NOT NULL AND TRIM(artifact_root) <> ''
601
- ORDER BY id`,
602
- ).all();
603
- }
604
-
605
- function registryAuthorityForPath(db, relativePath) {
606
- const rows = db.prepare(
607
- `SELECT owner_type, owner_id, kind, path, digest, content_hash,
608
- status, managed, provenance_json
609
- FROM artifacts
610
- WHERE path = ? AND status <> 'archived'
611
- ORDER BY updated_at DESC, rowid DESC`,
612
- ).all(relativePath);
613
- if (rows.length !== 1) return null;
614
- let provenance = {};
615
- try { provenance = JSON.parse(rows[0].provenance_json || '{}'); } catch { provenance = {}; }
616
- return {
617
- owner_type: rows[0].owner_type,
618
- owner_id: rows[0].owner_id,
619
- kind: rows[0].kind,
620
- path: rows[0].path,
621
- digest: rows[0].digest || rows[0].content_hash || null,
622
- status: rows[0].status,
623
- managed: rows[0].managed,
624
- provenance,
625
- };
626
- }
627
-
628
- function inspectPlanPublications(db, {
629
- rootDir = process.cwd(),
630
- } = {}) {
631
- let pending = 0;
632
- const transactions = [];
633
- const issues = [];
634
- for (const change of canonicalChangeRows(db)) {
635
- let paths;
636
- try {
637
- paths = inspectExistingArtifactDirectory(rootDir, change);
638
- if (!paths) continue;
639
- const identity = directoryIdentity(paths.directory);
640
- const inspected = runDirectoryWorker(paths, 'inspect', { identity });
641
- pending += inspected.pending;
642
- transactions.push(...inspected.transactions.map((item) => ({
643
- ...item,
644
- change_id: change.id,
645
- artifact_root: change.artifact_root,
646
- })));
647
- issues.push(...inspected.issues.map((item) => ({
648
- ...item,
649
- change_id: change.id,
650
- artifact_root: change.artifact_root,
651
- })));
652
- } catch (error) {
653
- pending += 1;
654
- issues.push({
655
- code: error.code || 'PLAN_RECOVERY_FAILED',
656
- change_id: change.id,
657
- artifact_root: change.artifact_root,
658
- message: error.message,
659
- });
660
- }
661
- }
662
- return {
663
- status: pending === 0 && issues.length === 0 ? 'pass' : 'fail',
664
- pending,
665
- transactions,
666
- issues,
667
- };
668
- }
669
-
670
- function recoverPlanPublications(db, {
671
- rootDir = process.cwd(),
672
- } = {}) {
673
- let recovered = 0;
674
- let finalized = 0;
675
- const issues = [];
676
- for (const change of canonicalChangeRows(db)) {
677
- let paths;
678
- try {
679
- paths = inspectExistingArtifactDirectory(rootDir, change);
680
- if (!paths) continue;
681
- const identity = directoryIdentity(paths.directory);
682
- const relative = (file) => path.relative(path.resolve(rootDir), file)
683
- .split(path.sep).join('/');
684
- const result = runDirectoryWorker(paths, 'recover', {
685
- identity,
686
- registry: {
687
- 'plan.json': registryAuthorityForPath(db, relative(paths.json)),
688
- 'plan.md': registryAuthorityForPath(db, relative(paths.md)),
689
- },
690
- });
691
- recovered += result.recovered;
692
- finalized += result.finalized;
693
- issues.push(...result.issues.map((item) => ({
694
- ...item,
695
- change_id: change.id,
696
- artifact_root: change.artifact_root,
697
- })));
698
- } catch (error) {
699
- issues.push({
700
- code: error.code || 'PLAN_RECOVERY_FAILED',
701
- change_id: change.id,
702
- artifact_root: change.artifact_root,
703
- message: error.message,
704
- });
705
- }
706
- }
707
- const after = inspectPlanPublications(db, { rootDir });
708
- return {
709
- recovered,
710
- finalized,
711
- pending: after.pending,
712
- issues: [...issues, ...after.issues],
713
- };
714
- }
715
-
716
- function loadChangePlanArtifact(projectRoot, change, {
717
- db = null,
718
- strict = false,
719
- } = {}) {
720
- try {
721
- const bytes = readChangePlanBytes(projectRoot, change);
722
- if (!bytes) return null;
723
- const relative = path.relative(
724
- path.resolve(projectRoot),
725
- changePlanPaths(projectRoot, change).json,
726
- ).split(path.sep).join('/');
727
- if (db) {
728
- const rows = db.prepare(
729
- `SELECT owner_type, owner_id, kind, digest, content_hash, status
730
- FROM artifacts
731
- WHERE path = ? AND status <> 'archived'
732
- ORDER BY updated_at DESC, rowid DESC`,
733
- ).all(relative);
734
- if (rows.length !== 1
735
- || rows[0].owner_type !== 'change'
736
- || rows[0].owner_id !== change.id
737
- || rows[0].kind !== 'execution_plan'
738
- || rows[0].status !== 'current') {
739
- const error = new Error(`canonical Change plan registry authority is invalid: ${relative}`);
740
- error.code = 'PLAN_ARTIFACT_INVALID';
741
- throw error;
742
- }
743
- const expected = rows[0].digest || rows[0].content_hash;
744
- const actual = digestContent(bytes);
745
- if (!expected || expected !== actual) {
746
- const error = new Error(`canonical Change plan bytes do not match registry authority: ${relative}`);
747
- error.code = 'PLAN_ARTIFACT_INVALID';
748
- throw error;
749
- }
750
- }
751
- const value = JSON.parse(bytes.toString('utf8'));
752
- if (value?.change_id !== change.id) {
753
- const error = new Error(`canonical Change plan is not bound to ${change.id}`);
754
- error.code = 'PLAN_ARTIFACT_INVALID';
755
- throw error;
756
- }
757
- return value;
758
- } catch (error) {
759
- if (strict) {
760
- if (error?.code === 'PLAN_ARTIFACT_INVALID') throw error;
761
- const wrapped = new Error(`canonical Change plan is unsafe or invalid: ${error.message}`);
762
- wrapped.code = 'PLAN_ARTIFACT_INVALID';
763
- wrapped.cause = error;
764
- throw wrapped;
765
- }
766
- return null;
767
- }
768
- }
769
-
770
- function selectSection(plan, section) {
771
- switch (section || 'all') {
772
- case 'tasks': return { ownership_forecast: plan.ownership_forecast };
773
- case 'topo': return { waves: plan.waves };
774
- case 'conflicts': return { conflict_surface: plan.conflict_surface };
775
- case 'all':
776
- default: return plan;
777
- }
778
- }
779
-
780
- module.exports = {
781
- savePlanArtifact,
782
- loadPlanArtifact,
783
- saveChangePlanArtifacts,
784
- prepareChangePlanPublication,
785
- inspectPlanPublications,
786
- recoverPlanPublications,
787
- loadChangePlanArtifact,
788
- changePlanPaths,
789
- selectSection,
790
- renderPlanMd,
791
- buildPlan,
792
- planRecoveryRequiredError,
793
- DEFAULT_ARTIFACT_RELPATH,
794
- };