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.
- package/.ultra-template/docs/research/README.md +4 -4
- package/.ultra-template/tasks/tasks.json +4 -2
- package/CHANGELOG.md +72 -0
- package/README.md +55 -39
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +143 -229
- package/docs/ARCHITECTURE.md +205 -494
- package/docs/ARTIFACT-AUTHORITY.md +35 -28
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +189 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +29 -34
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +108 -82
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-workflow.cjs +630 -45
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/state-ops.cjs +21 -3
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +757 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +10 -8
- package/skills/ultra-deliver/SKILL.md +17 -19
- package/skills/ultra-dev/SKILL.md +20 -11
- package/skills/ultra-doctor/SKILL.md +4 -4
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +20 -14
- package/skills/ultra-plan/SKILL.md +13 -9
- package/skills/ultra-research/SKILL.md +18 -9
- package/skills/ultra-review/SKILL.md +14 -9
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +10 -6
- package/skills/ultra-think/SKILL.md +8 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +43 -2806
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- 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
|
|
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
|
|
548
|
-
let
|
|
549
|
-
db,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
)
|
|
554
|
-
if (!
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
&&
|
|
581
|
-
&&
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|