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.
Files changed (44) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +16 -27
  3. package/adapters/_shared/interaction-contract.cjs +26 -4
  4. package/adapters/claude.js +3 -1
  5. package/adapters/kimi.js +4 -30
  6. package/adapters/opencode.js +50 -14
  7. package/commands/ultra-research.md +1 -1
  8. package/docs/AGENT-CONTEXT.md +23 -34
  9. package/docs/ARCHITECTURE.md +26 -12
  10. package/docs/DECISIONS.md +12 -8
  11. package/docs/LEGACY-CLI-CROSSWALK.md +9 -7
  12. package/docs/PLUGIN-ISOLATION-CONTRACT.md +54 -0
  13. package/docs/ROADMAP.md +5 -5
  14. package/docs/RUNTIME-COMPAT-MATRIX.md +26 -25
  15. package/docs/STATE-DB-ACCESS-POLICY.md +1 -1
  16. package/docs/WORKFLOW-LIFECYCLE.md +45 -25
  17. package/hooks/active_task_context.py +1 -1
  18. package/hooks/context_spine.py +1 -1
  19. package/hooks/health_check.py +19 -0
  20. package/hooks/pre_stop_check.py +1 -1
  21. package/hooks/workflow_checkpoint.py +1 -1
  22. package/hooks/workflow_context.py +1 -1
  23. package/hooks/workflow_resume.py +1 -1
  24. package/mcp-server/lib/decision-dialogue.cjs +84 -11
  25. package/mcp-server/lib/doctor.cjs +2 -1
  26. package/mcp-server/lib/state-db.cjs +143 -13
  27. package/mcp-server/lib/workflow-state.cjs +4 -4
  28. package/mcp-server/server.cjs +24 -8
  29. package/package.json +3 -4
  30. package/skills/ultra-change/SKILL.md +16 -8
  31. package/skills/ultra-init/SKILL.md +2 -1
  32. package/skills/ultra-plan/SKILL.md +20 -3
  33. package/skills/ultra-research/SKILL.md +25 -15
  34. package/skills/ultra-test/SKILL.md +5 -2
  35. package/skills/ultra-think/SKILL.md +2 -0
  36. package/skills/ultra-think/references/decision-dialogue.md +47 -11
  37. package/spec/cli-protocol.md +1 -0
  38. package/spec/mcp-tools.yaml +30 -3
  39. package/spec/schemas/state-db.sql +7 -4
  40. package/ultra-tools/cli.cjs +1 -1
  41. package/ultra-tools/commands/status.cjs +2 -1
  42. package/adapters/_shared/handbook.cjs +0 -569
  43. package/bin/handbook.js +0 -107
  44. package/docs/USER-HANDBOOK-CONTRACT.md +0 -120
@@ -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)
@@ -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("change_id"):
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("change_id"):
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({}))
@@ -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 (thread.status === 'cancelled') {
519
- throw new DecisionDialogueError('DECISION_THREAD_NOT_MUTABLE', `decision thread ${id} is cancelled`);
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
- selected ||= thread;
626
- if (thread.current_decision) blockers.push(`DECISION_AWAITING_OWNER:${thread.current_decision.id}`);
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
- else if (!thread.current_decision && !thread.items.some((item) => item.blocking && item.status === 'deferred')) {
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: selected,
641
- current_decision: selected?.current_decision || null,
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
- const currentThread = awaiting[0] || checkpointReady[0] || active[0] || null;
682
- const currentThreadId = currentThread?.id || staleArtifacts[0]?.thread_id || null;
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, checkpoint_ready: 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 = '18.0';
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 === EXPECTED_VERSION || existing) return false;
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, EXPECTED_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, EXPECTED_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 && fromVersion !== EXPECTED_VERSION && !workflowMigration) {
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 && fromVersion !== EXPECTED_VERSION && !authorityMigration) {
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 && fromVersion !== EXPECTED_VERSION && !semanticMigration) {
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 && ![DIALOGUE_SCHEMA_VERSION, EXPECTED_VERSION].includes(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 && ![GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION].includes(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 && fromVersion !== EXPECTED_VERSION && !adaptiveMigration) {
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
- EXPECTED_VERSION,
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
- 'Adaptive capability transitions, independent initialization, and recoverable workflow migration',
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, EXPECTED_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 the model's evidence-based coverage rationale`,
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 model-selected coverage or selected_steps',
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 the model\'s evidence-based coverage rationale',
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 model-selected coverage dispositions`,
704
+ `${mode} research requires accepted coverage dispositions`,
705
705
  );
706
706
  }
707
707
 
@@ -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 change = db.prepare(
822
- `SELECT id FROM changes WHERE status IN ('active', 'blocked', 'ready')
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 = db.prepare(
827
- `SELECT id FROM tasks WHERE change_id = ? AND status IN ('in_progress', 'pending', 'blocked')
828
- ORDER BY CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
829
- updated_at DESC, rowid DESC LIMIT 1`,
830
- ).get(change.id);
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.18.0",
4
- "description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
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/USER-HANDBOOK-CONTRACT.md",
58
+ "docs/PLUGIN-ISOLATION-CONTRACT.md",
60
59
  "docs/RUNTIME-COMPAT-MATRIX.md",
61
60
  "!**/__pycache__/**",
62
61
  "!**/*.pyc",