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,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
- };