ultra-builder-pro-cli 0.18.0 → 0.20.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/CHANGELOG.md +54 -1
- package/README.md +16 -27
- package/adapters/_shared/interaction-contract.cjs +26 -4
- package/adapters/claude.js +3 -1
- package/adapters/kimi.js +4 -30
- package/adapters/opencode.js +50 -14
- package/commands/ultra-research.md +1 -1
- package/docs/AGENT-CONTEXT.md +23 -34
- package/docs/ARCHITECTURE.md +26 -12
- package/docs/DECISIONS.md +12 -8
- package/docs/LEGACY-CLI-CROSSWALK.md +9 -7
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +54 -0
- package/docs/ROADMAP.md +5 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +26 -25
- package/docs/STATE-DB-ACCESS-POLICY.md +1 -1
- package/docs/WORKFLOW-LIFECYCLE.md +45 -25
- package/hooks/active_task_context.py +1 -1
- package/hooks/context_spine.py +1 -1
- package/hooks/health_check.py +19 -0
- package/hooks/pre_stop_check.py +1 -1
- package/hooks/workflow_checkpoint.py +1 -1
- package/hooks/workflow_context.py +1 -1
- package/hooks/workflow_resume.py +1 -1
- package/mcp-server/lib/decision-dialogue.cjs +84 -11
- package/mcp-server/lib/doctor.cjs +2 -1
- package/mcp-server/lib/state-db.cjs +143 -13
- package/mcp-server/lib/workflow-state.cjs +4 -4
- package/mcp-server/server.cjs +24 -8
- package/package.json +3 -4
- package/skills/ultra-change/SKILL.md +16 -8
- package/skills/ultra-init/SKILL.md +2 -1
- package/skills/ultra-plan/SKILL.md +20 -3
- package/skills/ultra-research/SKILL.md +25 -15
- package/skills/ultra-test/SKILL.md +5 -2
- package/skills/ultra-think/SKILL.md +2 -0
- package/skills/ultra-think/references/decision-dialogue.md +47 -11
- package/spec/cli-protocol.md +1 -0
- package/spec/mcp-tools.yaml +30 -3
- package/spec/schemas/state-db.sql +7 -4
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/status.cjs +2 -1
- package/adapters/_shared/handbook.cjs +0 -569
- package/bin/handbook.js +0 -107
- package/docs/USER-HANDBOOK-CONTRACT.md +0 -120
package/hooks/health_check.py
CHANGED
|
@@ -27,6 +27,22 @@ def hook_input() -> dict:
|
|
|
27
27
|
return {}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def has_active_workflow(db_path: Path) -> bool:
|
|
31
|
+
if not db_path.is_file():
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
uri = f"file:{db_path}?mode=ro"
|
|
35
|
+
with sqlite3.connect(uri, uri=True, timeout=1) as conn:
|
|
36
|
+
row = conn.execute(
|
|
37
|
+
"""SELECT 1 FROM workflow_runs
|
|
38
|
+
WHERE status IN ('active', 'blocked', 'ready')
|
|
39
|
+
LIMIT 1"""
|
|
40
|
+
).fetchone()
|
|
41
|
+
return row is not None
|
|
42
|
+
except sqlite3.Error:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
def inspect(root: Path) -> dict:
|
|
31
47
|
db_path = root / ".ultra" / "state.db"
|
|
32
48
|
report = {"status": "healthy", "project": str(root), "checks": {}}
|
|
@@ -171,6 +187,9 @@ def main() -> None:
|
|
|
171
187
|
if root is None:
|
|
172
188
|
print(json.dumps({}))
|
|
173
189
|
return
|
|
190
|
+
if not has_active_workflow(root / ".ultra" / "state.db"):
|
|
191
|
+
print(json.dumps({}))
|
|
192
|
+
return
|
|
174
193
|
report = inspect(root)
|
|
175
194
|
if report["status"] != "healthy":
|
|
176
195
|
print("[Ultra health] " + json.dumps(report, ensure_ascii=False, sort_keys=True), file=sys.stderr)
|
package/hooks/pre_stop_check.py
CHANGED
|
@@ -34,7 +34,7 @@ def main() -> None:
|
|
|
34
34
|
print(f"[pre_stop_check] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
35
35
|
allow_stop()
|
|
36
36
|
return
|
|
37
|
-
if breadcrumb and breadcrumb.get("
|
|
37
|
+
if breadcrumb and breadcrumb.get("workflow"):
|
|
38
38
|
allowed = ", ".join(breadcrumb.get("allowed_transitions") or []) or "none"
|
|
39
39
|
required = breadcrumb.get("required_transition") or "none"
|
|
40
40
|
print(
|
|
@@ -33,7 +33,7 @@ def main() -> None:
|
|
|
33
33
|
print(f"[workflow_checkpoint] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
34
34
|
print(json.dumps({}))
|
|
35
35
|
return
|
|
36
|
-
if not breadcrumb or not breadcrumb.get("
|
|
36
|
+
if not breadcrumb or not breadcrumb.get("workflow"):
|
|
37
37
|
print(json.dumps({}))
|
|
38
38
|
return
|
|
39
39
|
|
|
@@ -28,7 +28,7 @@ def main() -> None:
|
|
|
28
28
|
except ContextSpineError as exc:
|
|
29
29
|
print(f"[workflow_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
30
30
|
breadcrumb = None
|
|
31
|
-
if breadcrumb:
|
|
31
|
+
if breadcrumb and breadcrumb.get("workflow"):
|
|
32
32
|
context = render_breadcrumb(root, breadcrumb)
|
|
33
33
|
else:
|
|
34
34
|
print(json.dumps({}))
|
package/hooks/workflow_resume.py
CHANGED
|
@@ -64,7 +64,7 @@ def main() -> None:
|
|
|
64
64
|
text = None
|
|
65
65
|
try:
|
|
66
66
|
breadcrumb = read_breadcrumb(root)
|
|
67
|
-
if breadcrumb:
|
|
67
|
+
if breadcrumb and breadcrumb.get("workflow"):
|
|
68
68
|
text = render_breadcrumb(root, breadcrumb)
|
|
69
69
|
except ContextSpineError as exc:
|
|
70
70
|
print(f"[workflow_resume] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
@@ -120,6 +120,7 @@ function readDecisionThread(db, id) {
|
|
|
120
120
|
items,
|
|
121
121
|
started_at: row.started_at,
|
|
122
122
|
updated_at: row.updated_at,
|
|
123
|
+
completed_at: row.completed_at,
|
|
123
124
|
confirmed_at: row.confirmed_at,
|
|
124
125
|
};
|
|
125
126
|
}
|
|
@@ -384,6 +385,56 @@ function deferDecision(db, input = {}) {
|
|
|
384
385
|
});
|
|
385
386
|
}
|
|
386
387
|
|
|
388
|
+
function completeDecisionThread(db, input = {}) {
|
|
389
|
+
const id = requiredText(input.id, 'id');
|
|
390
|
+
const summary = requiredText(input.summary, 'summary');
|
|
391
|
+
return ops.tx(db, () => {
|
|
392
|
+
const thread = readDecisionThread(db, id);
|
|
393
|
+
if (!thread) {
|
|
394
|
+
throw new DecisionDialogueError('DECISION_THREAD_NOT_FOUND', `decision thread ${id} not found`);
|
|
395
|
+
}
|
|
396
|
+
if (thread.status === 'checkpoint_ready') {
|
|
397
|
+
throw new DecisionDialogueError(
|
|
398
|
+
'DECISION_CHECKPOINT_PENDING', `confirm or revise checkpoint ${id} before completing the thread`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (thread.status !== 'active') {
|
|
402
|
+
throw new DecisionDialogueError(
|
|
403
|
+
'DECISION_THREAD_NOT_MUTABLE', `decision thread ${id} is ${thread.status}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const open = thread.items.find((item) => item.status === 'open');
|
|
407
|
+
const deferredBlocking = thread.items.filter((item) => item.blocking && item.status === 'deferred');
|
|
408
|
+
if (open || deferredBlocking.length > 0) {
|
|
409
|
+
const blockers = [
|
|
410
|
+
...(open ? [`DECISION_AWAITING_OWNER:${open.id}`] : []),
|
|
411
|
+
...deferredBlocking.map((item) => `DECISION_DEFERRED_BLOCKING:${item.id}`),
|
|
412
|
+
];
|
|
413
|
+
throw new DecisionDialogueError(
|
|
414
|
+
'DECISION_ALIGNMENT_BLOCKING',
|
|
415
|
+
`decision thread ${id} still contains unresolved blocking state`,
|
|
416
|
+
{ blockers },
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const ts = nowIso();
|
|
420
|
+
db.prepare(
|
|
421
|
+
`UPDATE decision_threads
|
|
422
|
+
SET status = 'completed', summary_json = ?, completed_at = ?, updated_at = ?
|
|
423
|
+
WHERE id = ?`,
|
|
424
|
+
).run(JSON.stringify({
|
|
425
|
+
text: summary,
|
|
426
|
+
completion_kind: 'normalized_state',
|
|
427
|
+
completed_at: ts,
|
|
428
|
+
}), ts, ts, id);
|
|
429
|
+
ops.appendEventInTx(db, {
|
|
430
|
+
type: 'decision_thread_completed',
|
|
431
|
+
change_id: thread.change_id,
|
|
432
|
+
payload: { thread_id: id, item_count: thread.items.length },
|
|
433
|
+
});
|
|
434
|
+
return readDecisionThread(db, id);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
387
438
|
function supersedeDecision(db, input = {}) {
|
|
388
439
|
return ops.tx(db, () => {
|
|
389
440
|
const id = requiredText(input.id, 'id');
|
|
@@ -411,6 +462,15 @@ function supersedeDecision(db, input = {}) {
|
|
|
411
462
|
checkpoint_json = json_set(checkpoint_json, '$.invalidated', json('true'),
|
|
412
463
|
'$.invalidated_at', ?, '$.invalidation_reason', ?), updated_at = ? WHERE id = ?`,
|
|
413
464
|
).run(ts, reason, ts, thread.id);
|
|
465
|
+
} else if (thread.status === 'completed') {
|
|
466
|
+
db.prepare(
|
|
467
|
+
`UPDATE decision_threads
|
|
468
|
+
SET status = 'active', completed_at = NULL,
|
|
469
|
+
summary_json = json_set(summary_json, '$.invalidated', json('true'),
|
|
470
|
+
'$.invalidated_at', ?, '$.invalidation_reason', ?),
|
|
471
|
+
updated_at = ?
|
|
472
|
+
WHERE id = ?`,
|
|
473
|
+
).run(ts, reason, ts, thread.id);
|
|
414
474
|
}
|
|
415
475
|
insertQuestionInTx(db, readDecisionThread(db, thread.id), replacement, id);
|
|
416
476
|
return readDecisionThread(db, thread.id);
|
|
@@ -515,8 +575,10 @@ function checkpointDecisionThread(db, input = {}, { rootDir = process.cwd() } =
|
|
|
515
575
|
if (!thread) {
|
|
516
576
|
throw new DecisionDialogueError('DECISION_THREAD_NOT_FOUND', `decision thread ${id} not found`);
|
|
517
577
|
}
|
|
518
|
-
if (
|
|
519
|
-
throw new DecisionDialogueError(
|
|
578
|
+
if (['completed', 'cancelled'].includes(thread.status)) {
|
|
579
|
+
throw new DecisionDialogueError(
|
|
580
|
+
'DECISION_THREAD_NOT_MUTABLE', `decision thread ${id} is ${thread.status}`,
|
|
581
|
+
);
|
|
520
582
|
}
|
|
521
583
|
const open = thread.items.find((item) => item.status === 'open');
|
|
522
584
|
const deferredBlocking = thread.items.filter((item) => item.blocking && item.status === 'deferred');
|
|
@@ -612,6 +674,7 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
612
674
|
).all(params);
|
|
613
675
|
const blockers = [];
|
|
614
676
|
let selected = null;
|
|
677
|
+
let fallback = null;
|
|
615
678
|
for (const row of rows) {
|
|
616
679
|
const thread = readDecisionThread(db, row.id);
|
|
617
680
|
if (thread.status === 'confirmed') {
|
|
@@ -622,23 +685,25 @@ function decisionGate(db, bindings = {}, { rootDir = process.cwd() } = {}) {
|
|
|
622
685
|
}
|
|
623
686
|
continue;
|
|
624
687
|
}
|
|
625
|
-
|
|
626
|
-
|
|
688
|
+
fallback ||= thread;
|
|
689
|
+
const priorBlockerCount = blockers.length;
|
|
690
|
+
if (thread.current_decision?.blocking) {
|
|
691
|
+
blockers.push(`DECISION_AWAITING_OWNER:${thread.current_decision.id}`);
|
|
692
|
+
}
|
|
627
693
|
for (const item of thread.items) {
|
|
628
694
|
if (item.blocking && item.status === 'deferred') {
|
|
629
695
|
blockers.push(`DECISION_DEFERRED_BLOCKING:${item.id}`);
|
|
630
696
|
}
|
|
631
697
|
}
|
|
632
698
|
if (thread.status === 'checkpoint_ready') blockers.push(`DECISION_CHECKPOINT_CONFIRMATION_REQUIRED:${thread.id}`);
|
|
633
|
-
|
|
634
|
-
blockers.push(`DECISION_CHECKPOINT_REQUIRED:${thread.id}`);
|
|
635
|
-
}
|
|
699
|
+
if (blockers.length > priorBlockerCount) selected ||= thread;
|
|
636
700
|
}
|
|
701
|
+
const reported = selected || fallback;
|
|
637
702
|
return {
|
|
638
703
|
ready: blockers.length === 0,
|
|
639
704
|
blockers: [...new Set(blockers)],
|
|
640
|
-
thread:
|
|
641
|
-
current_decision:
|
|
705
|
+
thread: reported,
|
|
706
|
+
current_decision: reported?.current_decision || null,
|
|
642
707
|
};
|
|
643
708
|
}
|
|
644
709
|
|
|
@@ -656,8 +721,10 @@ function assertDecisionGate(db, bindings = {}, options = {}) {
|
|
|
656
721
|
function inspectDecisionHealth(db, { rootDir = process.cwd() } = {}) {
|
|
657
722
|
const threads = listDecisionThreads(db, { limit: 500 });
|
|
658
723
|
const active = threads.filter((thread) => thread.status === 'active');
|
|
724
|
+
const completed = threads.filter((thread) => thread.status === 'completed');
|
|
659
725
|
const checkpointReady = threads.filter((thread) => thread.status === 'checkpoint_ready');
|
|
660
726
|
const awaiting = active.filter((thread) => thread.current_decision);
|
|
727
|
+
const awaitingBlocking = awaiting.filter((thread) => thread.current_decision.blocking);
|
|
661
728
|
const deferredBlocking = threads.flatMap((thread) => thread.items.filter(
|
|
662
729
|
(item) => item.blocking && item.status === 'deferred',
|
|
663
730
|
));
|
|
@@ -678,15 +745,20 @@ function inspectDecisionHealth(db, { rootDir = process.cwd() } = {}) {
|
|
|
678
745
|
}
|
|
679
746
|
}
|
|
680
747
|
}
|
|
681
|
-
|
|
682
|
-
|
|
748
|
+
let currentThread = awaitingBlocking[0] || checkpointReady[0] || awaiting[0] || active[0] || null;
|
|
749
|
+
if (staleArtifacts.length > 0) {
|
|
750
|
+
currentThread = threads.find((thread) => thread.id === staleArtifacts[0].thread_id) || currentThread;
|
|
751
|
+
}
|
|
752
|
+
const currentThreadId = staleArtifacts[0]?.thread_id || currentThread?.id || null;
|
|
683
753
|
const status = staleArtifacts.length > 0
|
|
684
754
|
? 'fail'
|
|
685
755
|
: (active.length > 0 || checkpointReady.length > 0 || deferredBlocking.length > 0 ? 'warning' : 'pass');
|
|
686
756
|
return {
|
|
687
757
|
status,
|
|
688
758
|
active: active.length,
|
|
759
|
+
completed: completed.length,
|
|
689
760
|
awaiting_owner: awaiting.length,
|
|
761
|
+
awaiting_blocking: awaitingBlocking.length,
|
|
690
762
|
checkpoint_ready: checkpointReady.length,
|
|
691
763
|
deferred_blocking: deferredBlocking.length,
|
|
692
764
|
stale_artifacts: staleArtifacts,
|
|
@@ -704,6 +776,7 @@ module.exports = {
|
|
|
704
776
|
resolveDecision,
|
|
705
777
|
delegateDecision,
|
|
706
778
|
deferDecision,
|
|
779
|
+
completeDecisionThread,
|
|
707
780
|
supersedeDecision,
|
|
708
781
|
checkpointDecisionThread,
|
|
709
782
|
decisionGate,
|
|
@@ -67,7 +67,8 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
67
67
|
const decisionHealth = missing.length === 0
|
|
68
68
|
? decisions.inspectDecisionHealth(db, { rootDir })
|
|
69
69
|
: {
|
|
70
|
-
status: 'fail', active: 0, awaiting_owner: 0,
|
|
70
|
+
status: 'fail', active: 0, completed: 0, awaiting_owner: 0, awaiting_blocking: 0,
|
|
71
|
+
checkpoint_ready: 0,
|
|
71
72
|
deferred_blocking: 0, stale_artifacts: [], current: null, current_thread_id: null,
|
|
72
73
|
};
|
|
73
74
|
const baselineCheckStatus = baseline.status === 'pass'
|
|
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '
|
|
12
|
+
const EXPECTED_VERSION = '19.0';
|
|
13
13
|
const KIMI_SCHEMA_VERSION = '9.1';
|
|
14
14
|
const CONTEXT_SCHEMA_VERSION = '10.0';
|
|
15
15
|
const BASELINE_SCHEMA_VERSION = '11.0';
|
|
@@ -19,6 +19,8 @@ const AUTHORITY_SCHEMA_VERSION = '14.0';
|
|
|
19
19
|
const SEMANTIC_SCHEMA_VERSION = '15.0';
|
|
20
20
|
const DIALOGUE_SCHEMA_VERSION = '16.0';
|
|
21
21
|
const GIT_AUTHORITY_SCHEMA_VERSION = '17.0';
|
|
22
|
+
const ADAPTIVE_SCHEMA_VERSION = '18.0';
|
|
23
|
+
const DECISION_COMPLETION_SCHEMA_VERSION = '19.0';
|
|
22
24
|
|
|
23
25
|
const MIGRATED_GAPS = Object.freeze([{
|
|
24
26
|
id: 'legacy-rebaseline-required',
|
|
@@ -328,7 +330,9 @@ function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db))
|
|
|
328
330
|
const existing = db.prepare(
|
|
329
331
|
"SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
|
|
330
332
|
).get();
|
|
331
|
-
if (fromVersion
|
|
333
|
+
if ([ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION].includes(fromVersion) || existing) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
332
336
|
|
|
333
337
|
const obsoleteSteps = Object.freeze({
|
|
334
338
|
init: ['establish-baseline'],
|
|
@@ -455,6 +459,95 @@ function baselineSupportsUnbornGit(db) {
|
|
|
455
459
|
return typeof row?.sql === 'string' && row.sql.includes("'unborn'");
|
|
456
460
|
}
|
|
457
461
|
|
|
462
|
+
function decisionThreadsSupportCompletion(db) {
|
|
463
|
+
const row = db.prepare(
|
|
464
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'decision_threads'",
|
|
465
|
+
).get();
|
|
466
|
+
if (typeof row?.sql !== 'string' || !row.sql.includes("'completed'")) return false;
|
|
467
|
+
return columnNames(db, 'decision_threads').has('completed_at');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function upgradeDecisionThreadLifecycle(db) {
|
|
471
|
+
if (!tableNames(db).includes('decision_threads') || decisionThreadsSupportCompletion(db)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
475
|
+
db.pragma('foreign_keys = OFF');
|
|
476
|
+
try {
|
|
477
|
+
db.transaction(() => {
|
|
478
|
+
db.exec(`
|
|
479
|
+
DROP TABLE IF EXISTS decision_threads_completion_upgrade;
|
|
480
|
+
CREATE TABLE decision_threads_completion_upgrade (
|
|
481
|
+
id TEXT PRIMARY KEY,
|
|
482
|
+
purpose TEXT NOT NULL,
|
|
483
|
+
mode TEXT NOT NULL DEFAULT 'guided'
|
|
484
|
+
CHECK (mode IN ('guided', 'fast', 'autonomous', 'diagnostic')),
|
|
485
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
486
|
+
CHECK (status IN ('active', 'checkpoint_ready', 'completed', 'confirmed', 'cancelled')),
|
|
487
|
+
baseline_id TEXT REFERENCES baselines(id) ON DELETE SET NULL,
|
|
488
|
+
change_id TEXT REFERENCES changes(id) ON DELETE CASCADE,
|
|
489
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
490
|
+
summary_json TEXT NOT NULL DEFAULT '{}',
|
|
491
|
+
checkpoint_json TEXT NOT NULL DEFAULT '{}',
|
|
492
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
493
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
494
|
+
completed_at TEXT,
|
|
495
|
+
confirmed_at TEXT,
|
|
496
|
+
CHECK (baseline_id IS NOT NULL OR change_id IS NOT NULL OR workflow_run_id IS NOT NULL)
|
|
497
|
+
);
|
|
498
|
+
INSERT INTO decision_threads_completion_upgrade (
|
|
499
|
+
id, purpose, mode, status, baseline_id, change_id, workflow_run_id,
|
|
500
|
+
summary_json, checkpoint_json, started_at, updated_at, completed_at, confirmed_at
|
|
501
|
+
)
|
|
502
|
+
SELECT
|
|
503
|
+
id, purpose, mode, status, baseline_id, change_id, workflow_run_id,
|
|
504
|
+
summary_json, checkpoint_json, started_at, updated_at, NULL, confirmed_at
|
|
505
|
+
FROM decision_threads;
|
|
506
|
+
DROP TABLE decision_threads;
|
|
507
|
+
ALTER TABLE decision_threads_completion_upgrade RENAME TO decision_threads;
|
|
508
|
+
CREATE INDEX decision_threads_status
|
|
509
|
+
ON decision_threads(status, updated_at);
|
|
510
|
+
CREATE INDEX decision_threads_authority
|
|
511
|
+
ON decision_threads(baseline_id, change_id, workflow_run_id, status);
|
|
512
|
+
`);
|
|
513
|
+
db.prepare(
|
|
514
|
+
`UPDATE decision_threads
|
|
515
|
+
SET status = 'completed',
|
|
516
|
+
summary_json = CASE
|
|
517
|
+
WHEN summary_json = '{}' THEN ?
|
|
518
|
+
ELSE json_set(summary_json, '$.completion_kind', 'migrated_settled_thread')
|
|
519
|
+
END,
|
|
520
|
+
completed_at = updated_at
|
|
521
|
+
WHERE status = 'active'
|
|
522
|
+
AND EXISTS (
|
|
523
|
+
SELECT 1 FROM decision_items WHERE decision_items.thread_id = decision_threads.id
|
|
524
|
+
)
|
|
525
|
+
AND NOT EXISTS (
|
|
526
|
+
SELECT 1 FROM decision_items
|
|
527
|
+
WHERE decision_items.thread_id = decision_threads.id AND status = 'open'
|
|
528
|
+
)
|
|
529
|
+
AND NOT EXISTS (
|
|
530
|
+
SELECT 1 FROM decision_items
|
|
531
|
+
WHERE decision_items.thread_id = decision_threads.id
|
|
532
|
+
AND blocking = 1 AND status = 'deferred'
|
|
533
|
+
)`,
|
|
534
|
+
).run(JSON.stringify({
|
|
535
|
+
text: 'Existing normalized decision state was completed during schema migration.',
|
|
536
|
+
completion_kind: 'migrated_settled_thread',
|
|
537
|
+
}));
|
|
538
|
+
const violations = db.pragma('foreign_key_check');
|
|
539
|
+
if (violations.length > 0) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`decision completion migration produced ${violations.length} foreign key violation(s)`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
})();
|
|
545
|
+
} finally {
|
|
546
|
+
db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
|
|
458
551
|
function upgradeBaselineWorktreeConstraint(db, fromVersion = latestSchemaVersion(db)) {
|
|
459
552
|
if (!tableNames(db).includes('baselines') || baselineSupportsUnbornGit(db)) return false;
|
|
460
553
|
const previousVersion = fromVersion || 'unknown';
|
|
@@ -611,6 +704,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
611
704
|
})();
|
|
612
705
|
}
|
|
613
706
|
const gitStateChangedFrom = upgradeBaselineWorktreeConstraint(db, fromVersion);
|
|
707
|
+
const decisionCompletionChanged = upgradeDecisionThreadLifecycle(db);
|
|
614
708
|
db.transaction(() => {
|
|
615
709
|
applyCompatibleColumns(db);
|
|
616
710
|
const contextChanged = applyContextSpineUpgrade(db);
|
|
@@ -621,7 +715,8 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
621
715
|
fromVersion && ![
|
|
622
716
|
CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
|
|
623
717
|
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
|
|
624
|
-
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
|
|
718
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
719
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
625
720
|
].includes(fromVersion)
|
|
626
721
|
&& !contextMigration,
|
|
627
722
|
);
|
|
@@ -646,7 +741,8 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
646
741
|
if (fromVersion && ![
|
|
647
742
|
BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
|
|
648
743
|
AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
|
|
649
|
-
GIT_AUTHORITY_SCHEMA_VERSION,
|
|
744
|
+
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
745
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
650
746
|
].includes(fromVersion)
|
|
651
747
|
&& !baselineMigration) {
|
|
652
748
|
db.prepare(
|
|
@@ -715,7 +811,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
715
811
|
const workflowMigration = db.prepare(
|
|
716
812
|
"SELECT 1 FROM migration_history WHERE to_version = '13.0' AND status = 'success' LIMIT 1",
|
|
717
813
|
).get();
|
|
718
|
-
if (fromVersion &&
|
|
814
|
+
if (fromVersion && ![
|
|
815
|
+
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
|
|
816
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
817
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
818
|
+
].includes(fromVersion) && !workflowMigration) {
|
|
719
819
|
db.prepare(
|
|
720
820
|
`INSERT INTO migration_history
|
|
721
821
|
(from_version, to_version, direction, status, notes)
|
|
@@ -729,7 +829,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
729
829
|
const authorityMigration = db.prepare(
|
|
730
830
|
"SELECT 1 FROM migration_history WHERE to_version = '14.0' AND status = 'success' LIMIT 1",
|
|
731
831
|
).get();
|
|
732
|
-
if (fromVersion &&
|
|
832
|
+
if (fromVersion && ![
|
|
833
|
+
AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
|
|
834
|
+
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
835
|
+
].includes(fromVersion) && !authorityMigration) {
|
|
733
836
|
db.prepare(
|
|
734
837
|
`INSERT INTO migration_history
|
|
735
838
|
(from_version, to_version, direction, status, notes)
|
|
@@ -743,7 +846,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
743
846
|
const semanticMigration = db.prepare(
|
|
744
847
|
"SELECT 1 FROM migration_history WHERE to_version = '15.0' AND status = 'success' LIMIT 1",
|
|
745
848
|
).get();
|
|
746
|
-
if (fromVersion &&
|
|
849
|
+
if (fromVersion && ![
|
|
850
|
+
SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
|
|
851
|
+
ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
852
|
+
].includes(fromVersion) && !semanticMigration) {
|
|
747
853
|
db.prepare(
|
|
748
854
|
`INSERT INTO migration_history
|
|
749
855
|
(from_version, to_version, direction, status, notes)
|
|
@@ -757,7 +863,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
757
863
|
const dialogueMigration = db.prepare(
|
|
758
864
|
"SELECT 1 FROM migration_history WHERE to_version = '16.0' AND status = 'success' LIMIT 1",
|
|
759
865
|
).get();
|
|
760
|
-
if (fromVersion && ![
|
|
866
|
+
if (fromVersion && ![
|
|
867
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
868
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
869
|
+
].includes(fromVersion)
|
|
761
870
|
&& !dialogueMigration) {
|
|
762
871
|
db.prepare(
|
|
763
872
|
`INSERT INTO migration_history
|
|
@@ -772,7 +881,9 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
772
881
|
const gitStateMigration = db.prepare(
|
|
773
882
|
"SELECT 1 FROM migration_history WHERE to_version = '17.0' AND status = 'success' LIMIT 1",
|
|
774
883
|
).get();
|
|
775
|
-
if (fromVersion && ![
|
|
884
|
+
if (fromVersion && ![
|
|
885
|
+
GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
886
|
+
].includes(fromVersion)
|
|
776
887
|
&& !gitStateMigration) {
|
|
777
888
|
db.prepare(
|
|
778
889
|
`INSERT INTO migration_history
|
|
@@ -788,26 +899,44 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
|
|
|
788
899
|
const adaptiveMigration = db.prepare(
|
|
789
900
|
"SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
|
|
790
901
|
).get();
|
|
791
|
-
if (fromVersion &&
|
|
902
|
+
if (fromVersion && ![
|
|
903
|
+
ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
|
|
904
|
+
].includes(fromVersion) && !adaptiveMigration) {
|
|
792
905
|
db.prepare(
|
|
793
906
|
`INSERT INTO migration_history
|
|
794
907
|
(from_version, to_version, direction, status, notes)
|
|
795
908
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
796
909
|
).run(
|
|
797
910
|
fromVersion === GIT_AUTHORITY_SCHEMA_VERSION ? fromVersion : GIT_AUTHORITY_SCHEMA_VERSION,
|
|
798
|
-
|
|
911
|
+
ADAPTIVE_SCHEMA_VERSION,
|
|
799
912
|
adaptiveChanged
|
|
800
913
|
? 'Migrate active rigid workflows to adaptive capability transitions and independent initialization'
|
|
801
914
|
: 'Add adaptive capability transitions and independent initialization authority',
|
|
802
915
|
);
|
|
803
916
|
}
|
|
917
|
+
const completionMigration = db.prepare(
|
|
918
|
+
"SELECT 1 FROM migration_history WHERE to_version = '19.0' AND status = 'success' LIMIT 1",
|
|
919
|
+
).get();
|
|
920
|
+
if (fromVersion && fromVersion !== DECISION_COMPLETION_SCHEMA_VERSION && !completionMigration) {
|
|
921
|
+
db.prepare(
|
|
922
|
+
`INSERT INTO migration_history
|
|
923
|
+
(from_version, to_version, direction, status, notes)
|
|
924
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
925
|
+
).run(
|
|
926
|
+
ADAPTIVE_SCHEMA_VERSION,
|
|
927
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
928
|
+
decisionCompletionChanged
|
|
929
|
+
? 'Add non-ceremonial decision completion and migrate settled active threads'
|
|
930
|
+
: 'Add non-ceremonial decision completion authority',
|
|
931
|
+
);
|
|
932
|
+
}
|
|
804
933
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
805
934
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
806
935
|
db.prepare(
|
|
807
936
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
808
937
|
).run(
|
|
809
938
|
EXPECTED_VERSION,
|
|
810
|
-
'
|
|
939
|
+
'Non-ceremonial decision completion and host-neutral intent persistence',
|
|
811
940
|
);
|
|
812
941
|
})();
|
|
813
942
|
}
|
|
@@ -837,7 +966,8 @@ function initStateDb(dbPath) {
|
|
|
837
966
|
const legacyState = existing.size > 0
|
|
838
967
|
&& ![
|
|
839
968
|
WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
|
|
840
|
-
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
|
|
969
|
+
DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
|
|
970
|
+
DECISION_COMPLETION_SCHEMA_VERSION,
|
|
841
971
|
].includes(fromVersion);
|
|
842
972
|
backupPath = migrationBackup(db, dbPath, fromVersion, existing);
|
|
843
973
|
applyCompatibleColumns(db);
|
|
@@ -647,7 +647,7 @@ function normalizeResearchCoverage(mode, coverage, selectedSteps, metadata = {})
|
|
|
647
647
|
if (selectionReason.length < 3) {
|
|
648
648
|
throw new WorkflowStateError(
|
|
649
649
|
'WORKFLOW_SELECTION_REASON_REQUIRED',
|
|
650
|
-
`${mode} research requires
|
|
650
|
+
`${mode} research requires an evidence-based rationale for the accepted coverage`,
|
|
651
651
|
);
|
|
652
652
|
}
|
|
653
653
|
const synthesis = normalized.find((item) => item.step_id === '99-synthesis');
|
|
@@ -679,7 +679,7 @@ function normalizeResearchCoverage(mode, coverage, selectedSteps, metadata = {})
|
|
|
679
679
|
if (!Array.isArray(selectedSteps) || selectedSteps.length === 0) {
|
|
680
680
|
throw new WorkflowStateError(
|
|
681
681
|
'WORKFLOW_RESEARCH_COVERAGE_REQUIRED',
|
|
682
|
-
'custom research requires
|
|
682
|
+
'custom research requires accepted coverage or selected_steps',
|
|
683
683
|
);
|
|
684
684
|
}
|
|
685
685
|
const invalid = selectedSteps.find((item) => !known.has(item));
|
|
@@ -688,7 +688,7 @@ function normalizeResearchCoverage(mode, coverage, selectedSteps, metadata = {})
|
|
|
688
688
|
if (selectionReason.length < 3) {
|
|
689
689
|
throw new WorkflowStateError(
|
|
690
690
|
'WORKFLOW_SELECTION_REASON_REQUIRED',
|
|
691
|
-
'custom research requires
|
|
691
|
+
'custom research requires an evidence-based rationale for the accepted coverage',
|
|
692
692
|
);
|
|
693
693
|
}
|
|
694
694
|
const explicitCoverage = [...new Set([...selectedSteps, '99-synthesis'])].map((stepId) => ({
|
|
@@ -701,7 +701,7 @@ function normalizeResearchCoverage(mode, coverage, selectedSteps, metadata = {})
|
|
|
701
701
|
}
|
|
702
702
|
throw new WorkflowStateError(
|
|
703
703
|
'WORKFLOW_RESEARCH_COVERAGE_REQUIRED',
|
|
704
|
-
`${mode} research requires
|
|
704
|
+
`${mode} research requires accepted coverage dispositions`,
|
|
705
705
|
);
|
|
706
706
|
}
|
|
707
707
|
|
package/mcp-server/server.cjs
CHANGED
|
@@ -112,6 +112,7 @@ const DECISION_TOOLS = Object.freeze([
|
|
|
112
112
|
'decision.delegate',
|
|
113
113
|
'decision.defer',
|
|
114
114
|
'decision.supersede',
|
|
115
|
+
'decision.complete',
|
|
115
116
|
'decision.checkpoint',
|
|
116
117
|
]);
|
|
117
118
|
|
|
@@ -139,7 +140,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
139
140
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
140
141
|
'change.learning_propose', 'change.learning_resolve',
|
|
141
142
|
'decision.thread_start', 'decision.open', 'decision.resolve', 'decision.delegate',
|
|
142
|
-
'decision.defer', 'decision.supersede', 'decision.checkpoint',
|
|
143
|
+
'decision.defer', 'decision.supersede', 'decision.complete', 'decision.checkpoint',
|
|
143
144
|
'workflow.start', 'workflow.step', 'workflow.complete',
|
|
144
145
|
]);
|
|
145
146
|
|
|
@@ -576,6 +577,9 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
576
577
|
case 'decision.supersede': {
|
|
577
578
|
return { thread: decisions.supersedeDecision(db, input) };
|
|
578
579
|
}
|
|
580
|
+
case 'decision.complete': {
|
|
581
|
+
return { thread: decisions.completeDecisionThread(db, input) };
|
|
582
|
+
}
|
|
579
583
|
case 'decision.checkpoint': {
|
|
580
584
|
return {
|
|
581
585
|
thread: decisions.checkpointDecisionThread(
|
|
@@ -818,16 +822,28 @@ function appendHookLifecycleEvent({ rootDir, action, hookInput = {} } = {}) {
|
|
|
818
822
|
const db = openStateDb(dbPath);
|
|
819
823
|
try {
|
|
820
824
|
ensureSchemaVersion(db);
|
|
821
|
-
const
|
|
822
|
-
`SELECT
|
|
825
|
+
const workflow = db.prepare(
|
|
826
|
+
`SELECT change_id, task_id FROM workflow_runs
|
|
827
|
+
WHERE status IN ('active', 'blocked', 'ready') AND change_id IS NOT NULL
|
|
823
828
|
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
824
829
|
).get();
|
|
830
|
+
if (!workflow) return { recorded: false, reason: 'NO_ACTIVE_WORKFLOW' };
|
|
831
|
+
const change = db.prepare(
|
|
832
|
+
`SELECT id FROM changes
|
|
833
|
+
WHERE id = ? AND status IN ('active', 'blocked', 'ready')`,
|
|
834
|
+
).get(workflow.change_id);
|
|
825
835
|
if (!change) return { recorded: false, reason: 'NO_ACTIVE_CHANGE' };
|
|
826
|
-
const task =
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
836
|
+
const task = workflow.task_id
|
|
837
|
+
? db.prepare(
|
|
838
|
+
`SELECT id FROM tasks
|
|
839
|
+
WHERE id = ? AND change_id = ?
|
|
840
|
+
AND status IN ('in_progress', 'pending', 'blocked')`,
|
|
841
|
+
).get(workflow.task_id, change.id)
|
|
842
|
+
: db.prepare(
|
|
843
|
+
`SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
|
|
844
|
+
ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
|
|
845
|
+
updated_at DESC, rowid DESC LIMIT 1`,
|
|
846
|
+
).get(change.id);
|
|
831
847
|
const payload = {
|
|
832
848
|
agent_id: hookMetadata(hookInput.agent_id),
|
|
833
849
|
agent_type: hookMetadata(hookInput.agent_type),
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Explicitly invoked Ultra Builder Pro workflows for Claude Code, OpenCode, Codex CLI, and Kimi Code, with project-local MCP authority and lifecycle recovery.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
7
7
|
"ubp": "bin/install.js",
|
|
8
|
-
"ubp-handbook": "bin/handbook.js",
|
|
9
8
|
"ubp-orchestrator": "bin/orchestrator.js",
|
|
10
9
|
"ultra-tools": "ultra-tools/cli.cjs"
|
|
11
10
|
},
|
|
@@ -56,7 +55,7 @@
|
|
|
56
55
|
"docs/COMMIT-HASH-BACKFILL.md",
|
|
57
56
|
"docs/DECISIONS.md",
|
|
58
57
|
"docs/STATE-DB-ACCESS-POLICY.md",
|
|
59
|
-
"docs/
|
|
58
|
+
"docs/PLUGIN-ISOLATION-CONTRACT.md",
|
|
60
59
|
"docs/RUNTIME-COMPAT-MATRIX.md",
|
|
61
60
|
"!**/__pycache__/**",
|
|
62
61
|
"!**/*.pyc",
|