ultra-builder-pro-cli 0.24.1 → 0.25.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 (46) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +10 -0
  3. package/adapters/claude.js +104 -0
  4. package/adapters/codex.js +363 -40
  5. package/adapters/grok.js +1254 -58
  6. package/adapters/kimi.js +682 -13
  7. package/adapters/opencode.js +132 -11
  8. package/agents/code-reviewer.md +4 -5
  9. package/agents/debugger.md +8 -8
  10. package/docs/AGENT-CONTEXT.md +30 -4
  11. package/docs/ARCHITECTURE.md +24 -5
  12. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  13. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  14. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  15. package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
  16. package/mcp-server/lib/artifact-registry.cjs +188 -107
  17. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  18. package/mcp-server/lib/change-packet.cjs +4 -32
  19. package/mcp-server/lib/change-workflow.cjs +36 -2755
  20. package/mcp-server/lib/context-envelope.cjs +124 -51
  21. package/mcp-server/lib/decision-records.cjs +164 -78
  22. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  23. package/mcp-server/lib/legacy-change-workflow.cjs +3032 -0
  24. package/mcp-server/lib/managed-file-write.cjs +56 -0
  25. package/mcp-server/lib/prd-parser.cjs +2 -2
  26. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  27. package/mcp-server/lib/state-db.cjs +186 -8
  28. package/mcp-server/lib/state-ops.cjs +214 -31
  29. package/mcp-server/lib/task-contract.cjs +404 -0
  30. package/mcp-server/lib/task-expander.cjs +8 -3
  31. package/mcp-server/lib/task-ledger.cjs +175 -62
  32. package/mcp-server/lib/ultra-facade.cjs +1895 -172
  33. package/mcp-server/lib/worker-packet.cjs +2 -2
  34. package/mcp-server/server.cjs +24 -5
  35. package/orchestrator/parallel-orchestrator.cjs +114 -27
  36. package/orchestrator/session-runner.cjs +25 -136
  37. package/package.json +1 -1
  38. package/skills/ultra-change/SKILL.md +6 -1
  39. package/skills/ultra-dev/SKILL.md +9 -4
  40. package/skills/ultra-plan/SKILL.md +13 -4
  41. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  42. package/spec/mcp-tools.yaml +114 -9
  43. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  44. package/spec/schemas/state-db.sql +10 -6
  45. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  46. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -2,19 +2,24 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
5
6
 
6
7
  const artifactRegistry = require('./artifact-registry.cjs');
7
8
  const baselines = require('./baseline-workflow.cjs');
9
+ const canonical = require('./canonical-json.cjs');
8
10
  const changes = require('./change-workflow.cjs');
9
11
  const contextEnvelope = require('./context-envelope.cjs');
10
12
  const decisionRecords = require('./decision-records.cjs');
13
+ const deliveryTransaction = require('./delivery-transaction.cjs');
11
14
  const doctor = require('./doctor.cjs');
12
15
  const gitBootstrap = require('./git-bootstrap.cjs');
16
+ const runtimePaths = require('./runtime-paths.cjs');
13
17
  const { initProject } = require('./init-project.cjs');
14
18
  const ops = require('./state-ops.cjs');
15
19
  const planCheckpoint = require('./plan-checkpoint.cjs');
16
20
  const { readStableProjectFile } = require('./safe-project-file.cjs');
17
21
  const stageCheckpoints = require('./stage-checkpoints.cjs');
22
+ const taskContract = require('./task-contract.cjs');
18
23
  const taskLedger = require('./task-ledger.cjs');
19
24
  const workerPacket = require('./worker-packet.cjs');
20
25
  const sessionRunner = require('../../orchestrator/session-runner.cjs');
