ultra-builder-pro-cli 0.23.0 → 0.24.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 (95) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/.ultra-template/tasks/tasks.json +4 -2
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +55 -39
  5. package/adapters/_shared/codex-assets.cjs +121 -65
  6. package/adapters/_shared/interaction-contract.cjs +7 -1
  7. package/adapters/_shared/provenance.cjs +7 -2
  8. package/adapters/_shared/runtime-assets.cjs +3 -2
  9. package/adapters/_shared/runtime-native.cjs +132 -0
  10. package/adapters/claude.js +60 -30
  11. package/adapters/codex.js +178 -81
  12. package/adapters/grok.js +372 -0
  13. package/adapters/kimi.js +7 -3
  14. package/adapters/opencode.js +210 -32
  15. package/agents/code-reviewer.md +4 -2
  16. package/agents/debugger.md +4 -1
  17. package/agents/review-code.md +7 -3
  18. package/agents/review-comments.md +7 -4
  19. package/agents/review-coordinator.md +11 -7
  20. package/agents/review-design.md +8 -4
  21. package/agents/review-errors.md +8 -4
  22. package/agents/review-spec.md +8 -5
  23. package/agents/review-tests.md +7 -4
  24. package/agents/tdd-runner.md +5 -2
  25. package/bin/install.js +9 -4
  26. package/bin/orchestrator.js +4 -13
  27. package/docs/AGENT-CONTEXT.md +143 -229
  28. package/docs/ARCHITECTURE.md +205 -494
  29. package/docs/ARTIFACT-AUTHORITY.md +35 -28
  30. package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
  31. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  32. package/docs/ROADMAP.md +85 -110
  33. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  34. package/docs/SKILL-AUTHORING.md +3 -3
  35. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  36. package/docs/WORKFLOW-LIFECYCLE.md +189 -448
  37. package/hooks/README.md +11 -8
  38. package/hooks/active_task_context.py +16 -16
  39. package/hooks/adapters/grok.py +134 -0
  40. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  41. package/hooks/health_check.py +29 -34
  42. package/hooks/pre_stop_check.py +22 -12
  43. package/hooks/subagent_tracker.py +1 -1
  44. package/hooks/workflow_checkpoint.py +14 -13
  45. package/hooks/workflow_context.py +11 -12
  46. package/hooks/workflow_resume.py +14 -13
  47. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  48. package/mcp-server/lib/baseline-workflow.cjs +108 -82
  49. package/mcp-server/lib/canonical-json.cjs +19 -0
  50. package/mcp-server/lib/change-authority.cjs +26 -0
  51. package/mcp-server/lib/change-workflow.cjs +630 -45
  52. package/mcp-server/lib/context-envelope.cjs +508 -0
  53. package/mcp-server/lib/decision-records.cjs +337 -0
  54. package/mcp-server/lib/doctor.cjs +91 -33
  55. package/mcp-server/lib/init-project.cjs +78 -39
  56. package/mcp-server/lib/managed-file-write.cjs +116 -0
  57. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  58. package/mcp-server/lib/project-context.cjs +178 -0
  59. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  60. package/mcp-server/lib/state-db.cjs +155 -24
  61. package/mcp-server/lib/state-ops.cjs +21 -3
  62. package/mcp-server/lib/task-ledger.cjs +522 -13
  63. package/mcp-server/lib/ultra-facade.cjs +757 -442
  64. package/mcp-server/lib/worker-packet.cjs +358 -0
  65. package/mcp-server/server.cjs +200 -1103
  66. package/orchestrator/daemon.cjs +27 -9
  67. package/orchestrator/parallel-orchestrator.cjs +111 -76
  68. package/orchestrator/session-runner.cjs +135 -34
  69. package/package.json +10 -3
  70. package/skills/ultra-change/SKILL.md +10 -8
  71. package/skills/ultra-deliver/SKILL.md +17 -19
  72. package/skills/ultra-dev/SKILL.md +20 -11
  73. package/skills/ultra-doctor/SKILL.md +4 -4
  74. package/skills/ultra-init/SKILL.md +6 -5
  75. package/skills/ultra-init/references/brownfield-adoption.md +20 -14
  76. package/skills/ultra-plan/SKILL.md +13 -9
  77. package/skills/ultra-research/SKILL.md +18 -9
  78. package/skills/ultra-review/SKILL.md +14 -9
  79. package/skills/ultra-review/references/unified-schema.md +2 -0
  80. package/skills/ultra-review/scripts/review_wait.py +8 -0
  81. package/skills/ultra-status/SKILL.md +2 -2
  82. package/skills/ultra-test/SKILL.md +10 -6
  83. package/skills/ultra-think/SKILL.md +8 -7
  84. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  85. package/spec/cli-protocol.md +80 -199
  86. package/spec/mcp-tools.yaml +43 -2806
  87. package/spec/schemas/state-db.sql +107 -4
  88. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  89. package/ultra-tools/commands/status.cjs +369 -243
  90. package/mcp-server/lib/context-spine.cjs +0 -1208
  91. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  92. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  93. package/mcp-server/lib/spec-learning.cjs +0 -284
  94. package/mcp-server/lib/workflow-state.cjs +0 -3129
  95. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -23,7 +23,7 @@ const gitBootstrap = require('./git-bootstrap.cjs');
