ultra-builder-pro-cli 0.25.1 → 0.26.1

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 (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -1,1964 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
- const util = require('node:util');
7
- const Ajv = require('ajv/dist/2020');
8
- const addFormats = require('ajv-formats');
9
-
10
- const canonical = require('./canonical-json.cjs');
11
- const decisionRecords = require('./decision-records.cjs');
12
- const {
13
- restoreManagedFile,
14
- snapshotManagedFile,
15
- writeManagedFile,
16
- } = require('./managed-file-write.cjs');
17
- const ops = require('./state-ops.cjs');
18
- const { readStableProjectFile } = require('./safe-project-file.cjs');
19
- const stageCheckpoints = require('./stage-checkpoints.cjs');
20
- const taskContract = require('./task-contract.cjs');
21
- const taskLedgerSchema = require('../../spec/schemas/task-ledger.v2.schema.json');
22
-
23
- const taskLedgerAjv = new Ajv({ allErrors: true, strict: false });
24
- addFormats(taskLedgerAjv);
25
- const validateTaskLedgerSchema = taskLedgerAjv.compile(taskLedgerSchema);
26
-
27
- const LEDGER_KIND = 'ultra-team-task-ledger';
28
- const LEDGER_SCHEMA_VERSION = '2.0';
29
- const LEGACY_LEDGER_SCHEMA_VERSION = '1.0';
30
- const LEDGER_RELATIVE_PATH = '.ultra/tasks/tasks.json';
31
- const HASH_PATTERN = /^[0-9a-f]{64}$/;
32
- const MAX_ANCESTORS = 64;
33
- const PUBLICATION_ROLLBACK = Symbol('task-ledger-publication-rollback');
34
- const DURABLE_BASELINE_FIELDS = Object.freeze([
35
- 'id',
36
- 'project_name',
37
- 'project_type',
38
- 'stack',
39
- 'mode',
40
- 'status',
41
- 'scope',
42
- 'repository_revision',
43
- 'repository_branch',
44
- 'worktree_state',
45
- 'worktree_digest',
46
- 'worktree_accepted',
47
- 'known_red_accepted',
48
- 'spec_refs',
49
- 'evidence',
50
- 'verification',
51
- 'unknowns',
52
- 'gaps',
53
- 'classification',
54
- 'provider_refs',
55
- 'research_checkpoint_id',
56
- 'approved_by',
57
- 'approval_note',
58
- 'converged_at',
59
- ]);
60
- const LEGACY_DURABLE_BASELINE_FIELDS = Object.freeze(
61
- DURABLE_BASELINE_FIELDS.map((field) => (
62
- field === 'research_checkpoint_id' ? 'research_run_id' : field
63
- )),
64
- );
65
- const DURABLE_TASK_FIELDS = taskContract.DURABLE_TASK_FIELDS;
66
- const DURABLE_CHANGE_FIELDS = Object.freeze([
67
- 'id',
68
- 'title',
69
- 'kind',
70
- 'status',
71
- 'intent',
72
- 'docs_impact',
73
- 'provider_refs',
74
- 'baseline_bypass',
75
- 'contract',
76
- 'classification',
77
- 'research_disposition',
78
- 'base_commit',
79
- 'supersedes_id',
80
- 'artifact_root',
81
- 'closed_at',
82
- ]);
83
- const DURABLE_DECISION_FIELDS = Object.freeze([
84
- 'id',
85
- 'scope_type',
86
- 'scope_id',
87
- 'selection',
88
- 'effects',
89
- 'status',
90
- 'artifact_path',
91
- 'artifact_digest',
92
- 'supersedes_id',
93
- ]);
94
- const DURABLE_CHECKPOINT_FIELDS = Object.freeze([
95
- 'stage',
96
- 'scope_type',
97
- 'scope_id',
98
- 'revision',
99
- 'payload',
100
- 'evidence',
101
- 'diagnostics',
102
- 'status',
103
- ]);
104
-
105
- class TaskLedgerError extends Error {
106
- constructor(code, message, details) {
107
- super(message);
108
- this.name = 'TaskLedgerError';
109
- this.code = code;
110
- if (details) this.details = details;
111
- }
112
- }
113
-
114
- function ledgerPath(rootDir) {
115
- return path.join(path.resolve(rootDir || '.'), ...LEDGER_RELATIVE_PATH.split('/'));
116
- }
117
-
118
- function safeDirectoryChain(rootDir, components, { create = false } = {}) {
119
- const root = path.resolve(rootDir || '.');
120
- const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
121
- if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
122
- throw new TaskLedgerError('TASK_LEDGER_UNSAFE', `project root is unsafe: ${root}`);
123
- }
124
- const physicalRoot = fs.realpathSync.native(root);
125
- let current = root;
126
- for (const component of components) {
127
- current = path.join(current, component);
128
- let stat = fs.lstatSync(current, { throwIfNoEntry: false });
129
- if (!stat && create) {
130
- fs.mkdirSync(current);
131
- stat = fs.lstatSync(current);
132
- }
133
- if (!stat) return null;
134
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
135
- throw new TaskLedgerError(
136
- 'TASK_LEDGER_UNSAFE',
137
- `task ledger directory is unsafe: ${current}`,
138
- );
139
- }
140
- const physical = fs.realpathSync.native(current);
141
- if (physical !== physicalRoot && !physical.startsWith(`${physicalRoot}${path.sep}`)) {
142
- throw new TaskLedgerError(
143
- 'TASK_LEDGER_UNSAFE',
144
- `task ledger directory escapes the project: ${current}`,
145
- );
146
- }
147
- }
148
- return current;
149
- }
150
-
151
- function sha256(value) {
152
- return crypto.createHash('sha256').update(
153
- typeof value === 'string' ? value : JSON.stringify(value),
154
- ).digest('hex');
155
- }
156
-
157
- function parseJson(value, fallback) {
158
- if (value == null || value === '') return fallback;
159
- try { return JSON.parse(value); }
160
- catch { return fallback; }
161
- }
162
-
163
- function compactObject(entries) {
164
- return Object.fromEntries(entries.filter(([, value]) => value !== null && value !== undefined));
165
- }
166
-
167
- function portableBaseline(row) {
168
- if (!row) return null;
169
- return compactObject([
170
- ['id', row.id],
171
- ['project_name', row.project_name],
172
- ['project_type', row.project_type],
173
- ['stack', row.stack],
174
- ['mode', row.mode],
175
- ['status', row.status],
176
- ['scope', Array.isArray(row.scope) ? row.scope : parseJson(row.scope_json, ['.'])],
177
- ['repository_revision', row.repository_revision],
178
- ['repository_branch', row.repository_branch],
179
- ['worktree_state', row.worktree_state],
180
- ['worktree_digest', row.worktree_digest],
181
- ['worktree_accepted', Boolean(row.worktree_accepted)],
182
- ['known_red_accepted', Boolean(row.known_red_accepted)],
183
- ['spec_refs', Array.isArray(row.spec_refs) ? row.spec_refs : parseJson(row.spec_refs_json, [])],
184
- ['evidence', Array.isArray(row.evidence) ? row.evidence : parseJson(row.evidence_json, [])],
185
- ['verification', Array.isArray(row.verification)
186
- ? row.verification : parseJson(row.verification_json, [])],
187
- ['unknowns', Array.isArray(row.unknowns) ? row.unknowns : parseJson(row.unknowns_json, [])],
188
- ['gaps', Array.isArray(row.gaps) ? row.gaps : parseJson(row.gaps_json, [])],
189
- ['classification', row.classification && typeof row.classification === 'object'
190
- ? row.classification : parseJson(row.classification_json, {})],
191
- ['provider_refs', row.provider_refs && typeof row.provider_refs === 'object'
192
- ? row.provider_refs : parseJson(row.provider_refs_json, {})],
193
- ['research_checkpoint_id', row.research_checkpoint_id || row.research_run_id],
194
- ['approved_by', row.approved_by],
195
- ['approval_note', row.approval_note],
196
- ['converged_at', row.converged_at],
197
- ]);
198
- }
199
-
200
- function durableBaseline(row, previous = null) {
201
- if (!row) return null;
202
- const value = portableBaseline(row);
203
- const digest = sha256(value);
204
- if (previous?.digest === digest) return previous;
205
- return {
206
- ...value,
207
- revision: previous ? Number(previous.revision || 0) + 1 : 1,
208
- parent_digest: previous?.digest || null,
209
- digest,
210
- };
211
- }
212
-
213
- function readBaselineForLedger(db, previous = null) {
214
- const row = db.prepare(
215
- `SELECT * FROM baselines
216
- WHERE status <> 'superseded'
217
- ORDER BY CASE status WHEN 'ready' THEN 0 ELSE 1 END, updated_at DESC, rowid DESC
218
- LIMIT 1`,
219
- ).get();
220
- return durableBaseline(row, previous);
221
- }
222
-
223
- function portableChange(row) {
224
- return compactObject([
225
- ['id', row.id],
226
- ['title', row.title],
227
- ['kind', row.kind],
228
- ['status', ['blocked', 'ready'].includes(row.status) ? 'active' : row.status],
229
- ['intent', row.intent],
230
- ['docs_impact', parseJson(row.docs_impact_json, {})],
231
- ['provider_refs', parseJson(row.provider_refs_json, {})],
232
- ['baseline_bypass', parseJson(row.baseline_bypass_json, null)],
233
- ['contract', parseJson(row.contract_json, {})],
234
- ['classification', parseJson(row.classification_json, {})],
235
- ['research_disposition', parseJson(row.research_disposition_json, {})],
236
- ['base_commit', row.base_commit],
237
- ['supersedes_id', row.supersedes_id],
238
- ['artifact_root', row.artifact_root],
239
- ['closed_at', row.closed_at],
240
- ]);
241
- }
242
-
243
- function durableChange(row, previous = null) {
244
- const value = portableChange(row);
245
- const digest = sha256(value);
246
- if (previous?.digest === digest) return previous;
247
- return {
248
- ...value,
249
- revision: previous ? Number(previous.revision || 0) + 1 : 1,
250
- parent_digest: previous?.digest || null,
251
- digest,
252
- };
253
- }
254
-
255
- function readChangesForLedger(db, taskRows, previousChanges = new Map()) {
256
- const rows = db.prepare('SELECT * FROM changes ORDER BY id').all();
257
- const byId = new Map(rows.map((row) => [row.id, row]));
258
- for (const task of taskRows) {
259
- if (!task.change_id) continue;
260
- const row = byId.get(task.change_id);
261
- if (!row) {
262
- throw new TaskLedgerError(
263
- 'TASK_LEDGER_CHANGE_MISSING',
264
- `task ledger cannot publish missing owning change ${task.change_id}`,
265
- );
266
- }
267
- }
268
- return rows.map((row) => durableChange(row, previousChanges.get(row.id)));
269
- }
270
-
271
- function durableDecision(row) {
272
- const value = compactObject([
273
- ['id', row.id],
274
- ['scope_type', row.scope_type],
275
- ['scope_id', row.scope_id],
276
- ['selection', row.selection],
277
- ['effects', parseJson(row.effects_json, {})],
278
- ['status', row.status],
279
- ['artifact_path', row.artifact_path],
280
- ['artifact_digest', row.digest],
281
- ['supersedes_id', row.supersedes_id],
282
- ]);
283
- return {
284
- ...value,
285
- digest: sha256(value),
286
- };
287
- }
288
-
289
- function readDecisionsForLedger(db) {
290
- return db.prepare(
291
- `SELECT * FROM decision_records
292
- WHERE status IN ('accepted', 'superseded')
293
- ORDER BY scope_type, scope_id, created_at, id`,
294
- ).all().map(durableDecision);
295
- }
296
-
297
- function durableCheckpoint(row) {
298
- const value = compactObject([
299
- ['stage', row.stage],
300
- ['scope_type', row.scope_type],
301
- ['scope_id', row.scope_id],
302
- ['revision', row.revision],
303
- ['payload', parseJson(row.payload_json, {})],
304
- ['evidence', parseJson(row.evidence_json, [])],
305
- ['diagnostics', parseJson(row.diagnostics_json, [])],
306
- ['status', row.status],
307
- ]);
308
- return {
309
- ...value,
310
- digest: sha256(value),
311
- };
312
- }
313
-
314
- function readCheckpointsForLedger(db) {
315
- return db.prepare(
316
- `SELECT * FROM stage_checkpoints
317
- WHERE status = 'accepted'
318
- ORDER BY stage, scope_type, scope_id, revision`,
319
- ).all().map(durableCheckpoint);
320
- }
321
-
322
- function durableStatus(task, previous) {
323
- if (task.status !== 'in_progress') return task.status;
324
- if (previous && previous.status !== 'in_progress') return previous.status;
325
- return 'pending';
326
- }
327
-
328
- function assertValidTaskContract(entry) {
329
- try {
330
- taskContract.validateDurableTask(entry, { metadata: false });
331
- } catch (error) {
332
- if (!(error instanceof taskContract.TaskContractError)) throw error;
333
- throw new TaskLedgerError(
334
- 'TASK_LEDGER_INVALID',
335
- `task ledger entry ${entry?.id || '(unknown)'} violates the Task Contract: ${error.message}`,
336
- error.details,
337
- );
338
- }
339
- return entry;
340
- }
341
-
342
- function durableTask(task, previous = null) {
343
- const value = compactObject([
344
- ['id', task.id],
345
- ['title', task.title],
346
- ['type', task.type],
347
- ['priority', task.priority],
348
- ['complexity', task.complexity],
349
- ['estimated_days', task.estimated_days],
350
- ['status', durableStatus(task, previous)],
351
- ['deps', task.deps == null ? [] : task.deps],
352
- ['files_modified', task.files_modified == null ? [] : task.files_modified],
353
- ['stale', task.stale == null ? false : task.stale],
354
- ['trace_to', task.trace_to],
355
- ['outcome', task.outcome],
356
- ['slice_kind', task.slice_kind],
357
- ['public_seam', task.public_seam],
358
- ['verification_command', task.verification_command],
359
- ['acceptance', task.acceptance == null ? [] : task.acceptance],
360
- ['context_refs', task.context_refs == null ? [] : task.context_refs],
361
- ['docs_impact', task.docs_impact == null
362
- ? { status: 'unknown', files: [], rationale: null }
363
- : task.docs_impact],
364
- ['ownership', task.ownership == null ? {} : task.ownership],
365
- ['change_id', task.change_id],
366
- ['parent_id', task.parent_id],
367
- ]);
368
- const digest = sha256(value);
369
- const entry = {
370
- ...value,
371
- revision: previous ? Number(previous.revision || 0) + 1 : 1,
372
- parent_digest: previous?.digest || null,
373
- digest,
374
- };
375
- assertValidTaskContract(entry);
376
- if (previous && previous.digest === digest) return previous;
377
- return entry;
378
- }
379
-
380
- function statePayload({
381
- baseline,
382
- changes,
383
- decisions,
384
- checkpoints,
385
- tasks,
386
- }) {
387
- return {
388
- baseline: baseline || null,
389
- changes: Array.isArray(changes) ? changes : [],
390
- decisions: Array.isArray(decisions) ? decisions : [],
391
- checkpoints: Array.isArray(checkpoints) ? checkpoints : [],
392
- tasks: Array.isArray(tasks) ? tasks : [],
393
- };
394
- }
395
-
396
- function validateTask(entry) {
397
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
398
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every task ledger entry must be an object');
399
- }
400
- for (const field of [
401
- 'id', 'title', 'type', 'priority', 'status', 'revision', 'digest',
402
- ]) {
403
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
404
- throw new TaskLedgerError(
405
- 'TASK_LEDGER_INVALID',
406
- `task ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
407
- );
408
- }
409
- }
410
- if (!Object.prototype.hasOwnProperty.call(entry, 'parent_digest')) {
411
- throw new TaskLedgerError(
412
- 'TASK_LEDGER_INVALID',
413
- `task ledger entry ${entry.id || '(unknown)'} is missing parent_digest`,
414
- );
415
- }
416
- assertValidTaskContract(entry);
417
- if (!Number.isInteger(entry.revision) || entry.revision < 1) {
418
- throw new TaskLedgerError(
419
- 'TASK_LEDGER_INVALID',
420
- `task ledger entry ${entry.id} has an invalid revision`,
421
- );
422
- }
423
- if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(
424
- Object.fromEntries(DURABLE_TASK_FIELDS
425
- .filter((field) => entry[field] !== undefined)
426
- .map((field) => [field, entry[field]])),
427
- )) {
428
- throw new TaskLedgerError(
429
- 'TASK_LEDGER_DIGEST_MISMATCH',
430
- `task ledger entry ${entry.id} does not match its digest`,
431
- );
432
- }
433
- if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
434
- throw new TaskLedgerError(
435
- 'TASK_LEDGER_INVALID',
436
- `task ledger entry ${entry.id} has an invalid parent_digest`,
437
- );
438
- }
439
- }
440
-
441
- function validateLegacyTask(entry) {
442
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
443
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every legacy task entry must be an object');
444
- }
445
- for (const field of ['id', 'title', 'type', 'priority', 'status', 'revision', 'digest']) {
446
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
447
- throw new TaskLedgerError(
448
- 'TASK_LEDGER_INVALID',
449
- `legacy task entry ${entry.id || '(unknown)'} is missing ${field}`,
450
- );
451
- }
452
- }
453
- const value = Object.fromEntries(DURABLE_TASK_FIELDS
454
- .filter((field) => entry[field] !== undefined)
455
- .map((field) => [field, entry[field]]));
456
- if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
457
- throw new TaskLedgerError(
458
- 'TASK_LEDGER_DIGEST_MISMATCH',
459
- `legacy task entry ${entry.id} does not match its digest`,
460
- );
461
- }
462
- if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
463
- throw new TaskLedgerError(
464
- 'TASK_LEDGER_INVALID',
465
- `legacy task entry ${entry.id} has an invalid parent_digest`,
466
- );
467
- }
468
- }
469
-
470
- function validateBaseline(entry) {
471
- if (entry == null) return;
472
- if (typeof entry !== 'object' || Array.isArray(entry)) {
473
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'baseline ledger entry must be an object');
474
- }
475
- for (const field of [
476
- 'id', 'project_name', 'mode', 'status', 'scope', 'revision', 'digest',
477
- ]) {
478
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
479
- throw new TaskLedgerError(
480
- 'TASK_LEDGER_INVALID',
481
- `baseline ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
482
- );
483
- }
484
- }
485
- const value = Object.fromEntries(DURABLE_BASELINE_FIELDS
486
- .filter((field) => entry[field] !== undefined)
487
- .map((field) => [field, entry[field]]));
488
- if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
489
- throw new TaskLedgerError(
490
- 'TASK_LEDGER_DIGEST_MISMATCH',
491
- `baseline ledger entry ${entry.id} does not match its digest`,
492
- );
493
- }
494
- if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
495
- throw new TaskLedgerError(
496
- 'TASK_LEDGER_INVALID',
497
- `baseline ledger entry ${entry.id} has an invalid parent_digest`,
498
- );
499
- }
500
- }
501
-
502
- function validateLegacyBaseline(entry) {
503
- if (entry == null) return;
504
- if (typeof entry !== 'object' || Array.isArray(entry)) {
505
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'baseline ledger entry must be an object');
506
- }
507
- for (const field of [
508
- 'id', 'project_name', 'mode', 'status', 'scope', 'revision', 'digest',
509
- ]) {
510
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
511
- throw new TaskLedgerError(
512
- 'TASK_LEDGER_INVALID',
513
- `baseline ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
514
- );
515
- }
516
- }
517
- const value = Object.fromEntries(LEGACY_DURABLE_BASELINE_FIELDS
518
- .filter((field) => entry[field] !== undefined)
519
- .map((field) => [field, entry[field]]));
520
- if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
521
- throw new TaskLedgerError(
522
- 'TASK_LEDGER_DIGEST_MISMATCH',
523
- `baseline ledger entry ${entry.id} does not match its v1 digest`,
524
- );
525
- }
526
- if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
527
- throw new TaskLedgerError(
528
- 'TASK_LEDGER_INVALID',
529
- `baseline ledger entry ${entry.id} has an invalid parent_digest`,
530
- );
531
- }
532
- }
533
-
534
- function validateChange(entry) {
535
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
536
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every Change ledger entry must be an object');
537
- }
538
- for (const field of [
539
- 'id', 'title', 'kind', 'status', 'intent', 'artifact_root', 'revision', 'digest',
540
- ]) {
541
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
542
- throw new TaskLedgerError(
543
- 'TASK_LEDGER_INVALID',
544
- `Change ledger entry ${entry.id || '(unknown)'} is missing ${field}`,
545
- );
546
- }
547
- }
548
- const value = Object.fromEntries(DURABLE_CHANGE_FIELDS
549
- .filter((field) => entry[field] !== undefined)
550
- .map((field) => [field, entry[field]]));
551
- if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
552
- throw new TaskLedgerError(
553
- 'TASK_LEDGER_DIGEST_MISMATCH',
554
- `Change ledger entry ${entry.id} does not match its digest`,
555
- );
556
- }
557
- if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
558
- throw new TaskLedgerError(
559
- 'TASK_LEDGER_INVALID',
560
- `Change ledger entry ${entry.id} has an invalid parent_digest`,
561
- );
562
- }
563
- }
564
-
565
- function validateDecision(entry) {
566
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
567
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every decision entry must be an object');
568
- }
569
- for (const field of [
570
- 'id', 'scope_type', 'scope_id', 'selection', 'status',
571
- 'artifact_path', 'artifact_digest', 'digest',
572
- ]) {
573
- if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
574
- throw new TaskLedgerError(
575
- 'TASK_LEDGER_INVALID',
576
- `decision entry ${entry.id || '(unknown)'} is missing ${field}`,
577
- );
578
- }
579
- }
580
- const value = Object.fromEntries(DURABLE_DECISION_FIELDS
581
- .filter((field) => entry[field] !== undefined)
582
- .map((field) => [field, entry[field]]));
583
- if (!HASH_PATTERN.test(entry.artifact_digest)
584
- || !HASH_PATTERN.test(entry.digest)
585
- || entry.digest !== sha256(value)) {
586
- throw new TaskLedgerError(
587
- 'TASK_LEDGER_DIGEST_MISMATCH',
588
- `decision entry ${entry.id} does not match its digest`,
589
- );
590
- }
591
- }
592
-
593
- function validateCheckpoint(entry) {
594
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
595
- throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every checkpoint entry must be an object');
596
- }
597
- for (const field of [
598
- 'stage', 'scope_type', 'scope_id', 'revision', 'payload',
599
- 'evidence', 'diagnostics', 'status', 'digest',
600
- ]) {
601
- if (entry[field] === undefined || entry[field] === null) {
602
- throw new TaskLedgerError(
603
- 'TASK_LEDGER_INVALID',
604
- `checkpoint entry ${entry.stage || '(unknown)'} is missing ${field}`,
605
- );
606
- }
607
- }
608
- const value = Object.fromEntries(DURABLE_CHECKPOINT_FIELDS
609
- .filter((field) => entry[field] !== undefined)
610
- .map((field) => [field, entry[field]]));
611
- if (entry.status !== 'accepted'
612
- || !HASH_PATTERN.test(entry.digest)
613
- || entry.digest !== sha256(value)) {
614
- throw new TaskLedgerError(
615
- 'TASK_LEDGER_DIGEST_MISMATCH',
616
- `checkpoint ${entry.stage}:${entry.scope_type}:${entry.scope_id} is invalid`,
617
- );
618
- }
619
- }
620
-
621
- function validateLegacyLedger(document, file) {
622
- if (!document || document.kind !== LEDGER_KIND
623
- || document.schema_version !== LEGACY_LEDGER_SCHEMA_VERSION
624
- || !Array.isArray(document.tasks)
625
- || !Array.isArray(document.changes)
626
- || !Array.isArray(document.ancestors)) {
627
- return false;
628
- }
629
- validateLegacyBaseline(document.baseline);
630
- document.tasks.forEach(validateLegacyTask);
631
- document.changes.forEach(validateChange);
632
- const expected = sha256({
633
- baseline: document.baseline || null,
634
- changes: document.changes,
635
- tasks: document.tasks,
636
- });
637
- if (!HASH_PATTERN.test(document.state_digest || '')
638
- || document.state_digest !== expected) {
639
- throw new TaskLedgerError(
640
- 'TASK_LEDGER_DIGEST_MISMATCH',
641
- `${file} legacy state_digest does not match its durable payload`,
642
- );
643
- }
644
- return true;
645
- }
646
-
647
- function validateLedger(document, file = '(memory)') {
648
- if (document?.schema_version === LEGACY_LEDGER_SCHEMA_VERSION
649
- && validateLegacyLedger(document, file)) {
650
- throw new TaskLedgerError(
651
- 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED',
652
- `${file} is a valid v${LEGACY_LEDGER_SCHEMA_VERSION} ledger and must migrate to v${LEDGER_SCHEMA_VERSION}`,
653
- {
654
- from: LEGACY_LEDGER_SCHEMA_VERSION,
655
- to: LEDGER_SCHEMA_VERSION,
656
- },
657
- );
658
- }
659
- if (!document || typeof document !== 'object' || Array.isArray(document)
660
- || document.kind !== LEDGER_KIND
661
- || document.schema_version !== LEDGER_SCHEMA_VERSION
662
- || !Number.isInteger(document.generation)
663
- || document.generation < 0
664
- || !Array.isArray(document.ancestors)
665
- || !Array.isArray(document.tasks)
666
- || !Array.isArray(document.changes)
667
- || !Array.isArray(document.decisions)
668
- || !Array.isArray(document.checkpoints)) {
669
- throw new TaskLedgerError(
670
- 'TASK_LEDGER_INVALID',
671
- `${file} is not an Ultra team task ledger v${LEDGER_SCHEMA_VERSION}`,
672
- );
673
- }
674
- if (!validateTaskLedgerSchema(document)) {
675
- const first = validateTaskLedgerSchema.errors?.[0];
676
- const location = first?.instancePath || '(root)';
677
- throw new TaskLedgerError(
678
- 'TASK_LEDGER_INVALID',
679
- `${file} violates the exact v${LEDGER_SCHEMA_VERSION} schema at ${location}: `
680
- + `${first?.message || 'schema validation failed'}`,
681
- { errors: validateTaskLedgerSchema.errors || [] },
682
- );
683
- }
684
- if (document.ancestors.length > MAX_ANCESTORS
685
- || document.ancestors.some((digest) => !HASH_PATTERN.test(digest))) {
686
- throw new TaskLedgerError(
687
- 'TASK_LEDGER_INVALID',
688
- `${file} contains an invalid checkpoint ancestry chain`,
689
- );
690
- }
691
- validateBaseline(document.baseline);
692
- const seen = new Set();
693
- for (const task of document.tasks) {
694
- validateTask(task);
695
- if (seen.has(task.id)) {
696
- throw new TaskLedgerError('TASK_LEDGER_INVALID', `${file} contains duplicate task ${task.id}`);
697
- }
698
- seen.add(task.id);
699
- }
700
- const seenChanges = new Set();
701
- for (const change of document.changes) {
702
- validateChange(change);
703
- if (seenChanges.has(change.id)) {
704
- throw new TaskLedgerError(
705
- 'TASK_LEDGER_INVALID',
706
- `${file} contains duplicate Change ${change.id}`,
707
- );
708
- }
709
- seenChanges.add(change.id);
710
- }
711
- const seenDecisions = new Set();
712
- for (const decision of document.decisions) {
713
- validateDecision(decision);
714
- if (seenDecisions.has(decision.id)) {
715
- throw new TaskLedgerError(
716
- 'TASK_LEDGER_INVALID',
717
- `${file} contains duplicate decision ${decision.id}`,
718
- );
719
- }
720
- seenDecisions.add(decision.id);
721
- }
722
- const seenCheckpoints = new Set();
723
- for (const checkpoint of document.checkpoints) {
724
- validateCheckpoint(checkpoint);
725
- const key = `${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`;
726
- if (seenCheckpoints.has(key)) {
727
- throw new TaskLedgerError(
728
- 'TASK_LEDGER_INVALID',
729
- `${file} contains multiple accepted checkpoints for ${key}`,
730
- );
731
- }
732
- seenCheckpoints.add(key);
733
- }
734
- const expected = sha256(statePayload(document));
735
- if (!HASH_PATTERN.test(document.state_digest || '') || document.state_digest !== expected) {
736
- throw new TaskLedgerError(
737
- 'TASK_LEDGER_DIGEST_MISMATCH',
738
- `${file} state_digest does not match its durable payload`,
739
- );
740
- }
741
- if (document.parent_digest != null && !HASH_PATTERN.test(document.parent_digest)) {
742
- throw new TaskLedgerError('TASK_LEDGER_INVALID', `${file} has an invalid parent_digest`);
743
- }
744
- return document;
745
- }
746
-
747
- function readTaskLedger(rootDir, { optional = true } = {}) {
748
- const file = ledgerPath(rootDir);
749
- const directory = safeDirectoryChain(rootDir, ['.ultra', 'tasks']);
750
- if (!directory) {
751
- if (optional) return null;
752
- throw new TaskLedgerError('TASK_LEDGER_INVALID', `task ledger is missing: ${file}`);
753
- }
754
- let stat;
755
- try { stat = fs.lstatSync(file); }
756
- catch (error) {
757
- if (error.code === 'ENOENT' && optional) return null;
758
- throw error;
759
- }
760
- if (stat.isSymbolicLink() || !stat.isFile()) {
761
- throw new TaskLedgerError('TASK_LEDGER_UNSAFE', `task ledger must be a regular file: ${file}`);
762
- }
763
- let parsed;
764
- try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); }
765
- catch (error) {
766
- throw new TaskLedgerError('TASK_LEDGER_INVALID', `cannot read ${file}: ${error.message}`);
767
- }
768
- return validateLedger(parsed, file);
769
- }
770
-
771
- function writeLedgerAtomic(rootDir, document) {
772
- writeManagedFile(
773
- rootDir,
774
- LEDGER_RELATIVE_PATH,
775
- `${JSON.stringify(document, null, 2)}\n`,
776
- );
777
- return ledgerPath(rootDir);
778
- }
779
-
780
- function migrateLegacyProjection(db, rootDir, originalError) {
781
- const file = ledgerPath(rootDir);
782
- let bytes;
783
- let document;
784
- try {
785
- bytes = fs.readFileSync(file);
786
- document = JSON.parse(bytes.toString('utf8'));
787
- } catch {
788
- throw originalError;
789
- }
790
- if (document?.kind === LEDGER_KIND
791
- || !document || !Array.isArray(document.tasks)
792
- || !['4.4', '4.5'].includes(String(document.version || document.schema_version))) {
793
- throw originalError;
794
- }
795
- const legacyIds = document.tasks.map((task) => String(task?.id || '')).sort();
796
- if (legacyIds.some((id) => !id) || new Set(legacyIds).size !== legacyIds.length) {
797
- throw new TaskLedgerError(
798
- 'TASK_LEDGER_LEGACY_CONFLICT',
799
- 'legacy tasks projection has missing or duplicate task ids',
800
- );
801
- }
802
- const currentIds = ops.listTasks(db, { status: 'any', limit: 1000 })
803
- .map((task) => task.id)
804
- .sort();
805
- if (JSON.stringify(legacyIds) !== JSON.stringify(currentIds)) {
806
- throw new TaskLedgerError(
807
- 'TASK_LEDGER_LEGACY_CONFLICT',
808
- 'legacy tasks projection does not match current SQLite task authority',
809
- { legacy_task_ids: legacyIds, current_task_ids: currentIds },
810
- );
811
- }
812
- const currentById = new Map(
813
- ops.listTasks(db, { status: 'any', limit: 1000 }).map((task) => [task.id, task]),
814
- );
815
- for (const legacyTask of document.tasks) {
816
- const current = currentById.get(legacyTask.id);
817
- for (const field of DURABLE_TASK_FIELDS) {
818
- if (legacyTask[field] === undefined) continue;
819
- if (!util.isDeepStrictEqual(legacyTask[field], current[field])) {
820
- throw new TaskLedgerError(
821
- 'TASK_LEDGER_LEGACY_CONFLICT',
822
- `legacy task ${legacyTask.id} field ${field} does not match SQLite authority`,
823
- {
824
- task_id: legacyTask.id,
825
- field,
826
- legacy_value: legacyTask[field],
827
- current_value: current[field],
828
- },
829
- );
830
- }
831
- }
832
- }
833
- const digest = sha256(bytes);
834
- const backupDir = safeDirectoryChain(
835
- rootDir,
836
- ['.ultra', '.runtime', 'backups', 'task-ledger'],
837
- { create: true },
838
- );
839
- const backup = path.join(backupDir, `legacy-tasks-${digest.slice(0, 16)}.json`);
840
- if (fs.existsSync(backup)) {
841
- const stat = fs.lstatSync(backup);
842
- if (stat.isSymbolicLink() || !stat.isFile()
843
- || !fs.readFileSync(backup).equals(bytes)) {
844
- throw new TaskLedgerError(
845
- 'TASK_LEDGER_UNSAFE',
846
- `legacy task ledger backup is unsafe or conflicting: ${backup}`,
847
- );
848
- }
849
- } else {
850
- fs.writeFileSync(backup, bytes, { flag: 'wx' });
851
- }
852
- return { previous: null, migrated: true, backup };
853
- }
854
-
855
- function migrateLegacyTeamLedger(rootDir, originalError) {
856
- const file = ledgerPath(rootDir);
857
- let bytes;
858
- let document;
859
- try {
860
- bytes = fs.readFileSync(file);
861
- document = JSON.parse(bytes.toString('utf8'));
862
- if (!validateLegacyLedger(document, file)) throw originalError;
863
- } catch (error) {
864
- if (error instanceof TaskLedgerError) throw error;
865
- throw originalError;
866
- }
867
- const digest = sha256(bytes);
868
- const backupDir = safeDirectoryChain(
869
- rootDir,
870
- ['.ultra', '.runtime', 'backups', 'task-ledger'],
871
- { create: true },
872
- );
873
- const backup = path.join(backupDir, `team-ledger-v1-${digest.slice(0, 16)}.json`);
874
- if (fs.existsSync(backup)) {
875
- const stat = fs.lstatSync(backup);
876
- if (stat.isSymbolicLink() || !stat.isFile()
877
- || !fs.readFileSync(backup).equals(bytes)) {
878
- throw new TaskLedgerError(
879
- 'TASK_LEDGER_UNSAFE',
880
- `legacy team ledger backup is unsafe or conflicting: ${backup}`,
881
- );
882
- }
883
- } else {
884
- fs.writeFileSync(backup, bytes, { flag: 'wx' });
885
- }
886
- return {
887
- previous: {
888
- ...document,
889
- decisions: [],
890
- checkpoints: [],
891
- },
892
- migrated: true,
893
- backup,
894
- migration_kind: 'team-ledger-v1',
895
- };
896
- }
897
-
898
- function priorLedgerForPublish(db, rootDir) {
899
- try {
900
- return { previous: readTaskLedger(rootDir), migrated: false, backup: null };
901
- } catch (error) {
902
- if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
903
- return migrateLegacyTeamLedger(rootDir, error);
904
- }
905
- if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
906
- throw error;
907
- }
908
- return migrateLegacyProjection(db, rootDir, error);
909
- }
910
- }
911
-
912
- function syncTaskLedger(db, { rootDir = '.' } = {}) {
913
- let document;
914
- try {
915
- document = readTaskLedger(rootDir);
916
- } catch (error) {
917
- if (error.code === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
918
- return { status: 'legacy', imported: 0, already_current: false };
919
- }
920
- if (!['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(error.code)) {
921
- throw error;
922
- }
923
- try {
924
- const raw = JSON.parse(fs.readFileSync(ledgerPath(rootDir), 'utf8'));
925
- if (raw && Array.isArray(raw.tasks)
926
- && ['4.4', '4.5'].includes(String(raw.version || raw.schema_version))) {
927
- return { status: 'legacy', imported: 0, already_current: false };
928
- }
929
- } catch {
930
- // Preserve the typed ledger error from the validated read.
931
- }
932
- throw error;
933
- }
934
- if (!document) {
935
- return { status: 'missing', imported: 0, already_current: false };
936
- }
937
- const checkpoint = latestImportedCheckpoint(db);
938
- if (checkpoint.state_digest === document.state_digest) {
939
- return {
940
- status: 'current',
941
- imported: 0,
942
- preserved: document.tasks.length,
943
- deleted: 0,
944
- imported_changes: 0,
945
- preserved_changes: document.changes.length,
946
- imported_decisions: 0,
947
- preserved_decisions: document.decisions.length,
948
- imported_checkpoints: 0,
949
- preserved_checkpoints: document.checkpoints.length,
950
- imported_baseline: false,
951
- preserved_baseline: Boolean(document.baseline),
952
- generation: document.generation,
953
- state_digest: document.state_digest,
954
- requires_plan_revalidation: false,
955
- requires_baseline_revalidation: false,
956
- already_current: true,
957
- };
958
- }
959
- return {
960
- status: 'imported',
961
- ...importTaskLedger(db, { rootDir }),
962
- };
963
- }
964
-
965
- function taskDigestMap(tasks) {
966
- return Object.fromEntries(tasks.map((task) => [task.id, task.digest]));
967
- }
968
-
969
- function checkpointDigestMap(checkpoints) {
970
- return Object.fromEntries(checkpoints.map((checkpoint) => [
971
- `${checkpoint.stage}:${checkpoint.scope_type}:${checkpoint.scope_id}`,
972
- checkpoint.digest,
973
- ]));
974
- }
975
-
976
- function recordLedgerEvent(db, type, ledger, extra = {}) {
977
- ops.appendEvent(db, {
978
- type,
979
- payload: {
980
- generation: ledger.generation,
981
- state_digest: ledger.state_digest,
982
- baseline_digest: ledger.baseline?.digest || null,
983
- baseline_checkpoint: ledger.baseline || null,
984
- task_digests: taskDigestMap(ledger.tasks),
985
- change_digests: taskDigestMap(ledger.changes),
986
- decision_digests: taskDigestMap(ledger.decisions || []),
987
- checkpoint_digests: checkpointDigestMap(ledger.checkpoints || []),
988
- ...extra,
989
- },
990
- });
991
- }
992
-
993
- function publishTaskLedger(db, {
994
- rootDir = '.',
995
- reason = 'manual_checkpoint',
996
- } = {}) {
997
- let prior = priorLedgerForPublish(db, rootDir);
998
- if (prior.previous && !prior.migrated) {
999
- syncTaskLedger(db, { rootDir });
1000
- prior = {
1001
- previous: readTaskLedger(rootDir),
1002
- migrated: false,
1003
- backup: null,
1004
- };
1005
- }
1006
- const previous = prior.previous;
1007
- const priorTasks = new Map((previous?.tasks || []).map((task) => [task.id, task]));
1008
- const priorChanges = new Map((previous?.changes || []).map((change) => [change.id, change]));
1009
- const rows = ops.listTasks(db, { status: 'any', limit: 1000 })
1010
- .sort((left, right) => left.id.localeCompare(right.id));
1011
- const tasks = rows.map((task) => durableTask(task, priorTasks.get(task.id)));
1012
- const baseline = readBaselineForLedger(db, previous?.baseline || null);
1013
- if (baseline?.gaps?.some((gap) => (
1014
- gap.id === 'team-ledger-revalidation-required' && gap.status === 'open'
1015
- ))) {
1016
- throw new TaskLedgerError(
1017
- 'TASK_LEDGER_REVALIDATION_REQUIRED',
1018
- 'the imported team baseline must converge in this checkout before publication',
1019
- { baseline_id: baseline.id, ledger_path: LEDGER_RELATIVE_PATH },
1020
- );
1021
- }
1022
- const changes = readChangesForLedger(db, rows, priorChanges);
1023
- const decisions = readDecisionsForLedger(db);
1024
- const checkpoints = readCheckpointsForLedger(db);
1025
- const digest = sha256(statePayload({
1026
- baseline,
1027
- changes,
1028
- decisions,
1029
- checkpoints,
1030
- tasks,
1031
- }));
1032
- if (previous?.state_digest === digest) {
1033
- return {
1034
- changed: false,
1035
- path: ledgerPath(rootDir),
1036
- ledger: previous,
1037
- };
1038
- }
1039
- const document = {
1040
- kind: LEDGER_KIND,
1041
- schema_version: LEDGER_SCHEMA_VERSION,
1042
- generation: Number(previous?.generation || 0) + 1,
1043
- parent_digest: previous?.state_digest || null,
1044
- ancestors: previous
1045
- ? [previous.state_digest, ...(previous.ancestors || [])].slice(0, MAX_ANCESTORS)
1046
- : [],
1047
- state_digest: digest,
1048
- published_at: new Date().toISOString(),
1049
- reason,
1050
- baseline,
1051
- changes,
1052
- decisions,
1053
- checkpoints,
1054
- tasks,
1055
- };
1056
- validateLedger(document);
1057
- const snapshot = snapshotManagedFile(rootDir, LEDGER_RELATIVE_PATH);
1058
- let file;
1059
- try {
1060
- file = writeLedgerAtomic(rootDir, document);
1061
- recordLedgerEvent(db, 'task_ledger_published', document, {
1062
- reason,
1063
- migrated_legacy_projection: prior.migrated,
1064
- legacy_backup_path: prior.backup,
1065
- });
1066
- } catch (error) {
1067
- try {
1068
- restoreManagedFile(rootDir, snapshot);
1069
- } catch (rollbackError) {
1070
- throw new TaskLedgerError(
1071
- 'STATE_CORRUPT',
1072
- 'task ledger publication failed and the previous ledger bytes could not be restored',
1073
- {
1074
- cause: error.code || error.message,
1075
- rollback: rollbackError.code || rollbackError.message,
1076
- path: LEDGER_RELATIVE_PATH,
1077
- },
1078
- );
1079
- }
1080
- throw error;
1081
- }
1082
- const result = {
1083
- changed: true,
1084
- path: file,
1085
- ledger: document,
1086
- migrated_legacy_projection: prior.migrated,
1087
- legacy_backup_path: prior.backup,
1088
- };
1089
- Object.defineProperty(result, PUBLICATION_ROLLBACK, {
1090
- configurable: false,
1091
- enumerable: false,
1092
- writable: false,
1093
- value: { rootDir: path.resolve(rootDir), snapshot },
1094
- });
1095
- return result;
1096
- }
1097
-
1098
- function rollbackTaskLedgerPublication(publication) {
1099
- const rollback = publication?.[PUBLICATION_ROLLBACK];
1100
- if (!rollback) return { restored: false, reason: 'NO_PUBLICATION_ROLLBACK' };
1101
- restoreManagedFile(rollback.rootDir, rollback.snapshot);
1102
- return {
1103
- restored: true,
1104
- path: rollback.snapshot.path,
1105
- previous_existed: rollback.snapshot.existed,
1106
- };
1107
- }
1108
-
1109
- function latestImportedCheckpoint(db) {
1110
- const row = db.prepare(
1111
- `SELECT payload_json FROM events
1112
- WHERE type IN ('task_ledger_imported', 'task_ledger_published')
1113
- ORDER BY id DESC LIMIT 1`,
1114
- ).get();
1115
- const payload = parseJson(row?.payload_json, {});
1116
- return {
1117
- generation: Number.isInteger(payload.generation) ? payload.generation : null,
1118
- state_digest: HASH_PATTERN.test(payload.state_digest || '') ? payload.state_digest : null,
1119
- task_digests: payload.task_digests && typeof payload.task_digests === 'object'
1120
- ? payload.task_digests
1121
- : {},
1122
- change_digests: payload.change_digests && typeof payload.change_digests === 'object'
1123
- ? payload.change_digests
1124
- : {},
1125
- decision_digests: payload.decision_digests && typeof payload.decision_digests === 'object'
1126
- ? payload.decision_digests
1127
- : {},
1128
- checkpoint_digests: payload.checkpoint_digests
1129
- && typeof payload.checkpoint_digests === 'object'
1130
- ? payload.checkpoint_digests
1131
- : {},
1132
- baseline_digest: HASH_PATTERN.test(payload.baseline_digest || '')
1133
- ? payload.baseline_digest
1134
- : null,
1135
- baseline_checkpoint: payload.baseline_checkpoint
1136
- && typeof payload.baseline_checkpoint === 'object'
1137
- ? payload.baseline_checkpoint
1138
- : null,
1139
- };
1140
- }
1141
-
1142
- function baselineForLocalCheckout(baseline) {
1143
- const value = portableBaseline(baseline);
1144
- const requiresRevalidation = baseline.status === 'ready';
1145
- if (!requiresRevalidation) return { value, requiresRevalidation: false };
1146
- const gaps = [...(value.gaps || [])];
1147
- if (requiresRevalidation && !gaps.some((gap) => gap.id === 'team-ledger-revalidation-required')) {
1148
- gaps.push({
1149
- id: 'team-ledger-revalidation-required',
1150
- category: 'baseline_blocker',
1151
- status: 'open',
1152
- blocking: true,
1153
- summary: 'The imported team checkpoint must be revalidated against this checkout.',
1154
- evidence_refs: [LEDGER_RELATIVE_PATH],
1155
- owner: 'project-owner',
1156
- });
1157
- }
1158
- return {
1159
- value: compactObject(Object.entries({
1160
- ...value,
1161
- status: value.mode === 'greenfield' ? 'draft' : 'adopting',
1162
- worktree_accepted: false,
1163
- known_red_accepted: false,
1164
- gaps,
1165
- research_checkpoint_id: undefined,
1166
- approved_by: undefined,
1167
- approval_note: undefined,
1168
- converged_at: undefined,
1169
- })),
1170
- requiresRevalidation: true,
1171
- };
1172
- }
1173
-
1174
- function writeBaseline(db, baseline, { update = false } = {}) {
1175
- const local = baselineForLocalCheckout(baseline);
1176
- const value = local.value;
1177
- if (update) {
1178
- db.prepare(
1179
- `UPDATE baselines SET
1180
- project_name = ?, project_type = ?, stack = ?, mode = ?, status = ?,
1181
- scope_json = ?, repository_revision = ?, repository_branch = ?,
1182
- worktree_state = ?, worktree_digest = ?, worktree_accepted = ?,
1183
- known_red_accepted = ?, spec_refs_json = ?, evidence_json = ?,
1184
- verification_json = ?, unknowns_json = ?, gaps_json = ?,
1185
- classification_json = ?, provider_refs_json = ?,
1186
- research_checkpoint_id = ?, research_run_id = NULL,
1187
- approved_by = ?, approval_note = ?, converged_at = ?, updated_at = ?
1188
- WHERE id = ?`,
1189
- ).run(
1190
- value.project_name,
1191
- value.project_type || null,
1192
- value.stack || null,
1193
- value.mode,
1194
- value.status,
1195
- JSON.stringify(value.scope || ['.']),
1196
- value.repository_revision || null,
1197
- value.repository_branch || null,
1198
- value.worktree_state || 'unavailable',
1199
- value.worktree_digest || null,
1200
- value.worktree_accepted ? 1 : 0,
1201
- value.known_red_accepted ? 1 : 0,
1202
- JSON.stringify(value.spec_refs || []),
1203
- JSON.stringify(value.evidence || []),
1204
- JSON.stringify(value.verification || []),
1205
- JSON.stringify(value.unknowns || []),
1206
- JSON.stringify(value.gaps || []),
1207
- JSON.stringify(value.classification || {}),
1208
- JSON.stringify(value.provider_refs || {}),
1209
- value.research_checkpoint_id || null,
1210
- value.approved_by || null,
1211
- value.approval_note || null,
1212
- value.converged_at || null,
1213
- new Date().toISOString(),
1214
- value.id,
1215
- );
1216
- return local;
1217
- }
1218
- db.prepare(
1219
- `INSERT INTO baselines
1220
- (id, project_name, project_type, stack, mode, status, repository_root, scope_json,
1221
- repository_revision, repository_branch, worktree_state, worktree_digest,
1222
- worktree_accepted, known_red_accepted, spec_refs_json, evidence_json,
1223
- verification_json, unknowns_json, gaps_json, classification_json,
1224
- provider_refs_json, research_checkpoint_id, research_run_id,
1225
- approved_by, approval_note, converged_at)
1226
- VALUES (?, ?, ?, ?, ?, ?, '.', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)`,
1227
- ).run(
1228
- value.id,
1229
- value.project_name,
1230
- value.project_type || null,
1231
- value.stack || null,
1232
- value.mode,
1233
- value.status,
1234
- JSON.stringify(value.scope || ['.']),
1235
- value.repository_revision || null,
1236
- value.repository_branch || null,
1237
- value.worktree_state || 'unavailable',
1238
- value.worktree_digest || null,
1239
- value.worktree_accepted ? 1 : 0,
1240
- value.known_red_accepted ? 1 : 0,
1241
- JSON.stringify(value.spec_refs || []),
1242
- JSON.stringify(value.evidence || []),
1243
- JSON.stringify(value.verification || []),
1244
- JSON.stringify(value.unknowns || []),
1245
- JSON.stringify(value.gaps || []),
1246
- JSON.stringify(value.classification || {}),
1247
- JSON.stringify(value.provider_refs || {}),
1248
- value.research_checkpoint_id || null,
1249
- value.approved_by || null,
1250
- value.approval_note || null,
1251
- value.converged_at || null,
1252
- );
1253
- return local;
1254
- }
1255
-
1256
- function localBaselineMatchesRevalidation(row, checkpointBaseline) {
1257
- if (!row || !checkpointBaseline || row.id !== checkpointBaseline.id
1258
- || checkpointBaseline.status !== 'ready') return false;
1259
- const expected = baselineForLocalCheckout(checkpointBaseline).value;
1260
- return util.isDeepStrictEqual(portableBaseline(row), expected);
1261
- }
1262
-
1263
- function mergeBaseline(db, incoming, checkpoint) {
1264
- if (!incoming) {
1265
- return { imported: false, preserved: false, requiresRevalidation: false };
1266
- }
1267
- const active = db.prepare(
1268
- `SELECT * FROM baselines
1269
- WHERE status <> 'superseded'
1270
- ORDER BY CASE status WHEN 'ready' THEN 0 ELSE 1 END, updated_at DESC, rowid DESC
1271
- LIMIT 1`,
1272
- ).get();
1273
- if (!active) {
1274
- const written = writeBaseline(db, incoming);
1275
- return {
1276
- imported: true,
1277
- preserved: false,
1278
- requiresRevalidation: written.requiresRevalidation,
1279
- };
1280
- }
1281
-
1282
- const current = durableBaseline(active, null);
1283
- const lastDigest = checkpoint.baseline_digest;
1284
- const effectiveCurrentDigest = localBaselineMatchesRevalidation(
1285
- active,
1286
- checkpoint.baseline_checkpoint,
1287
- ) ? lastDigest : current.digest;
1288
- if (active.id === incoming.id && current.digest === incoming.digest) {
1289
- return { imported: false, preserved: true, requiresRevalidation: false };
1290
- }
1291
- if (!lastDigest) {
1292
- throw new TaskLedgerError(
1293
- 'TASK_LEDGER_BASELINE_CONFLICT',
1294
- `baseline ${active.id} exists locally without a common team checkpoint`,
1295
- {
1296
- local_baseline_id: active.id,
1297
- incoming_baseline_id: incoming.id,
1298
- local_digest: current.digest,
1299
- incoming_digest: incoming.digest,
1300
- },
1301
- );
1302
- }
1303
- if (effectiveCurrentDigest !== lastDigest && incoming.digest === lastDigest) {
1304
- return { imported: false, preserved: true, requiresRevalidation: false };
1305
- }
1306
- if (effectiveCurrentDigest !== lastDigest && incoming.digest !== lastDigest) {
1307
- throw new TaskLedgerError(
1308
- 'TASK_LEDGER_BASELINE_CONFLICT',
1309
- 'baseline changed both locally and in the Git ledger',
1310
- {
1311
- local_baseline_id: active.id,
1312
- incoming_baseline_id: incoming.id,
1313
- last_digest: lastDigest,
1314
- local_digest: current.digest,
1315
- incoming_digest: incoming.digest,
1316
- },
1317
- );
1318
- }
1319
- if (active.id !== incoming.id) {
1320
- db.prepare(
1321
- "UPDATE baselines SET status = 'superseded', updated_at = ? WHERE id = ?",
1322
- ).run(new Date().toISOString(), active.id);
1323
- const written = writeBaseline(db, incoming);
1324
- return {
1325
- imported: true,
1326
- preserved: false,
1327
- requiresRevalidation: written.requiresRevalidation,
1328
- };
1329
- }
1330
- const written = writeBaseline(db, incoming, { update: true });
1331
- return {
1332
- imported: true,
1333
- preserved: false,
1334
- requiresRevalidation: written.requiresRevalidation,
1335
- };
1336
- }
1337
-
1338
- function insertChange(db, change) {
1339
- db.prepare(
1340
- `INSERT INTO changes
1341
- (id, title, kind, status, intent, docs_impact_json, provider_refs_json,
1342
- baseline_bypass_json, contract_json, classification_json,
1343
- research_disposition_json, base_commit, supersedes_id, artifact_root, closed_at)
1344
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1345
- ).run(
1346
- change.id,
1347
- change.title,
1348
- change.kind,
1349
- change.status,
1350
- change.intent,
1351
- JSON.stringify(change.docs_impact || {}),
1352
- JSON.stringify(change.provider_refs || {}),
1353
- change.baseline_bypass ? JSON.stringify(change.baseline_bypass) : null,
1354
- JSON.stringify(change.contract || {}),
1355
- JSON.stringify(change.classification || {}),
1356
- JSON.stringify(change.research_disposition || {}),
1357
- change.base_commit || null,
1358
- change.supersedes_id || null,
1359
- change.artifact_root,
1360
- change.closed_at || null,
1361
- );
1362
- }
1363
-
1364
- function updateChangeFromLedger(db, change) {
1365
- db.prepare(
1366
- `UPDATE changes SET
1367
- title = ?, kind = ?, status = ?, intent = ?, docs_impact_json = ?,
1368
- provider_refs_json = ?, baseline_bypass_json = ?, contract_json = ?,
1369
- classification_json = ?, research_disposition_json = ?, base_commit = ?,
1370
- supersedes_id = ?, artifact_root = ?, closed_at = ?, updated_at = ?
1371
- WHERE id = ?`,
1372
- ).run(
1373
- change.title,
1374
- change.kind,
1375
- change.status,
1376
- change.intent,
1377
- JSON.stringify(change.docs_impact || {}),
1378
- JSON.stringify(change.provider_refs || {}),
1379
- change.baseline_bypass ? JSON.stringify(change.baseline_bypass) : null,
1380
- JSON.stringify(change.contract || {}),
1381
- JSON.stringify(change.classification || {}),
1382
- JSON.stringify(change.research_disposition || {}),
1383
- change.base_commit || null,
1384
- change.supersedes_id || null,
1385
- change.artifact_root,
1386
- change.closed_at || null,
1387
- new Date().toISOString(),
1388
- change.id,
1389
- );
1390
- }
1391
-
1392
- function insertTaskFromLedger(db, task) {
1393
- const created = ops.createTask(db, {
1394
- id: task.id,
1395
- title: task.title,
1396
- type: task.type,
1397
- priority: task.priority,
1398
- complexity: task.complexity,
1399
- estimated_days: task.estimated_days,
1400
- deps: task.deps || [],
1401
- files_modified: task.files_modified || [],
1402
- stale: Boolean(task.stale),
1403
- trace_to: task.trace_to,
1404
- outcome: task.outcome,
1405
- slice_kind: task.slice_kind,
1406
- public_seam: task.public_seam,
1407
- verification_command: task.verification_command,
1408
- acceptance: task.acceptance || [],
1409
- context_refs: task.context_refs || [],
1410
- docs_impact: task.docs_impact || { status: 'unknown', files: [], rationale: null },
1411
- ownership: task.ownership || {},
1412
- change_id: task.change_id || null,
1413
- parent_id: null,
1414
- });
1415
- if (task.status !== created.status || Boolean(task.stale) !== created.stale) {
1416
- updateTaskFromLedger(db, task);
1417
- }
1418
- return ops.readTask(db, task.id);
1419
- }
1420
-
1421
- function updateTaskFromLedger(db, task) {
1422
- db.prepare(
1423
- `UPDATE tasks SET
1424
- title = ?, type = ?, priority = ?, complexity = ?, estimated_days = ?,
1425
- status = ?, deps = ?, files_modified = ?, session_id = NULL, stale = ?,
1426
- trace_to = ?, outcome = ?, slice_kind = ?, public_seam = ?,
1427
- verification_command = ?, acceptance_json = ?, context_refs_json = ?,
1428
- docs_impact_json = ?, ownership_json = ?, change_id = ?, updated_at = ?
1429
- WHERE id = ?`,
1430
- ).run(
1431
- task.title,
1432
- task.type,
1433
- task.priority,
1434
- task.complexity ?? null,
1435
- task.estimated_days ?? null,
1436
- task.status,
1437
- JSON.stringify(task.deps || []),
1438
- JSON.stringify(task.files_modified || []),
1439
- task.stale ? 1 : 0,
1440
- task.trace_to || null,
1441
- task.outcome || null,
1442
- task.slice_kind || null,
1443
- task.public_seam || null,
1444
- task.verification_command || null,
1445
- JSON.stringify(task.acceptance || []),
1446
- JSON.stringify(task.context_refs || []),
1447
- JSON.stringify(task.docs_impact || { status: 'unknown', files: [], rationale: null }),
1448
- JSON.stringify(task.ownership || {}),
1449
- task.change_id || null,
1450
- new Date().toISOString(),
1451
- task.id,
1452
- );
1453
- }
1454
-
1455
- function decisionDocument(rootDir, entry) {
1456
- const read = readStableProjectFile(rootDir, entry.artifact_path, { encoding: 'utf8' });
1457
- let document;
1458
- try { document = JSON.parse(read.text); }
1459
- catch (error) {
1460
- throw new TaskLedgerError(
1461
- 'TASK_LEDGER_DECISION_INVALID',
1462
- `decision artifact ${entry.artifact_path} is invalid JSON: ${error.message}`,
1463
- );
1464
- }
1465
- const { digest, ...value } = document;
1466
- if (digest !== entry.artifact_digest || canonical.digest(value) !== digest) {
1467
- throw new TaskLedgerError(
1468
- 'TASK_LEDGER_DECISION_CONFLICT',
1469
- `decision artifact ${entry.artifact_path} does not match the team ledger`,
1470
- {
1471
- decision_id: entry.id,
1472
- ledger_digest: entry.artifact_digest,
1473
- artifact_digest: digest || null,
1474
- },
1475
- );
1476
- }
1477
- return document;
1478
- }
1479
-
1480
- function importDecisionEntry(db, rootDir, entry) {
1481
- const existing = decisionRecords.readDecision(db, entry.id);
1482
- if (existing) {
1483
- if (existing.digest !== entry.artifact_digest) {
1484
- throw new TaskLedgerError(
1485
- 'TASK_LEDGER_DECISION_CONFLICT',
1486
- `decision ${entry.id} changed both locally and in the team ledger`,
1487
- );
1488
- }
1489
- return false;
1490
- }
1491
- const document = decisionDocument(rootDir, entry);
1492
- const accepted = decisionRecords.acceptDecision(db, {
1493
- id: document.id,
1494
- scope: document.scope.type === 'baseline'
1495
- ? { baseline_id: document.scope.id }
1496
- : { change_id: document.scope.id },
1497
- question: document.question,
1498
- recommendation: document.recommendation,
1499
- selection: document.selection,
1500
- effects: document.effects,
1501
- non_goals: document.non_goals,
1502
- owner: document.owner,
1503
- source: document.source,
1504
- provenance: document.provenance,
1505
- applied_refs: document.applied_refs,
1506
- supersedes_id: document.supersedes_id || undefined,
1507
- }, { rootDir });
1508
- if (accepted.digest !== entry.artifact_digest) {
1509
- throw new TaskLedgerError(
1510
- 'TASK_LEDGER_DECISION_CONFLICT',
1511
- `decision ${entry.id} changed while it was imported`,
1512
- );
1513
- }
1514
- return true;
1515
- }
1516
-
1517
- function mergeTeamDecisions(db, rootDir, entries) {
1518
- const pending = new Map(entries.map((entry) => [entry.id, entry]));
1519
- let imported = 0;
1520
- while (pending.size > 0) {
1521
- let progressed = false;
1522
- for (const [id, entry] of pending) {
1523
- if (entry.supersedes_id
1524
- && pending.has(entry.supersedes_id)
1525
- && !decisionRecords.readDecision(db, entry.supersedes_id)) {
1526
- continue;
1527
- }
1528
- if (importDecisionEntry(db, rootDir, entry)) imported += 1;
1529
- pending.delete(id);
1530
- progressed = true;
1531
- }
1532
- if (!progressed) {
1533
- throw new TaskLedgerError(
1534
- 'TASK_LEDGER_DECISION_CONFLICT',
1535
- 'decision supersession graph cannot be imported deterministically',
1536
- { decision_ids: [...pending.keys()].sort() },
1537
- );
1538
- }
1539
- }
1540
- return imported;
1541
- }
1542
-
1543
- function checkpointKey(entry) {
1544
- return `${entry.stage}:${entry.scope_type}:${entry.scope_id}`;
1545
- }
1546
-
1547
- function mergeTeamCheckpoints(db, entries, lastDigests = {}) {
1548
- let imported = 0;
1549
- for (const entry of entries) {
1550
- const current = db.prepare(
1551
- `SELECT * FROM stage_checkpoints
1552
- WHERE stage = ? AND scope_type = ? AND scope_id = ? AND status = 'accepted'
1553
- ORDER BY revision DESC LIMIT 1`,
1554
- ).get(entry.stage, entry.scope_type, entry.scope_id);
1555
- const currentDurable = current ? durableCheckpoint(current) : null;
1556
- if (currentDurable?.digest === entry.digest) continue;
1557
- const key = checkpointKey(entry);
1558
- const last = lastDigests[key] || null;
1559
- if (currentDurable && (!last || currentDurable.digest !== last)) {
1560
- throw new TaskLedgerError(
1561
- 'TASK_LEDGER_CHECKPOINT_CONFLICT',
1562
- `checkpoint ${key} changed both locally and in the team ledger`,
1563
- {
1564
- checkpoint: key,
1565
- local_digest: currentDurable.digest,
1566
- incoming_digest: entry.digest,
1567
- last_digest: last,
1568
- },
1569
- );
1570
- }
1571
- if (current) {
1572
- db.prepare(
1573
- `UPDATE stage_checkpoints SET status = 'superseded',
1574
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
1575
- WHERE id = ?`,
1576
- ).run(current.id);
1577
- }
1578
- const importedId = `checkpoint-team-${entry.digest.slice(0, 24)}`;
1579
- const importedDigest = stageCheckpoints.checkpointDigest({
1580
- stage: entry.stage,
1581
- scope_type: entry.scope_type,
1582
- scope_id: entry.scope_id,
1583
- revision: entry.revision,
1584
- payload: entry.payload,
1585
- evidence: entry.evidence,
1586
- diagnostics: entry.diagnostics,
1587
- context_envelope_id: null,
1588
- supersedes_id: current?.id || null,
1589
- });
1590
- db.prepare(
1591
- `INSERT INTO stage_checkpoints
1592
- (id, stage, scope_type, scope_id, revision, status, payload_json,
1593
- evidence_json, diagnostics_json, context_envelope_id, digest,
1594
- supersedes_id, idempotency_key, accepted_at)
1595
- VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?, ?, NULL, ?, ?, ?,
1596
- strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
1597
- ).run(
1598
- importedId,
1599
- entry.stage,
1600
- entry.scope_type,
1601
- entry.scope_id,
1602
- entry.revision,
1603
- JSON.stringify(entry.payload),
1604
- JSON.stringify(entry.evidence),
1605
- JSON.stringify(entry.diagnostics),
1606
- importedDigest,
1607
- current?.id || null,
1608
- `team-import:${entry.digest}`,
1609
- );
1610
- imported += 1;
1611
- }
1612
- return imported;
1613
- }
1614
-
1615
- function importTaskLedger(db, {
1616
- rootDir = '.',
1617
- } = {}) {
1618
- const document = readTaskLedger(rootDir, { optional: false });
1619
- const checkpoint = latestImportedCheckpoint(db);
1620
- const lastDigests = checkpoint.task_digests;
1621
- const lastChangeDigests = checkpoint.change_digests;
1622
- if (checkpoint.state_digest && checkpoint.state_digest !== document.state_digest) {
1623
- if (document.generation < checkpoint.generation) {
1624
- throw new TaskLedgerError(
1625
- 'TASK_LEDGER_STALE',
1626
- `task ledger generation ${document.generation} is older than local generation ${checkpoint.generation}`,
1627
- {
1628
- incoming_generation: document.generation,
1629
- local_generation: checkpoint.generation,
1630
- },
1631
- );
1632
- }
1633
- if (document.generation === checkpoint.generation
1634
- || !document.ancestors.includes(checkpoint.state_digest)) {
1635
- throw new TaskLedgerError(
1636
- 'TASK_LEDGER_HISTORY_CONFLICT',
1637
- 'task ledger does not descend from the last local team checkpoint',
1638
- {
1639
- incoming_generation: document.generation,
1640
- local_generation: checkpoint.generation,
1641
- incoming_parent_digest: document.parent_digest,
1642
- local_state_digest: checkpoint.state_digest,
1643
- },
1644
- );
1645
- }
1646
- }
1647
- if (checkpoint.state_digest === document.state_digest) {
1648
- return {
1649
- imported: 0,
1650
- preserved: document.tasks.length,
1651
- deleted: 0,
1652
- imported_changes: 0,
1653
- preserved_changes: document.changes.length,
1654
- imported_decisions: 0,
1655
- preserved_decisions: document.decisions.length,
1656
- imported_checkpoints: 0,
1657
- preserved_checkpoints: document.checkpoints.length,
1658
- imported_baseline: false,
1659
- preserved_baseline: Boolean(document.baseline),
1660
- generation: document.generation,
1661
- state_digest: document.state_digest,
1662
- requires_plan_revalidation: false,
1663
- requires_baseline_revalidation: false,
1664
- already_current: true,
1665
- ledger: document,
1666
- };
1667
- }
1668
- const parentUpdates = [];
1669
- let imported = 0;
1670
- let preserved = 0;
1671
- let deleted = 0;
1672
- let importedChanges = 0;
1673
- let preservedChanges = 0;
1674
- let importedDecisions = 0;
1675
- let importedCheckpoints = 0;
1676
- let baselineImport = {
1677
- imported: false,
1678
- preserved: false,
1679
- requiresRevalidation: false,
1680
- };
1681
- const changesById = new Set((document.changes || []).map((change) => change.id));
1682
- for (const task of document.tasks) {
1683
- if (task.change_id && !changesById.has(task.change_id)
1684
- && !db.prepare('SELECT 1 FROM changes WHERE id = ?').get(task.change_id)) {
1685
- throw new TaskLedgerError(
1686
- 'TASK_LEDGER_CHANGE_MISSING',
1687
- `task ${task.id} references missing change ${task.change_id}`,
1688
- );
1689
- }
1690
- }
1691
-
1692
- ops.tx(db, () => {
1693
- baselineImport = mergeBaseline(db, document.baseline, checkpoint);
1694
- for (const incoming of document.changes) {
1695
- const row = db.prepare('SELECT * FROM changes WHERE id = ?').get(incoming.id);
1696
- if (!row) {
1697
- insertChange(db, incoming);
1698
- importedChanges += 1;
1699
- continue;
1700
- }
1701
- const current = durableChange(row, null);
1702
- const last = lastChangeDigests[incoming.id] || null;
1703
- if (current.digest === incoming.digest) {
1704
- preservedChanges += 1;
1705
- continue;
1706
- }
1707
- if (!last) {
1708
- throw new TaskLedgerError(
1709
- 'TASK_LEDGER_CHANGE_CONFLICT',
1710
- `Change ${incoming.id} exists locally without a common team checkpoint`,
1711
- {
1712
- change_id: incoming.id,
1713
- local_digest: current.digest,
1714
- incoming_digest: incoming.digest,
1715
- },
1716
- );
1717
- }
1718
- if (current.digest !== last && incoming.digest === last) {
1719
- preservedChanges += 1;
1720
- continue;
1721
- }
1722
- if (current.digest !== last && incoming.digest !== last) {
1723
- throw new TaskLedgerError(
1724
- 'TASK_LEDGER_CHANGE_CONFLICT',
1725
- `Change ${incoming.id} changed both locally and in the Git ledger`,
1726
- {
1727
- change_id: incoming.id,
1728
- last_digest: last,
1729
- local_digest: current.digest,
1730
- incoming_digest: incoming.digest,
1731
- },
1732
- );
1733
- }
1734
- updateChangeFromLedger(db, incoming);
1735
- importedChanges += 1;
1736
- }
1737
- for (const incoming of document.tasks) {
1738
- const current = ops.readTask(db, incoming.id);
1739
- const last = lastDigests[incoming.id] || null;
1740
- if (!current) {
1741
- insertTaskFromLedger(db, incoming);
1742
- parentUpdates.push([incoming.id, incoming.parent_id || null]);
1743
- imported += 1;
1744
- continue;
1745
- }
1746
- const currentDurable = durableTask(current, null);
1747
- if (currentDurable.digest === incoming.digest) {
1748
- preserved += 1;
1749
- parentUpdates.push([incoming.id, incoming.parent_id || null]);
1750
- continue;
1751
- }
1752
- const runningSession = db.prepare(
1753
- "SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
1754
- ).get(incoming.id);
1755
- if (current.session_id || runningSession) {
1756
- throw new TaskLedgerError(
1757
- 'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
1758
- `task ${incoming.id} changed in Git while a local session still owns it`,
1759
- {
1760
- task_id: incoming.id,
1761
- local_status: current.status,
1762
- task_session_id: current.session_id || null,
1763
- running_session_id: runningSession?.sid || null,
1764
- },
1765
- );
1766
- }
1767
- if (current.status === 'in_progress' && last && incoming.digest !== last) {
1768
- throw new TaskLedgerError(
1769
- 'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
1770
- `task ${incoming.id} changed in Git while a local session is in progress`,
1771
- { task_id: incoming.id, local_status: current.status },
1772
- );
1773
- }
1774
- if (last && currentDurable.digest !== last && incoming.digest === last) {
1775
- preserved += 1;
1776
- continue;
1777
- }
1778
- if (last && currentDurable.digest !== last && incoming.digest !== last) {
1779
- throw new TaskLedgerError(
1780
- 'TASK_LEDGER_CONFLICT',
1781
- `task ${incoming.id} changed both locally and in the Git ledger`,
1782
- {
1783
- task_id: incoming.id,
1784
- last_digest: last,
1785
- local_digest: currentDurable.digest,
1786
- incoming_digest: incoming.digest,
1787
- },
1788
- );
1789
- }
1790
- updateTaskFromLedger(db, incoming);
1791
- parentUpdates.push([incoming.id, incoming.parent_id || null]);
1792
- imported += 1;
1793
- }
1794
- const incomingIds = new Set(document.tasks.map((task) => task.id));
1795
- for (const [id, last] of Object.entries(lastDigests)) {
1796
- if (incomingIds.has(id)) continue;
1797
- const current = ops.readTask(db, id);
1798
- if (!current) continue;
1799
- const activeSession = db.prepare(
1800
- "SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
1801
- ).get(id);
1802
- if (current.status === 'in_progress' || current.session_id || activeSession) {
1803
- throw new TaskLedgerError(
1804
- 'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
1805
- `task ${id} was removed from Git while local work is in progress`,
1806
- { task_id: id, local_status: current.status },
1807
- );
1808
- }
1809
- const currentDurable = durableTask(current, null);
1810
- if (currentDurable.digest !== last) {
1811
- throw new TaskLedgerError(
1812
- 'TASK_LEDGER_CONFLICT',
1813
- `task ${id} was removed from Git after it changed locally`,
1814
- {
1815
- task_id: id,
1816
- last_digest: last,
1817
- local_digest: currentDurable.digest,
1818
- incoming_digest: null,
1819
- },
1820
- );
1821
- }
1822
- const sessionCount = db.prepare(
1823
- 'SELECT COUNT(*) AS count FROM sessions WHERE task_id = ?',
1824
- ).get(id).count;
1825
- if (sessionCount > 0) {
1826
- throw new TaskLedgerError(
1827
- 'TASK_LEDGER_CONFLICT',
1828
- `task ${id} cannot be removed because local session history still references it`,
1829
- { task_id: id, session_count: sessionCount },
1830
- );
1831
- }
1832
- ops.deleteTask(db, id, { rootDir });
1833
- deleted += 1;
1834
- }
1835
- for (const [id, parentId] of parentUpdates) {
1836
- if (parentId && !db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(parentId)) {
1837
- throw new TaskLedgerError(
1838
- 'TASK_LEDGER_PARENT_MISSING',
1839
- `task ${id} references missing parent ${parentId}`,
1840
- );
1841
- }
1842
- db.prepare('UPDATE tasks SET parent_id = ? WHERE id = ?').run(parentId, id);
1843
- }
1844
- importedDecisions = mergeTeamDecisions(db, rootDir, document.decisions);
1845
- importedCheckpoints = mergeTeamCheckpoints(
1846
- db,
1847
- document.checkpoints,
1848
- checkpoint.checkpoint_digests,
1849
- );
1850
- recordLedgerEvent(db, 'task_ledger_imported', document, {
1851
- imported,
1852
- preserved,
1853
- deleted,
1854
- imported_changes: importedChanges,
1855
- preserved_changes: preservedChanges,
1856
- imported_decisions: importedDecisions,
1857
- imported_checkpoints: importedCheckpoints,
1858
- });
1859
- });
1860
- return {
1861
- imported,
1862
- preserved,
1863
- deleted,
1864
- imported_changes: importedChanges,
1865
- preserved_changes: preservedChanges,
1866
- imported_decisions: importedDecisions,
1867
- preserved_decisions: document.decisions.length - importedDecisions,
1868
- imported_checkpoints: importedCheckpoints,
1869
- preserved_checkpoints: document.checkpoints.length - importedCheckpoints,
1870
- imported_baseline: baselineImport.imported,
1871
- preserved_baseline: baselineImport.preserved,
1872
- generation: document.generation,
1873
- state_digest: document.state_digest,
1874
- requires_plan_revalidation: imported > 0
1875
- || deleted > 0
1876
- || importedChanges > 0
1877
- || baselineImport.imported,
1878
- requires_baseline_revalidation: baselineImport.requiresRevalidation,
1879
- already_current: false,
1880
- ledger: document,
1881
- };
1882
- }
1883
-
1884
- function inspectTaskLedger(db, { rootDir = '.' } = {}) {
1885
- const document = readTaskLedger(rootDir);
1886
- if (!document) {
1887
- return {
1888
- status: 'missing',
1889
- path: ledgerPath(rootDir),
1890
- generation: null,
1891
- state_digest: null,
1892
- };
1893
- }
1894
- const localTasks = ops.listTasks(db, { status: 'any', limit: 1000 });
1895
- const previous = new Map(document.tasks.map((task) => [task.id, task]));
1896
- const local = localTasks.map((task) => durableTask(task, previous.get(task.id)))
1897
- .sort((left, right) => left.id.localeCompare(right.id));
1898
- const localChanges = readChangesForLedger(
1899
- db,
1900
- localTasks,
1901
- new Map(document.changes.map((change) => [change.id, change])),
1902
- );
1903
- const localDecisions = readDecisionsForLedger(db);
1904
- const localCheckpoints = readCheckpointsForLedger(db);
1905
- const localBaseline = readBaselineForLedger(db, document.baseline || null);
1906
- const localDigest = sha256(statePayload({
1907
- baseline: localBaseline,
1908
- changes: localChanges,
1909
- decisions: localDecisions,
1910
- checkpoints: localCheckpoints,
1911
- tasks: local,
1912
- }));
1913
- const baselineRequiresRevalidation = Array.isArray(localBaseline?.gaps)
1914
- && localBaseline.gaps.some((gap) => (
1915
- gap.id === 'team-ledger-revalidation-required' && gap.status === 'open'
1916
- ));
1917
- const importedPayloadDigest = baselineRequiresRevalidation
1918
- ? sha256(statePayload({
1919
- baseline: document.baseline,
1920
- changes: localChanges,
1921
- decisions: localDecisions,
1922
- checkpoints: localCheckpoints,
1923
- tasks: local,
1924
- }))
1925
- : null;
1926
- const status = localDigest === document.state_digest
1927
- ? 'current'
1928
- : (importedPayloadDigest === document.state_digest
1929
- ? 'revalidation_required'
1930
- : 'drifted');
1931
- return {
1932
- status,
1933
- path: ledgerPath(rootDir),
1934
- generation: document.generation,
1935
- state_digest: document.state_digest,
1936
- local_state_digest: localDigest,
1937
- baseline_revalidation_required: baselineRequiresRevalidation,
1938
- };
1939
- }
1940
-
1941
- module.exports = {
1942
- DURABLE_BASELINE_FIELDS,
1943
- DURABLE_CHANGE_FIELDS,
1944
- DURABLE_CHECKPOINT_FIELDS,
1945
- DURABLE_DECISION_FIELDS,
1946
- DURABLE_TASK_FIELDS,
1947
- LEDGER_KIND,
1948
- LEDGER_RELATIVE_PATH,
1949
- LEDGER_SCHEMA_VERSION,
1950
- TaskLedgerError,
1951
- durableChange,
1952
- durableCheckpoint,
1953
- durableDecision,
1954
- durableBaseline,
1955
- durableTask,
1956
- importTaskLedger,
1957
- inspectTaskLedger,
1958
- ledgerPath,
1959
- publishTaskLedger,
1960
- readTaskLedger,
1961
- rollbackTaskLedgerPublication,
1962
- syncTaskLedger,
1963
- validateLedger,
1964
- };