@@ -59,13 +64,16 @@ const HARD_ERROR_CODES = new Set([
59
64
  'OUTPUT_SCHEMA_DRIFT',
60
65
  'PATH_AUTHORITY_VIOLATION',
61
66
  'PLAN_RECOVERY_REQUIRED',
67
+ 'RECORD_FILE_ROLLBACK_REQUIRED',
62
68
  'RUNTIME_ABI_MISMATCH',
63
69
  'RUNTIME_NATIVE_MISSING',
64
70
  'SCHEMA_VERSION_MISMATCH',
71
+ 'SESSION_RECEIPT_RECOVERY_REQUIRED',
65
72
  'SESSION_STATUS_CONFLICT',
66
73
  'STATE_CORRUPT',
67
74
  'STATE_DB_ERROR',
68
75
  'STATE_DB_MISSING',
76
+ 'STATE_PERSISTENCE_FAILED',
69
77
  'TASK_LEDGER_BASELINE_CONFLICT',
70
78
  'TASK_LEDGER_CHANGE_CONFLICT',
71
79
  'TASK_LEDGER_HISTORY_CONFLICT',
@@ -96,45 +104,111 @@ function parseJson(value, fallback = null) {
96
104
  catch { return fallback; }
97
105
  }
98
106
 
99
- function priorResult(db, idempotencyKey, operation) {
100
- if (!db || !idempotencyKey) return null;
107
+ function requestDigest(operation, request) {
108
+ return canonical.digest({ operation, request });
109
+ }
110
+
111
+ function priorResult(db, {
112
+ idempotencyKey,
113
+ operation,
114
+ requestDigest: expectedDigest,
115
+ }) {
116
+ if (!db || !idempotencyKey || !expectedDigest) return { found: false };
101
117
  const rows = db.prepare(
102
118
  `SELECT payload_json FROM events
103
119
  WHERE type = 'ultra_kernel_call'
104
- ORDER BY id DESC LIMIT 2000`,
120
+ ORDER BY id DESC`,
105
121
  ).all();
106
122
  for (const row of rows) {
107
123
  const payload = parseJson(row.payload_json, {});
108
- if (payload.idempotency_key === idempotencyKey
109
- && payload.operation === operation
110
- && payload.accepted === true) {
111
- return payload.result;
124
+ if (payload.idempotency_key !== idempotencyKey || payload.accepted !== true) continue;
125
+ if (payload.operation === operation
126
+ && (payload.request_digest === expectedDigest
127
+ || typeof payload.request_digest !== 'string')) {
128
+ return { found: true, result: payload.result };
112
129
  }
130
+ throw Object.assign(
131
+ new Error(`idempotency key ${idempotencyKey} was already used for another request`),
132
+ {
133
+ code: 'IDEMPOTENCY_KEY_CONFLICT',
134
+ details: {
135
+ idempotency_key: idempotencyKey,
136
+ prior_operation: payload.operation || null,
137
+ requested_operation: operation,
138
+ prior_request_digest: payload.request_digest,
139
+ requested_request_digest: expectedDigest,
140
+ },
141
+ },
142
+ );
113
143
  }
114
- return null;
144
+ return { found: false };
115
145
  }
116
146
 
117
- function rememberResult(db, {
147
+ function rememberResultInTx(db, {
118
148
  idempotencyKey,
119
149
  operation,
150
+ requestDigest: acceptedRequestDigest,
120
151
  result,
121
152
  changeId = null,
122
153
  taskId = null,
123
154
  }) {
124
155
  if (!db || !idempotencyKey) return;
125
- ops.appendEvent(db, {
156
+ ops.appendEventInTx(db, {
126
157
  type: 'ultra_kernel_call',
127
158
  change_id: changeId,
128
159
  task_id: taskId,
129
160
  payload: {
130
161
  idempotency_key: idempotencyKey,
131
162
  operation,
163
+ request_digest: acceptedRequestDigest,
132
164
  accepted: true,
133
165
  result,
134
166
  },
135
167
  });
136
168
  }
137
169
 
170
+ function rememberRejectedAttempt(db, tool, input, result) {
171
+ if (!db || result?.accepted !== false) return result;
172
+ const nested = Array.isArray(result.results)
173
+ ? result.results.flatMap((item) => item.diagnostics || [])
174
+ : [];
175
+ const diagnostics = [...(result.diagnostics || []), ...nested]
176
+ .map((item) => ({
177
+ code: item.code || 'NEEDS_ATTENTION',
178
+ severity: item.severity || 'needs_attention',
179
+ message: item.message || null,
180
+ ...(item.details === undefined ? {} : { details: item.details }),
181
+ }));
182
+ const entries = Array.isArray(input.entries) ? input.entries : [];
183
+ const changeIds = [...new Set(entries.flatMap((entry) => [
184
+ entry.data?.change_id,
185
+ entry.kind === 'change_contract' ? entry.data?.id : null,
186
+ ]).filter(Boolean))];
187
+ const taskIds = [...new Set(entries.flatMap((entry) => [
188
+ entry.kind.startsWith('task_') ? entry.data?.id : null,
189
+ input.scope?.task_id,
190
+ ]).filter(Boolean))];
191
+ const scopeChangeId = input.scope?.change_id || input.change_id || null;
192
+ ops.appendEvent(db, {
193
+ type: 'ultra_kernel_attempt',
194
+ change_id: changeIds.length === 1 ? changeIds[0] : scopeChangeId,
195
+ task_id: taskIds.length === 1 ? taskIds[0] : null,
196
+ payload: {
197
+ tool,
198
+ accepted: false,
199
+ mutable: result.mutable !== false,
200
+ operations: entries.map((entry) => `${entry.kind}:${entry.action}`),
201
+ idempotency_keys: entries.length > 0
202
+ ? entries.map((entry) => entry.idempotency_key).filter(Boolean)
203
+ : [input.idempotency_key].filter(Boolean),
204
+ scope: input.scope || (input.change_id ? { change_id: input.change_id } : {}),
205
+ blockers: result.blockers || [],
206
+ diagnostics,
207
+ },
208
+ });
209
+ return result;
210
+ }
211
+
138
212
  function diagnostic(error, severity = 'needs_attention') {
139
213
  return {
140
214
  code: errorCode(error),
@@ -144,6 +218,799 @@ function diagnostic(error, severity = 'needs_attention') {
144
218
  };
145
219
  }
146
220
 
221
+ function validationError(message, details) {
222
+ return Object.assign(new Error(message), {
223
+ code: 'VALIDATION_ERROR',
224
+ ...(details === undefined ? {} : { details }),
225
+ });
226
+ }
227
+
228
+ function stablePersistenceError(error, operation) {
229
+ const code = errorCode(error);
230
+ if (!code.startsWith('SQLITE_') && code !== 'STATE_DB_ERROR') return error;
231
+ return Object.assign(
232
+ new Error(`${operation} could not persist its atomic receipt`),
233
+ {
234
+ code: 'STATE_PERSISTENCE_FAILED',
235
+ cause: error,
236
+ details: { cause: code },
237
+ },
238
+ );
239
+ }
240
+
241
+ function assertExactFields(value, allowed, field) {
242
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
243
+ throw validationError(`${field} must be an object`);
244
+ }
245
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
246
+ if (unknown.length > 0) {
247
+ throw validationError(`${field}.${unknown[0]} is not allowed`, {
248
+ field: `${field}.${unknown[0]}`,
249
+ });
250
+ }
251
+ }
252
+
253
+ const RECORD_DATA_FIELDS = Object.freeze({
254
+ 'baseline:initialize': [
255
+ 'target_dir', 'project_name', 'project_type', 'stack', 'mode', 'scope',
256
+ 'resume', 'overwrite', 'git_mode', 'source_template',
257
+ ],
258
+ 'baseline:start': [
259
+ 'id', 'project_name', 'project_type', 'stack', 'mode', 'repository_revision',
260
+ 'replace_migrated', 'replace_ready', 'replacement_authorization', 'scope',
261
+ 'provider_refs', 'classification',
262
+ ],
263
+ 'baseline:observe': [
264
+ 'id', 'repository_revision', 'scope', 'spec_refs', 'evidence', 'verification',
265
+ 'unknowns', 'gaps', 'classification', 'provider_refs',
266
+ ],
267
+ 'baseline:revise': [
268
+ 'id', 'repository_revision', 'scope', 'spec_refs', 'evidence', 'verification',
269
+ 'unknowns', 'gaps', 'classification', 'provider_refs',
270
+ ],
271
+ 'baseline:accept': [
272
+ 'id', 'expected_revision', 'approved_by', 'approval_note',
273
+ 'accept_dirty_worktree', 'accept_known_red',
274
+ ],
275
+ 'change_contract:open': [
276
+ 'id', 'title', 'kind', 'intent', 'docs_impact', 'provider_refs',
277
+ 'baseline_bypass', 'contract', 'classification', 'research_disposition',
278
+ 'base_commit',
279
+ ],
280
+ 'change_contract:revise': ['id', 'patch'],
281
+ 'change_contract:cancel': ['id'],
282
+ 'change_contract:supersede': ['id', 'successor_id', 'title', 'kind', 'intent'],
283
+ 'decision:accept': [
284
+ 'id', 'scope', 'question', 'recommendation', 'selection', 'effects',
285
+ 'non_goals', 'owner', 'source', 'provenance', 'applied_refs', 'supersedes_id',
286
+ ],
287
+ 'task_contract:define': taskContract.TASK_CONTRACT_DEFINE_FIELDS,
288
+ 'task_contract:revise': ['id', 'patch'],
289
+ 'task_contract:remove': ['id'],
290
+ 'task_outcome:start': ['id'],
291
+ 'task_outcome:complete': ['id', 'packet_digest'],
292
+ 'task_outcome:block': ['id'],
293
+ 'task_outcome:reopen': ['id'],
294
+ 'task_outcome:attest_commit': ['id', 'completion_commit'],
295
+ 'artifact:bind': [
296
+ 'id', 'owner_type', 'owner_id', 'change_id', 'task_id', 'kind', 'path',
297
+ 'status', 'source_refs', 'consumer_refs', 'provenance', 'metadata',
298
+ 'content_digest', 'expected_before_digest',
299
+ ],
300
+ 'event:append': ['type', 'task_id', 'change_id', 'session_id', 'runtime', 'payload'],
301
+ });
302
+
303
+ function requireText(value, field, { nullable = false } = {}) {
304
+ if (value === null && nullable) return;
305
+ if (typeof value !== 'string') {
306
+ throw validationError(`${field} must be a string${nullable ? ' or null' : ''}`, { field });
307
+ }
308
+ }
309
+
310
+ function optionalText(value, key, field, options) {
311
+ if (Object.hasOwn(value, key)) requireText(value[key], `${field}.${key}`, options);
312
+ }
313
+
314
+ function optionalBoolean(value, key, field) {
315
+ if (Object.hasOwn(value, key) && typeof value[key] !== 'boolean') {
316
+ throw validationError(`${field}.${key} must be a boolean`, { field: `${field}.${key}` });
317
+ }
318
+ }
319
+
320
+ function optionalObject(value, key, field) {
321
+ if (!Object.hasOwn(value, key)) return;
322
+ const candidate = value[key];
323
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
324
+ throw validationError(`${field}.${key} must be an object`, { field: `${field}.${key}` });
325
+ }
326
+ }
327
+
328
+ function optionalArray(value, key, field) {
329
+ if (Object.hasOwn(value, key) && !Array.isArray(value[key])) {
330
+ throw validationError(`${field}.${key} must be an array`, { field: `${field}.${key}` });
331
+ }
332
+ }
333
+
334
+ function validateStringArray(value, field) {
335
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
336
+ throw validationError(`${field} must be an array of strings`, { field });
337
+ }
338
+ }
339
+
340
+ function validateObjectArray(value, field, allowed = null) {
341
+ if (!Array.isArray(value)) {
342
+ throw validationError(`${field} must be an array`, { field });
343
+ }
344
+ value.forEach((item, index) => {
345
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
346
+ throw validationError(`${field}[${index}] must be an object`, {
347
+ field: `${field}[${index}]`,
348
+ });
349
+ }
350
+ if (allowed) assertExactFields(item, allowed, `${field}[${index}]`);
351
+ });
352
+ }
353
+
354
+ function validateDocsImpact(value, field) {
355
+ assertExactFields(value, ['status', 'files', 'rationale'], field);
356
+ optionalText(value, 'status', field);
357
+ if (Object.hasOwn(value, 'files')) validateStringArray(value.files, `${field}.files`);
358
+ optionalText(value, 'rationale', field, { nullable: true });
359
+ }
360
+
361
+ function validateChangeAuthority(value, field) {
362
+ for (const key of ['title', 'intent', 'status', 'base_commit']) {
363
+ optionalText(value, key, field);
364
+ }
365
+ for (const key of [
366
+ 'docs_impact', 'provider_refs', 'baseline_bypass', 'contract',
367
+ 'classification', 'research_disposition',
368
+ ]) {
369
+ optionalObject(value, key, field);
370
+ }
371
+ if (Object.hasOwn(value, 'docs_impact')) {
372
+ validateDocsImpact(value.docs_impact, `${field}.docs_impact`);
373
+ }
374
+ if (Object.hasOwn(value, 'contract')) {
375
+ const contractField = `${field}.contract`;
376
+ assertExactFields(
377
+ value.contract,
378
+ ['outcome', 'acceptance', 'non_goals', 'public_seams', 'recovery', 'unresolved_decisions'],
379
+ contractField,
380
+ );
381
+ optionalText(value.contract, 'outcome', contractField);
382
+ for (const key of ['acceptance', 'non_goals', 'public_seams', 'unresolved_decisions']) {
383
+ optionalArray(value.contract, key, contractField);
384
+ }
385
+ for (const key of ['non_goals', 'public_seams']) {
386
+ if (Object.hasOwn(value.contract, key)) {
387
+ validateStringArray(value.contract[key], `${contractField}.${key}`);
388
+ }
389
+ }
390
+ if (Object.hasOwn(value.contract, 'acceptance')) {
391
+ validateObjectArray(
392
+ value.contract.acceptance,
393
+ `${contractField}.acceptance`,
394
+ ['id', 'criterion', 'verification'],
395
+ );
396
+ value.contract.acceptance.forEach((item, index) => {
397
+ for (const key of ['id', 'criterion', 'verification']) {
398
+ optionalText(item, key, `${contractField}.acceptance[${index}]`);
399
+ }
400
+ });
401
+ }
402
+ if (Object.hasOwn(value.contract, 'recovery')) {
403
+ assertExactFields(
404
+ value.contract.recovery,
405
+ ['strategy', 'verification'],
406
+ `${contractField}.recovery`,
407
+ );
408
+ for (const key of ['strategy', 'verification']) {
409
+ optionalText(value.contract.recovery, key, `${contractField}.recovery`);
410
+ }
411
+ }
412
+ if (Object.hasOwn(value.contract, 'unresolved_decisions')) {
413
+ validateObjectArray(
414
+ value.contract.unresolved_decisions,
415
+ `${contractField}.unresolved_decisions`,
416
+ ['id', 'summary', 'blocking', 'owner'],
417
+ );
418
+ value.contract.unresolved_decisions.forEach((item, index) => {
419
+ for (const key of ['id', 'summary']) {
420
+ optionalText(item, key, `${contractField}.unresolved_decisions[${index}]`);
421
+ }
422
+ optionalBoolean(
423
+ item,
424
+ 'blocking',
425
+ `${contractField}.unresolved_decisions[${index}]`,
426
+ );
427
+ optionalText(
428
+ item,
429
+ 'owner',
430
+ `${contractField}.unresolved_decisions[${index}]`,
431
+ { nullable: true },
432
+ );
433
+ });
434
+ }
435
+ }
436
+ if (Object.hasOwn(value, 'classification')) {
437
+ const classificationField = `${field}.classification`;
438
+ assertExactFields(value.classification, ['rationale', 'risk_flags'], classificationField);
439
+ optionalText(value.classification, 'rationale', classificationField);
440
+ if (Object.hasOwn(value.classification, 'risk_flags')) {
441
+ validateStringArray(value.classification.risk_flags, `${classificationField}.risk_flags`);
442
+ }
443
+ }
444
+ if (Object.hasOwn(value, 'research_disposition')) {
445
+ const researchField = `${field}.research_disposition`;
446
+ assertExactFields(
447
+ value.research_disposition,
448
+ ['status', 'mode', 'selected_steps', 'rationale'],
449
+ researchField,
450
+ );
451
+ optionalText(value.research_disposition, 'status', researchField);
452
+ optionalText(value.research_disposition, 'mode', researchField, { nullable: true });
453
+ optionalText(value.research_disposition, 'rationale', researchField);
454
+ if (Object.hasOwn(value.research_disposition, 'selected_steps')) {
455
+ validateStringArray(
456
+ value.research_disposition.selected_steps,
457
+ `${researchField}.selected_steps`,
458
+ );
459
+ }
460
+ }
461
+ if (Object.hasOwn(value, 'baseline_bypass')) {
462
+ const bypassField = `${field}.baseline_bypass`;
463
+ assertExactFields(value.baseline_bypass, ['reason', 'approved_by'], bypassField);
464
+ optionalText(value.baseline_bypass, 'reason', bypassField);
465
+ optionalText(value.baseline_bypass, 'approved_by', bypassField);
466
+ }
467
+ }
468
+
469
+ function validateBaselineData(action, data, field) {
470
+ const textFields = action === 'initialize'
471
+ ? ['target_dir', 'project_name', 'project_type', 'stack', 'mode', 'git_mode', 'source_template']
472
+ : action === 'start'
473
+ ? ['id', 'project_name', 'project_type', 'stack', 'mode', 'repository_revision']
474
+ : action === 'accept'
475
+ ? ['id', 'expected_revision', 'approved_by', 'approval_note']
476
+ : ['id', 'repository_revision'];
477
+ textFields.forEach((key) => optionalText(data, key, field));
478
+ if (action === 'initialize') {
479
+ optionalBoolean(data, 'resume', field);
480
+ optionalBoolean(data, 'overwrite', field);
481
+ }
482
+ if (action === 'start') {
483
+ optionalBoolean(data, 'replace_migrated', field);
484
+ optionalBoolean(data, 'replace_ready', field);
485
+ }
486
+ if (action === 'accept') {
487
+ optionalBoolean(data, 'accept_dirty_worktree', field);
488
+ optionalBoolean(data, 'accept_known_red', field);
489
+ }
490
+ for (const key of ['classification', 'provider_refs', 'replacement_authorization']) {
491
+ optionalObject(data, key, field);
492
+ }
493
+ if (Object.hasOwn(data, 'replacement_authorization')) {
494
+ const authorizationField = `${field}.replacement_authorization`;
495
+ assertExactFields(
496
+ data.replacement_authorization,
497
+ ['approved_by', 'reason'],
498
+ authorizationField,
499
+ );
500
+ optionalText(data.replacement_authorization, 'approved_by', authorizationField);
501
+ optionalText(data.replacement_authorization, 'reason', authorizationField);
502
+ }
503
+ for (const key of [
504
+ 'scope', 'spec_refs', 'evidence', 'verification', 'unknowns', 'gaps',
505
+ ]) {
506
+ optionalArray(data, key, field);
507
+ }
508
+ if (Object.hasOwn(data, 'scope')) validateStringArray(data.scope, `${field}.scope`);
509
+ if (Object.hasOwn(data, 'spec_refs')) {
510
+ validateObjectArray(data.spec_refs, `${field}.spec_refs`, ['kind', 'path']);
511
+ data.spec_refs.forEach((item, index) => {
512
+ optionalText(item, 'kind', `${field}.spec_refs[${index}]`);
513
+ optionalText(item, 'path', `${field}.spec_refs[${index}]`);
514
+ });
515
+ }
516
+ if (Object.hasOwn(data, 'evidence')) {
517
+ validateObjectArray(data.evidence, `${field}.evidence`, ['kind', 'ref', 'summary']);
518
+ data.evidence.forEach((item, index) => {
519
+ for (const key of ['kind', 'ref', 'summary']) {
520
+ optionalText(item, key, `${field}.evidence[${index}]`);
521
+ }
522
+ });
523
+ }
524
+ if (Object.hasOwn(data, 'verification')) {
525
+ validateObjectArray(
526
+ data.verification,
527
+ `${field}.verification`,
528
+ ['name', 'command', 'status', 'evidence', 'rationale'],
529
+ );
530
+ data.verification.forEach((item, index) => {
531
+ for (const key of ['name', 'command', 'status', 'evidence', 'rationale']) {
532
+ optionalText(item, key, `${field}.verification[${index}]`);
533
+ }
534
+ });
535
+ }
536
+ if (Object.hasOwn(data, 'unknowns')) {
537
+ validateObjectArray(
538
+ data.unknowns,
539
+ `${field}.unknowns`,
540
+ ['summary', 'blocking', 'owner'],
541
+ );
542
+ data.unknowns.forEach((item, index) => {
543
+ optionalText(item, 'summary', `${field}.unknowns[${index}]`);
544
+ optionalBoolean(item, 'blocking', `${field}.unknowns[${index}]`);
545
+ optionalText(item, 'owner', `${field}.unknowns[${index}]`);
546
+ });
547
+ }
548
+ if (Object.hasOwn(data, 'gaps')) {
549
+ validateObjectArray(
550
+ data.gaps,
551
+ `${field}.gaps`,
552
+ ['id', 'category', 'status', 'blocking', 'summary', 'evidence_refs', 'owner', 'resolution'],
553
+ );
554
+ data.gaps.forEach((item, index) => {
555
+ for (const key of ['id', 'category', 'status', 'summary']) {
556
+ optionalText(item, key, `${field}.gaps[${index}]`);
557
+ }
558
+ optionalBoolean(item, 'blocking', `${field}.gaps[${index}]`);
559
+ optionalText(item, 'owner', `${field}.gaps[${index}]`, { nullable: true });
560
+ optionalText(item, 'resolution', `${field}.gaps[${index}]`, { nullable: true });
561
+ if (Object.hasOwn(item, 'evidence_refs')) {
562
+ validateStringArray(item.evidence_refs, `${field}.gaps[${index}].evidence_refs`);
563
+ }
564
+ });
565
+ }
566
+ }
567
+
568
+ function validateDecisionData(data, field) {
569
+ for (const key of ['id', 'question', 'recommendation', 'selection', 'owner', 'source']) {
570
+ optionalText(data, key, field);
571
+ }
572
+ optionalText(data, 'supersedes_id', field, { nullable: true });
573
+ optionalObject(data, 'scope', field);
574
+ optionalObject(data, 'effects', field);
575
+ optionalObject(data, 'provenance', field);
576
+ optionalArray(data, 'non_goals', field);
577
+ optionalArray(data, 'applied_refs', field);
578
+ if (Object.hasOwn(data, 'non_goals')) {
579
+ validateStringArray(data.non_goals, `${field}.non_goals`);
580
+ }
581
+ if (Object.hasOwn(data, 'scope')) {
582
+ assertExactFields(data.scope, ['baseline_id', 'change_id'], `${field}.scope`);
583
+ optionalText(data.scope, 'baseline_id', `${field}.scope`);
584
+ optionalText(data.scope, 'change_id', `${field}.scope`);
585
+ }
586
+ if (Object.hasOwn(data, 'applied_refs')) {
587
+ validateObjectArray(
588
+ data.applied_refs,
589
+ `${field}.applied_refs`,
590
+ ['ref', 'field', 'digest'],
591
+ );
592
+ data.applied_refs.forEach((item, index) => {
593
+ for (const key of ['ref', 'field', 'digest']) {
594
+ optionalText(item, key, `${field}.applied_refs[${index}]`);
595
+ }
596
+ });
597
+ }
598
+ }
599
+
600
+ function validateArtifactData(data, field) {
601
+ for (const key of ['id', 'owner_type', 'owner_id', 'kind', 'path', 'status', 'content_digest']) {
602
+ optionalText(data, key, field);
603
+ }
604
+ for (const key of ['change_id', 'task_id', 'expected_before_digest']) {
605
+ optionalText(data, key, field, { nullable: true });
606
+ }
607
+ for (const key of ['source_refs', 'consumer_refs']) {
608
+ optionalArray(data, key, field);
609
+ if (!Object.hasOwn(data, key)) continue;
610
+ validateObjectArray(data[key], `${field}.${key}`, ['type', 'id', 'relation']);
611
+ data[key].forEach((item, index) => {
612
+ for (const referenceField of ['type', 'id', 'relation']) {
613
+ optionalText(item, referenceField, `${field}.${key}[${index}]`);
614
+ }
615
+ });
616
+ }
617
+ optionalObject(data, 'provenance', field);
618
+ optionalObject(data, 'metadata', field);
619
+ }
620
+
621
+ function validateRecordEntry(entry) {
622
+ const operation = `${entry.kind}:${entry.action}`;
623
+ const allowed = RECORD_DATA_FIELDS[operation];
624
+ if (!allowed) {
625
+ throw validationError(`unsupported ${entry.kind} action: ${entry.action}`);
626
+ }
627
+ const field = `${operation}.data`;
628
+ assertExactFields(entry.data, allowed, field);
629
+ if (entry.kind === 'baseline') validateBaselineData(entry.action, entry.data, field);
630
+ else if (entry.kind === 'change_contract') {
631
+ optionalText(entry.data, 'id', field);
632
+ if (entry.action === 'open') {
633
+ for (const key of ['title', 'kind', 'intent']) optionalText(entry.data, key, field);
634
+ validateChangeAuthority(entry.data, field);
635
+ } else if (entry.action === 'revise') {
636
+ assertExactFields(
637
+ entry.data.patch,
638
+ [
639
+ 'title', 'intent', 'status', 'docs_impact', 'provider_refs',
640
+ 'contract', 'classification', 'research_disposition',
641
+ ],
642
+ `${field}.patch`,
643
+ );
644
+ validateChangeAuthority(entry.data.patch, `${field}.patch`);
645
+ } else if (entry.action === 'supersede') {
646
+ requireNonEmptyText(entry.data.id, `${field}.id`);
647
+ requireNonEmptyText(entry.data.successor_id, `${field}.successor_id`);
648
+ for (const key of ['title', 'kind', 'intent']) {
649
+ optionalText(entry.data, key, field);
650
+ }
651
+ }
652
+ } else if (entry.kind === 'decision') validateDecisionData(entry.data, field);
653
+ else if (entry.kind === 'artifact') validateArtifactData(entry.data, field);
654
+ else if (entry.kind === 'event') {
655
+ optionalText(entry.data, 'type', field);
656
+ for (const key of ['task_id', 'change_id', 'session_id', 'runtime']) {
657
+ optionalText(entry.data, key, field, { nullable: true });
658
+ }
659
+ optionalObject(entry.data, 'payload', field);
660
+ }
661
+ }
662
+
663
+ function snapshotRecordFile(file) {
664
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false });
665
+ if (!stat) return { existed: false, bytes: null, mode: null };
666
+ if (!stat.isFile() || stat.isSymbolicLink()) {
667
+ throw Object.assign(
668
+ new Error(`record file target is not a regular file: ${file}`),
669
+ { code: 'PATH_AUTHORITY_VIOLATION' },
670
+ );
671
+ }
672
+ return { existed: true, bytes: fs.readFileSync(file), mode: stat.mode };
673
+ }
674
+
675
+ function restoreRecordFile(file, snapshot) {
676
+ const current = fs.lstatSync(file, { throwIfNoEntry: false });
677
+ if (!snapshot.existed) {
678
+ if (!current) return;
679
+ if (!current.isFile() && !current.isSymbolicLink()) {
680
+ throw new Error(`record rollback target changed type: ${file}`);
681
+ }
682
+ fs.rmSync(file, { force: true });
683
+ return;
684
+ }
685
+ if (current?.isFile() && !current.isSymbolicLink()) {
686
+ if (fs.readFileSync(file).equals(snapshot.bytes)) return;
687
+ } else if (current) {
688
+ throw new Error(`record rollback target changed type: ${file}`);
689
+ }
690
+ fs.mkdirSync(path.dirname(file), { recursive: true });
691
+ fs.writeFileSync(file, snapshot.bytes, { mode: snapshot.mode });
692
+ }
693
+
694
+ function prepareRecordFileRollback(entry, rootDir) {
695
+ const id = entry.data?.id;
696
+ if (typeof id !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(id)) return null;
697
+ if (entry.kind === 'change_contract' && entry.action === 'open') {
698
+ const directory = path.resolve(rootDir, '.ultra', 'changes', 'active', id);
699
+ const existed = fs.existsSync(directory);
700
+ return () => {
701
+ if (!existed && fs.existsSync(directory)) {
702
+ fs.rmSync(directory, { recursive: true, force: true });
703
+ }
704
+ };
705
+ }
706
+ if (entry.kind === 'change_contract' && entry.action === 'revise') {
707
+ const file = path.resolve(
708
+ rootDir,
709
+ '.ultra',
710
+ 'changes',
711
+ 'active',
712
+ id,
713
+ 'intent.md',
714
+ );
715
+ const snapshot = snapshotRecordFile(file);
716
+ return () => restoreRecordFile(file, snapshot);
717
+ }
718
+ if (entry.kind === 'decision' && entry.action === 'accept') {
719
+ const scope = entry.data.scope;
720
+ let relative = null;
721
+ if (typeof scope?.baseline_id === 'string') {
722
+ relative = path.join('.ultra', 'decisions', 'baseline', `${id}.json`);
723
+ } else if (typeof scope?.change_id === 'string'
724
+ && /^[a-zA-Z0-9_-]+$/.test(scope.change_id)) {
725
+ relative = path.join(
726
+ '.ultra',
727
+ 'changes',
728
+ 'active',
729
+ scope.change_id,
730
+ 'decisions',
731
+ `${id}.json`,
732
+ );
733
+ }
734
+ if (!relative) return null;
735
+ const file = path.resolve(rootDir, relative);
736
+ const snapshot = snapshotRecordFile(file);
737
+ return () => restoreRecordFile(file, snapshot);
738
+ }
739
+ return null;
740
+ }
741
+
742
+ function rollbackRecordFiles(rollback, error) {
743
+ if (!rollback) return;
744
+ try {
745
+ rollback();
746
+ } catch (cause) {
747
+ throw Object.assign(
748
+ new Error(
749
+ `record failed and managed file rollback requires recovery: `
750
+ + `${error.message}; ${cause.message}`,
751
+ ),
752
+ {
753
+ code: 'RECORD_FILE_ROLLBACK_REQUIRED',
754
+ cause: error,
755
+ details: {
756
+ original_error: errorCode(error),
757
+ rollback_error: cause.message,
758
+ },
759
+ },
760
+ );
761
+ }
762
+ }
763
+
764
+ function rejectedAttemptAudit(db) {
765
+ return db.prepare(
766
+ `SELECT id, ts, task_id, change_id, payload_json
767
+ FROM events
768
+ WHERE type = 'ultra_kernel_attempt'
769
+ ORDER BY id DESC
770
+ LIMIT 5`,
771
+ ).all().map((row) => {
772
+ const payload = parseJson(row.payload_json, {});
773
+ return {
774
+ event_id: row.id,
775
+ recorded_at: row.ts,
776
+ tool: payload.tool || null,
777
+ task_id: row.task_id || null,
778
+ change_id: row.change_id || null,
779
+ operations: (payload.operations || []).slice(0, 12),
780
+ blockers: (payload.blockers || []).slice(0, 12),
781
+ diagnostics: (payload.diagnostics || []).slice(0, 8).map((item) => ({
782
+ code: item.code || 'NEEDS_ATTENTION',
783
+ severity: item.severity || 'needs_attention',
784
+ message: typeof item.message === 'string'
785
+ ? item.message.slice(0, 500)
786
+ : null,
787
+ })),
788
+ };
789
+ });
790
+ }
791
+
792
+ function requireNonEmptyText(value, field, { minLength = 1 } = {}) {
793
+ requireText(value, field);
794
+ if (value.trim().length < minLength) {
795
+ throw validationError(`${field} must contain at least ${minLength} characters`, { field });
796
+ }
797
+ }
798
+
799
+ function validateContextInput(input) {
800
+ assertExactFields(input, ['stage', 'scope', 'detail'], 'context');
801
+ if (Object.hasOwn(input, 'stage')) {
802
+ requireText(input.stage, 'context.stage');
803
+ if (!['project', 'research', 'plan', 'dev', 'test', 'review', 'deliver'].includes(input.stage)) {
804
+ throw validationError(`unsupported context stage: ${input.stage}`);
805
+ }
806
+ }
807
+ if (Object.hasOwn(input, 'detail')) {
808
+ requireText(input.detail, 'context.detail');
809
+ if (!['summary', 'full'].includes(input.detail)) {
810
+ throw validationError(`unsupported context detail: ${input.detail}`);
811
+ }
812
+ }
813
+ if (Object.hasOwn(input, 'scope')) {
814
+ assertExactFields(input.scope, ['change_id', 'task_id'], 'context.scope');
815
+ for (const key of ['change_id', 'task_id']) {
816
+ if (Object.hasOwn(input.scope, key)) {
817
+ requireNonEmptyText(input.scope[key], `context.scope.${key}`);
818
+ }
819
+ }
820
+ }
821
+ }
822
+
823
+ function validateCheckpointInput(input) {
824
+ assertExactFields(
825
+ input,
826
+ ['stage', 'scope', 'payload', 'idempotency_key'],
827
+ 'checkpoint',
828
+ );
829
+ requireText(input.stage, 'checkpoint.stage');
830
+ if (!['research', 'plan', 'dev', 'test', 'review', 'deliver'].includes(input.stage)) {
831
+ throw validationError(`unsupported checkpoint stage: ${input.stage}`);
832
+ }
833
+ assertExactFields(input.scope, ['change_id', 'task_id'], 'checkpoint.scope');
834
+ for (const key of ['change_id', 'task_id']) {
835
+ if (Object.hasOwn(input.scope, key)) {
836
+ requireNonEmptyText(input.scope[key], `checkpoint.scope.${key}`);
837
+ }
838
+ }
839
+ assertExactFields(
840
+ input.payload,
841
+ Object.keys(input.payload || {}),
842
+ 'checkpoint.payload',
843
+ );
844
+ requireNonEmptyText(input.idempotency_key, 'checkpoint.idempotency_key', {
845
+ minLength: 3,
846
+ });
847
+ if (Object.hasOwn(input.payload, 'evidence')) {
848
+ validateObjectArray(
849
+ input.payload.evidence,
850
+ 'checkpoint.payload.evidence',
851
+ ['kind', 'ref', 'summary', 'digest', 'artifact_id'],
852
+ );
853
+ input.payload.evidence.forEach((item, index) => {
854
+ for (const key of ['kind', 'ref', 'summary', 'digest', 'artifact_id']) {
855
+ optionalText(item, key, `checkpoint.payload.evidence[${index}]`);
856
+ }
857
+ });
858
+ }
859
+ if (Object.hasOwn(input.payload, 'diagnostics')) {
860
+ validateObjectArray(
861
+ input.payload.diagnostics,
862
+ 'checkpoint.payload.diagnostics',
863
+ ['code', 'severity', 'message', 'details'],
864
+ );
865
+ input.payload.diagnostics.forEach((item, index) => {
866
+ requireNonEmptyText(item.code, `checkpoint.payload.diagnostics[${index}].code`);
867
+ optionalText(item, 'severity', `checkpoint.payload.diagnostics[${index}]`);
868
+ optionalText(item, 'message', `checkpoint.payload.diagnostics[${index}]`);
869
+ });
870
+ }
871
+ }
872
+
873
+ function validateSyncInput(input) {
874
+ requireText(input.action, 'sync.action');
875
+ const allowed = {
876
+ inspect: ['action'],
877
+ import: ['action'],
878
+ migrate: ['action'],
879
+ publish: ['action', 'reason', 'idempotency_key'],
880
+ }[input.action];
881
+ if (!allowed) throw validationError(`unsupported sync action: ${input.action}`);
882
+ assertExactFields(input, allowed, `sync:${input.action}`);
883
+ if (input.action === 'publish') {
884
+ optionalText(input, 'reason', 'sync:publish');
885
+ requireNonEmptyText(input.idempotency_key, 'sync:publish.idempotency_key', {
886
+ minLength: 3,
887
+ });
888
+ }
889
+ }
890
+
891
+ const SESSION_PAYLOAD_FIELDS = Object.freeze({
892
+ admission: [],
893
+ acquire: [
894
+ 'role', 'runtime', 'output_path', 'output_schema', 'evidence_refs',
895
+ 'diff_range', 'changed_files', 'takeover', 'worktree_base',
896
+ ],
897
+ get: [],
898
+ list: [],
899
+ heartbeat: [],
900
+ release: ['status', 'remove_worktree'],
901
+ });
902
+
903
+ function validateSessionInput(input) {
904
+ requireText(input.action, 'session.action');
905
+ const payloadFields = SESSION_PAYLOAD_FIELDS[input.action];
906
+ if (!payloadFields) throw validationError(`unsupported session action: ${input.action}`);
907
+ const mutating = ['acquire', 'heartbeat', 'release'].includes(input.action);
908
+ assertExactFields(
909
+ input,
910
+ mutating
911
+ ? ['action', 'scope', 'payload', 'idempotency_key']
912
+ : ['action', 'scope', 'payload'],
913
+ `session:${input.action}`,
914
+ );
915
+ const scope = input.scope || {};
916
+ const payload = input.payload || {};
917
+ const scopeFields = ['get', 'heartbeat', 'release'].includes(input.action)
918
+ ? ['sid']
919
+ : ['task_id'];
920
+ assertExactFields(scope, scopeFields, `session:${input.action}.scope`);
921
+ assertExactFields(payload, payloadFields, `session:${input.action}.payload`);
922
+ if (input.action !== 'list' || Object.hasOwn(scope, 'task_id')) {
923
+ const key = scopeFields[0];
924
+ requireNonEmptyText(scope[key], `session:${input.action}.scope.${key}`);
925
+ }
926
+ if (mutating) {
927
+ requireNonEmptyText(
928
+ input.idempotency_key,
929
+ `session:${input.action}.idempotency_key`,
930
+ { minLength: 3 },
931
+ );
932
+ }
933
+ if (input.action === 'release') {
934
+ if (Object.hasOwn(payload, 'status')) {
935
+ requireText(payload.status, 'session:release.payload.status');
936
+ if (!['completed', 'crashed'].includes(payload.status)) {
937
+ throw validationError(`unsupported session release status: ${payload.status}`);
938
+ }
939
+ }
940
+ optionalBoolean(payload, 'remove_worktree', 'session:release.payload');
941
+ }
942
+ if (input.action === 'acquire') {
943
+ for (const key of ['role', 'runtime', 'output_path', 'diff_range', 'worktree_base']) {
944
+ optionalText(payload, key, 'session:acquire.payload');
945
+ }
946
+ optionalObject(payload, 'output_schema', 'session:acquire.payload');
947
+ optionalArray(payload, 'evidence_refs', 'session:acquire.payload');
948
+ optionalArray(payload, 'changed_files', 'session:acquire.payload');
949
+ optionalBoolean(payload, 'takeover', 'session:acquire.payload');
950
+ if (Object.hasOwn(payload, 'changed_files')) {
951
+ validateStringArray(payload.changed_files, 'session:acquire.payload.changed_files');
952
+ }
953
+ if (Object.hasOwn(payload, 'evidence_refs')) {
954
+ payload.evidence_refs.forEach((item, index) => {
955
+ if (typeof item === 'string') return;
956
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
957
+ throw validationError(
958
+ `session:acquire.payload.evidence_refs[${index}] must be a string or object`,
959
+ );
960
+ }
961
+ });
962
+ }
963
+ }
964
+ }
965
+
966
+ function validateArchiveInput(input) {
967
+ assertExactFields(input, ['change_id', 'payload', 'idempotency_key'], 'archive');
968
+ requireNonEmptyText(input.change_id, 'archive.change_id');
969
+ requireNonEmptyText(input.idempotency_key, 'archive.idempotency_key', { minLength: 3 });
970
+ assertExactFields(
971
+ input.payload,
972
+ [
973
+ 'summary', 'baseline_updates', 'no_baseline_change_reason',
974
+ 'reconciliation_path',
975
+ ],
976
+ 'archive.payload',
977
+ );
978
+ for (const key of ['summary', 'no_baseline_change_reason', 'reconciliation_path']) {
979
+ optionalText(input.payload, key, 'archive.payload');
980
+ }
981
+ if (Object.hasOwn(input.payload, 'baseline_updates')) {
982
+ validateStringArray(input.payload.baseline_updates, 'archive.payload.baseline_updates');
983
+ }
984
+ }
985
+
986
+ function validateRecordInput(input) {
987
+ assertExactFields(input, ['entries'], 'record');
988
+ if (!Array.isArray(input.entries) || input.entries.length === 0) {
989
+ throw validationError('record.entries must be a non-empty array', {
990
+ field: 'record.entries',
991
+ });
992
+ }
993
+ input.entries.forEach((entry, index) => {
994
+ const field = `record.entries[${index}]`;
995
+ assertExactFields(entry, ['kind', 'action', 'data', 'idempotency_key'], field);
996
+ requireNonEmptyText(entry.kind, `${field}.kind`);
997
+ requireNonEmptyText(entry.action, `${field}.action`);
998
+ assertExactFields(
999
+ entry.data,
1000
+ Object.keys(entry.data || {}),
1001
+ `${field}.data`,
1002
+ );
1003
+ requireNonEmptyText(entry.idempotency_key, `${field}.idempotency_key`, {
1004
+ minLength: 3,
1005
+ });
1006
+ });
1007
+ }
1008
+
1009
+ function validateDoctorInput(input) {
1010
+ assertExactFields(input, ['repair'], 'doctor');
1011
+ optionalBoolean(input, 'repair', 'doctor');
1012
+ }
1013
+
147
1014
  function selectedScope(db, input = {}) {
148
1015
  const scope = input.scope || {};
149
1016
  const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
@@ -197,7 +1064,29 @@ function readContext(db, input = {}, {
197
1064
  rootDir = process.cwd(),
198
1065
  runtime = 'unknown',
199
1066
  } = {}) {
200
- return contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
1067
+ validateContextInput(input);
1068
+ const result = contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
1069
+ const rejectedAttempts = rejectedAttemptAudit(db);
1070
+ let visible = {
1071
+ ...result,
1072
+ audit: {
1073
+ rejected_attempts: rejectedAttempts,
1074
+ },
1075
+ };
1076
+ delete visible.bytes;
1077
+ const limit = input.detail === 'full'
1078
+ ? contextEnvelope.FULL_LIMIT
1079
+ : contextEnvelope.SUMMARY_LIMIT;
1080
+ while (Buffer.byteLength(JSON.stringify(visible)) > limit
1081
+ && rejectedAttempts.length > 0) {
1082
+ rejectedAttempts.pop();
1083
+ }
1084
+ if (Buffer.byteLength(JSON.stringify(visible)) > limit) {
1085
+ visible = { ...result };
1086
+ delete visible.bytes;
1087
+ }
1088
+ visible.bytes = Buffer.byteLength(JSON.stringify(visible));
1089
+ return visible;
201
1090
  }
202
1091
 
203
1092
  function initializeProject(data = {}) {
@@ -241,15 +1130,15 @@ function baselineRecord(db, action, data, rootDir) {
241
1130
 
242
1131
  function changeContractRecord(db, action, data, rootDir) {
243
1132
  if (action === 'open') {
244
- return changes.createChange(db, data, { rootDir, kernelMode: true });
1133
+ return changes.createChange(db, data, { rootDir });
245
1134
  }
246
1135
  if (action === 'revise') {
247
1136
  return {
248
1137
  change: changes.updateChange(
249
1138
  db,
250
1139
  data.id,
251
- data.patch || {},
252
- { rootDir, kernelMode: true },
1140
+ data.patch,
1141
+ { rootDir },
253
1142
  ),
254
1143
  };
255
1144
  }
@@ -259,25 +1148,128 @@ function changeContractRecord(db, action, data, rootDir) {
259
1148
  db,
260
1149
  data.id,
261
1150
  { status: 'cancelled' },
262
- { rootDir, kernelMode: true },
1151
+ { rootDir },
263
1152
  ),
264
1153
  };
265
1154
  }
1155
+ if (action === 'supersede') {
1156
+ return changes.supersedeChange(db, data, { rootDir });
1157
+ }
266
1158
  throw Object.assign(new Error(`unsupported change_contract action: ${action}`), {
267
1159
  code: 'VALIDATION_ERROR',
268
1160
  });
269
1161
  }
270
1162
 
271
- function taskContractRecord(db, action, data) {
272
- if (action === 'define') return { task: ops.createTask(db, data) };
273
- if (action === 'revise') return { task: ops.patchTask(db, data.id, data.patch || {}) };
274
- if (action === 'remove') return ops.deleteTask(db, data.id, { force: data.force === true });
1163
+ function taskContractRecord(db, action, data, rootDir) {
1164
+ if (action === 'define') {
1165
+ return { task: ops.createTask(db, taskContract.normalizeTaskDefinition(data)) };
1166
+ }
1167
+ if (action === 'revise') {
1168
+ assertExactFields(data, ['id', 'patch'], 'task_contract.revise.data');
1169
+ const id = taskContract.normalizeTaskId(data.id);
1170
+ return {
1171
+ task: ops.patchTask(db, id, taskContract.normalizeTaskPatch(data.patch)),
1172
+ };
1173
+ }
1174
+ if (action === 'remove') {
1175
+ assertExactFields(data, ['id'], 'task_contract.remove.data');
1176
+ return ops.deleteTask(
1177
+ db,
1178
+ taskContract.normalizeTaskId(data.id),
1179
+ { rootDir },
1180
+ );
1181
+ }
275
1182
  throw Object.assign(new Error(`unsupported task_contract action: ${action}`), {
276
1183
  code: 'VALIDATION_ERROR',
277
1184
  });
278
1185
  }
279
1186
 
1187
+ function resolveIntegratedCommit(rootDir, completionCommit) {
1188
+ if (typeof completionCommit !== 'string'
1189
+ || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(completionCommit)) {
1190
+ throw Object.assign(
1191
+ new Error('completion_commit must be a lowercase Git commit SHA'),
1192
+ { code: 'COMPLETION_COMMIT_INVALID' },
1193
+ );
1194
+ }
1195
+ const resolved = spawnSync(
1196
+ 'git',
1197
+ ['rev-parse', '--verify', `${completionCommit}^{commit}`],
1198
+ {
1199
+ cwd: rootDir,
1200
+ encoding: 'utf8',
1201
+ stdio: ['ignore', 'pipe', 'pipe'],
1202
+ },
1203
+ );
1204
+ if (resolved.status !== 0) {
1205
+ throw Object.assign(
1206
+ new Error(`completion commit ${completionCommit} was not found in this repository`),
1207
+ { code: 'COMPLETION_COMMIT_NOT_FOUND' },
1208
+ );
1209
+ }
1210
+ const commit = String(resolved.stdout || '').trim();
1211
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(commit)) {
1212
+ throw Object.assign(
1213
+ new Error(`completion commit ${completionCommit} could not be resolved`),
1214
+ { code: 'COMPLETION_COMMIT_NOT_FOUND' },
1215
+ );
1216
+ }
1217
+ const integrated = spawnSync(
1218
+ 'git',
1219
+ ['merge-base', '--is-ancestor', commit, 'HEAD'],
1220
+ {
1221
+ cwd: rootDir,
1222
+ encoding: 'utf8',
1223
+ stdio: ['ignore', 'pipe', 'pipe'],
1224
+ },
1225
+ );
1226
+ if (integrated.status !== 0) {
1227
+ throw Object.assign(
1228
+ new Error(`completion commit ${commit} is not an ancestor of the current HEAD`),
1229
+ { code: 'COMPLETION_COMMIT_NOT_INTEGRATED' },
1230
+ );
1231
+ }
1232
+ return commit;
1233
+ }
1234
+
1235
+ function attestTaskCompletionCommit(db, data, rootDir) {
1236
+ assertExactFields(data, ['id', 'completion_commit'], 'task_outcome.attest_commit.data');
1237
+ const id = taskContract.normalizeTaskId(data.id);
1238
+ const task = ops.readTask(db, id);
1239
+ if (!task) {
1240
+ throw Object.assign(new Error(`task ${id} does not exist`), { code: 'TASK_NOT_FOUND' });
1241
+ }
1242
+ if (task.status !== 'completed') {
1243
+ throw Object.assign(
1244
+ new Error(`task ${id} must be completed before its commit can be attested`),
1245
+ {
1246
+ code: 'TASK_NOT_COMPLETED',
1247
+ details: { task_id: id, status: task.status },
1248
+ },
1249
+ );
1250
+ }
1251
+ const commit = resolveIntegratedCommit(rootDir, data.completion_commit);
1252
+ if (task.completion_commit) {
1253
+ if (task.completion_commit === commit) return { task };
1254
+ throw Object.assign(
1255
+ new Error(`task ${id} already attests completion commit ${task.completion_commit}`),
1256
+ {
1257
+ code: 'COMPLETION_COMMIT_CONFLICT',
1258
+ details: {
1259
+ task_id: id,
1260
+ existing_commit: task.completion_commit,
1261
+ requested_commit: commit,
1262
+ },
1263
+ },
1264
+ );
1265
+ }
1266
+ return { task: ops.patchTask(db, id, { completion_commit: commit }) };
1267
+ }
1268
+
280
1269
  function taskOutcomeRecord(db, action, data, rootDir) {
1270
+ if (action === 'attest_commit') {
1271
+ return attestTaskCompletionCommit(db, data, rootDir);
1272
+ }
281
1273
  const status = {
282
1274
  start: 'in_progress',
283
1275
  complete: 'completed',
@@ -290,6 +1282,11 @@ function taskOutcomeRecord(db, action, data, rootDir) {
290
1282
  });
291
1283
  }
292
1284
  if (action === 'complete') {
1285
+ assertExactFields(data, ['id', 'packet_digest'], 'task_outcome.complete.data');
1286
+ taskContract.normalizeTaskId(data.id);
1287
+ if (typeof data.packet_digest !== 'string' || !data.packet_digest) {
1288
+ throw validationError('task_outcome.complete.data.packet_digest must be a string');
1289
+ }
293
1290
  const packet = db.prepare(
294
1291
  `SELECT id, packet_digest, packet_path, output_path
295
1292
  FROM worker_packets
@@ -352,9 +1349,9 @@ function taskOutcomeRecord(db, action, data, rootDir) {
352
1349
  packet_path: packet.packet_path,
353
1350
  },
354
1351
  }, { rootDir });
355
- }
356
- if (data.patch && Object.keys(data.patch).length > 0) {
357
- ops.patchTask(db, data.id, data.patch);
1352
+ } else {
1353
+ assertExactFields(data, ['id'], `task_outcome.${action}.data`);
1354
+ taskContract.normalizeTaskId(data.id);
358
1355
  }
359
1356
  return { task: ops.updateTaskStatus(db, data.id, status) };
360
1357
  }
@@ -366,14 +1363,26 @@ function applyRecord(db, entry, { rootDir }) {
366
1363
  code: 'VALIDATION_ERROR',
367
1364
  });