23
23
  const runtimePaths = require('./runtime-paths.cjs');
24
24
  const runtimeState = require('./runtime-state.cjs');
25
25
  const ops = require('./state-ops.cjs');
26
- const workflows = require('./workflow-state.cjs');
26
+ const stageCheckpoints = require('./stage-checkpoints.cjs');
27
27
 
28
28
  const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
29
29
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
@@ -544,42 +544,73 @@ function projectionMigrationError(projection) {
544
544
  );
545
545
  }
546
546
 
547
- function ensureInitializationWorkflows(db, baseline, { rootDir }) {
548
- let initRun = workflows.listWorkflows(
549
- db, { kind: 'init', baseline_id: baseline.id, limit: 1 }, { rootDir },
550
- )[0];
551
- const researchRun = workflows.listWorkflows(
552
- db, { kind: 'research', baseline_id: baseline.id, limit: 1 }, { rootDir },
553
- )[0];
554
- if (!initRun) {
555
- initRun = workflows.startWorkflow(db, {
556
- kind: 'init', baseline_id: baseline.id,
557
- subject: `Initialize Ultra authority for ${baseline.project_name}.`,
558
- metadata: { repository_revision: baseline.repository_revision, scope: baseline.scope },
559
- }, { rootDir });
560
- for (const stepId of ['inspect-authority', 'classify-repository', 'scaffold-authority']) {
561
- initRun = workflows.recordWorkflowStep(db, {
562
- id: initRun.id, step_id: stepId, status: 'completed',
563
- }, { rootDir });
564
- }
565
- initRun = workflows.recordWorkflowStep(db, {
566
- id: initRun.id,
567
- step_id: 'verify-initialization',
568
- status: 'completed',
547
+ function ensureInitializationCheckpoint(db, baseline) {
548
+ let initCheckpoint = stageCheckpoints.currentCheckpoint(
549
+ db,
550
+ 'init',
551
+ { baseline_id: baseline.id },
552
+ { includeDraft: false },
553
+ );
554
+ if (!initCheckpoint) {
555
+ const draft = stageCheckpoints.saveDraft(db, {
556
+ stage: 'init',
557
+ scope: { baseline_id: baseline.id },
558
+ payload: {
559
+ project_name: baseline.project_name,
560
+ repository_revision: baseline.repository_revision,
561
+ scope: baseline.scope,
562
+ },
569
563
  evidence: [{
570
564
  kind: 'authority',
571
565
  ref: '.ultra/.runtime/state.db',
572
566
  summary: 'State authority, scaffold, projection, and repository classification were verified.',
573
567
  }],
574
- }, { rootDir });
575
- initRun = workflows.completeWorkflow(db, { id: initRun.id }, { rootDir });
568
+ diagnostics: [],
569
+ idempotency_key: `init:${baseline.id}:draft`,
570
+ });
571
+ initCheckpoint = stageCheckpoints.acceptDraft(db, {
572
+ id: draft.id,
573
+ idempotency_key: `init:${baseline.id}:accept`,
574
+ });
575
+ }
576
+ let researchCheckpoint = stageCheckpoints.currentCheckpoint(
577
+ db,
578
+ 'research',
579
+ { baseline_id: baseline.id },
580
+ { includeDraft: false },
581
+ );
582
+ const legacyResearch = db.prepare(
583
+ `SELECT id, mode, status FROM workflow_runs
584
+ WHERE kind = 'research' AND baseline_id = ?
585
+ ORDER BY started_at DESC, rowid DESC LIMIT 1`,
586
+ ).get(baseline.id);
587
+ if (!researchCheckpoint && legacyResearch?.status === 'completed') {
588
+ const draft = stageCheckpoints.saveDraft(db, {
589
+ stage: 'research',
590
+ scope: { baseline_id: baseline.id },
591
+ payload: {
592
+ migration_source: 'workflow_runs',
593
+ legacy_run_id: legacyResearch.id,
594
+ mode: legacyResearch.mode,
595
+ },
596
+ evidence: [{
597
+ kind: 'legacy_workflow',
598
+ ref: legacyResearch.id,
599
+ summary: 'Completed legacy research provenance migrated during project resume.',
600
+ }],
601
+ diagnostics: [],
602
+ idempotency_key: `research:${baseline.id}:legacy:${legacyResearch.id}:draft`,
603
+ });
604
+ researchCheckpoint = stageCheckpoints.acceptDraft(db, {
605
+ id: draft.id,
606
+ idempotency_key: `research:${baseline.id}:legacy:${legacyResearch.id}:accept`,
607
+ });
576
608
  }
577
609
  const compatibilityOnly = baseline.mode === 'migrated';
578
610
  const provenanceComplete = !compatibilityOnly
579
611
  && baseline.status === 'ready'
580
- && initRun.status === 'completed'
581
- && researchRun?.status === 'completed'
582
- && baseline.research_run_id === researchRun.id;
612
+ && initCheckpoint.status === 'accepted'
613
+ && researchCheckpoint?.status === 'accepted';
583
614
  const readyWithoutProvenance = baseline.status === 'ready' && !provenanceComplete;
584
615
  const allowedTransitions = provenanceComplete
585
616
  ? ['ultra-change', 'ultra-research', 'ultra-status', 'ultra-doctor']
@@ -587,11 +618,19 @@ function ensureInitializationWorkflows(db, baseline, { rootDir }) {
587
618
  ? ['ultra-doctor', 'ultra-research', 'ultra-status']
588
619
  : ['ultra-research', 'ultra-status', 'ultra-doctor']);
589
620
  return {
590
- init_id: initRun.id,
591
- init_status: initRun.status,
592
- research_id: researchRun?.id || null,
593
- research_mode: researchRun?.mode || null,
594
- research_status: researchRun?.status || 'not_started',
621
+ init: {
622
+ id: initCheckpoint.id,
623
+ status: initCheckpoint.status,
624
+ revision: initCheckpoint.revision,
625
+ digest: initCheckpoint.digest,
626
+ },
627
+ research: researchCheckpoint ? {
628
+ id: researchCheckpoint.id,
629
+ status: researchCheckpoint.status,
630
+ revision: researchCheckpoint.revision,
631
+ digest: researchCheckpoint.digest,
632
+ mode: researchCheckpoint.payload?.mode || null,
633
+ } : null,
595
634
  provenance_status: compatibilityOnly
596
635
  ? 'compatibility_only'
597
636
  : (provenanceComplete ? 'complete' : (readyWithoutProvenance ? 'incomplete' : 'pending_research')),
@@ -623,7 +662,7 @@ function resumeProject({
623
662
  let initialized;
624
663
  let copiedFiles = [];
625
664
  let baselineResult;
626
- let workflowResult;
665
+ let checkpointResult;
627
666
  let teamCheckpoint;
628
667
  let resumeSnapshot;
629
668
  const completeResume = () => {
@@ -660,7 +699,7 @@ function resumeProject({
660
699
  classification: repositoryProfile,
661
700
  }, { rootDir: absTarget });
662
701
  }
663
- workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
702
+ checkpointResult = ensureInitializationCheckpoint(db, baselineResult);
664
703
  if (projection?.kind === 'legacy-task-projection') {
665
704
  const publication = taskLedger.publishTaskLedger(db, {
666
705
  rootDir: absTarget,
@@ -782,7 +821,7 @@ function resumeProject({
782
821
  copied_files: copiedFiles,
783
822
  repository_profile: repositoryProfile,
784
823
  git: gitSetup.result,
785
- workflow: workflowResult,
824
+ checkpoint: checkpointResult,
786
825
  };
787
826
  if (teamCheckpoint) {
788
827
  result.team_checkpoint = {
@@ -889,7 +928,7 @@ function initProject({
889
928
 
890
929
  let copiedFiles;
891
930
  let baselineResult;
892
- let workflowResult;
931
+ let checkpointResult;
893
932
  let gitSetup;
894
933
  try {
895
934
  fs.mkdirSync(ultraDir, { recursive: true });
@@ -908,7 +947,7 @@ function initProject({
908
947
  scope: scope === undefined ? ['.'] : scope,
909
948
  classification: repositoryProfile,
910
949
  }, { rootDir: absTarget });
911
- workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
950
+ checkpointResult = ensureInitializationCheckpoint(db, baselineResult);
912
951
  ops.appendEvent(db, {
913
952
  type: 'project_initialized',
914
953
  payload: {
@@ -966,7 +1005,7 @@ function initProject({
966
1005
  copied_files: copiedFiles,
967
1006
  repository_profile: repositoryProfile,
968
1007
  git: gitSetup.result,
969
- workflow: workflowResult,
1008
+ checkpoint: checkpointResult,
970
1009
  };
971
1010
  if (backupPath) result.backup_path = backupPath;
972
1011
  return result;
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const { normalizeProjectRelative } = require('./safe-project-file.cjs');
8
+
9
+ class ManagedFileWriteError extends Error {
10
+ constructor(code, message, details) {
11
+ super(message);
12
+ this.name = 'ManagedFileWriteError';
13
+ this.code = code;
14
+ if (details) this.details = details;
15
+ }
16
+ }
17
+
18
+ function physicalProjectRoot(rootDir) {
19
+ const root = path.resolve(rootDir);
20
+ const stat = fs.lstatSync(root, { throwIfNoEntry: false });
21
+ if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) {
22
+ throw new ManagedFileWriteError('PATH_AUTHORITY_VIOLATION', `unsafe project root: ${root}`);
23
+ }
24
+ return { root, physical: fs.realpathSync.native(root) };
25
+ }
26
+
27
+ function ensureDirectory(rootDir, relativeDir) {
28
+ const project = physicalProjectRoot(rootDir);
29
+ const components = normalizeProjectRelative(`${relativeDir}/.managed-target`).split('/').slice(0, -1);
30
+ let current = project.physical;
31
+ for (const component of components) {
32
+ current = path.join(current, component);
33
+ let stat = fs.lstatSync(current, { throwIfNoEntry: false });
34
+ if (!stat) {
35
+ fs.mkdirSync(current, { mode: 0o700 });
36
+ stat = fs.lstatSync(current);
37
+ }
38
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
39
+ throw new ManagedFileWriteError(
40
+ 'PATH_AUTHORITY_VIOLATION',
41
+ `managed artifact ancestor is unsafe: ${path.relative(project.root, current)}`,
42
+ );
43
+ }
44
+ const physical = fs.realpathSync.native(current);
45
+ if (physical !== project.physical && !physical.startsWith(`${project.physical}${path.sep}`)) {
46
+ throw new ManagedFileWriteError(
47
+ 'PATH_AUTHORITY_VIOLATION',
48
+ `managed artifact ancestor escapes the project: ${relativeDir}`,
49
+ );
50
+ }
51
+ }
52
+ return { project, directory: current };
53
+ }
54
+
55
+ function writeManagedFile(rootDir, relativePath, bytes, { mode = 0o600 } = {}) {
56
+ const relative = normalizeProjectRelative(relativePath);
57
+ const directoryRelative = path.posix.dirname(relative);
58
+ const basename = path.posix.basename(relative);
59
+ const prepared = ensureDirectory(rootDir, directoryRelative);
60
+ const target = path.join(prepared.directory, basename);
61
+ const existing = fs.lstatSync(target, { throwIfNoEntry: false });
62
+ if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
63
+ throw new ManagedFileWriteError(
64
+ 'PATH_AUTHORITY_VIOLATION',
65
+ `managed artifact target is unsafe: ${relative}`,
66
+ );
67
+ }
68
+ const tempName = `.${basename}.${process.pid}.${crypto.randomUUID()}.tmp`;
69
+ const temporary = path.join(prepared.directory, tempName);
70
+ let fd;
71
+ try {
72
+ fd = fs.openSync(
73
+ temporary,
74
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL
75
+ | (fs.constants.O_NOFOLLOW || 0),
76
+ mode,
77
+ );
78
+ fs.writeFileSync(fd, bytes);
79
+ fs.fsyncSync(fd);
80
+ fs.closeSync(fd);
81
+ fd = undefined;
82
+ fs.renameSync(temporary, target);
83
+ const targetStat = fs.lstatSync(target);
84
+ if (!targetStat.isFile() || targetStat.isSymbolicLink()) {
85
+ throw new ManagedFileWriteError(
86
+ 'PATH_AUTHORITY_VIOLATION',
87
+ `managed artifact publication changed identity: ${relative}`,
88
+ );
89
+ }
90
+ return {
91
+ path: relative,
92
+ digest: crypto.createHash('sha256').update(bytes).digest('hex'),
93
+ bytes: Buffer.byteLength(bytes),
94
+ };
95
+ } catch (error) {
96
+ if (fd !== undefined) fs.closeSync(fd);
97
+ try { fs.unlinkSync(temporary); } catch {}
98
+ if (error instanceof ManagedFileWriteError) throw error;
99
+ const wrapped = new ManagedFileWriteError(
100
+ 'PATH_AUTHORITY_VIOLATION',
101
+ `managed artifact write failed: ${relative}`,
102
+ { cause: error.message },
103
+ );
104
+ throw wrapped;
105
+ }
106
+ }
107
+
108
+ function writeManagedJson(rootDir, relativePath, value) {
109
+ return writeManagedFile(rootDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
110
+ }
111
+
112
+ module.exports = {
113
+ ManagedFileWriteError,
114
+ writeManagedFile,
115
+ writeManagedJson,
116
+ };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const artifacts = require('./artifact-registry.cjs');
6
+ const changes = require('./change-workflow.cjs');
7
+ const ops = require('./state-ops.cjs');
8
+ const planStore = require('./plan-store.cjs');
9
+ const taskLedger = require('./task-ledger.cjs');
10
+
11
+ class PlanCheckpointError extends Error {
12
+ constructor(code, message, details) {
13
+ super(message);
14
+ this.name = 'PlanCheckpointError';
15
+ this.code = code;
16
+ if (details !== undefined) this.details = details;
17
+ }
18
+ }
19
+
20
+ function publishPlan(db, {
21
+ change_id: changeId,
22
+ context,
23
+ } = {}, {
24
+ rootDir = process.cwd(),
25
+ markRecoveryRequired = null,
26
+ } = {}) {
27
+ const change = changes.readChange(db, changeId);
28
+ if (!change) {
29
+ throw new PlanCheckpointError('CHANGE_NOT_FOUND', `change ${changeId} not found`);
30
+ }
31
+ if (change.status !== 'active') {
32
+ throw new PlanCheckpointError(
33
+ 'CHANGE_NOT_MUTABLE',
34
+ `change ${change.id} is ${change.status}`,
35
+ );
36
+ }
37
+ const tasks = ops.listTasks(db, { change_id: change.id });
38
+ if (tasks.length === 0) {
39
+ throw new PlanCheckpointError('NO_TASKS', 'a Plan checkpoint requires at least one task');
40
+ }
41
+ if (!context?.id || !context?.artifact_path || !context?.digest) {
42
+ throw new PlanCheckpointError(
43
+ 'PLAN_CONTEXT_REQUIRED',
44
+ 'a Plan checkpoint requires one canonical Context Envelope',
45
+ );
46
+ }
47
+
48
+ const plan = planStore.buildPlan(tasks, { changeId: change.id });
49
+ const publication = planStore.prepareChangePlanPublication(plan, {
50
+ rootDir,
51
+ change,
52
+ tasks,
53
+ context: {
54
+ snapshot_id: context.id,
55
+ manifest_path: context.artifact_path,
56
+ manifest_digest: context.digest,
57
+ },
58
+ });
59
+ const contextArtifact = db.prepare(
60
+ `SELECT id FROM artifacts
61
+ WHERE path = ? AND status <> 'archived'
62
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
63
+ ).get(context.artifact_path);
64
+ const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
65
+ digest,
66
+ input: {
67
+ owner_type: 'change',
68
+ owner_id: change.id,
69
+ kind,
70
+ path: path.relative(rootDir, file).split(path.sep).join('/'),
71
+ source_refs: [
72
+ { type: 'change', id: change.id, relation: 'planned_for' },
73
+ ...(contextArtifact
74
+ ? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
75
+ : []),
76
+ ...tasks.map((task) => ({
77
+ type: 'task',
78
+ id: task.id,
79
+ relation: 'compiled_from_task_contract',
80
+ })),
81
+ ],
82
+ consumer_refs: [{
83
+ type: 'external',
84
+ id: 'ultra-dev',
85
+ relation: 'consumed_by',
86
+ }],
87
+ provenance: {
88
+ writer: 'ultra.checkpoint',
89
+ stage: 'plan',
90
+ publication_transaction_id: publication.transaction_id,
91
+ },
92
+ metadata: {
93
+ context_envelope_id: context.id,
94
+ context_digest: context.digest,
95
+ terminal_role: true,
96
+ },
97
+ },
98
+ }));
99
+
100
+ try {
101
+ ops.tx(db, () => {
102
+ const preflight = artifactInputs.map(({ input }) => (
103
+ artifacts.preflightArtifactPublication(db, input, { rootDir })
104
+ ));
105
+ publication.publish();
106
+ artifactInputs.forEach(({ input, digest }, index) => {
107
+ artifacts.recordArtifactInTx(db, {
108
+ ...input,
109
+ id: preflight[index].artifact_id || undefined,
110
+ expected_before_digest: preflight[index].expected_before_digest,
111
+ content_digest: digest,
112
+ }, { rootDir });
113
+ });
114
+ ops.appendEventInTx(db, {
115
+ type: 'plan_checkpoint_published',
116
+ change_id: change.id,
117
+ payload: {
118
+ context_envelope_id: context.id,
119
+ context_digest: context.digest,
120
+ plan_path: path.relative(rootDir, publication.plan_path).split(path.sep).join('/'),
121
+ plan_md_path: path.relative(rootDir, publication.plan_md_path).split(path.sep).join('/'),
122
+ publication_transaction_id: publication.transaction_id,
123
+ wave_count: plan.waves.length,
124
+ },
125
+ });
126
+ });
127
+ } catch (error) {
128
+ const rollback = publication.rollback();
129
+ const failure = rollback?.rolled_back === false
130
+ ? planStore.planRecoveryRequiredError(error, rollback, publication.transaction_id)
131
+ : error;
132
+ if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
133
+ markRecoveryRequired?.({
134
+ recovered: 0,
135
+ finalized: 0,
136
+ pending: 1,
137
+ issues: [failure.details?.rollback_issue || {
138
+ code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
139
+ message: failure.cause?.message || failure.message,
140
+ }],
141
+ });
142
+ }
143
+ throw failure;
144
+ }
145
+
146
+ try {
147
+ publication.commit();
148
+ } catch (error) {
149
+ markRecoveryRequired?.({
150
+ recovered: 0,
151
+ finalized: 0,
152
+ pending: 1,
153
+ issues: [error.details?.issue || {
154
+ code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
155
+ message: error.cause?.message || error.message,
156
+ }],
157
+ });
158
+ throw error;
159
+ }
160
+
161
+ return {
162
+ plan_path: path.relative(rootDir, publication.plan_path).split(path.sep).join('/'),
163
+ plan_md_path: path.relative(rootDir, publication.plan_md_path).split(path.sep).join('/'),
164
+ wave_count: plan.waves.length,
165
+ task_count: tasks.length,
166
+ task_contract_digests: Object.fromEntries(
167
+ tasks.map((task) => [task.id, taskLedger.durableTask(task).digest]),
168
+ ),
169
+ context_envelope_id: context.id,
170
+ context_digest: context.digest,
171
+ };
172
+ }
173
+
174
+ module.exports = {
175
+ PlanCheckpointError,
176
+ publishPlan,
177
+ };
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const Database = require('better-sqlite3');
7
+
8
+ const contextEnvelope = require('./context-envelope.cjs');
9
+ const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
10
+ const runtimePaths = require('./runtime-paths.cjs');
11
+
12
+ function diagnosticEnvelope(rootDir, runtime, code, details = {}) {
13
+ return {
14
+ schema_version: '1.0',
15
+ digest: null,
16
+ detail: 'summary',
17
+ envelope: {
18
+ project: {
19
+ root: '.',
20
+ runtime,
21
+ health: 'needs_attention',
22
+ },
23
+ git: null,
24
+ baseline: null,
25
+ change: null,
26
+ task: null,
27
+ decisions: [],
28
+ checkpoints: [],
29
+ evidence: [],
30
+ execution: { stage: 'project', session: null },
31
+ team_checkpoint: null,
32
+ diagnostics: {
33
+ warnings: [],
34
+ needs_attention: [{
35
+ code,
36
+ severity: 'needs_attention',
37
+ ...details,
38
+ }],
39
+ hard_conflicts: [],
40
+ },
41
+ },
42
+ bytes: 0,
43
+ root: path.resolve(rootDir),
44
+ };
45
+ }
46
+
47
+ function tableNames(db) {
48
+ return new Set(db.prepare(
49
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
50
+ ).all().map((row) => row.name));
51
+ }
52
+
53
+ function latestSchemaVersion(db) {
54
+ return db.prepare(
55
+ 'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
56
+ ).get()?.version || null;
57
+ }
58
+
59
+ function readProjectContextEnvelope(rootDir, {
60
+ runtime = 'hook',
61
+ stage = 'project',
62
+ scope = {},
63
+ } = {}) {
64
+ const root = path.resolve(rootDir || process.cwd());
65
+ if (!fs.existsSync(path.join(root, '.ultra'))) return null;
66
+ const dbPath = runtimePaths.locateStateDb(root);
67
+ if (!fs.existsSync(dbPath)) {
68
+ return diagnosticEnvelope(root, runtime, 'STATE_DB_MISSING');
69
+ }
70
+
71
+ let db;
72
+ try {
73
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
74
+ db.pragma('query_only = ON');
75
+ db.pragma('busy_timeout = 1000');
76
+ const tables = tableNames(db);
77
+ if (!tables.has('schema_version')) {
78
+ return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_MIGRATION_REQUIRED', {
79
+ version: null,
80
+ });
81
+ }
82
+ const version = latestSchemaVersion(db);
83
+ if (version !== EXPECTED_VERSION) {
84
+ return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_MIGRATION_REQUIRED', {
85
+ version,
86
+ expected: EXPECTED_VERSION,
87
+ });
88
+ }
89
+ const missing = REQUIRED_TABLES.filter((name) => !tables.has(name));
90
+ if (missing.length > 0) {
91
+ return diagnosticEnvelope(root, runtime, 'STATE_SCHEMA_INCOMPLETE', {
92
+ missing_tables: missing,
93
+ });
94
+ }
95
+ return {
96
+ ...contextEnvelope.buildEnvelope(
97
+ db,
98
+ { detail: 'summary', stage, scope },
99
+ { rootDir: root, runtime },
100
+ ),
101
+ root,
102
+ };
103
+ } catch (error) {
104
+ return diagnosticEnvelope(root, runtime, 'STATE_DB_UNREADABLE', {
105
+ cause: error.code || error.message,
106
+ });
107
+ } finally {
108
+ if (db) db.close();
109
+ }
110
+ }
111
+
112
+ function compact(value, maximum = 240) {
113
+ const normalized = String(value || '').replace(/\s+/g, ' ').trim();
114
+ if (normalized.length <= maximum) return normalized;
115
+ return `${normalized.slice(0, maximum - 3).trimEnd()}...`;
116
+ }
117
+
118
+ function renderProjectContextEnvelope(rootDir, context) {
119
+ if (!context?.envelope) return '';
120
+ const root = path.resolve(rootDir || context.root || process.cwd());
121
+ const envelope = context.envelope;
122
+ const baseline = envelope.baseline || {};
123
+ const change = envelope.change || {};
124
+ const task = envelope.task || {};
125
+ const lines = [
126
+ '[Ultra Context Envelope]',
127
+ `Project: ${root}`,
128
+ `Stage: ${envelope.execution?.stage || 'project'}`,
129
+ `Baseline: ${baseline.id || 'none'} (${
130
+ baseline.mode ? `${baseline.mode}/` : ''
131
+ }${baseline.status || 'none'})`,
132
+ `Change: ${change.id || 'none'} (${change.status || 'none'})`,
133
+ `Task: ${task.id || 'none'} (${task.status || 'none'})`,
134
+ ];
135
+ for (const decision of (envelope.decisions || []).slice(0, 5)) {
136
+ lines.push(
137
+ `Accepted decision: ${decision.id || 'unknown'} — ${
138
+ compact(decision.decision || decision.summary || decision.question || 'recorded')
139
+ }`,
140
+ );
141
+ }
142
+ for (const checkpoint of (envelope.checkpoints || []).slice(0, 5)) {
143
+ lines.push(
144
+ `Checkpoint: ${checkpoint.stage || 'unknown'} ${
145
+ checkpoint.status || 'recorded'
146
+ } (${checkpoint.id || 'unknown'})`,
147
+ );
148
+ }
149
+ const warnings = envelope.diagnostics?.warnings || [];
150
+ const attention = envelope.diagnostics?.needs_attention || [];
151
+ const conflicts = envelope.diagnostics?.hard_conflicts || [];
152
+ if (warnings.length > 0) {
153
+ lines.push(`Warnings: ${warnings.map((item) => item.code).join(', ')}`);
154
+ }
155
+ if (attention.length > 0) {
156
+ lines.push(`Needs attention: ${attention.map((item) => item.code).join(', ')}`);
157
+ }
158
+ if (conflicts.length > 0) {
159
+ lines.push(`Hard conflicts: ${conflicts.map((item) => item.code).join(', ')}`);
160
+ }
161
+ if (envelope.team_checkpoint) {
162
+ lines.push(
163
+ `Team checkpoint: ${envelope.team_checkpoint.status || 'unknown'} ${
164
+ envelope.team_checkpoint.state_digest || ''
165
+ }`.trim(),
166
+ );
167
+ }
168
+ lines.push(
169
+ 'Authority: digest-bound .ultra documents plus checkout-local .ultra/.runtime/state.db.',
170
+ 'Hooks are observational. Use ultra.context for the complete live envelope and public Ultra tools for changes.',
171
+ );
172
+ return lines.join('\n');
173
+ }
174
+
175
+ module.exports = {
176
+ readProjectContextEnvelope,
177
+ renderProjectContextEnvelope,
178
+ };