ultra-builder-pro-cli 0.19.1 → 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 +90 -1
- package/README.md +62 -29
- package/adapters/_shared/codex-assets.cjs +69 -11
- package/adapters/_shared/interaction-contract.cjs +60 -5
- 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/commands/ultra-research.md +1 -1
- package/docs/AGENT-CONTEXT.md +79 -45
- package/docs/ARCHITECTURE.md +75 -38
- package/docs/ARTIFACT-AUTHORITY.md +234 -0
- package/docs/DECISIONS.md +20 -10
- package/docs/LEGACY-CLI-CROSSWALK.md +15 -11
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
- package/docs/ROADMAP.md +21 -6
- package/docs/RUNTIME-COMPAT-MATRIX.md +30 -13
- package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
- package/docs/WORKFLOW-LIFECYCLE.md +105 -54
- 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 +427 -12
- 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 +375 -12
- 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 +397 -13
- package/mcp-server/lib/state-ops.cjs +248 -49
- package/mcp-server/lib/telemetry.cjs +12 -3
- package/mcp-server/lib/workflow-state.cjs +614 -62
- package/mcp-server/server.cjs +282 -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 +38 -13
- 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 +68 -18
- package/skills/ultra-research/SKILL.md +45 -32
- package/skills/ultra-review/SKILL.md +9 -5
- package/skills/ultra-status/SKILL.md +9 -4
- package/skills/ultra-test/SKILL.md +15 -7
- package/skills/ultra-think/SKILL.md +6 -0
- package/skills/ultra-think/references/decision-dialogue.md +61 -14
- package/skills/ultra-verify/SKILL.md +4 -1
- package/spec/cli-protocol.md +19 -7
- package/spec/mcp-tools.yaml +531 -46
- 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 +57 -17
- 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 +34 -7
- 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
|
|
|
@@ -112,6 +120,7 @@ const DECISION_TOOLS = Object.freeze([
|
|
|
112
120
|
'decision.delegate',
|
|
113
121
|
'decision.defer',
|
|
114
122
|
'decision.supersede',
|
|
123
|
+
'decision.complete',
|
|
115
124
|
'decision.checkpoint',
|
|
116
125
|
]);
|
|
117
126
|
|
|
@@ -119,10 +128,16 @@ const SYSTEM_TOOLS = Object.freeze([
|
|
|
119
128
|
'system.doctor',
|
|
120
129
|
]);
|
|
121
130
|
|
|
131
|
+
const ARTIFACT_TOOLS = Object.freeze([
|
|
132
|
+
'artifact.record',
|
|
133
|
+
'artifact.get',
|
|
134
|
+
]);
|
|
135
|
+
|
|
122
136
|
const REGISTERED_TOOLS = Object.freeze([
|
|
123
137
|
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
124
138
|
...DECISION_TOOLS,
|
|
125
139
|
...WORKFLOW_TOOLS,
|
|
140
|
+
...ARTIFACT_TOOLS,
|
|
126
141
|
...SYSTEM_TOOLS,
|
|
127
142
|
]);
|
|
128
143
|
|
|
@@ -136,11 +151,14 @@ const MUTATING_TOOLS = new Set([
|
|
|
136
151
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
137
152
|
'plan.export',
|
|
138
153
|
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
139
|
-
'change.create', 'change.update', 'change.
|
|
154
|
+
'change.create', 'change.update', 'change.delta', 'change.documentation_reconcile',
|
|
155
|
+
'change.context', 'change.converge', 'change.archive',
|
|
140
156
|
'change.learning_propose', 'change.learning_resolve',
|
|
141
157
|
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
142
|
-
'decision.defer', 'decision.supersede', 'decision.checkpoint',
|
|
143
|
-
'workflow.start', 'workflow.step', 'workflow.
|
|
158
|
+
'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
|
|
159
|
+
'workflow.start', 'workflow.step', 'workflow.revise', 'workflow.supersede',
|
|
160
|
+
'workflow.complete',
|
|
161
|
+
'artifact.record',
|
|
144
162
|
]);
|
|
145
163
|
|
|
146
164
|
function loadRegisteredTools() {
|
|
@@ -259,12 +277,11 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
259
277
|
return { session };
|
|
260
278
|
}
|
|
261
279
|
case 'session.admission_check': {
|
|
262
|
-
sessionRunner.
|
|
280
|
+
return sessionRunner.admissionCheck(
|
|
263
281
|
db,
|
|
264
282
|
ctx.rootDir || process.cwd(),
|
|
265
283
|
input.task_id,
|
|
266
284
|
);
|
|
267
|
-
return ops.admissionCheck(db, input.task_id);
|
|
268
285
|
}
|
|
269
286
|
case 'session.list': {
|
|
270
287
|
const sessions = ops.listActiveSessions(db, { task_id: input.task_id });
|
|
@@ -409,41 +426,175 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
409
426
|
err.code = 'CHANGE_NOT_MUTABLE';
|
|
410
427
|
throw err;
|
|
411
428
|
}
|
|
412
|
-
const
|
|
413
|
-
? input.out_path
|
|
414
|
-
: path.join(rootDir, input.out_path);
|
|
415
|
-
const tasks = ops.listTasks(db, { tag: input.tag, change_id: change.id });
|
|
429
|
+
const tasks = ops.listTasks(db, { change_id: change.id });
|
|
416
430
|
if (tasks.length === 0) {
|
|
417
431
|
const err = new Error('no tasks to plan');
|
|
418
432
|
err.code = 'NO_TASKS';
|
|
419
433
|
throw err;
|
|
420
434
|
}
|
|
421
|
-
const
|
|
422
|
-
const { plan_path } = planStore.savePlanArtifact(plan, abs, input.format || 'json');
|
|
423
|
-
ops.appendEvent(db, {
|
|
424
|
-
type: 'plan_exported',
|
|
435
|
+
const context = contextSpine.validateContextSnapshot(db, {
|
|
425
436
|
change_id: change.id,
|
|
426
|
-
|
|
427
|
-
|
|
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,
|
|
428
468
|
},
|
|
429
469
|
});
|
|
430
|
-
|
|
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
|
+
};
|
|
431
580
|
}
|
|
432
581
|
case 'plan.get': {
|
|
433
582
|
const rootDir = ctx.rootDir || process.cwd();
|
|
434
|
-
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
|
+
});
|
|
435
593
|
if (!loaded) {
|
|
436
594
|
const err = new Error('no plan has been computed yet');
|
|
437
595
|
err.code = 'NO_PLAN';
|
|
438
596
|
throw err;
|
|
439
597
|
}
|
|
440
|
-
if (loaded.change_id !== input.change_id) {
|
|
441
|
-
const err = new Error(
|
|
442
|
-
`execution plan belongs to ${loaded.change_id || '(unbound)'}, not ${input.change_id}`,
|
|
443
|
-
);
|
|
444
|
-
err.code = 'PLAN_CHANGE_MISMATCH';
|
|
445
|
-
throw err;
|
|
446
|
-
}
|
|
447
598
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
448
599
|
}
|
|
449
600
|
case 'baseline.start': {
|
|
@@ -545,6 +696,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
545
696
|
case 'change.archive': {
|
|
546
697
|
return changes.archiveChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
547
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
|
+
}
|
|
548
707
|
case 'decision.thread_start': {
|
|
549
708
|
return { thread: decisions.startDecisionThread(db, input) };
|
|
550
709
|
}
|
|
@@ -576,6 +735,13 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
576
735
|
case 'decision.supersede': {
|
|
577
736
|
return { thread: decisions.supersedeDecision(db, input) };
|
|
578
737
|
}
|
|
738
|
+
case 'decision.complete': {
|
|
739
|
+
return {
|
|
740
|
+
thread: decisions.completeDecisionThread(
|
|
741
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
742
|
+
),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
579
745
|
case 'decision.checkpoint': {
|
|
580
746
|
return {
|
|
581
747
|
thread: decisions.checkpointDecisionThread(
|
|
@@ -614,6 +780,18 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
614
780
|
),
|
|
615
781
|
};
|
|
616
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
|
+
}
|
|
617
795
|
case 'workflow.complete': {
|
|
618
796
|
return {
|
|
619
797
|
workflow: workflows.completeWorkflow(
|
|
@@ -621,6 +799,14 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
621
799
|
),
|
|
622
800
|
};
|
|
623
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
|
+
}
|
|
624
810
|
case 'system.doctor': {
|
|
625
811
|
return doctor.runDoctor(db, {
|
|
626
812
|
rootDir: ctx.rootDir || process.cwd(),
|
|
@@ -653,8 +839,55 @@ function startServer({
|
|
|
653
839
|
project = projector.projectAll,
|
|
654
840
|
}) {
|
|
655
841
|
let db = null;
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
891
|
return db;
|
|
659
892
|
};
|
|
660
893
|
|
|
@@ -719,14 +952,22 @@ function startServer({
|
|
|
719
952
|
let result;
|
|
720
953
|
let toolDb = null;
|
|
721
954
|
try {
|
|
722
|
-
toolDb = STATELESS_TOOLS.has(name) ? null : getDb();
|
|
955
|
+
toolDb = STATELESS_TOOLS.has(name) ? null : getDb(name);
|
|
723
956
|
if (toolDb) assertStateAuthority(toolDb, rootDir);
|
|
724
957
|
result = await dispatchTool(
|
|
725
958
|
name,
|
|
726
959
|
args,
|
|
727
960
|
toolDb,
|
|
728
|
-
{
|
|
961
|
+
{
|
|
962
|
+
rootDir,
|
|
963
|
+
sessionId,
|
|
964
|
+
projector: project,
|
|
965
|
+
markPlanRecoveryRequired,
|
|
966
|
+
},
|
|
729
967
|
);
|
|
968
|
+
if (name === 'system.doctor' && args.repair === true) {
|
|
969
|
+
planRecovery = result.repair?.plan_publications || null;
|
|
970
|
+
}
|
|
730
971
|
} catch (err) {
|
|
731
972
|
const code = err.code || (err instanceof ops.StateOpsError ? err.code : 'STATE_DB_ERROR');
|
|
732
973
|
toolError = code;
|
|
@@ -785,12 +1026,18 @@ function startServer({
|
|
|
785
1026
|
}
|
|
786
1027
|
|
|
787
1028
|
async function main() {
|
|
788
|
-
const dbPath = process.env.UBP_DB_PATH
|
|
789
|
-
? path.resolve(process.env.UBP_DB_PATH)
|
|
790
|
-
: path.resolve('.ultra', 'state.db');
|
|
791
1029
|
const rootDir = process.env.UBP_ROOT_DIR
|
|
792
1030
|
? path.resolve(process.env.UBP_ROOT_DIR)
|
|
793
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;
|
|
794
1041
|
const sessionId = process.env.UBP_SESSION_ID || null;
|
|
795
1042
|
const handle = startServer({ dbPath, rootDir, sessionId });
|
|
796
1043
|
const transport = new StdioServerTransport();
|
|
@@ -812,7 +1059,7 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
812
1059
|
throw error;
|
|
813
1060
|
}
|
|
814
1061
|
const root = path.resolve(rootDir || process.cwd());
|
|
815
|
-
const dbPath =
|
|
1062
|
+
const dbPath = runtimePaths.locateStateDb(root);
|
|
816
1063
|
if (!fs.existsSync(dbPath)) return { recorded: false, reason: 'STATE_DB_MISSING' };
|
|
817
1064
|
|
|
818
1065
|
const db = openStateDb(dbPath);
|
|
@@ -874,11 +1121,13 @@ module.exports = {
|
|
|
874
1121
|
BASELINE_TOOLS,
|
|
875
1122
|
CHANGE_TOOLS,
|
|
876
1123
|
WORKFLOW_TOOLS,
|
|
1124
|
+
ARTIFACT_TOOLS,
|
|
877
1125
|
SYSTEM_TOOLS,
|
|
878
1126
|
REGISTERED_TOOLS,
|
|
879
1127
|
STATELESS_TOOLS,
|
|
880
1128
|
MUTATING_TOOLS,
|
|
881
1129
|
appendHookLifecycleEvent,
|
|
1130
|
+
findProjectRoot: runtimePaths.findProjectRoot,
|
|
882
1131
|
readProjectBreadcrumb,
|
|
883
1132
|
renderProjectBreadcrumb,
|
|
884
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) {
|