368
1365
  }
1366
+ validateRecordEntry({ kind, action, data });
369
1367
  if (kind === 'baseline') return baselineRecord(db, action, data, rootDir);
370
1368
  if (kind === 'change_contract') return changeContractRecord(db, action, data, rootDir);
371
1369
  if (kind === 'decision' && action === 'accept') {
372
1370
  return { decision: decisionRecords.acceptDecision(db, data, { rootDir }) };
373
1371
  }
374
- if (kind === 'task_contract') return taskContractRecord(db, action, data);
1372
+ if (kind === 'task_contract') return taskContractRecord(db, action, data, rootDir);
375
1373
  if (kind === 'task_outcome') return taskOutcomeRecord(db, action, data, rootDir);
376
1374
  if (kind === 'artifact' && action === 'bind') {
1375
+ const legacyWorkflowReference = data.owner_type === 'workflow'
1376
+ || [...(data.source_refs || []), ...(data.consumer_refs || [])]
1377
+ .some((reference) => reference?.type === 'workflow');
1378
+ if (legacyWorkflowReference) {
1379
+ throw Object.assign(
1380
+ new Error(
1381
+ 'workflow-owned artifact authority is retired and remains read-only history',
1382
+ ),
1383
+ { code: 'LEGACY_AUTHORITY_READ_ONLY' },
1384
+ );
1385
+ }
377
1386
  return artifactRegistry.recordArtifact(db, data, { rootDir });
378
1387
  }
