ultra-builder-pro-cli 0.20.0 → 0.21.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 +26 -5
- package/.ultra-template/specs/discovery.md +1 -1
- package/.ultra-template/tasks/tasks.json +1 -1
- package/CHANGELOG.md +66 -0
- package/README.md +54 -24
- 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 +30 -28
- package/bin/orchestrator.js +64 -24
- package/docs/AGENT-CONTEXT.md +74 -43
- package/docs/ARCHITECTURE.md +63 -31
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +10 -4
- package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +19 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +70 -39
- package/hooks/README.md +6 -5
- package/hooks/active_task_context.py +97 -12
- 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 +1126 -0
- package/mcp-server/lib/baseline-workflow.cjs +9 -4
- 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 +111 -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 +373 -11
- package/mcp-server/lib/git-bootstrap.cjs +459 -47
- package/mcp-server/lib/init-project.cjs +190 -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 +174 -40
- 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-db.cjs +263 -9
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +610 -58
- package/mcp-server/server.cjs +278 -33
- 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 +26 -8
- package/skills/ultra-deliver/SKILL.md +23 -11
- package/skills/ultra-dev/SKILL.md +33 -6
- package/skills/ultra-doctor/SKILL.md +6 -2
- package/skills/ultra-init/SKILL.md +4 -2
- package/skills/ultra-plan/SKILL.md +49 -16
- package/skills/ultra-research/SKILL.md +30 -27
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +10 -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 +18 -7
- package/spec/mcp-tools.yaml +502 -44
- package/spec/schemas/change-delta.v1.schema.json +153 -0
- 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 +50 -13
- 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 +995 -80
- 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/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
|
|
|
@@ -30,10 +30,14 @@ const planStore = require('./lib/plan-store.cjs');
|
|
|
30
30
|
const { initProject } = require('./lib/init-project.cjs');
|
|
31
31
|
const baselines = require('./lib/baseline-workflow.cjs');
|
|
32
32
|
const changes = require('./lib/change-workflow.cjs');
|
|
33
|
+
const contextSpine = require('./lib/context-spine.cjs');
|
|
33
34
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
35
|
+
const runtimePaths = require('./lib/runtime-paths.cjs');
|
|
36
|
+
const gitBootstrap = require('./lib/git-bootstrap.cjs');
|
|
34
37
|
const workflows = require('./lib/workflow-state.cjs');
|
|
35
38
|
const decisions = require('./lib/decision-dialogue.cjs');
|
|
36
39
|
const doctor = require('./lib/doctor.cjs');
|
|
40
|
+
const artifactRegistry = require('./lib/artifact-registry.cjs');
|
|
37
41
|
const {
|
|
38
42
|
readProjectBreadcrumb,
|
|
39
43
|
renderProjectBreadcrumb,
|
|
@@ -85,6 +89,8 @@ const BASELINE_TOOLS = Object.freeze([
|
|
|
85
89
|
const CHANGE_TOOLS = Object.freeze([
|
|
86
90
|
'change.create',
|
|
87
91
|
'change.update',
|
|
92
|
+
'change.delta',
|
|
93
|
+
'change.documentation_reconcile',
|
|
88
94
|
'change.get',
|
|
89
95
|
'change.list',
|
|
90
96
|
'change.context',
|
|
@@ -100,6 +106,8 @@ const WORKFLOW_TOOLS = Object.freeze([
|
|
|
100
106
|
'workflow.get',
|
|
101
107
|
'workflow.list',
|
|
102
108
|
'workflow.step',
|
|
109
|
+
'workflow.revise',
|
|
110
|
+
'workflow.supersede',
|
|
103
111
|
'workflow.complete',
|
|
104
112
|
]);
|
|
105
113
|
|
|
@@ -120,10 +128,16 @@ const SYSTEM_TOOLS = Object.freeze([
|
|
|
120
128
|
'system.doctor',
|
|
121
129
|
]);
|
|
122
130
|
|
|
131
|
+
const ARTIFACT_TOOLS = Object.freeze([
|
|
132
|
+
'artifact.record',
|
|
133
|
+
'artifact.get',
|
|
134
|
+
]);
|
|
135
|
+
|
|
123
136
|
const REGISTERED_TOOLS = Object.freeze([
|
|
124
137
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
125
138
|
...DECISION_TOOLS,
|
|
126
139
|
...WORKFLOW_TOOLS,
|
|
140
|
+
...ARTIFACT_TOOLS,
|
|
127
141
|
...SYSTEM_TOOLS,
|
|
128
142
|
]);
|
|
129
143
|
|
|
@@ -137,11 +151,14 @@ const MUTATING_TOOLS = new Set([
|
|
|
137
151
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
138
152
|
'plan.export',
|
|
139
153
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
140
|
-
'change.create', 'change.update', 'change.
|
|
154
|
+
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
155
|
+
'change.context', 'change.converge', 'change.archive',
|
|
141
156
|
'change.learning_propose', 'change.learning_resolve',
|
|
142
157
|
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
143
158
|
'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
|
|
144
|
-
'workflow.start', 'workflow.step', 'workflow.
|
|
159
|
+
'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
|
|
160
|
+
'workflow.complete',
|
|
161
|
+
'artifact.record',
|
|
145
162
|
]);
|
|
146
163
|
|
|
147
164
|
function loadRegisteredTools() {
|
|
@@ -260,12 +277,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
260
277
|
return { session };
|
|
261
278
|
}
|
|
262
279
|
case 'session.admission_check': {
|
|
263
|
-
sessionRunner.
|
|
280
|
+
return sessionRunner.admissionCheck(
|
|
264
281
|
db,
|
|
265
282
|
ctx.rootDir || process.cwd(),
|
|
266
283
|
input.task_id,
|
|
267
284
|
);
|
|
268
|
-
return ops.admissionCheck(db, input.task_id);
|
|
269
285
|
}
|
|
270
286
|
case 'session.list': {
|
|
271
287
|
const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
|
|
@@ -410,41 +426,175 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
410
426
|
err.code = 'CHANGE_NOT_MUTABLE';
|
|
411
427
|
throw err;
|
|
412
428
|
}
|
|
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 });
|
|
429
|
+
const tasks = ops.listTasks(db, { change_id: change.id });
|
|
417
430
|
if (tasks.length === 0) {
|
|
418
431
|
const err = new Error('no tasks to plan');
|
|
419
432
|
err.code = 'NO_TASKS';
|
|
420
433
|
throw err;
|
|
421
434
|
}
|
|
422
|
-
const
|
|
423
|
-
const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
|
|
424
|
-
ops.appendEvent(db, {
|
|
425
|
-
type: 'plan_exported',
|
|
435
|
+
const context = contextSpine.validateContextSnapshot(db, {
|
|
426
436
|
change_id: change.id,
|
|
427
|
-
|
|
428
|
-
|
|
437
|
+
task_id: null,
|
|
438
|
+
role: 'plan',
|
|
439
|
+
gate: 'planning',
|
|
440
|
+
}, { rootDir });
|
|
441
|
+
if (!context.snapshot || context.blockers.length > 0) {
|
|
442
|
+
const err = new Error('plan export requires a current planning Context Manifest');
|
|
443
|
+
err.code = context.snapshot ? 'PLAN_CONTEXT_STALE' : 'PLAN_CONTEXT_REQUIRED';
|
|
444
|
+
err.details = { blockers: context.blockers };
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
const plan = planStore.buildPlan(tasks, { changeId: change.id });
|
|
448
|
+
const planRun = db.prepare(
|
|
449
|
+
`SELECT id FROM workflow_runs
|
|
450
|
+
WHERE kind = 'plan' AND change_id = ? AND task_id IS NULL
|
|
451
|
+
AND status IN ('active', 'blocked', 'ready')
|
|
452
|
+
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
453
|
+
).get(change.id);
|
|
454
|
+
const owner = { type: 'change', id: change.id };
|
|
455
|
+
const contextArtifact = db.prepare(
|
|
456
|
+
`SELECT id FROM artifacts
|
|
457
|
+
WHERE path = ? AND status <> 'archived'
|
|
458
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
459
|
+
).get(context.snapshot.manifest_path);
|
|
460
|
+
const publication = planStore.prepareChangePlanPublication(plan, {
|
|
461
|
+
rootDir,
|
|
462
|
+
change,
|
|
463
|
+
tasks,
|
|
464
|
+
context: {
|
|
465
|
+
snapshot_id: context.snapshot.id,
|
|
466
|
+
manifest_path: context.snapshot.manifest_path,
|
|
467
|
+
manifest_digest: context.snapshot.manifest_hash,
|
|
429
468
|
},
|
|
430
469
|
});
|
|
431
|
-
|
|
470
|
+
const artifactInputs = publication.artifacts.map(({ kind, path: file, digest }) => ({
|
|
471
|
+
kind,
|
|
472
|
+
file,
|
|
473
|
+
digest,
|
|
474
|
+
input: {
|
|
475
|
+
owner_type: owner.type,
|
|
476
|
+
owner_id: owner.id,
|
|
477
|
+
kind,
|
|
478
|
+
path: path.relative(rootDir, file),
|
|
479
|
+
source_refs: [
|
|
480
|
+
{ type: 'change', id: change.id, relation: 'planned_for' },
|
|
481
|
+
...(contextArtifact
|
|
482
|
+
? [{ type: 'artifact', id: contextArtifact.id, relation: 'compiled_from' }]
|
|
483
|
+
: []),
|
|
484
|
+
...tasks.map((task) => ({
|
|
485
|
+
type: 'task', id: task.id, relation: 'compiled_from_task_contract',
|
|
486
|
+
})),
|
|
487
|
+
],
|
|
488
|
+
consumer_refs: planRun
|
|
489
|
+
? [{ type: 'workflow', id: planRun.id, relation: 'verified_by' }]
|
|
490
|
+
: [],
|
|
491
|
+
provenance: {
|
|
492
|
+
writer: 'plan.export',
|
|
493
|
+
workflow_run_id: planRun?.id || null,
|
|
494
|
+
publication_transaction_id: publication.transaction_id,
|
|
495
|
+
},
|
|
496
|
+
metadata: {
|
|
497
|
+
context_snapshot_id: context.snapshot.id,
|
|
498
|
+
context_digest: context.snapshot.manifest_hash,
|
|
499
|
+
terminal_role: !planRun,
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
}));
|
|
503
|
+
try {
|
|
504
|
+
ops.tx(db, () => {
|
|
505
|
+
const preflight = artifactInputs.map((artifact) => (
|
|
506
|
+
artifactRegistry.preflightArtifactPublication(
|
|
507
|
+
db, artifact.input, { rootDir },
|
|
508
|
+
)
|
|
509
|
+
));
|
|
510
|
+
publication.publish();
|
|
511
|
+
const recorded = artifactInputs.map((artifact, index) => (
|
|
512
|
+
artifactRegistry.recordArtifactInTx(db, {
|
|
513
|
+
...artifact.input,
|
|
514
|
+
id: preflight[index].artifact_id || undefined,
|
|
515
|
+
expected_before_digest: preflight[index].expected_before_digest,
|
|
516
|
+
content_digest: artifact.digest,
|
|
517
|
+
}, { rootDir })
|
|
518
|
+
));
|
|
519
|
+
ops.appendEventInTx(db, {
|
|
520
|
+
type: 'plan_exported',
|
|
521
|
+
change_id: change.id,
|
|
522
|
+
payload: {
|
|
523
|
+
change_id: change.id,
|
|
524
|
+
plan_path: publication.plan_path,
|
|
525
|
+
plan_md_path: publication.plan_md_path,
|
|
526
|
+
publication_transaction_id: publication.transaction_id,
|
|
527
|
+
wave_count: plan.waves.length,
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
return recorded;
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const rollback = publication.rollback();
|
|
534
|
+
const failure = rollback?.rolled_back === false
|
|
535
|
+
? planStore.planRecoveryRequiredError(
|
|
536
|
+
error,
|
|
537
|
+
rollback,
|
|
538
|
+
publication.transaction_id,
|
|
539
|
+
)
|
|
540
|
+
: error;
|
|
541
|
+
if (failure.code === 'PLAN_RECOVERY_REQUIRED') {
|
|
542
|
+
const issue = failure.details?.rollback_issue
|
|
543
|
+
|| failure.details?.issue
|
|
544
|
+
|| {
|
|
545
|
+
code: failure.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
546
|
+
message: failure.cause?.message || failure.message,
|
|
547
|
+
};
|
|
548
|
+
ctx.markPlanRecoveryRequired?.({
|
|
549
|
+
recovered: 0,
|
|
550
|
+
finalized: 0,
|
|
551
|
+
pending: 1,
|
|
552
|
+
issues: [issue],
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
throw failure;
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
publication.commit();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
const issue = error.details?.issue || {
|
|
561
|
+
code: error.cause?.code || 'PLAN_RECOVERY_FAILED',
|
|
562
|
+
message: error.cause?.message || error.message,
|
|
563
|
+
};
|
|
564
|
+
ctx.markPlanRecoveryRequired?.({
|
|
565
|
+
recovered: 0,
|
|
566
|
+
finalized: 0,
|
|
567
|
+
pending: 1,
|
|
568
|
+
issues: [issue],
|
|
569
|
+
});
|
|
570
|
+
// The registry and target bytes committed atomically before cleanup.
|
|
571
|
+
// The remaining journal is still authoritative recovery work, so the
|
|
572
|
+
// caller must see a typed failure and later mutations must stop.
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
plan_path: publication.plan_path,
|
|
577
|
+
plan_md_path: publication.plan_md_path,
|
|
578
|
+
wave_count: plan.waves.length,
|
|
579
|
+
};
|
|
432
580
|
}
|
|
433
581
|
case 'plan.get': {
|
|
434
582
|
const rootDir = ctx.rootDir || process.cwd();
|
|
435
|
-
const
|
|
583
|
+
const change = changes.readChange(db, input.change_id);
|
|
584
|
+
if (!change) {
|
|
585
|
+
const err = new Error(`change ${input.change_id} not found`);
|
|
586
|
+
err.code = 'CHANGE_NOT_FOUND';
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
const loaded = planStore.loadChangePlanArtifact(rootDir, change, {
|
|
590
|
+
db,
|
|
591
|
+
strict: true,
|
|
592
|
+
});
|
|
436
593
|
if (!loaded) {
|
|
437
594
|
const err = new Error('no plan has been computed yet');
|
|
438
595
|
err.code = 'NO_PLAN';
|
|
439
596
|
throw err;
|
|
440
597
|
}
|
|
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
598
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
449
599
|
}
|
|
450
600
|
case 'baseline.start': {
|
|
@@ -546,6 +696,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
546
696
|
case 'change.archive': {
|
|
547
697
|
return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
548
698
|
}
|
|
699
|
+
case 'change.delta': {
|
|
700
|
+
return changes.recordDelta(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
701
|
+
}
|
|
702
|
+
case 'change.documentation_reconcile': {
|
|
703
|
+
return changes.recordDocumentationReconciliation(
|
|
704
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
705
|
+
);
|
|
706
|
+
}
|
|
549
707
|
case 'decision.thread_start': {
|
|
550
708
|
return { thread: decisions.startDecisionThread(db, input) };
|
|
551
709
|
}
|
|
@@ -578,7 +736,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
578
736
|
return { thread: decisions.supersedeDecision(db, input) };
|
|
579
737
|
}
|
|
580
738
|
case 'decision.complete': {
|
|
581
|
-
return {
|
|
739
|
+
return {
|
|
740
|
+
thread: decisions.completeDecisionThread(
|
|
741
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
742
|
+
),
|
|
743
|
+
};
|
|
582
744
|
}
|
|
583
745
|
case 'decision.checkpoint': {
|
|
584
746
|
return {
|
|
@@ -618,6 +780,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
618
780
|
),
|
|
619
781
|
};
|
|
620
782
|
}
|
|
783
|
+
case 'workflow.revise': {
|
|
784
|
+
return {
|
|
785
|
+
workflow: workflows.reviseWorkflow(
|
|
786
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
787
|
+
),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
case 'workflow.supersede': {
|
|
791
|
+
return workflows.supersedeWorkflow(
|
|
792
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
793
|
+
);
|
|
794
|
+
}
|
|
621
795
|
case 'workflow.complete': {
|
|
622
796
|
return {
|
|
623
797
|
workflow: workflows.completeWorkflow(
|
|
@@ -625,6 +799,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
625
799
|
),
|
|
626
800
|
};
|
|
627
801
|
}
|
|
802
|
+
case 'artifact.record': {
|
|
803
|
+
return artifactRegistry.recordArtifact(
|
|
804
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
case 'artifact.get': {
|
|
808
|
+
return { artifact: artifactRegistry.getArtifact(db, input) };
|
|
809
|
+
}
|
|
628
810
|
case 'system.doctor': {
|
|
629
811
|
return doctor.runDoctor(db, {
|
|
630
812
|
rootDir: ctx.rootDir || process.cwd(),
|
|
@@ -657,8 +839,55 @@ function startServer({
|
|
|
657
839
|
project = projector.projectAll,
|
|
658
840
|
}) {
|
|
659
841
|
let db = null;
|
|
660
|
-
|
|
661
|
-
|
|
842
|
+
let planRecovery = null;
|
|
843
|
+
let authorityDbPath = path.resolve(dbPath);
|
|
844
|
+
const markPlanRecoveryRequired = (details) => {
|
|
845
|
+
planRecovery = {
|
|
846
|
+
recovered: Number(details?.recovered || 0),
|
|
847
|
+
finalized: Number(details?.finalized || 0),
|
|
848
|
+
pending: Math.max(1, Number(details?.pending || 0)),
|
|
849
|
+
issues: Array.isArray(details?.issues) ? details.issues : [],
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
const getDb = (toolName = null) => {
|
|
853
|
+
if (!db) {
|
|
854
|
+
const projectPaths = runtimePaths.pathsFor(rootDir);
|
|
855
|
+
if ([projectPaths.legacyStateDbPath, projectPaths.stateDbPath].includes(authorityDbPath)) {
|
|
856
|
+
// The packaged launcher has no explicit DB override. Its default path
|
|
857
|
+
// is the future canonical runtime location, which can legitimately be
|
|
858
|
+
// absent while a legacy project still owns its project-level DB. Admit and
|
|
859
|
+
// migrate that project before applying configured-authority binding.
|
|
860
|
+
authorityDbPath = runtimePaths.ensureRuntimeState(rootDir, {
|
|
861
|
+
admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
|
|
862
|
+
}).stateDbPath;
|
|
863
|
+
} else {
|
|
864
|
+
// Only a real external/task-linked override reaches binding
|
|
865
|
+
// validation. It may never invent or borrow project authority.
|
|
866
|
+
runtimePaths.locateStateDb(rootDir, {
|
|
867
|
+
env: { UBP_DB_PATH: authorityDbPath },
|
|
868
|
+
});
|
|
869
|
+
runtimePaths.ensureRuntimeState(rootDir, {
|
|
870
|
+
env: { UBP_DB_PATH: authorityDbPath },
|
|
871
|
+
allowConfiguredRuntimeLink: true,
|
|
872
|
+
migrateState: false,
|
|
873
|
+
admitStorageBoundary: () => gitBootstrap.ensureExistingProjectStorageBoundary(rootDir),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
db = initStateDb(authorityDbPath).db;
|
|
877
|
+
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
878
|
+
}
|
|
879
|
+
if (toolName !== 'system.doctor'
|
|
880
|
+
&& (planRecovery?.pending > 0 || planRecovery?.issues?.length > 0)) {
|
|
881
|
+
planRecovery = planStore.recoverPlanPublications(db, { rootDir });
|
|
882
|
+
if (planRecovery.pending > 0 || planRecovery.issues.length > 0) {
|
|
883
|
+
const error = new Error(
|
|
884
|
+
'plan publication recovery is incomplete; run system.doctor with repair=true',
|
|
885
|
+
);
|
|
886
|
+
error.code = 'PLAN_RECOVERY_REQUIRED';
|
|
887
|
+
error.details = planRecovery;
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
662
891
|
return db;
|
|
663
892
|
};
|
|
664
893
|
|
|
@@ -723,14 +952,22 @@ function startServer({
|
|
|
723
952
|
let result;
|
|
724
953
|
let toolDb = null;
|
|
725
954
|
try {
|
|
726
|
-
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
955
|
+
toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
|
|
727
956
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
728
957
|
result = await dispatchTool(
|
|
729
958
|
name,
|
|
730
959
|
args,
|
|
731
960
|
toolDb,
|
|
732
|
-
{
|
|
961
|
+
{
|
|
962
|
+
rootDir,
|
|
963
|
+
sessionId,
|
|
964
|
+
projector: project,
|
|
965
|
+
markPlanRecoveryRequired,
|
|
966
|
+
},
|
|
733
967
|
);
|
|
968
|
+
if (name === 'system.doctor' && args.repair === true) {
|
|
969
|
+
planRecovery = result.repair?.plan_publications || null;
|
|
970
|
+
}
|
|
734
971
|
} catch (err) {
|
|
735
972
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
736
973
|
toolError = code;
|
|
@@ -789,12 +1026,18 @@ function startServer({
|
|
|
789
1026
|
}
|
|
790
1027
|
|
|
791
1028
|
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
1029
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
796
1030
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
797
1031
|
: path.resolve('.');
|
|
1032
|
+
// Keep tools/list side-effect free. Root/layout validation happens inside the
|
|
1033
|
+
// first state-backed tool call so clients receive a typed MCP error instead
|
|
1034
|
+
// of a transport-level startup failure.
|
|
1035
|
+
const configuredDb = typeof process.env.UBP_DB_PATH === 'string'
|
|
1036
|
+
? process.env.UBP_DB_PATH.trim()
|
|
1037
|
+
: '';
|
|
1038
|
+
const dbPath = configuredDb
|
|
1039
|
+
? path.resolve(configuredDb)
|
|
1040
|
+
: runtimePaths.pathsFor(rootDir).stateDbPath;
|
|
798
1041
|
const sessionId = process.env.UBP_SESSION_ID || null;
|
|
799
1042
|
const handle = startServer({ dbPath, rootDir, sessionId });
|
|
800
1043
|
const transport = new StdioServerTransport();
|
|
@@ -816,7 +1059,7 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
816
1059
|
throw error;
|
|
817
1060
|
}
|
|
818
1061
|
const root = path.resolve(rootDir || process.cwd());
|
|
819
|
-
const dbPath =
|
|
1062
|
+
const dbPath = runtimePaths.locateStateDb(root);
|
|
820
1063
|
if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
|
|
821
1064
|
|
|
822
1065
|
const db = openStateDb(dbPath);
|
|
@@ -878,11 +1121,13 @@ module.exports = {
|
|
|
878
1121
|
BASELINE_TOOLS,
|
|
879
1122
|
CHANGE_TOOLS,
|
|
880
1123
|
WORKFLOW_TOOLS,
|
|
1124
|
+
ARTIFACT_TOOLS,
|
|
881
1125
|
SYSTEM_TOOLS,
|
|
882
1126
|
REGISTERED_TOOLS,
|
|
883
1127
|
STATELESS_TOOLS,
|
|
884
1128
|
MUTATING_TOOLS,
|
|
885
1129
|
appendHookLifecycleEvent,
|
|
1130
|
+
findProjectRoot: runtimePaths.findProjectRoot,
|
|
886
1131
|
readProjectBreadcrumb,
|
|
887
1132
|
renderProjectBreadcrumb,
|
|
888
1133
|
};
|
package/orchestrator/daemon.cjs
CHANGED
|
@@ -97,7 +97,7 @@ function runDaemon({
|
|
|
97
97
|
const children = [];
|
|
98
98
|
let bootRecovery;
|
|
99
99
|
try {
|
|
100
|
-
bootRecovery = recovery.recoverOnBoot(db);
|
|
100
|
+
bootRecovery = recovery.recoverOnBoot(db, { repoRoot });
|
|
101
101
|
} catch (error) {
|
|
102
102
|
runtimeState.recordIncident(db, {
|
|
103
103
|
code: 'BOOT_RECOVERY_FAILED', severity: 'critical', retryable: true,
|
|
@@ -208,17 +208,9 @@ function runDaemon({
|
|
|
208
208
|
runtime,
|
|
209
209
|
command,
|
|
210
210
|
args: commandArgs,
|
|
211
|
+
mark_task_started: true,
|
|
211
212
|
});
|
|
212
213
|
handle.task_id = task.id;
|
|
213
|
-
try {
|
|
214
|
-
ops.patchTask(db, task.id, { status: 'in_progress' });
|
|
215
|
-
} catch (error) {
|
|
216
|
-
runner.closeSession(
|
|
217
|
-
{ db, repoRoot, sid: handle.sid },
|
|
218
|
-
{ status: 'crashed', remove_worktree: false },
|
|
219
|
-
);
|
|
220
|
-
throw error;
|
|
221
|
-
}
|
|
222
214
|
settleExecution(handle, task);
|
|
223
215
|
children.push(handle);
|
|
224
216
|
} catch (err) {
|