ultra-builder-pro-cli 0.24.2 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/adapters/claude.js +104 -0
  3. package/adapters/codex.js +363 -40
  4. package/adapters/grok.js +1254 -58
  5. package/adapters/kimi.js +682 -13
  6. package/adapters/opencode.js +132 -11
  7. package/docs/AGENT-CONTEXT.md +14 -0
  8. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  9. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  10. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  11. package/mcp-server/lib/artifact-registry.cjs +181 -51
  12. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  13. package/mcp-server/lib/change-workflow.cjs +21 -16
  14. package/mcp-server/lib/context-envelope.cjs +124 -51
  15. package/mcp-server/lib/decision-records.cjs +164 -78
  16. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  17. package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
  18. package/mcp-server/lib/managed-file-write.cjs +56 -0
  19. package/mcp-server/lib/prd-parser.cjs +2 -2
  20. package/mcp-server/lib/runtime-paths.cjs +43 -16
  21. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  22. package/mcp-server/lib/state-db.cjs +186 -8
  23. package/mcp-server/lib/state-ops.cjs +214 -31
  24. package/mcp-server/lib/task-contract.cjs +404 -0
  25. package/mcp-server/lib/task-expander.cjs +2 -2
  26. package/mcp-server/lib/task-ledger.cjs +175 -62
  27. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  28. package/mcp-server/lib/worker-packet.cjs +2 -2
  29. package/mcp-server/server.cjs +23 -4
  30. package/orchestrator/parallel-orchestrator.cjs +114 -27
  31. package/orchestrator/session-runner.cjs +25 -136
  32. package/package.json +1 -1
  33. package/skills/ultra-change/SKILL.md +6 -1
  34. package/skills/ultra-dev/SKILL.md +9 -4
  35. package/skills/ultra-plan/SKILL.md +13 -4
  36. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  37. package/spec/mcp-tools.yaml +114 -9
  38. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  39. package/spec/schemas/state-db.sql +10 -6
  40. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  41. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -121,13 +121,13 @@ function abandonWorkerPacket(db, id, reason) {
121
121
  return ops.tx(db, () => {
122
122
  const row = db.prepare('SELECT * FROM worker_packets WHERE id = ?').get(id);
123
123
  if (!row || row.status === 'abandoned') return row || null;
124
- if (row.status !== 'pending') return row;
124
+ if (!['pending', 'assigned'].includes(row.status)) return row;
125
125
  db.prepare(
126
126
  `UPDATE worker_packets
127
127
  SET status = 'abandoned',
128
128
  abandoned_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
129
129
  abandon_reason = ?
130
- WHERE id = ? AND status = 'pending'`,
130
+ WHERE id = ? AND status IN ('pending', 'assigned')`,
131
131
  ).run(String(reason || 'session acquisition failed'), id);
132
132
  ops.appendEventInTx(db, {
133
133
  type: 'worker_packet_abandoned',
@@ -68,6 +68,24 @@ function errorResponse(code, message, retriable = false, details = undefined) {
68
68
  };
69
69
  }
70
70
 
71
+ function publicToolError(error) {
72
+ const code = String(error?.code || 'STATE_DB_ERROR');
73
+ if (code.startsWith('SQLITE_')) {
74
+ return {
75
+ code: 'STATE_DB_ERROR',
76
+ message: 'Ultra state persistence failed',
77
+ retriable: code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED',
78
+ details: undefined,
79
+ };
80
+ }
81
+ return {
82
+ code,
83
+ message: error?.message || 'Ultra state operation failed',
84
+ retriable: Boolean(error?.retriable),
85
+ details: error?.details,
86
+ };
87
+ }
88
+
71
89
  function isInitializeCall(name, input = {}) {
72
90
  return name === 'ultra.record'
73
91
  && Array.isArray(input.entries)
@@ -313,11 +331,12 @@ function startServer({
313
331
  } catch (error) {
314
332
  toolError = error.code || 'STATE_DB_ERROR';
315
333
  emitTelemetry();
334
+ const visibleError = publicToolError(error);
316
335
  return errorResponse(
317
- toolError,
318
- error.message,
319
- Boolean(error.retriable),
320
- error.details,
336
+ visibleError.code,
337
+ visibleError.message,
338
+ visibleError.retriable,
339
+ visibleError.details,
321
340
  );
322
341
  }
323
342
  });
@@ -27,6 +27,8 @@ const { isDeepStrictEqual } = require('node:util');
27
27
  const planStore = require('../mcp-server/lib/plan-store.cjs');
28
28
  const ops = require('../mcp-server/lib/state-ops.cjs');
29
29
  const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
30
+ const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
31
+ const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
30
32
  const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
31
33
  const runner = require('./session-runner.cjs');
32
34
  const dispatchRules = require('./dispatch-rules.cjs');
@@ -460,6 +462,10 @@ function reservePreparedTask(db, repoRoot, prepared, ctx) {
460
462
  const { onError } = ctx;
461
463
  let handle;
462
464
  let packet = null;
465
+ let authority = null;
466
+ const priorContextIds = new Set(
467
+ db.prepare('SELECT id FROM context_envelopes').all().map((row) => row.id),
468
+ );
463
469
  try {
464
470
  if (!task.change_id) {
465
471
  throw planError(
@@ -467,36 +473,44 @@ function reservePreparedTask(db, repoRoot, prepared, ctx) {
467
473
  `orchestrator task ${task.id} must belong to the accepted Change`,
468
474
  );
469
475
  }
470
- packet = workerPackets.createWorkerPacket(db, {
471
- role: 'implement',
472
- task_id: task.id,
473
- runtime: decision.runtime,
474
- output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
475
- output_schema: {
476
- type: 'object',
477
- required: ['packet_digest'],
478
- properties: {
479
- packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
476
+ authority = runtimePaths.ensureRuntimeState(repoRoot, {
477
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
478
+ });
479
+ ops.tx(db, () => {
480
+ packet = workerPackets.createWorkerPacket(db, {
481
+ role: 'implement',
482
+ task_id: task.id,
483
+ runtime: decision.runtime,
484
+ output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
485
+ output_schema: {
486
+ type: 'object',
487
+ required: ['packet_digest'],
488
+ properties: {
489
+ packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
490
+ },
480
491
  },
481
- },
482
- }, { rootDir: repoRoot });
483
- handle = runner.spawnSession({
484
- db, repoRoot,
485
- task_id: task.id,
486
- runtime: decision.runtime,
487
- command: ctx.deferStart ? null : command,
488
- args: ctx.deferStart ? [] : args,
489
- mark_task_started: true,
490
- kernel_mode: true,
491
- packet_digest: packet.packet_digest,
492
+ }, { rootDir: repoRoot });
493
+ workerPackets.markWorkerPacketAssigned(db, packet.id);
494
+ handle = runner.spawnSession({
495
+ db, repoRoot,
496
+ task_id: task.id,
497
+ runtime: decision.runtime,
498
+ authority,
499
+ command: null,
500
+ args: [],
501
+ mark_task_started: true,
502
+ kernel_mode: true,
503
+ packet_digest: packet.packet_digest,
504
+ });
505
+ handle.task_id = task.id;
506
+ handle.packet = packet;
492
507
  });
493
- handle.task_id = task.id;
494
- workerPackets.markWorkerPacketAssigned(db, packet.id);
495
- handle.packet = packet;
496
508
  } catch (err) {
497
- if (packet?.id) {
498
- workerPackets.abandonWorkerPacket(db, packet.id, err.code || err.message);
499
- }
509
+ rollbackFailedReservation(db, repoRoot, {
510
+ handle,
511
+ packet,
512
+ priorContextIds,
513
+ });
500
514
  const authorityBlocked = authorityBlockedResult(err, task.id, {
501
515
  reservationFailed: true,
502
516
  });
@@ -523,6 +537,69 @@ function reservePreparedTask(db, repoRoot, prepared, ctx) {
523
537
  };
524
538
  }
525
539
 
540
+ function rollbackFailedReservation(db, repoRoot, {
541
+ handle,
542
+ packet,
543
+ priorContextIds,
544
+ }) {
545
+ if (handle?.worktree_path) {
546
+ try {
547
+ runner._internal.reconcileRemovedWorktree(
548
+ repoRoot,
549
+ handle.worktree_path,
550
+ { sid: handle.sid },
551
+ );
552
+ } catch { /* the caller preserves the original reservation error */ }
553
+ }
554
+ if (handle?.artifact_dir) {
555
+ const expected = path.resolve(
556
+ repoRoot,
557
+ '.ultra',
558
+ '.runtime',
559
+ 'sessions',
560
+ handle.sid,
561
+ );
562
+ const actual = path.resolve(handle.artifact_dir);
563
+ if (actual === expected) {
564
+ try { fs.rmSync(actual, { recursive: true, force: true }); }
565
+ catch { /* the caller preserves the original reservation error */ }
566
+ }
567
+ }
568
+ if (packet?.created && !db.prepare(
569
+ 'SELECT 1 FROM worker_packets WHERE id = ?',
570
+ ).get(packet.id)) {
571
+ const expected = path.resolve(
572
+ repoRoot,
573
+ '.ultra',
574
+ '.runtime',
575
+ 'worker-packets',
576
+ `${packet.id}.json`,
577
+ );
578
+ const actual = path.resolve(repoRoot, packet.packet_path);
579
+ if (actual === expected) {
580
+ try { fs.rmSync(actual, { force: true }); }
581
+ catch { /* the caller preserves the original reservation error */ }
582
+ }
583
+ }
584
+ const context = packet?.context_envelope;
585
+ if (context?.id
586
+ && !priorContextIds.has(context.id)
587
+ && !db.prepare('SELECT 1 FROM context_envelopes WHERE id = ?').get(context.id)) {
588
+ const actual = path.resolve(repoRoot, context.path);
589
+ const runtimeContextRoot = path.resolve(
590
+ repoRoot,
591
+ '.ultra',
592
+ '.runtime',
593
+ 'contexts',
594
+ );
595
+ if (actual.startsWith(`${runtimeContextRoot}${path.sep}`)
596
+ && path.basename(actual) === `${context.id}.json`) {
597
+ try { fs.rmSync(actual, { force: true }); }
598
+ catch { /* the caller preserves the original reservation error */ }
599
+ }
600
+ }
601
+ }
602
+
526
603
  async function runTask(db, repoRoot, task, wave, ctx) {
527
604
  const prepared = ctx.prepared || prepareTaskDispatch(task, wave, ctx);
528
605
  const reservation = reservePreparedTask(db, repoRoot, prepared, ctx);
@@ -560,6 +637,16 @@ function unwindReservations(db, repoRoot, reservations, ctx) {
560
637
  if (ctx.onError) ctx.onError(error);
561
638
  continue;
562
639
  }
640
+ try {
641
+ workerPackets.abandonWorkerPacket(
642
+ db,
643
+ handle.packet?.id,
644
+ 'parallel reservation cancelled before execution',
645
+ );
646
+ } catch (error) {
647
+ errors.push(error);
648
+ if (ctx.onError) ctx.onError(error);
649
+ }
563
650
  const task = ops.readTask(db, handle.task_id);
564
651
  if (task?.status === 'in_progress') {
565
652
  try {
@@ -21,16 +21,6 @@ const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
21
21
  const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
22
22
  const closeJournal = require('./session-close-journal.cjs');
23
23
 
24
- function legacyContextSpine() {
25
- return module.require(path.join(
26
- __dirname,
27
- '..',
28
- 'mcp-server',
29
- 'lib',
30
- 'context-spine.cjs',
31
- ));
32
- }
33
-
34
24
  const EXPECTED_EXECUTION_GATE_CODES = new Set([
35
25
  'TASK_EXECUTION_CONTRACT_INCOMPLETE',
36
26
  'WORKFLOW_TASK_NOT_EXECUTABLE',
@@ -559,77 +549,12 @@ function mintSessionId() {
559
549
  function assertSessionTaskReady(db, repoRoot, taskId) {
560
550
  const task = ops.readTask(db, taskId);
561
551
  if (!task) return null; // admissionCheck reports the canonical TASK_NOT_FOUND error.
562
- // Unbound rows remain readable for legacy recovery, but every current
563
- // change-owned task must pass the same plan gate as a dev workflow.
564
- if (!task.change_id) return task;
565
- ops.assertTaskExecutionContract(task);
566
552
  if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
567
553
  throw new SessionRunnerError(
568
554
  'TASK_NOT_EXECUTABLE',
569
555
  `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
570
556
  );
571
557
  }
572
- const incompleteDependencies = (task.deps || []).filter((id) => {
573
- const dependency = ops.readTask(db, id);
574
- return !dependency || !['completed', 'expanded'].includes(dependency.status);
575
- });
576
- if (incompleteDependencies.length > 0) {
577
- throw new SessionRunnerError(
578
- 'TASK_DEPENDENCIES_INCOMPLETE',
579
- `task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
580
- );
581
- }
582
- const plan = checkpoints.currentCheckpoint(
583
- db,
584
- 'plan',
585
- { change_id: task.change_id },
586
- { includeDraft: false },
587
- );
588
- if (!plan || checkpoints.checkpointDigest(plan) !== plan.digest) {
589
- throw new SessionRunnerError(
590
- 'PLAN_CHECKPOINT_REQUIRED',
591
- `task ${task.id} requires an accepted Plan checkpoint`,
592
- );
593
- }
594
- const implementationContext = legacyContextSpine().validateContextSnapshot(db, {
595
- change_id: task.change_id,
596
- task_id: task.id,
597
- role: 'implement',
598
- gate: 'implementation',
599
- }, {
600
- rootDir: repoRoot,
601
- require_current_checkout: true,
602
- });
603
- if (!implementationContext.snapshot) {
604
- throw new SessionRunnerError(
605
- 'IMPLEMENTATION_CONTEXT_REQUIRED',
606
- `task ${task.id} requires a ready implementation Context before session admission`,
607
- { blockers: implementationContext.blockers },
608
- );
609
- }
610
- if (implementationContext.blockers.length > 0) {
611
- throw new SessionRunnerError(
612
- 'IMPLEMENTATION_CONTEXT_STALE',
613
- `task ${task.id} implementation Context is stale`,
614
- { blockers: implementationContext.blockers },
615
- );
616
- }
617
- const planningContext = legacyContextSpine().validateContextSnapshot(db, {
618
- change_id: task.change_id,
619
- task_id: null,
620
- role: 'plan',
621
- gate: 'planning',
622
- }, {
623
- rootDir: repoRoot,
624
- require_current_checkout: true,
625
- });
626
- if (!planningContext.snapshot || planningContext.blockers.length > 0) {
627
- throw new SessionRunnerError(
628
- 'PLAN_CONTEXT_STALE',
629
- `change ${task.change_id} planning Context is stale at session admission`,
630
- { blockers: planningContext.blockers },
631
- );
632
- }
633
558
  return task;
634
559
  }
635
560
 
@@ -638,65 +563,26 @@ function assertKernelSessionTaskReady(db, repoRoot, taskId, packetDigest) {
638
563
  if (!task) {
639
564
  throw new SessionRunnerError('TASK_NOT_FOUND', `task ${taskId} not found`);
640
565
  }
641
- ops.assertTaskExecutionContract(task);
642
566
  if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
643
567
  throw new SessionRunnerError(
644
568
  'TASK_NOT_EXECUTABLE',
645
569
  `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
646
570
  );
647
571
  }
648
- const incompleteDependencies = (task.deps || []).filter((id) => {
649
- const dependency = ops.readTask(db, id);
650
- return !dependency || !['completed', 'expanded'].includes(dependency.status);
651
- });
652
- if (incompleteDependencies.length > 0) {
572
+ const packet = packetDigest
573
+ ? db.prepare(
574
+ `SELECT id FROM worker_packets
575
+ WHERE packet_digest = ? AND scope_type = 'task' AND scope_id = ?
576
+ AND status IN ('pending', 'assigned')`,
577
+ ).get(packetDigest, task.id)
578
+ : null;
579
+ if (!packet) {
653
580
  throw new SessionRunnerError(
654
- 'TASK_DEPENDENCIES_INCOMPLETE',
655
- `task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
581
+ 'WORKER_PACKET_REQUIRED',
582
+ `task ${task.id} requires the exact Worker Packet prepared for this session`,
656
583
  );
657
584
  }
658
- if (task.change_id) {
659
- const plan = checkpoints.currentCheckpoint(
660
- db,
661
- 'plan',
662
- { change_id: task.change_id },
663
- { includeDraft: false },
664
- );
665
- if (!plan) {
666
- throw new SessionRunnerError(
667
- 'PLAN_CHECKPOINT_REQUIRED',
668
- `task ${task.id} requires an accepted Plan checkpoint`,
669
- );
670
- }
671
- const plannedDigest = plan.payload?.plan?.task_contract_digests?.[task.id];
672
- if (!plannedDigest) {
673
- throw new SessionRunnerError(
674
- 'PLAN_TASK_CONTRACT_MISSING',
675
- `accepted Plan checkpoint does not bind task ${task.id}`,
676
- );
677
- }
678
- const packet = packetDigest
679
- ? db.prepare(
680
- `SELECT id, task_digest FROM worker_packets
681
- WHERE packet_digest = ? AND scope_type = 'task' AND scope_id = ?
682
- AND status IN ('pending', 'assigned')`,
683
- ).get(packetDigest, task.id)
684
- : null;
685
- if (!packet) {
686
- throw new SessionRunnerError(
687
- 'WORKER_PACKET_REQUIRED',
688
- `task ${task.id} requires the exact Worker Packet prepared for this session`,
689
- );
690
- }
691
- workerPackets.readWorkerPacket(db, packet.id, { rootDir: repoRoot });
692
- if (packet.task_digest !== plannedDigest) {
693
- throw new SessionRunnerError(
694
- 'PLAN_TASK_CONTRACT_STALE',
695
- `task ${task.id} no longer matches its accepted Plan checkpoint`,
696
- );
697
- }
698
- return task;
699
- }
585
+ workerPackets.readWorkerPacket(db, packet.id, { rootDir: repoRoot });
700
586
  return task;
701
587
  }
702
588
 
@@ -783,6 +669,7 @@ function spawnSession({
783
669
  repoRoot,
784
670
  task_id,
785
671
  runtime,
672
+ authority: preadmittedAuthority = null,
786
673
  takeover = false,
787
674
  command = null,
788
675
  args = [],
@@ -818,17 +705,19 @@ function spawnSession({
818
705
  // or any filesystem mutation creates a new worktree.
819
706
  if (kernel_mode) assertKernelSessionTaskReady(db, repoRoot, task_id, packet_digest);
820
707
  else assertSessionTaskReady(db, repoRoot, task_id);
821
- let authority;
822
- try {
823
- authority = runtimePaths.ensureRuntimeState(repoRoot, {
824
- admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
825
- });
826
- } catch (error) {
827
- throw new SessionRunnerError(
828
- 'WORKTREE_AUTHORITY_NOT_IGNORED',
829
- `cannot establish the project Ultra storage boundary: ${error.message}`,
830
- { cause: error },
831
- );
708
+ let authority = preadmittedAuthority;
709
+ if (!authority) {
710
+ try {
711
+ authority = runtimePaths.ensureRuntimeState(repoRoot, {
712
+ admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(repoRoot),
713
+ });
714
+ } catch (error) {
715
+ throw new SessionRunnerError(
716
+ 'WORKTREE_AUTHORITY_NOT_IGNORED',
717
+ `cannot establish the project Ultra storage boundary: ${error.message}`,
718
+ { cause: error },
719
+ );
720
+ }
832
721
  }
833
722
  const pendingClose = closeJournal.findForTask(repoRoot, task_id);
834
723
  if (pendingClose) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra-builder-pro-cli",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "Explicitly invoked Ultra Builder Pro workflows for Claude Code, OpenCode, Codex CLI, Kimi Code, and Grok Build, with a project-local MCP persistence and safety kernel.",
5
5
  "bin": {
6
6
  "ultra-builder-pro-cli": "bin/install.js",
@@ -18,7 +18,9 @@ checkpoint accepts the downstream authority.
18
18
  4. Convert the request into observable outcome, executable acceptance, non-goals,
19
19
  public seams, recovery, verification, documentation impact, risk, and research
20
20
  disposition.
21
- 5. Recommend `quick`, `standard`, `major`, or `incident` from evidence. Ask only when
21
+ 5. Recommend `quick`, `standard`, `major`, or `incident` from evidence, or record a
22
+ clearer bounded repository-specific kind when those profiles lose meaning. These
23
+ are model-owned labels, not SQLite enums. Ask only when
22
24
  a choice changes accepted product intent, compatibility, security, material cost,
23
25
  external effects, or recovery.
24
26
 
@@ -42,6 +44,9 @@ genuinely dependent evidence stale. Readiness is derived, not a stored absorbing
42
44
  state. Semantic diagnostics remain advisory; structural, digest, path, or concurrency
43
45
  conflicts require repair and retry of the same draft.
44
46
  Use `change_contract / cancel` only when the owner intentionally abandons the Change.
47
+ An archived or cancelled Change remains immutable history. When new evidence requires
48
+ continued work, use `change_contract / supersede` to create one linked active successor;
49
+ never patch SQLite, reopen the historical row, or duplicate the relationship manually.
45
50
 
46
51
  Publish the durable Change contract with `ultra.sync { action: publish }` when it is a
47
52
  useful team handoff. Recommend bounded `ultra-research` for a real evidence gap or
@@ -30,7 +30,7 @@ before changing logic. Implement the smallest complete vertical slice. Ask the o
30
30
  only when new evidence changes product intent, compatibility, security, material cost,
31
31
  external effects, or recovery.
32
32
 
33
- Use typed `ultra.record` entries for `task_outcome / start|block|complete`,
33
+ Use typed `ultra.record` entries for `task_outcome / start|block|complete|attest_commit`,
34
34
  `event / append`, `artifact / bind`, `decision / accept`, and corrected
35
35
  `task_contract / revise`. Every worker output must repeat the exact `packet_digest`;
36
36
  the parent registers it only after packet, output path, schema, and digest validation.
@@ -49,9 +49,14 @@ reports outcome diagnostics. The model decides whether the implemented slice sat
49
49
  the Task; semantic warnings remain visible. A rejection means the declared authority,
50
50
  path, digest, or concurrency boundary was unsafe and leaves the draft mutable.
51
51
 
52
- After acceptance, mark the task durably completed, publish with `ultra.sync`, and
53
- create at most one authorized local task commit containing code and semantic
54
- artifacts. Record its SHA only in local state; never create a bookkeeping commit.
52
+ After acceptance:
53
+
54
+ 1. Record `task_outcome / complete { id, packet_digest }`; never include a patch or SHA.
55
+ 2. Publish the durable completed status with `ultra.sync`.
56
+ 3. Create at most one authorized local task commit containing code, semantic
57
+ artifacts, and the team checkpoint.
58
+ 4. Record `task_outcome / attest_commit { id, completion_commit }` with that integrated
59
+ commit's full SHA. This is local mechanical proof and must not create another commit.
55
60
 
56
61
  Release the lease through `ultra.session`. Preserve a dirty or unintegrated worktree
57
62
  on interruption. Commit, push, tag, publish, deploy, or destructive cleanup require
@@ -22,7 +22,12 @@ digests, context, and team checkpoint in one semantic operation.
22
22
  for a material unresolved choice.
23
23
  6. Design a walking skeleton and vertical slices. Each task owns an observable outcome,
24
24
  public seam, verification command, acceptance mapping, target seams, bounded
25
- context refs, documentation impact, recovery, owner, and dependencies.
25
+ context refs, documentation impact, recovery, owner, and dependencies. Set the
26
+ published `type`, `priority`, optional bounded `complexity`/`estimated_days`,
27
+ `slice_kind`, and string-array `deps`/`files_modified` deliberately. Prefer the
28
+ shared vocabulary when it communicates the work, but use a bounded
29
+ repository-specific label when it is clearer; SQLite validates structure and does
30
+ not choose business taxonomy.
26
31
 
27
32
  Large sources stay lazy. Use digest freshness for accepted inputs that must remain
28
33
  byte-current, existence for expected implementation targets, and advisory only when
@@ -34,8 +39,11 @@ question.
34
39
  ## Record and checkpoint
35
40
 
36
41
  Use one `ultra.record` batch with `task_contract / define` for new tasks and
37
- `task_contract / revise` for corrected contracts. Read back the draft with
38
- `ultra.context`.
42
+ `task_contract / revise` for corrected contracts, including a wrong `title` or `type`,
43
+ before accepting the Plan checkpoint. Read back the draft with `ultra.context`.
44
+ Never write status, session, freshness, runtime tag, generated Context, or
45
+ completion-commit fields through a Task Contract; Task outcomes and the runtime own
46
+ them.
39
47
  Never read or write `.ultra/tasks/tasks.json`; use `ultra.sync` for the Git team
40
48
  checkpoint, and never edit the local runtime projection directly.
41
49
 
@@ -49,7 +57,8 @@ idempotency_key: stable semantic Plan checkpoint id
49
57
  ```
50
58
 
51
59
  The model decides whether decomposition, acceptance coverage, and context are
52
- sufficient and records any deliberate omission. The checkpoint compiles or reuses the
60
+ sufficient and records any deliberate omission. Missing semantic detail remains a
61
+ visible warning and does not deny an otherwise safe Session lease. The checkpoint compiles or reuses the
53
62
  content-addressed Context Envelope, reports advisory coverage diagnostics, exports
54
63
  `plan.json` and `plan.md` inside the same recoverable publication, accepts one Plan
55
64
  revision, and publishes the team checkpoint. Do not export separately.
@@ -11,8 +11,10 @@ change-bound research records.
11
11
  Markdown anchor through `trace_to`.
12
12
  5. Check that every task reaches a public consumer and owns its error, documentation,
13
13
  verification, and recovery obligations.
14
- 6. Validate dependencies and ensure a quick profile contains exactly one task.
14
+ 6. Validate dependencies. A `quick` profile normally has one vertical task, but the
15
+ model may use the smallest evidence-backed graph that satisfies the accepted Change.
15
16
 
16
- Treat the MCP completion result as the final planning gate. It derives the coverage
17
- matrix from durable records and rejects unresolved decisions, missing research,
18
- uncovered acceptance, orphan tasks, incomplete contracts, or invalid topology.
17
+ Treat the MCP result as a persistence receipt plus typed diagnostics. Structural,
18
+ digest, path, concurrency, and exact topology conflicts must be repaired. The model
19
+ owns semantic completeness, acceptance coverage, research sufficiency, and whether a
20
+ warning requires another planning revision before execution.