ultra-builder-pro-cli 0.20.0 → 0.22.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.
- package/.ultra-template/docs/research/README.md +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +10 -3
- package/.ultra-template/templates/task-context.md +19 -0
- package/CHANGELOG.md +106 -0
- package/README.md +215 -41
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +36 -3
- package/adapters/_shared/runtime-assets.cjs +41 -1
- package/adapters/claude.js +7 -5
- package/adapters/codex.js +1 -1
- package/adapters/kimi.js +2 -8
- package/adapters/opencode.js +43 -30
- package/bin/orchestrator.js +64 -24
- package/docs/AGENT-CONTEXT.md +89 -49
- package/docs/ARCHITECTURE.md +112 -45
- package/docs/ARTIFACT-AUTHORITY.md +274 -0
- package/docs/COMMIT-HASH-BACKFILL.md +33 -90
- package/docs/DECISIONS.md +14 -6
- package/docs/LEGACY-CLI-CROSSWALK.md +16 -6
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -4
- package/docs/ROADMAP.md +20 -6
- package/docs/RUNTIME-COMPAT-MATRIX.md +29 -16
- package/docs/STATE-DB-ACCESS-POLICY.md +43 -13
- package/docs/WORKFLOW-LIFECYCLE.md +105 -50
- package/hooks/README.md +10 -8
- package/hooks/active_task_context.py +105 -18
- package/hooks/context_spine.py +16 -4
- package/hooks/health_check.py +5 -4
- package/hooks/pre_stop_check.py +5 -2
- package/hooks/runtime_paths.py +396 -0
- package/hooks/subagent_tracker.py +5 -2
- package/hooks/workflow_checkpoint.py +16 -3
- package/hooks/workflow_context.py +11 -3
- package/hooks/workflow_resume.py +7 -4
- package/mcp-server/breadcrumb.cjs +20 -2
- package/mcp-server/lib/archive-journal.cjs +1026 -111
- package/mcp-server/lib/archive-mutation-worker.py +365 -0
- package/mcp-server/lib/artifact-registry.cjs +1127 -0
- package/mcp-server/lib/baseline-workflow.cjs +69 -28
- package/mcp-server/lib/change-packet.cjs +818 -0
- package/mcp-server/lib/change-workflow.cjs +932 -176
- package/mcp-server/lib/context-paths.cjs +142 -0
- package/mcp-server/lib/context-spine.cjs +511 -45
- package/mcp-server/lib/decision-dialogue.cjs +345 -3
- package/mcp-server/lib/delivery-transaction.cjs +471 -0
- package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
- package/mcp-server/lib/doctor.cjs +401 -11
- package/mcp-server/lib/git-bootstrap.cjs +458 -47
- package/mcp-server/lib/init-project.cjs +240 -95
- package/mcp-server/lib/plan-publication-worker.cjs +830 -0
- package/mcp-server/lib/plan-store.cjs +700 -5
- package/mcp-server/lib/project-breadcrumb.cjs +23 -4
- package/mcp-server/lib/projector.cjs +211 -42
- package/mcp-server/lib/runtime-paths.cjs +1687 -0
- package/mcp-server/lib/safe-project-file.cjs +385 -0
- package/mcp-server/lib/spec-learning.cjs +28 -12
- package/mcp-server/lib/state-authority.cjs +19 -3
- package/mcp-server/lib/state-db.cjs +263 -9
- package/mcp-server/lib/state-ops.cjs +254 -50
- package/mcp-server/lib/task-ledger.cjs +1342 -0
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +610 -58
- package/mcp-server/server.cjs +423 -42
- package/orchestrator/daemon.cjs +2 -10
- package/orchestrator/parallel-orchestrator.cjs +314 -50
- package/orchestrator/planner/plan-builder.cjs +1 -1
- package/orchestrator/recovery.cjs +125 -7
- package/orchestrator/session-close-journal-worker.cjs +5 -0
- package/orchestrator/session-close-journal.cjs +1014 -0
- package/orchestrator/session-runner.cjs +679 -69
- package/orchestrator/worktree-manager.cjs +186 -35
- package/package.json +2 -3
- package/skills/cc-collab/SKILL.md +4 -1
- package/skills/codex-collab/SKILL.md +5 -2
- package/skills/ultra-change/SKILL.md +34 -8
- package/skills/ultra-deliver/SKILL.md +26 -11
- package/skills/ultra-dev/SKILL.md +52 -10
- package/skills/ultra-doctor/SKILL.md +16 -3
- package/skills/ultra-init/SKILL.md +22 -11
- package/skills/ultra-plan/SKILL.md +58 -18
- package/skills/ultra-research/SKILL.md +37 -35
- package/skills/ultra-review/SKILL.md +14 -8
- package/skills/ultra-status/SKILL.md +18 -9
- package/skills/ultra-test/SKILL.md +11 -5
- package/skills/ultra-think/SKILL.md +6 -2
- package/skills/ultra-think/references/decision-dialogue.md +17 -6
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +21 -7
- package/spec/mcp-tools.yaml +629 -44
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- package/spec/schemas/context-file.v4.5.schema.json +1 -1
- package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +51 -14
- package/spec/schemas/task-ledger.v1.schema.json +217 -0
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/ultra-tools/cli.cjs +3 -2
- package/ultra-tools/commands/db.cjs +76 -25
- package/ultra-tools/commands/migrate.cjs +1000 -82
- package/ultra-tools/commands/session.cjs +34 -8
- package/ultra-tools/commands/status.cjs +32 -6
- package/ultra-tools/commands/system.cjs +129 -22
- package/.ultra-template/tasks/contexts/TEMPLATE.md +0 -19
- package/commands/learn.md +0 -10
- package/orchestrator/skill-miner.cjs +0 -133
- package/output-styles/coding-vibes.md +0 -67
- package/output-styles/structural-thinking.md +0 -61
- package/skills/learn/SKILL.md +0 -58
package/mcp-server/server.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// MCP server (stdio JSON-RPC) exposing task.* + session.* tools backed by
|
|
4
|
-
// .ultra/state.db. Uses the low-level Server API from
|
|
4
|
+
// .ultra/.runtime/state.db. Uses the low-level Server API from
|
|
5
5
|
// @modelcontextprotocol/server so we can pass raw JSON Schema (Draft 2020-12)
|
|
6
6
|
// straight from spec/mcp-tools.yaml without translating to zod.
|
|
7
7
|
|
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const { assertStateAuthority } = require('./lib/state-authority.cjs');
|
|
23
23
|
const ops = require('./lib/state-ops.cjs');
|
|
24
24
|
const projector = require('./lib/projector.cjs');
|
|
25
|
+
const taskLedger = require('./lib/task-ledger.cjs');
|
|
25
26
|
const telemetry = require('./lib/telemetry.cjs');
|
|
26
27
|
const topo = require('./lib/topo.cjs');
|
|
27
28
|
const parser = require('./lib/prd-parser.cjs');
|
|
@@ -30,10 +31,14 @@ const planStore = require('./lib/plan-store.cjs');
|
|
|
30
31
|
const { initProject } = require('./lib/init-project.cjs');
|
|
31
32
|
const baselines = require('./lib/baseline-workflow.cjs');
|
|
32
33
|
const changes = require('./lib/change-workflow.cjs');
|
|
34
|
+
const contextSpine = require('./lib/context-spine.cjs');
|
|
33
35
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
36
|
+
const runtimePaths = require('./lib/runtime-paths.cjs');
|
|
37
|
+
const gitBootstrap = require('./lib/git-bootstrap.cjs');
|
|
34
38
|
const workflows = require('./lib/workflow-state.cjs');
|
|
35
39
|
const decisions = require('./lib/decision-dialogue.cjs');
|
|
36
40
|
const doctor = require('./lib/doctor.cjs');
|
|
41
|
+
const artifactRegistry = require('./lib/artifact-registry.cjs');
|
|
37
42
|
const {
|
|
38
43
|
readProjectBreadcrumb,
|
|
39
44
|
renderProjectBreadcrumb,
|
|
@@ -58,6 +63,9 @@ const TASK_TOOLS = Object.freeze([
|
|
|
58
63
|
'task.dependency_topo',
|
|
59
64
|
'task.parse_prd',
|
|
60
65
|
'task.expand',
|
|
66
|
+
'task.ledger_get',
|
|
67
|
+
'task.ledger_publish',
|
|
68
|
+
'task.ledger_import',
|
|
61
69
|
]);
|
|
62
70
|
|
|
63
71
|
const SESSION_TOOLS = Object.freeze([
|
|
@@ -85,6 +93,8 @@ const BASELINE_TOOLS = Object.freeze([
|
|
|
85
93
|
const CHANGE_TOOLS = Object.freeze([
|
|
86
94
|
'change.create',
|
|
87
95
|
'change.update',
|
|
96
|
+
'change.delta',
|
|
97
|
+
'change.documentation_reconcile',
|
|
88
98
|
'change.get',
|
|
89
99
|
'change.list',
|
|
90
100
|
'change.context',
|
|
@@ -100,6 +110,8 @@ const WORKFLOW_TOOLS = Object.freeze([
|
|
|
100
110
|
'workflow.get',
|
|
101
111
|
'workflow.list',
|
|
102
112
|
'workflow.step',
|
|
113
|
+
'workflow.revise',
|
|
114
|
+
'workflow.supersede',
|
|
103
115
|
'workflow.complete',
|
|
104
116
|
]);
|
|
105
117
|
|
|
@@ -120,10 +132,16 @@ const SYSTEM_TOOLS = Object.freeze([
|
|
|
120
132
|
'system.doctor',
|
|
121
133
|
]);
|
|
122
134
|
|
|
135
|
+
const ARTIFACT_TOOLS = Object.freeze([
|
|
136
|
+
'artifact.record',
|
|
137
|
+
'artifact.get',
|
|
138
|
+
]);
|
|
139
|
+
|
|
123
140
|
const REGISTERED_TOOLS = Object.freeze([
|
|
124
141
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
125
142
|
...DECISION_TOOLS,
|
|
126
143
|
...WORKFLOW_TOOLS,
|
|
144
|
+
...ARTIFACT_TOOLS,
|
|
127
145
|
...SYSTEM_TOOLS,
|
|
128
146
|
]);
|
|
129
147
|
|
|
@@ -134,14 +152,26 @@ const STATELESS_TOOLS = new Set(['task.init_project']);
|
|
|
134
152
|
const MUTATING_TOOLS = new Set([
|
|
135
153
|
'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
|
|
136
154
|
'task.parse_prd', 'task.expand',
|
|
155
|
+
'task.ledger_import',
|
|
137
156
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
138
157
|
'plan.export',
|
|
139
158
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
140
|
-
'change.create', 'change.update', 'change.
|
|
159
|
+
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
160
|
+
'change.context', 'change.converge', 'change.archive',
|
|
141
161
|
'change.learning_propose', 'change.learning_resolve',
|
|
142
162
|
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
143
163
|
'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
|
|
144
|
-
'workflow.start', 'workflow.step', 'workflow.
|
|
164
|
+
'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
|
|
165
|
+
'workflow.complete',
|
|
166
|
+
'artifact.record',
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const TEAM_LEDGER_SYNC_TOOLS = new Set([
|
|
170
|
+
'task.create', 'task.update', 'task.delete', 'task.parse_prd', 'task.expand',
|
|
171
|
+
'plan.export',
|
|
172
|
+
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
173
|
+
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
174
|
+
'change.converge', 'change.archive',
|
|
145
175
|
]);
|
|
146
176
|
|
|
147
177
|
function loadRegisteredTools() {
|
|
@@ -167,7 +197,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
167
197
|
return { id: task.id, status: task.status, created_at: task.created_at };
|
|
168
198
|
}
|
|
169
199
|
case 'task.update': {
|
|
200
|
+
const before = ops.readTask(db, input.id);
|
|
170
201
|
const task = ops.patchTask(db, input.id, input.patch || {});
|
|
202
|
+
const durableChanged = before
|
|
203
|
+
&& taskLedger.durableTask(before).digest !== taskLedger.durableTask(task).digest;
|
|
204
|
+
if (durableChanged) {
|
|
205
|
+
taskLedger.publishTaskLedger(db, {
|
|
206
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
207
|
+
reason: before.status !== task.status
|
|
208
|
+
? `task_${task.status}`
|
|
209
|
+
: 'task_contract_updated',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
171
212
|
return { ok: true, task };
|
|
172
213
|
}
|
|
173
214
|
case 'task.list': {
|
|
@@ -184,7 +225,12 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
184
225
|
return { task };
|
|
185
226
|
}
|
|
186
227
|
case 'task.delete': {
|
|
187
|
-
|
|
228
|
+
const result = ops.deleteTask(db, input.id, { force: !!input.force });
|
|
229
|
+
taskLedger.publishTaskLedger(db, {
|
|
230
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
231
|
+
reason: 'task_deleted',
|
|
232
|
+
});
|
|
233
|
+
return result;
|
|
188
234
|
}
|
|
189
235
|
case 'task.init_project': {
|
|
190
236
|
return initProject(input);
|
|
@@ -260,12 +306,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
260
306
|
return { session };
|
|
261
307
|
}
|
|
262
308
|
case 'session.admission_check': {
|
|
263
|
-
sessionRunner.
|
|
309
|
+
return sessionRunner.admissionCheck(
|
|
264
310
|
db,
|
|
265
311
|
ctx.rootDir || process.cwd(),
|
|
266
312
|
input.task_id,
|
|
267
313
|
);
|
|
268
|
-
return ops.admissionCheck(db, input.task_id);
|
|
269
314
|
}
|
|
270
315
|
case 'session.list': {
|
|
271
316
|
const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
|
|
@@ -395,8 +440,58 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
395
440
|
children: input.children,
|
|
396
441
|
rootDir,
|
|
397
442
|
});
|
|
443
|
+
taskLedger.publishTaskLedger(db, {
|
|
444
|
+
rootDir,
|
|
445
|
+
reason: 'task_expanded',
|
|
446
|
+
});
|
|
398
447
|
return { parent_id: result.parent_id, children: result.children };
|
|
399
448
|
}
|
|
449
|
+
case 'task.ledger_get': {
|
|
450
|
+
const rootDir = ctx.rootDir || process.cwd();
|
|
451
|
+
return {
|
|
452
|
+
ledger: taskLedger.readTaskLedger(rootDir),
|
|
453
|
+
health: taskLedger.inspectTaskLedger(db, { rootDir }),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
case 'task.ledger_publish': {
|
|
457
|
+
const result = taskLedger.publishTaskLedger(db, {
|
|
458
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
459
|
+
reason: input.reason || 'manual_checkpoint',
|
|
460
|
+
});
|
|
461
|
+
const output = {
|
|
462
|
+
changed: result.changed,
|
|
463
|
+
path: path.relative(ctx.rootDir || process.cwd(), result.path),
|
|
464
|
+
generation: result.ledger.generation,
|
|
465
|
+
state_digest: result.ledger.state_digest,
|
|
466
|
+
};
|
|
467
|
+
if (result.migrated_legacy_projection) {
|
|
468
|
+
output.migrated_legacy_projection = true;
|
|
469
|
+
output.legacy_backup_path = path.relative(
|
|
470
|
+
ctx.rootDir || process.cwd(),
|
|
471
|
+
result.legacy_backup_path,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return output;
|
|
475
|
+
}
|
|
476
|
+
case 'task.ledger_import': {
|
|
477
|
+
const result = taskLedger.importTaskLedger(db, {
|
|
478
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
imported: result.imported,
|
|
482
|
+
preserved: result.preserved,
|
|
483
|
+
deleted: result.deleted,
|
|
484
|
+
imported_changes: result.imported_changes,
|
|
485
|
+
preserved_changes: result.preserved_changes,
|
|
486
|
+
imported_baseline: result.imported_baseline,
|
|
487
|
+
preserved_baseline: result.preserved_baseline,
|
|
488
|
+
generation: result.generation,
|
|
489
|
+
state_digest: result.state_digest,
|
|
490
|
+
requires_plan_revalidation: result.requires_plan_revalidation,
|
|
491
|
+
requires_baseline_revalidation: result.requires_baseline_revalidation,
|
|
492
|
+
already_current: result.already_current,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
400
495
|
case 'plan.export': {
|
|
401
496
|
const rootDir = ctx.rootDir || process.cwd();
|
|
402
497
|
const change = changes.readChange(db, input.change_id);
|
|
@@ -410,41 +505,179 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
410
505
|
err.code = 'CHANGE_NOT_MUTABLE';
|
|
411
506
|
throw err;
|
|
412
507
|
}
|
|
413
|
-
const
|
|
414
|
-
? input.out_path
|
|
415
|
-
: path.join(rootDir, input.out_path);
|
|
416
|
-
const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
|
|
508
|
+
const tasks = ops.listTasks(db, { change_id: change.id });
|
|
417
509
|
if (tasks.length === 0) {
|
|
418
510
|
const err = new Error('no tasks to plan');
|
|
419
511
|
err.code = 'NO_TASKS';
|
|
420
512
|
throw err;
|
|
421
513
|
}
|
|
422
|
-
const
|
|
423
|
-
const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
|
|
424
|
-
ops.appendEvent(db, {
|
|
425
|
-
type: 'plan_exported',
|
|
514
|
+
const context = contextSpine.validateContextSnapshot(db, {
|
|
426
515
|
change_id: change.id,
|
|
427
|
-
|
|
428
|
-
|
|
516
|
+
task_id: null,
|
|
517
|
+
role: 'plan',
|
|
518
|
+
gate: 'planning',
|
|
519
|
+
}, { rootDir });
|
|
520
|
+
if (!context.snapshot || context.blockers.length > 0) {
|
|
521
|
+
const err = new Error('plan export requires a current planning Context Manifest');
|
|
522
|
+
err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
|
|
523
|
+
err.details = { blockers: context.blockers };
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
const plan = planStore.buildPlan(tasks, { changeId: change.id });
|
|
527
|
+
const planRun = db.prepare(
|
|
528
|
+
`SELECT id FROM workflow_runs
|
|
529
|
+
WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
|
|
530
|
+
AND status IN ('active', 'blocked', 'ready')
|
|
531
|
+
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
532
|
+
).get(change.id);
|
|
533
|
+
const owner = { type: 'change', id: change.id };
|
|
534
|
+
const contextArtifact = db.prepare(
|
|
535
|
+
`SELECT id FROM artifacts
|
|
536
|
+
WHERE path = ? AND status <> 'archived'
|
|
537
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
538
|
+
).get(context.snapshot.manifest_path);
|
|
539
|
+
const publication = planStore.prepareChangePlanPublication(plan, {
|
|
540
|
+
rootDir,
|
|
541
|
+
change,
|
|
542
|
+
tasks,
|
|
543
|
+
context: {
|
|
544
|
+
snapshot_id: context.snapshot.id,
|
|
545
|
+
manifest_path: context.snapshot.manifest_path,
|
|
546
|
+
manifest_digest: context.snapshot.manifest_hash,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
|
|
550
|
+
kind,
|
|
551
|
+
file,
|
|
552
|
+
digest,
|
|
553
|
+
input: {
|
|
554
|
+
owner_type: owner.type,
|
|
555
|
+
owner_id: owner.id,
|
|
556
|
+
kind,
|
|
557
|
+
path: path.relative(rootDir, file),
|
|
558
|
+
source_refs: [
|
|
559
|
+
{ type: 'change', id: change.id, relation: 'planned_for' },
|
|
560
|
+
...(contextArtifact
|
|
561
|
+
? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
|
|
562
|
+
: []),
|
|
563
|
+
...tasks.map((task) => ({
|
|
564
|
+
type: 'task', id: task.id, relation: 'compiled_from_task_contract',
|
|
565
|
+
})),
|
|
566
|
+
],
|
|
567
|
+
consumer_refs: planRun
|
|
568
|
+
? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
|
|
569
|
+
: [],
|
|
570
|
+
provenance: {
|
|
571
|
+
writer: 'plan.export',
|
|
572
|
+
workflow_run_id: planRun?.id || null,
|
|
573
|
+
publication_transaction_id: publication.transaction_id,
|
|
574
|
+
},
|
|
575
|
+
metadata: {
|
|
576
|
+
context_snapshot_id: context.snapshot.id,
|
|
577
|
+
context_digest: context.snapshot.manifest_hash,
|
|
578
|
+
terminal_role: !planRun,
|
|
579
|
+
},
|
|
429
580
|
},
|
|
581
|
+
}));
|
|
582
|
+
try {
|
|
583
|
+
ops.tx(db, () => {
|
|
584
|
+
const preflight = artifactInputs.map((artifact) => (
|
|
585
|
+
artifactRegistry.preflightArtifactPublication(
|
|
586
|
+
db, artifact.input, { rootDir },
|
|
587
|
+
)
|
|
588
|
+
));
|
|
589
|
+
publication.publish();
|
|
590
|
+
const recorded = artifactInputs.map((artifact, index) => (
|
|
591
|
+
artifactRegistry.recordArtifactInTx(db, {
|
|
592
|
+
...artifact.input,
|
|
593
|
+
id: preflight[index].artifact_id || undefined,
|
|
594
|
+
expected_before_digest: preflight[index].expected_before_digest,
|
|
595
|
+
content_digest: artifact.digest,
|
|
596
|
+
}, { rootDir })
|
|
597
|
+
));
|
|
598
|
+
ops.appendEventInTx(db, {
|
|
599
|
+
type: 'plan_exported',
|
|
600
|
+
change_id: change.id,
|
|
601
|
+
payload: {
|
|
602
|
+
change_id: change.id,
|
|
603
|
+
plan_path: publication.plan_path,
|
|
604
|
+
plan_md_path: publication.plan_md_path,
|
|
605
|
+
publication_transaction_id: publication.transaction_id,
|
|
606
|
+
wave_count: plan.waves.length,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
return recorded;
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
const rollback = publication.rollback();
|
|
613
|
+
const failure = rollback?.rolled_back === false
|
|
614
|
+
? planStore.planRecoveryRequiredError(
|
|
615
|
+
error,
|
|
616
|
+
rollback,
|
|
617
|
+
publication.transaction_id,
|
|
618
|
+
)
|
|
619
|
+
: error;
|
|
620
|
+
if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
|
|
621
|
+
const issue = failure.details?.rollback_issue
|
|
622
|
+
|| failure.details?.issue
|
|
623
|
+
|| {
|
|
624
|
+
code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
625
|
+
message: failure.cause?.message || failure.message,
|
|
626
|
+
};
|
|
627
|
+
ctx.markPlanRecoveryRequired?.({
|
|
628
|
+
recovered: 0,
|
|
629
|
+
finalized: 0,
|
|
630
|
+
pending: 1,
|
|
631
|
+
issues: [issue],
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
throw failure;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
publication.commit();
|
|
638
|
+
} catch (error) {
|
|
639
|
+
const issue = error.details?.issue || {
|
|
640
|
+
code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
641
|
+
message: error.cause?.message || error.message,
|
|
642
|
+
};
|
|
643
|
+
ctx.markPlanRecoveryRequired?.({
|
|
644
|
+
recovered: 0,
|
|
645
|
+
finalized: 0,
|
|
646
|
+
pending: 1,
|
|
647
|
+
issues: [issue],
|
|
648
|
+
});
|
|
649
|
+
// The registry and target bytes committed atomically before cleanup.
|
|
650
|
+
// The remaining journal is still authoritative recovery work, so the
|
|
651
|
+
// caller must see a typed failure and later mutations must stop.
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
taskLedger.publishTaskLedger(db, {
|
|
655
|
+
rootDir,
|
|
656
|
+
reason: 'plan_accepted',
|
|
430
657
|
});
|
|
431
|
-
return {
|
|
658
|
+
return {
|
|
659
|
+
plan_path: publication.plan_path,
|
|
660
|
+
plan_md_path: publication.plan_md_path,
|
|
661
|
+
wave_count: plan.waves.length,
|
|
662
|
+
};
|
|
432
663
|
}
|
|
433
664
|
case 'plan.get': {
|
|
434
665
|
const rootDir = ctx.rootDir || process.cwd();
|
|
435
|
-
const
|
|
666
|
+
const change = changes.readChange(db, input.change_id);
|
|
667
|
+
if (!change) {
|
|
668
|
+
const err = new Error(`change ${input.change_id} not found`);
|
|
669
|
+
err.code = 'CHANGE_NOT_FOUND';
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
|
|
673
|
+
db,
|
|
674
|
+
strict: true,
|
|
675
|
+
});
|
|
436
676
|
if (!loaded) {
|
|
437
677
|
const err = new Error('no plan has been computed yet');
|
|
438
678
|
err.code = 'NO_PLAN';
|
|
439
679
|
throw err;
|
|
440
680
|
}
|
|
441
|
-
if (loaded.change_id !== input.change_id) {
|
|
442
|
-
const err = new Error(
|
|
443
|
-
`execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
|
|
444
|
-
);
|
|
445
|
-
err.code = 'PLAN_CHANGE_MISMATCH';
|
|
446
|
-
throw err;
|
|
447
|
-
}
|
|
448
681
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
449
682
|
}
|
|
450
683
|
case 'baseline.start': {
|
|
@@ -476,21 +709,42 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
476
709
|
};
|
|
477
710
|
}
|
|
478
711
|
case 'baseline.converge': {
|
|
479
|
-
|
|
712
|
+
const result = baselines.convergeBaseline(
|
|
480
713
|
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
481
714
|
);
|
|
715
|
+
if (result.ready) {
|
|
716
|
+
taskLedger.publishTaskLedger(db, {
|
|
717
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
718
|
+
reason: 'baseline_converged',
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
482
722
|
}
|
|
483
723
|
case 'change.create': {
|
|
484
724
|
const { randomUUID } = require('node:crypto');
|
|
485
725
|
const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
|
|
486
|
-
|
|
726
|
+
const result = changes.createChange(
|
|
727
|
+
db,
|
|
728
|
+
{ ...input, id },
|
|
729
|
+
{ rootDir: ctx.rootDir || process.cwd() },
|
|
730
|
+
);
|
|
731
|
+
taskLedger.publishTaskLedger(db, {
|
|
732
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
733
|
+
reason: 'change_created',
|
|
734
|
+
});
|
|
735
|
+
return result;
|
|
487
736
|
}
|
|
488
737
|
case 'change.update': {
|
|
738
|
+
const change = changes.updateChange(
|
|
739
|
+
db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
740
|
+
);
|
|
741
|
+
taskLedger.publishTaskLedger(db, {
|
|
742
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
743
|
+
reason: 'change_updated',
|
|
744
|
+
});
|
|
489
745
|
return {
|
|
490
746
|
ok: true,
|
|
491
|
-
change
|
|
492
|
-
db, input.id, input.patch || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
493
|
-
),
|
|
747
|
+
change,
|
|
494
748
|
};
|
|
495
749
|
}
|
|
496
750
|
case 'change.get': {
|
|
@@ -541,10 +795,32 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
541
795
|
};
|
|
542
796
|
}
|
|
543
797
|
case 'change.converge': {
|
|
544
|
-
|
|
798
|
+
const result = changes.convergeChange(
|
|
799
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
800
|
+
);
|
|
801
|
+
taskLedger.publishTaskLedger(db, {
|
|
802
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
803
|
+
reason: 'change_converged',
|
|
804
|
+
});
|
|
805
|
+
return result;
|
|
545
806
|
}
|
|
546
807
|
case 'change.archive': {
|
|
547
|
-
|
|
808
|
+
const result = changes.archiveChange(
|
|
809
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
810
|
+
);
|
|
811
|
+
taskLedger.publishTaskLedger(db, {
|
|
812
|
+
rootDir: ctx.rootDir || process.cwd(),
|
|
813
|
+
reason: 'change_archived',
|
|
814
|
+
});
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
case 'change.delta': {
|
|
818
|
+
return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
819
|
+
}
|
|
820
|
+
case 'change.documentation_reconcile': {
|
|
821
|
+
return changes.recordDocumentationReconciliation(
|
|
822
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
823
|
+
);
|
|
548
824
|
}
|
|
549
825
|
case 'decision.thread_start': {
|
|
550
826
|
return { thread: decisions.startDecisionThread(db, input) };
|
|
@@ -578,7 +854,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
578
854
|
return { thread: decisions.supersedeDecision(db, input) };
|
|
579
855
|
}
|
|
580
856
|
case 'decision.complete': {
|
|
581
|
-
return {
|
|
857
|
+
return {
|
|
858
|
+
thread: decisions.completeDecisionThread(
|
|
859
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
860
|
+
),
|
|
861
|
+
};
|
|
582
862
|
}
|
|
583
863
|
case 'decision.checkpoint': {
|
|
584
864
|
return {
|
|
@@ -618,6 +898,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
618
898
|
),
|
|
619
899
|
};
|
|
620
900
|
}
|
|
901
|
+
case 'workflow.revise': {
|
|
902
|
+
return {
|
|
903
|
+
workflow: workflows.reviseWorkflow(
|
|
904
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
905
|
+
),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
case 'workflow.supersede': {
|
|
909
|
+
return workflows.supersedeWorkflow(
|
|
910
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
911
|
+
);
|
|
912
|
+
}
|
|
621
913
|
case 'workflow.complete': {
|
|
622
914
|
return {
|
|
623
915
|
workflow: workflows.completeWorkflow(
|
|
@@ -625,6 +917,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
625
917
|
),
|
|
626
918
|
};
|
|
627
919
|
}
|
|
920
|
+
case 'artifact.record': {
|
|
921
|
+
return artifactRegistry.recordArtifact(
|
|
922
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
case 'artifact.get': {
|
|
926
|
+
return { artifact: artifactRegistry.getArtifact(db, input) };
|
|
927
|
+
}
|
|
628
928
|
case 'system.doctor': {
|
|
629
929
|
return doctor.runDoctor(db, {
|
|
630
930
|
rootDir: ctx.rootDir || process.cwd(),
|
|
@@ -657,8 +957,66 @@ function startServer({
|
|
|
657
957
|
project = projector.projectAll,
|
|
658
958
|
}) {
|
|
659
959
|
let db = null;
|
|
660
|
-
|
|
661
|
-
|
|
960
|
+
let planRecovery = null;
|
|
961
|
+
let authorityDbPath = path.resolve(dbPath);
|
|
962
|
+
const markPlanRecoveryRequired = (details) => {
|
|
963
|
+
planRecovery = {
|
|
964
|
+
recovered: Number(details?.recovered || 0),
|
|
965
|
+
finalized: Number(details?.finalized || 0),
|
|
966
|
+
pending: Math.max(1, Number(details?.pending || 0)),
|
|
967
|
+
issues: Array.isArray(details?.issues) ? details.issues : [],
|
|
968
|
+
};
|
|
969
|
+
};
|
|
970
|
+
const getDb = (toolName = null) => {
|
|
971
|
+
if (!db) {
|
|
972
|
+
const projectPaths = runtimePaths.pathsFor(rootDir);
|
|
973
|
+
if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
|
|
974
|
+
// The packaged launcher has no explicit DB override. Its default path
|
|
975
|
+
// is the future canonical runtime location, which can legitimately be
|
|
976
|
+
// absent while a legacy project still owns its project-level DB. Admit and
|
|
977
|
+
// migrate that project before applying configured-authority binding.
|
|
978
|
+
authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
|
|
979
|
+
admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
|
|
980
|
+
}).stateDbPath;
|
|
981
|
+
} else {
|
|
982
|
+
// Only a real external/task-linked override reaches binding
|
|
983
|
+
// validation. It may never invent or borrow project authority.
|
|
984
|
+
runtimePaths.locateStateDb(rootDir, {
|
|
985
|
+
env: { UBP_DB_PATH: authorityDbPath },
|
|
986
|
+
});
|
|
987
|
+
runtimePaths.ensureRuntimeState(rootDir, {
|
|
988
|
+
env: { UBP_DB_PATH: authorityDbPath },
|
|
989
|
+
allowConfiguredRuntimeLink: true,
|
|
990
|
+
migrateState: false,
|
|
991
|
+
admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
db = initStateDb(authorityDbPath).db;
|
|
995
|
+
const teamSync = assertStateAuthority(db, rootDir, { importTeamLedger: true });
|
|
996
|
+
if (teamSync?.status === 'imported') {
|
|
997
|
+
runtimeState.enqueueProjection(db, {
|
|
998
|
+
tool_name: 'task.ledger_import',
|
|
999
|
+
});
|
|
1000
|
+
runtimeState.processProjectionJobs(db, {
|
|
1001
|
+
rootDir,
|
|
1002
|
+
project,
|
|
1003
|
+
limit: 500,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
1007
|
+
}
|
|
1008
|
+
if (toolName !== 'system.doctor'
|
|
1009
|
+
&& (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
|
|
1010
|
+
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
1011
|
+
if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
|
|
1012
|
+
const error = new Error(
|
|
1013
|
+
'plan publication recovery is incomplete; run system.doctor with repair=true',
|
|
1014
|
+
);
|
|
1015
|
+
error.code = 'PLAN_RECOVERY_REQUIRED';
|
|
1016
|
+
error.details = planRecovery;
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
662
1020
|
return db;
|
|
663
1021
|
};
|
|
664
1022
|
|
|
@@ -723,14 +1081,25 @@ function startServer({
|
|
|
723
1081
|
let result;
|
|
724
1082
|
let toolDb = null;
|
|
725
1083
|
try {
|
|
726
|
-
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
1084
|
+
toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
|
|
727
1085
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
1086
|
+
if (toolDb && TEAM_LEDGER_SYNC_TOOLS.has(name)) {
|
|
1087
|
+
taskLedger.syncTaskLedger(toolDb, { rootDir });
|
|
1088
|
+
}
|
|
728
1089
|
result = await dispatchTool(
|
|
729
1090
|
name,
|
|
730
1091
|
args,
|
|
731
1092
|
toolDb,
|
|
732
|
-
{
|
|
1093
|
+
{
|
|
1094
|
+
rootDir,
|
|
1095
|
+
sessionId,
|
|
1096
|
+
projector: project,
|
|
1097
|
+
markPlanRecoveryRequired,
|
|
1098
|
+
},
|
|
733
1099
|
);
|
|
1100
|
+
if (name === 'system.doctor' && args.repair === true) {
|
|
1101
|
+
planRecovery = result.repair?.plan_publications || null;
|
|
1102
|
+
}
|
|
734
1103
|
} catch (err) {
|
|
735
1104
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
736
1105
|
toolError = code;
|
|
@@ -739,7 +1108,11 @@ function startServer({
|
|
|
739
1108
|
}
|
|
740
1109
|
|
|
741
1110
|
let runtimeMeta = null;
|
|
742
|
-
|
|
1111
|
+
const shouldProject = toolDb
|
|
1112
|
+
&& projectOnWrite
|
|
1113
|
+
&& MUTATING_TOOLS.has(name)
|
|
1114
|
+
&& !(name === 'task.ledger_import' && result.already_current === true);
|
|
1115
|
+
if (shouldProject) {
|
|
743
1116
|
const job = runtimeState.enqueueProjection(toolDb, { tool_name: name });
|
|
744
1117
|
const processed = runtimeState.processProjectionJobs(toolDb, { rootDir, project, limit: 500 });
|
|
745
1118
|
const own = processed.jobs.find((item) => item.id === job.id);
|
|
@@ -789,12 +1162,18 @@ function startServer({
|
|
|
789
1162
|
}
|
|
790
1163
|
|
|
791
1164
|
async function main() {
|
|
792
|
-
const dbPath = process.env.UBP_DB_PATH
|
|
793
|
-
? path.resolve(process.env.UBP_DB_PATH)
|
|
794
|
-
: path.resolve('.ultra', 'state.db');
|
|
795
1165
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
796
1166
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
797
1167
|
: path.resolve('.');
|
|
1168
|
+
// Keep tools/list side-effect free. Root/layout validation happens inside the
|
|
1169
|
+
// first state-backed tool call so clients receive a typed MCP error instead
|
|
1170
|
+
// of a transport-level startup failure.
|
|
1171
|
+
const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
|
|
1172
|
+
? process.env.UBP_DB_PATH.trim()
|
|
1173
|
+
: '';
|
|
1174
|
+
const dbPath = configuredDb
|
|
1175
|
+
? path.resolve(configuredDb)
|
|
1176
|
+
: runtimePaths.pathsFor(rootDir).stateDbPath;
|
|
798
1177
|
const sessionId = process.env.UBP_SESSION_ID || null;
|
|
799
1178
|
const handle = startServer({ dbPath, rootDir, sessionId });
|
|
800
1179
|
const transport = new StdioServerTransport();
|
|
@@ -816,7 +1195,7 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
816
1195
|
throw error;
|
|
817
1196
|
}
|
|
818
1197
|
const root = path.resolve(rootDir || process.cwd());
|
|
819
|
-
const dbPath =
|
|
1198
|
+
const dbPath = runtimePaths.locateStateDb(root);
|
|
820
1199
|
if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
|
|
821
1200
|
|
|
822
1201
|
const db = openStateDb(dbPath);
|
|
@@ -878,11 +1257,13 @@ module.exports = {
|
|
|
878
1257
|
BASELINE_TOOLS,
|
|
879
1258
|
CHANGE_TOOLS,
|
|
880
1259
|
WORKFLOW_TOOLS,
|
|
1260
|
+
ARTIFACT_TOOLS,
|
|
881
1261
|
SYSTEM_TOOLS,
|
|
882
1262
|
REGISTERED_TOOLS,
|
|
883
1263
|
STATELESS_TOOLS,
|
|
884
1264
|
MUTATING_TOOLS,
|
|
885
1265
|
appendHookLifecycleEvent,
|
|
1266
|
+
findProjectRoot: runtimePaths.findProjectRoot,
|
|
886
1267
|
readProjectBreadcrumb,
|
|
887
1268
|
renderProjectBreadcrumb,
|
|
888
1269
|
};
|