379
1388
  if (kind === 'event' && action === 'append') {
@@ -388,52 +1397,102 @@ function applyRecord(db, entry, { rootDir }) {
388
1397
  async function record(db, input = {}, {
389
1398
  rootDir = process.cwd(),
390
1399
  } = {}) {
391
- const results = [];
392
- for (const entry of input.entries || []) {
1400
+ validateRecordInput(input);
1401
+ const entries = input.entries;
1402
+ const runEntry = (entry) => {
1403
+ const data = entry.data === undefined ? {} : entry.data;
1404
+ const normalizedEntry = { ...entry, data };
1405
+ validateRecordEntry(normalizedEntry);
393
1406
  const operation = `${entry.kind}:${entry.action}`;
394
- const cached = priorResult(db, entry.idempotency_key, operation);
395
- if (cached) {
396
- results.push({
1407
+ const acceptedRequestDigest = requestDigest(operation, {
1408
+ kind: entry.kind,
1409
+ action: entry.action,
1410
+ data,
1411
+ });
1412
+ const cached = priorResult(db, {
1413
+ idempotencyKey: entry.idempotency_key,
1414
+ operation,
1415
+ requestDigest: acceptedRequestDigest,
1416
+ });
1417
+ if (cached.found) {
1418
+ return {
397
1419
  kind: entry.kind,
398
1420
  action: entry.action,
399
1421
  accepted: true,
400
1422
  idempotent: true,
401
- result: cached,
402
- });
403
- continue;
1423
+ result: cached.result,
1424
+ };
404
1425
  }
1426
+ const rollbackFiles = prepareRecordFileRollback(normalizedEntry, rootDir);
405
1427
  try {
406
- const result = applyRecord(db, entry, { rootDir });
407
- rememberResult(db, {
1428
+ const result = applyRecord(db, normalizedEntry, { rootDir });
1429
+ rememberResultInTx(db, {
408
1430
  idempotencyKey: entry.idempotency_key,
409
1431
  operation,
1432
+ requestDigest: acceptedRequestDigest,
410
1433
  result,
411
- changeId: entry.data?.change_id || entry.data?.id || null,
412
- taskId: entry.kind.startsWith('task_') ? entry.data?.id || null : null,
1434
+ changeId: data.change_id || data.id || null,
1435
+ taskId: entry.kind.startsWith('task_') ? data.id || null : null,
413
1436
  });
414
- results.push({
1437
+ return {
415
1438
  kind: entry.kind,
416
1439
  action: entry.action,
417
1440
  accepted: true,
418
1441
  idempotent: false,
419
1442
  result,
420
1443
  diagnostics: result?.diagnostics || [],
421
- });
1444
+ };
1445
+ } catch (error) {
1446
+ rollbackRecordFiles(rollbackFiles, error);
1447
+ throw error;
1448
+ }
1449
+ };
1450
+
1451
+ if (!db) {
1452
+ const results = entries.map((entry) => {
1453
+ try {
1454
+ return runEntry(entry);
1455
+ } catch (error) {
1456
+ if (isHardError(error)) throw error;
1457
+ return {
1458
+ kind: entry.kind,
1459
+ action: entry.action,
1460
+ accepted: false,
1461
+ mutable: true,
1462
+ diagnostics: [diagnostic(error)],
1463
+ };
1464
+ }
1465
+ });
1466
+ return {
1467
+ accepted: results.every((item) => item.accepted),
1468
+ mutable: true,
1469
+ results,
1470
+ };
1471
+ }
1472
+
1473
+ const attempted = entries.map((entry) => {
1474
+ try {
1475
+ return ops.tx(db, () => runEntry(entry));
422
1476
  } catch (error) {
1477
+ const persistenceError = stablePersistenceError(
1478
+ error,
1479
+ `${entry.kind}:${entry.action} record`,
1480
+ );
1481
+ if (persistenceError !== error) throw persistenceError;
423
1482
  if (isHardError(error)) throw error;
424
- results.push({
1483
+ return {
425
1484
  kind: entry.kind,
426
1485
  action: entry.action,
427
1486
  accepted: false,
428
1487
  mutable: true,
429
1488
  diagnostics: [diagnostic(error)],
430
- });
1489
+ };
431
1490
  }
432
- }
1491
+ });
433
1492
  return {
434
- accepted: results.every((item) => item.accepted),
1493
+ accepted: attempted.every((item) => item.accepted),
435
1494
  mutable: true,
436
- results,
1495
+ results: attempted,
437
1496
  };
438
1497
  }
439
1498
 
@@ -621,12 +1680,60 @@ function normalizeCheckpointEvidence(db, input, rootDir) {
621
1680
  });
622
1681
  }
623
1682
 
624
- async function checkpoint(db, input = {}, {
1683
+ function checkpointMutation(db, input = {}, {
625
1684
  rootDir = process.cwd(),
626
1685
  runtime = 'unknown',
627
1686
  markPlanRecoveryRequired = null,
628
1687
  } = {}) {
629
1688
  const scope = selectedScope(db, input);
1689
+ const checkpointScope = scope.task_id
1690
+ ? { task_id: scope.task_id }
1691
+ : scope.change_id
1692
+ ? { change_id: scope.change_id }
1693
+ : scope.baseline_id
1694
+ ? { baseline_id: scope.baseline_id }
1695
+ : { project_id: 'project' };
1696
+ const retry = stageCheckpoints.findIdempotentDraft(db, {
1697
+ stage: input.stage,
1698
+ scope: checkpointScope,
1699
+ payload: input.payload || {},
1700
+ idempotency_key: `${input.idempotency_key}:draft`,
1701
+ });
1702
+ if (retry) {
1703
+ const hasHardConflict = retry.diagnostics.some(
1704
+ (item) => item.severity === 'hard_conflict',
1705
+ );
1706
+ const accepted = retry.status === 'draft' && !hasHardConflict
1707
+ ? stageCheckpoints.acceptDraft(db, {
1708
+ id: retry.id,
1709
+ idempotency_key: `${input.idempotency_key}:accept`,
1710
+ })
1711
+ : retry;
1712
+ let result = retry.payload?.plan || null;
1713
+ if (input.stage === 'plan' && accepted.status === 'accepted') {
1714
+ result = {
1715
+ ...result,
1716
+ team_checkpoint: taskLedger.publishTaskLedger(db, {
1717
+ rootDir,
1718
+ reason: 'plan_checkpoint_retry',
1719
+ }),
1720
+ };
1721
+ }
1722
+ return {
1723
+ accepted: accepted.status === 'accepted',
1724
+ mutable: accepted.status !== 'accepted',
1725
+ idempotent: true,
1726
+ diagnostics: accepted.diagnostics,
1727
+ blockers: accepted.diagnostics
1728
+ .filter((item) => item.severity === 'hard_conflict')
1729
+ .map((item) => item.code),
1730
+ checkpoint: accepted,
1731
+ context: accepted.context_envelope_id
1732
+ ? contextEnvelope.readEnvelope(db, accepted.context_envelope_id, { rootDir })
1733
+ : null,
1734
+ result,
1735
+ };
1736
+ }
630
1737
  const context = contextEnvelope.persistEnvelope(db, {
631
1738
  stage: input.stage,
632
1739
  scope,
@@ -665,13 +1772,7 @@ async function checkpoint(db, input = {}, {
665
1772
 
666
1773
  const draft = stageCheckpoints.saveDraft(db, {
667
1774
  stage: input.stage,
668
- scope: scope.task_id
669
- ? { task_id: scope.task_id }
670
- : scope.change_id
671
- ? { change_id: scope.change_id }
672
- : scope.baseline_id
673
- ? { baseline_id: scope.baseline_id }
674
- : { project_id: 'project' },
1775
+ scope: checkpointScope,
675
1776
  payload: input.stage === 'plan' && result
676
1777
  ? { ...(input.payload || {}), plan: result }
677
1778
  : (input.payload || {}),
@@ -715,20 +1816,68 @@ async function checkpoint(db, input = {}, {
715
1816
  };
716
1817
  }
717
1818
 
1819
+ function checkpoint(db, input = {}, options = {}) {
1820
+ validateCheckpointInput(input);
1821
+ try {
1822
+ return checkpointMutation(db, input, options);
1823
+ } catch (error) {
1824
+ throw stablePersistenceError(error, 'checkpoint publication');
1825
+ }
1826
+ }
1827
+
1828
+ function migrationRequired(error) {
1829
+ return {
1830
+ status: 'migration_required',
1831
+ migration: {
1832
+ required: true,
1833
+ code: errorCode(error),
1834
+ action: 'ultra.sync migrate',
1835
+ },
1836
+ };
1837
+ }
1838
+
1839
+ function corruptLedgerInspection(error, rootDir) {
1840
+ return {
1841
+ status: 'corrupt',
1842
+ path: taskLedger.ledgerPath(rootDir),
1843
+ diagnostics: [diagnostic(error, 'hard_conflict')],
1844
+ recovery: {
1845
+ actions: [
1846
+ {
1847
+ action: 'doctor',
1848
+ command: 'ultra.doctor',
1849
+ description: 'Inspect the managed authority and its deterministic repairs.',
1850
+ },
1851
+ {
1852
+ action: 'restore',
1853
+ path: taskLedger.LEDGER_RELATIVE_PATH,
1854
+ description: 'Restore the last trusted ledger bytes from version control or backup.',
1855
+ },
1856
+ ],
1857
+ },
1858
+ };
1859
+ }
1860
+
718
1861
  function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
1862
+ validateSyncInput(input);
719
1863
  gitBootstrap.ensureExistingProjectStorageBoundary(rootDir);
720
1864
  if (input.action === 'inspect') {
721
1865
  try {
722
1866
  return taskLedger.inspectTaskLedger(db, { rootDir });
723
1867
  } catch (error) {
724
- return {
725
- status: 'migration_required',
726
- migration: {
727
- required: true,
728
- code: errorCode(error),
729
- action: 'ultra.sync migrate',
730
- },
731
- };
1868
+ if (errorCode(error) === 'TASK_LEDGER_SCHEMA_MIGRATION_REQUIRED') {
1869
+ return migrationRequired(error);
1870
+ }
1871
+ if (['TASK_LEDGER_INVALID', 'TASK_LEDGER_DIGEST_MISMATCH'].includes(errorCode(error))) {
1872
+ try {
1873
+ const legacy = taskLedger.syncTaskLedger(db, { rootDir });
1874
+ if (legacy.status === 'legacy') return migrationRequired(error);
1875
+ } catch {
1876
+ // Keep the original validated read error as the public diagnostic.
1877
+ }
1878
+ return corruptLedgerInspection(error, rootDir);
1879
+ }
1880
+ throw error;
732
1881
  }
733
1882
  }
734
1883
  if (input.action === 'import') return taskLedger.importTaskLedger(db, { rootDir });
@@ -743,22 +1892,56 @@ function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
743
1892
  };
744
1893
  }
745
1894
  if (input.action === 'publish') {
746
- const cached = priorResult(db, input.idempotency_key, 'sync:publish');
747
- if (cached) return { ...cached, idempotent: true };
748
- const result = taskLedger.publishTaskLedger(db, {
749
- rootDir,
750
- reason: input.reason || 'manual_checkpoint',
751
- });
752
- rememberResult(db, {
753
- idempotencyKey: input.idempotency_key,
754
- operation: 'sync:publish',
755
- result,
1895
+ const operation = 'sync:publish';
1896
+ const reason = input.reason || 'manual_checkpoint';
1897
+ const acceptedRequestDigest = requestDigest(operation, {
1898
+ action: input.action,
1899
+ reason,
756
1900
  });
757
- return result;
1901
+ let publication = null;
1902
+ try {
1903
+ return ops.tx(db, () => {
1904
+ const cached = priorResult(db, {
1905
+ idempotencyKey: input.idempotency_key,
1906
+ operation,
1907
+ requestDigest: acceptedRequestDigest,
1908
+ });
1909
+ if (cached.found) return { ...cached.result, idempotent: true };
1910
+ publication = taskLedger.publishTaskLedger(db, { rootDir, reason });
1911
+ rememberResultInTx(db, {
1912
+ idempotencyKey: input.idempotency_key,
1913
+ operation,
1914
+ requestDigest: acceptedRequestDigest,
1915
+ result: publication,
1916
+ });
1917
+ return publication;
1918
+ });
1919
+ } catch (error) {
1920
+ if (publication) {
1921
+ try {
1922
+ taskLedger.rollbackTaskLedgerPublication(publication);
1923
+ } catch (rollbackError) {
1924
+ throw Object.assign(
1925
+ new Error(
1926
+ `sync publication failed and the team ledger could not be restored: `
1927
+ + `${error.message}; ${rollbackError.message}`,
1928
+ ),
1929
+ {
1930
+ code: 'STATE_CORRUPT',
1931
+ cause: error,
1932
+ details: {
1933
+ original_error: errorCode(error),
1934
+ rollback_error: errorCode(rollbackError),
1935
+ path: taskLedger.LEDGER_RELATIVE_PATH,
1936
+ },
1937
+ },
1938
+ );
1939
+ }
1940
+ }
1941
+ throw stablePersistenceError(error, 'sync publication');
1942
+ }
758
1943
  }
759
- throw Object.assign(new Error(`unsupported sync action: ${input.action}`), {
760
- code: 'VALIDATION_ERROR',
761
- });
1944
+ throw validationError(`unsupported sync action: ${input.action}`);
762
1945
  }
763
1946
 
764
1947
  function sessionDiagnostic(error) {
@@ -771,11 +1954,285 @@ function sessionDiagnostic(error) {
771
1954
  };
772
1955
  }
773
1956
 
1957
+ function attachRecovery(error, recovery) {
1958
+ if (!recovery) return error;
1959
+ error.details = {
1960
+ ...(error.details || {}),
1961
+ recovery,
1962
+ };
1963
+ return error;
1964
+ }
1965
+
1966
+ function idempotentSessionMutation(db, input, mutate, {
1967
+ rollbackEffect = null,
1968
+ prepareEffect = null,
1969
+ } = {}) {
1970
+ const operation = `session:${input.action}`;
1971
+ const request = Object.fromEntries(
1972
+ ['action', 'scope', 'payload']
1973
+ .filter((key) => Object.hasOwn(input, key))
1974
+ .map((key) => [key, input[key]]),
1975
+ );
1976
+ const acceptedRequestDigest = requestDigest(operation, request);
1977
+ let mutationCompleted = false;
1978
+ let effectPrepared = false;
1979
+ let result = null;
1980
+ try {
1981
+ const transactionalMutation = () => {
1982
+ const cached = priorResult(db, {
1983
+ idempotencyKey: input.idempotency_key,
1984
+ operation,
1985
+ requestDigest: acceptedRequestDigest,
1986
+ });
1987
+ if (cached.found) return { ...cached.result, idempotent: true };
1988
+ if (prepareEffect) {
1989
+ prepareEffect();
1990
+ effectPrepared = true;
1991
+ }
1992
+ result = mutate();
1993
+ mutationCompleted = true;
1994
+ rememberResultInTx(db, {
1995
+ idempotencyKey: input.idempotency_key,
1996
+ operation,
1997
+ requestDigest: acceptedRequestDigest,
1998
+ result,
1999
+ taskId: input.scope?.task_id || null,
2000
+ });
2001
+ return result;
2002
+ };
2003
+ return ops.tx(db, transactionalMutation);
2004
+ } catch (error) {
2005
+ let recovery = null;
2006
+ if ((effectPrepared || mutationCompleted) && rollbackEffect) {
2007
+ recovery = rollbackEffect(result, error);
2008
+ }
2009
+ const recovered = attachRecovery(
2010
+ stablePersistenceError(error, `${operation} receipt`),
2011
+ recovery,
2012
+ );
2013
+ if (recovered !== error || isHardError(recovered)) throw recovered;
2014
+ return sessionDiagnostic(recovered);
2015
+ }
2016
+ }
2017
+
2018
+ function removeRolledBackManagedFile(db, {
2019
+ rootDir,
2020
+ table,
2021
+ idField,
2022
+ id,
2023
+ relative,
2024
+ expectedPrefix,
2025
+ expectedBasename,
2026
+ }) {
2027
+ if (!relative || db.prepare(
2028
+ `SELECT 1 FROM ${table} WHERE ${idField} = ?`,
2029
+ ).get(id)) return;
2030
+ if (path.isAbsolute(relative)) {
2031
+ throw validationError(`managed rollback path must be project-relative: ${relative}`);
2032
+ }
2033
+ const absolute = path.resolve(rootDir, relative);
2034
+ const prefix = path.resolve(rootDir, expectedPrefix);
2035
+ if (!absolute.startsWith(`${prefix}${path.sep}`)
2036
+ || path.basename(absolute) !== expectedBasename) {
2037
+ throw Object.assign(
2038
+ new Error(`managed rollback path escaped its authority: ${relative}`),
2039
+ { code: 'PATH_AUTHORITY_VIOLATION' },
2040
+ );
2041
+ }
2042
+ restoreRecordFile(absolute, { existed: false, bytes: null, mode: null });
2043
+ }
2044
+
2045
+ function snapshotAcquisitionEffect(db, taskId) {
2046
+ return {
2047
+ context_ids: new Set(db.prepare(
2048
+ `SELECT * FROM context_envelopes
2049
+ WHERE scope_type = 'task' AND scope_id = ?`,
2050
+ ).all(taskId).map((row) => row.id)),
2051
+ prior_session: db.prepare(
2052
+ `SELECT * FROM sessions
2053
+ WHERE task_id = ? AND status = 'running'
2054
+ ORDER BY rowid DESC LIMIT 1`,
2055
+ ).get(taskId) || null,
2056
+ };
2057
+ }
2058
+
2059
+ function rollbackAcquisitionEffect(db, rootDir, acquisition, error) {
2060
+ const issues = [];
2061
+ let priorSessionReconciled = false;
2062
+ const attempt = (action, run) => {
2063
+ try { run(); }
2064
+ catch (cause) {
2065
+ issues.push({
2066
+ action,
2067
+ code: errorCode(cause),
2068
+ message: cause.message,
2069
+ });
2070
+ }
2071
+ };
2072
+ if (acquisition.handle?.sid && acquisition.handle?.worktree_path) {
2073
+ attempt('remove_worktree', () => {
2074
+ sessionRunner._internal.reconcileRemovedWorktree(
2075
+ rootDir,
2076
+ acquisition.handle.worktree_path,
2077
+ { sid: acquisition.handle.sid },
2078
+ );
2079
+ });
2080
+ }
2081
+ if (acquisition.handle?.sid && acquisition.handle?.artifact_dir) {
2082
+ attempt('remove_artifact_dir', () => {
2083
+ const expected = path.resolve(
2084
+ rootDir,
2085
+ '.ultra',
2086
+ '.runtime',
2087
+ 'sessions',
2088
+ acquisition.handle.sid,
2089
+ );
2090
+ const actual = path.resolve(acquisition.handle.artifact_dir);
2091
+ if (actual !== expected) {
2092
+ throw Object.assign(
2093
+ new Error(`session artifact rollback escaped its authority: ${actual}`),
2094
+ { code: 'PATH_AUTHORITY_VIOLATION' },
2095
+ );
2096
+ }
2097
+ const stat = fs.lstatSync(actual, { throwIfNoEntry: false });
2098
+ if (stat?.isSymbolicLink() || stat && !stat.isDirectory()) {
2099
+ throw Object.assign(
2100
+ new Error(`session artifact rollback target is unsafe: ${actual}`),
2101
+ { code: 'PATH_AUTHORITY_VIOLATION' },
2102
+ );
2103
+ }
2104
+ if (stat) fs.rmSync(actual, { recursive: true });
2105
+ });
2106
+ }
2107
+ if (acquisition.packet?.created) {
2108
+ const packet = acquisition.packet;
2109
+ attempt('remove_worker_packet', () => removeRolledBackManagedFile(db, {
2110
+ rootDir,
2111
+ table: 'worker_packets',
2112
+ idField: 'id',
2113
+ id: packet.id,
2114
+ relative: packet.packet_path,
2115
+ expectedPrefix: path.join('.ultra', '.runtime', 'worker-packets'),
2116
+ expectedBasename: `${packet.id}.json`,
2117
+ }));
2118
+ }
2119
+ const context = acquisition.packet?.context_envelope;
2120
+ if (context?.id && !acquisition.snapshot?.context_ids.has(context.id)) {
2121
+ attempt('remove_context_envelope', () => removeRolledBackManagedFile(db, {
2122
+ rootDir,
2123
+ table: 'context_envelopes',
2124
+ idField: 'id',
2125
+ id: context.id,
2126
+ relative: context.path,
2127
+ expectedPrefix: '.ultra',
2128
+ expectedBasename: `${context.id}.json`,
2129
+ }));
2130
+ }
2131
+ const priorSession = acquisition.snapshot?.prior_session;
2132
+ if (priorSession?.pid
2133
+ && !sessionRunner._internal.processIsExecuting(priorSession.pid)) {
2134
+ attempt('reconcile_terminated_prior_session', () => {
2135
+ const current = ops.readSession(db, priorSession.sid);
2136
+ if (current?.status === 'running' && current.pid === priorSession.pid) {
2137
+ ops.updateSession(db, priorSession.sid, { status: 'crashed', pid: null });
2138
+ priorSessionReconciled = true;
2139
+ }
2140
+ });
2141
+ }
2142
+ if (issues.length > 0) {
2143
+ throw Object.assign(
2144
+ new Error(
2145
+ `session acquisition receipt failed and effect rollback requires recovery: `
2146
+ + `${error.message}`,
2147
+ ),
2148
+ {
2149
+ code: 'SESSION_RECEIPT_RECOVERY_REQUIRED',
2150
+ cause: error,
2151
+ details: {
2152
+ issues,
2153
+ retry: 'Run ultra.doctor with repair=true, then retry the exact ultra.session request.',
2154
+ },
2155
+ },
2156
+ );
2157
+ }
2158
+ return {
2159
+ status: 'rolled_back',
2160
+ prior_session_reconciled: priorSessionReconciled,
2161
+ retry: 'Retry the exact ultra.session request with the same idempotency_key.',
2162
+ };
2163
+ }
2164
+
2165
+ function recoverReleaseReceiptFailure(db, rootDir, session, payload, error) {
2166
+ if (!session) {
2167
+ throw Object.assign(
2168
+ new Error('session release receipt failed after its session authority disappeared'),
2169
+ {
2170
+ code: 'SESSION_RECEIPT_RECOVERY_REQUIRED',
2171
+ cause: error,
2172
+ details: { retry: 'Run ultra.doctor with repair=true before retrying release.' },
2173
+ },
2174
+ );
2175
+ }
2176
+ const requestedStatus = payload.status || 'completed';
2177
+ const removedWorktree = payload.remove_worktree === true;
2178
+ if (!removedWorktree && !session.pid) {
2179
+ return {
2180
+ status: 'rolled_back',
2181
+ retry: 'Retry the exact ultra.session release request with the same idempotency_key.',
2182
+ };
2183
+ }
2184
+ const closeJournal = sessionRunner._internal.closeJournal;
2185
+ try {
2186
+ if (removedWorktree) {
2187
+ closeJournal.prepare(rootDir, {
2188
+ sid: session.sid,
2189
+ task_id: session.task_id,
2190
+ requested_status: requestedStatus,
2191
+ worktree_path: session.worktree_path,
2192
+ });
2193
+ sessionRunner._internal.reconcileRemovedWorktree(
2194
+ rootDir,
2195
+ session.worktree_path,
2196
+ { sid: session.sid },
2197
+ );
2198
+ closeJournal.update(rootDir, session.sid, {
2199
+ phase: 'worktree_removed',
2200
+ error: null,
2201
+ });
2202
+ }
2203
+ ops.updateSession(db, session.sid, { status: requestedStatus, pid: null });
2204
+ if (removedWorktree) closeJournal.discard(rootDir, session.sid);
2205
+ } catch (cause) {
2206
+ throw Object.assign(
2207
+ new Error(
2208
+ `session release receipt failed and close recovery requires attention: `
2209
+ + `${error.message}; ${cause.message}`,
2210
+ ),
2211
+ {
2212
+ code: 'SESSION_RECEIPT_RECOVERY_REQUIRED',
2213
+ cause: error,
2214
+ details: {
2215
+ recovery_error: errorCode(cause),
2216
+ sid: session.sid,
2217
+ retry: 'Run ultra.doctor with repair=true, then retry the exact release request.',
2218
+ },
2219
+ },
2220
+ );
2221
+ }
2222
+ return {
2223
+ status: 'terminal_recovered',
2224
+ sid: session.sid,
2225
+ session_status: requestedStatus,
2226
+ retry: 'Retry the exact ultra.session release request to persist its receipt.',
2227
+ };
2228
+ }
2229
+
774
2230
  async function session(db, input = {}, {
775
2231
  rootDir = process.cwd(),
776
2232
  runtime = 'unknown',
777
2233
  sessionId = null,
778
2234
  } = {}) {
2235
+ validateSessionInput(input);
779
2236
  const scope = input.scope || {};
780
2237
  const payload = input.payload || {};
781
2238
  if (input.action === 'get') {
@@ -785,22 +2242,37 @@ async function session(db, input = {}, {
785
2242
  const sessions = ops.listActiveSessions(db, { task_id: scope.task_id });
786
2243
  return { sessions, count: sessions.length };
787
2244
  }
788
- if (input.action === 'heartbeat') return ops.heartbeatSession(db, scope.sid);
789
- if (input.action === 'release') {
790
- if (sessionId) {
791
- return sessionDiagnostic(Object.assign(
792
- new Error('a worker session is settled by its parent host'),
793
- { code: 'WORKER_SESSION_PARENT_OWNED' },
794
- ));
795
- }
796
- return sessionRunner.closeSession(
797
- { db, repoRoot: rootDir, sid: scope.sid },
798
- {
799
- status: payload.status || 'completed',
800
- remove_worktree: payload.remove_worktree === true,
801
- },
2245
+ if (input.action === 'heartbeat') {
2246
+ return idempotentSessionMutation(
2247
+ db,
2248
+ input,
2249
+ () => ops.heartbeatSession(db, scope.sid),
802
2250
  );
803
2251
  }
2252
+ if (input.action === 'release') {
2253
+ const releasingSession = ops.readSession(db, scope.sid);
2254
+ return idempotentSessionMutation(db, input, () => {
2255
+ if (sessionId) {
2256
+ return sessionDiagnostic(Object.assign(
2257
+ new Error('a worker session is settled by its parent host'),
2258
+ { code: 'WORKER_SESSION_PARENT_OWNED' },
2259
+ ));
2260
+ }
2261
+ return sessionRunner.closeSession(
2262
+ { db, repoRoot: rootDir, sid: scope.sid },
2263
+ {
2264
+ status: payload.status || 'completed',
2265
+ remove_worktree: payload.remove_worktree === true,
2266
+ },
2267
+ );
2268
+ }, {
2269
+ rollbackEffect: (result, error) => (
2270
+ result?.accepted === false
2271
+ ? null
2272
+ : recoverReleaseReceiptFailure(db, rootDir, releasingSession, payload, error)
2273
+ ),
2274
+ });
2275
+ }
804
2276
  const taskId = scope.task_id;
805
2277
  if (input.action === 'admission') {
806
2278
  try {
@@ -808,13 +2280,13 @@ async function session(db, input = {}, {
808
2280
  if (!task) throw Object.assign(new Error(`task ${taskId} not found`), { code: 'TASK_NOT_FOUND' });
809
2281
  const attention = ops.taskContractBlockers(task).map((code) => ({
810
2282
  code,
811
- severity: 'needs_attention',
2283
+ severity: 'warning',
812
2284
  }));
813
2285
  const lease = ops.admissionCheck(db, taskId);
814
2286
  return {
815
- accepted: attention.length === 0 && lease.can_spawn,
2287
+ accepted: lease.can_spawn,
816
2288
  mutable: true,
817
- can_acquire: attention.length === 0 && lease.can_spawn,
2289
+ can_acquire: lease.can_spawn,
818
2290
  diagnostics: [
819
2291
  ...attention,
820
2292
  ...(lease.can_spawn ? [] : [{
@@ -829,95 +2301,341 @@ async function session(db, input = {}, {
829
2301
  return sessionDiagnostic(error);
830
2302
  }
831
2303
  }
832
- if (input.action !== 'acquire') {
833
- throw Object.assign(new Error(`unsupported session action: ${input.action}`), {
834
- code: 'VALIDATION_ERROR',
2304
+ const acquisition = {
2305
+ task_id: taskId,
2306
+ packet: null,
2307
+ handle: null,
2308
+ snapshot: null,
2309
+ authority: null,
2310
+ };
2311
+ try {
2312
+ acquisition.authority = runtimePaths.ensureRuntimeState(rootDir, {
2313
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
835
2314
  });
2315
+ } catch (error) {
2316
+ return sessionDiagnostic(error);
836
2317
  }
837
- let packet = null;
838
- try {
839
- packet = workerPacket.createWorkerPacket(db, {
840
- role: payload.role || 'implement',
841
- task_id: taskId,
842
- runtime: payload.runtime || runtime,
843
- output_path: payload.output_path
844
- || `.ultra/changes/active/${ops.readTask(db, taskId)?.change_id}/delivery/${taskId}-outcome.json`,
845
- output_schema: payload.output_schema,
846
- evidence_refs: payload.evidence_refs,
847
- diff_range: payload.diff_range,
848
- changed_files: payload.changed_files,
849
- }, { rootDir });
850
- const handle = sessionRunner.spawnSession({
2318
+ return idempotentSessionMutation(db, input, () => {
2319
+ try {
2320
+ acquisition.packet = workerPacket.createWorkerPacket(db, {
2321
+ role: payload.role || 'implement',
2322
+ task_id: taskId,
2323
+ runtime: payload.runtime || runtime,
2324
+ output_path: payload.output_path
2325
+ || `.ultra/changes/active/${ops.readTask(db, taskId)?.change_id}/delivery/${taskId}-outcome.json`,
2326
+ output_schema: payload.output_schema,
2327
+ evidence_refs: payload.evidence_refs,
2328
+ diff_range: payload.diff_range,
2329
+ changed_files: payload.changed_files,
2330
+ }, { rootDir });
2331
+ acquisition.handle = sessionRunner.spawnSession({
2332
+ db,
2333
+ repoRoot: rootDir,
2334
+ task_id: taskId,
2335
+ runtime: payload.runtime || runtime,
2336
+ authority: acquisition.authority,
2337
+ takeover: payload.takeover === true,
2338
+ worktree_base: payload.worktree_base,
2339
+ kernel_mode: true,
2340
+ mark_task_started: true,
2341
+ packet_digest: acquisition.packet.packet_digest,
2342
+ });
2343
+ workerPacket.markWorkerPacketAssigned(db, acquisition.packet.id);
2344
+ return {
2345
+ accepted: true,
2346
+ sid: acquisition.handle.sid,
2347
+ worktree_path: acquisition.handle.worktree_path,
2348
+ artifact_dir: acquisition.handle.artifact_dir,
2349
+ lease_expires_at: acquisition.handle.lease_expires_at,
2350
+ packet: acquisition.packet,
2351
+ };
2352
+ } catch (error) {
2353
+ if (acquisition.packet?.id) {
2354
+ throw error;
2355
+ }
2356
+ return sessionDiagnostic(error);
2357
+ }
2358
+ }, {
2359
+ prepareEffect: () => {
2360
+ acquisition.snapshot = snapshotAcquisitionEffect(db, taskId);
2361
+ },
2362
+ rollbackEffect: (_result, error) => rollbackAcquisitionEffect(
851
2363
  db,
852
- repoRoot: rootDir,
853
- task_id: taskId,
854
- runtime: payload.runtime || runtime,
855
- takeover: payload.takeover === true,
856
- worktree_base: payload.worktree_base,
857
- kernel_mode: true,
858
- mark_task_started: true,
859
- packet_digest: packet.packet_digest,
2364
+ rootDir,
2365
+ acquisition,
2366
+ error,
2367
+ ),
2368
+ });
2369
+ }
2370
+
2371
+ function snapshotManagedDirectory(directory) {
2372
+ const rootStat = fs.lstatSync(directory, { throwIfNoEntry: false });
2373
+ if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
2374
+ throw Object.assign(
2375
+ new Error(`archive source must be a managed directory: ${directory}`),
2376
+ { code: 'ARCHIVE_PATH_UNSAFE' },
2377
+ );
2378
+ }
2379
+ const entries = [];
2380
+ const walk = (parent, relativeParent = '') => {
2381
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
2382
+ const relative = path.join(relativeParent, entry.name);
2383
+ const absolute = path.join(parent, entry.name);
2384
+ const stat = fs.lstatSync(absolute);
2385
+ if (stat.isSymbolicLink()) {
2386
+ throw Object.assign(
2387
+ new Error(`archive source contains a symbolic link: ${relative}`),
2388
+ { code: 'ARCHIVE_PATH_UNSAFE' },
2389
+ );
2390
+ }
2391
+ if (stat.isDirectory()) {
2392
+ entries.push({ type: 'directory', relative, mode: stat.mode });
2393
+ walk(absolute, relative);
2394
+ } else if (stat.isFile()) {
2395
+ entries.push({
2396
+ type: 'file',
2397
+ relative,
2398
+ mode: stat.mode,
2399
+ bytes: fs.readFileSync(absolute),
2400
+ });
2401
+ } else {
2402
+ throw Object.assign(
2403
+ new Error(`archive source contains an unsupported entry: ${relative}`),
2404
+ { code: 'ARCHIVE_PATH_UNSAFE' },
2405
+ );
2406
+ }
2407
+ }
2408
+ };
2409
+ walk(directory);
2410
+ return {
2411
+ source: directory,
2412
+ mode: rootStat.mode,
2413
+ entries,
2414
+ destination: null,
2415
+ };
2416
+ }
2417
+
2418
+ function prepareArchiveDirectoryRollback(change, rootDir) {
2419
+ if (!change || change.status === 'archived') return null;
2420
+ const expected = path.resolve(
2421
+ rootDir,
2422
+ '.ultra',
2423
+ 'changes',
2424
+ 'active',
2425
+ change.id,
2426
+ );
2427
+ const actual = path.resolve(rootDir, change.artifact_root);
2428
+ if (actual !== expected) {
2429
+ throw Object.assign(
2430
+ new Error(`active Change artifact root escaped its authority: ${change.artifact_root}`),
2431
+ { code: 'ARCHIVE_PATH_UNSAFE' },
2432
+ );
2433
+ }
2434
+ return snapshotManagedDirectory(actual);
2435
+ }
2436
+
2437
+ function restoreArchiveDirectory(rootDir, changeId, rollback) {
2438
+ if (!rollback?.destination) return;
2439
+ const archiveRoot = path.resolve(rootDir, '.ultra', 'changes', 'archive');
2440
+ const destination = path.resolve(rollback.destination);
2441
+ if (path.dirname(destination) !== archiveRoot
2442
+ || !path.basename(destination).endsWith(`-${changeId}`)) {
2443
+ throw Object.assign(
2444
+ new Error(`archive rollback destination escaped its authority: ${destination}`),
2445
+ { code: 'ARCHIVE_PATH_UNSAFE' },
2446
+ );
2447
+ }
2448
+ const destinationStat = fs.lstatSync(destination, { throwIfNoEntry: false });
2449
+ if (!destinationStat || destinationStat.isSymbolicLink() || !destinationStat.isDirectory()) {
2450
+ throw Object.assign(
2451
+ new Error(`archive rollback destination is unavailable or unsafe: ${destination}`),
2452
+ { code: 'ARCHIVE_RECOVERY_REQUIRED' },
2453
+ );
2454
+ }
2455
+ if (fs.lstatSync(rollback.source, { throwIfNoEntry: false })) {
2456
+ throw Object.assign(
2457
+ new Error(`archive rollback source already exists: ${rollback.source}`),
2458
+ { code: 'ARCHIVE_RECOVERY_REQUIRED' },
2459
+ );
2460
+ }
2461
+ fs.renameSync(destination, rollback.source);
2462
+ for (const entry of fs.readdirSync(rollback.source)) {
2463
+ fs.rmSync(path.join(rollback.source, entry), { recursive: true });
2464
+ }
2465
+ const directories = rollback.entries
2466
+ .filter((entry) => entry.type === 'directory')
2467
+ .sort((left, right) => left.relative.length - right.relative.length);
2468
+ for (const entry of directories) {
2469
+ const target = path.join(rollback.source, entry.relative);
2470
+ fs.mkdirSync(target, { recursive: true, mode: entry.mode });
2471
+ }
2472
+ for (const entry of rollback.entries.filter((item) => item.type === 'file')) {
2473
+ const target = path.join(rollback.source, entry.relative);
2474
+ fs.mkdirSync(path.dirname(target), { recursive: true });
2475
+ fs.writeFileSync(target, entry.bytes, { mode: entry.mode });
2476
+ }
2477
+ fs.chmodSync(rollback.source, rollback.mode);
2478
+ }
2479
+
2480
+ function rollbackArchivePublication({
2481
+ publication,
2482
+ directory,
2483
+ rootDir,
2484
+ changeId,
2485
+ }, error) {
2486
+ const issues = [];
2487
+ try {
2488
+ deliveryTransaction.rollbackDeliveryTransaction({
2489
+ rootDir,
2490
+ changeId,
2491
+ });
2492
+ } catch (cause) {
2493
+ issues.push({
2494
+ action: 'restore_baseline_delivery',
2495
+ code: errorCode(cause),
2496
+ message: cause.message,
2497
+ });
2498
+ }
2499
+ if (publication) {
2500
+ try { taskLedger.rollbackTaskLedgerPublication(publication); }
2501
+ catch (cause) {
2502
+ issues.push({
2503
+ action: 'restore_team_ledger',
2504
+ code: errorCode(cause),
2505
+ message: cause.message,
2506
+ });
2507
+ }
2508
+ }
2509
+ if (directory?.destination) {
2510
+ try { restoreArchiveDirectory(rootDir, changeId, directory); }
2511
+ catch (cause) {
2512
+ issues.push({
2513
+ action: 'restore_active_change',
2514
+ code: errorCode(cause),
2515
+ message: cause.message,
2516
+ });
2517
+ }
2518
+ }
2519
+ if (issues.length > 0) {
2520
+ throw Object.assign(
2521
+ new Error(
2522
+ `archive publication failed and filesystem recovery requires attention: `
2523
+ + `${error.message}`,
2524
+ ),
2525
+ {
2526
+ code: 'ARCHIVE_RECOVERY_REQUIRED',
2527
+ cause: error,
2528
+ details: {
2529
+ change_id: changeId,
2530
+ issues,
2531
+ recovery: 'Run ultra.doctor with repair=true before retrying archive.',
2532
+ },
2533
+ },
2534
+ );
2535
+ }
2536
+ }
2537
+
2538
+ function finishArchiveDeliveryReceipt(rootDir, changeId, output) {
2539
+ try {
2540
+ deliveryTransaction.completeDeliveryTransaction({
2541
+ rootDir,
2542
+ changeId,
860
2543
  });
861
- workerPacket.markWorkerPacketAssigned(db, packet.id);
2544
+ return output;
2545
+ } catch (error) {
862
2546
  return {
863
- accepted: true,
864
- sid: handle.sid,
865
- worktree_path: handle.worktree_path,
866
- artifact_dir: handle.artifact_dir,
867
- lease_expires_at: handle.lease_expires_at,
868
- packet,
2547
+ ...output,
2548
+ recovery_warning: [
2549
+ output.recovery_warning,
2550
+ `DELIVERY_JOURNAL_CLEANUP_PENDING:${error.message}`,
2551
+ ].filter(Boolean).join(';'),
869
2552
  };
870
- } catch (error) {
871
- if (packet?.id) {
872
- workerPacket.abandonWorkerPacket(db, packet.id, error.code || error.message);
873
- }
874
- return sessionDiagnostic(error);
875
2553
  }
876
2554
  }
877
2555
 
878
2556
  async function archive(db, input = {}, {
879
2557
  rootDir = process.cwd(),
880
2558
  } = {}) {
881
- const cached = priorResult(db, input.idempotency_key, 'archive');
882
- if (cached) return { ...cached, idempotent: true };
883
- const change = changes.readChange(db, input.change_id);
884
- if (!change) {
885
- return {
886
- accepted: false,
887
- mutable: true,
888
- blockers: ['CHANGE_NOT_FOUND'],
889
- diagnostics: [{
890
- code: 'CHANGE_NOT_FOUND',
891
- severity: 'needs_attention',
892
- message: `change ${input.change_id} not found`,
893
- }],
894
- };
2559
+ validateArchiveInput(input);
2560
+ const operation = 'archive';
2561
+ const acceptedRequestDigest = requestDigest(operation, {
2562
+ change_id: input.change_id,
2563
+ payload: input.payload,
2564
+ });
2565
+ let publication = null;
2566
+ const existing = priorResult(db, {
2567
+ idempotencyKey: input.idempotency_key,
2568
+ operation,
2569
+ requestDigest: acceptedRequestDigest,
2570
+ });
2571
+ if (existing.found) {
2572
+ return finishArchiveDeliveryReceipt(
2573
+ rootDir,
2574
+ input.change_id,
2575
+ { ...existing.result, idempotent: true },
2576
+ );
895
2577
  }
2578
+ const directoryRollback = prepareArchiveDirectoryRollback(
2579
+ changes.readChange(db, input.change_id),
2580
+ rootDir,
2581
+ );
896
2582
  try {
897
- const result = changes.archiveChange(
898
- db,
899
- { id: input.change_id, ...(input.payload || {}) },
900
- { rootDir, kernelMode: true },
901
- );
902
- const output = {
903
- accepted: true,
904
- mutable: false,
905
- blockers: [],
906
- diagnostics: [],
907
- result,
908
- team_checkpoint: taskLedger.publishTaskLedger(db, {
909
- rootDir,
910
- reason: 'change_archived',
911
- }),
912
- };
913
- rememberResult(db, {
914
- idempotencyKey: input.idempotency_key,
915
- operation: 'archive',
916
- result: output,
917
- changeId: input.change_id,
2583
+ const output = ops.tx(db, () => {
2584
+ const cached = priorResult(db, {
2585
+ idempotencyKey: input.idempotency_key,
2586
+ operation,
2587
+ requestDigest: acceptedRequestDigest,
2588
+ });
2589
+ if (cached.found) return { ...cached.result, idempotent: true };
2590
+ const change = changes.readChange(db, input.change_id);
2591
+ if (!change) {
2592
+ return {
2593
+ accepted: false,
2594
+ mutable: true,
2595
+ blockers: ['CHANGE_NOT_FOUND'],
2596
+ diagnostics: [{
2597
+ code: 'CHANGE_NOT_FOUND',
2598
+ severity: 'needs_attention',
2599
+ message: `change ${input.change_id} not found`,
2600
+ }],
2601
+ };
2602
+ }
2603
+ const result = changes.archiveChange(
2604
+ db,
2605
+ { ...input.payload, id: input.change_id },
2606
+ { rootDir, deferDeliveryCleanup: true },
2607
+ );
2608
+ if (directoryRollback) directoryRollback.destination = result.archive_path;
2609
+ const output = {
2610
+ accepted: true,
2611
+ mutable: false,
2612
+ blockers: [],
2613
+ diagnostics: [],
2614
+ result,
2615
+ team_checkpoint: (publication = taskLedger.publishTaskLedger(db, {
2616
+ rootDir,
2617
+ reason: 'change_archived',
2618
+ })),
2619
+ };
2620
+ rememberResultInTx(db, {
2621
+ idempotencyKey: input.idempotency_key,
2622
+ operation,
2623
+ requestDigest: acceptedRequestDigest,
2624
+ result: output,
2625
+ changeId: input.change_id,
2626
+ });
2627
+ return output;
918
2628
  });
919
- return output;
2629
+ return finishArchiveDeliveryReceipt(rootDir, input.change_id, output);
920
2630
  } catch (error) {
2631
+ rollbackArchivePublication({
2632
+ publication,
2633
+ directory: directoryRollback,
2634
+ rootDir,
2635
+ changeId: input.change_id,
2636
+ }, error);
2637
+ const persistenceError = stablePersistenceError(error, 'archive publication');
2638
+ if (persistenceError !== error) throw persistenceError;
921
2639
  if (isHardError(error)) throw error;
922
2640
  return {
923
2641
  accepted: false,
@@ -929,22 +2647,27 @@ async function archive(db, input = {}, {
929
2647
  }
930
2648
 
931
2649
  async function dispatch(name, input, db, context = {}) {
932
- if (name === 'ultra.context') return readContext(db, input, context);
933
- if (name === 'ultra.record') return record(db, input, context);
934
- if (name === 'ultra.checkpoint') return checkpoint(db, input, context);
935
- if (name === 'ultra.sync') return sync(db, input, context);
936
- if (name === 'ultra.session') return session(db, input, context);
937
- if (name === 'ultra.archive') return archive(db, input, context);
2650
+ let result;
2651
+ if (name === 'ultra.context') result = readContext(db, input, context);
2652
+ else if (name === 'ultra.record') result = await record(db, input, context);
2653
+ else if (name === 'ultra.checkpoint') result = await checkpoint(db, input, context);
2654
+ else if (name === 'ultra.sync') result = sync(db, input, context);
2655
+ else if (name === 'ultra.session') result = await session(db, input, context);
2656
+ else if (name === 'ultra.archive') result = await archive(db, input, context);
938
2657
  if (name === 'ultra.doctor') {
939
- return doctor.runDoctor(db, {
2658
+ validateDoctorInput(input);
2659
+ result = doctor.runDoctor(db, {
940
2660
  rootDir: context.rootDir || process.cwd(),
941
2661
  repair: input.repair === true,
942
2662
  project: context.projector,
943
2663
  });
944
2664
  }
945
- const error = new Error(`unhandled public tool ${name}`);
946
- error.code = 'UNKNOWN_TOOL';
947
- throw error;
2665
+ if (result === undefined) {
2666
+ const error = new Error(`unhandled public tool ${name}`);
2667
+ error.code = 'UNKNOWN_TOOL';
2668
+ throw error;
2669
+ }
2670
+ return rememberRejectedAttempt(db, name, input, result);
948
2671
  }
949
2672
 
950
2673
  module.exports = {