ultra-builder-pro-cli 0.15.0 → 0.17.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 (71) hide show
  1. package/.ultra-template/specs/research-distillate.md +4 -3
  2. package/CHANGELOG.md +120 -1
  3. package/README.md +67 -30
  4. package/adapters/_shared/codex-assets.cjs +10 -5
  5. package/adapters/_shared/handbook.cjs +3 -2
  6. package/adapters/_shared/interaction-contract.cjs +61 -0
  7. package/adapters/claude.js +1 -1
  8. package/adapters/opencode.js +1 -1
  9. package/bin/orchestrator.js +175 -19
  10. package/commands/ultra-change.md +1 -1
  11. package/commands/ultra-deliver.md +3 -3
  12. package/commands/ultra-init.md +1 -1
  13. package/commands/ultra-plan.md +1 -1
  14. package/commands/ultra-research.md +2 -2
  15. package/commands/ultra-review.md +10 -0
  16. package/commands/ultra-status.md +1 -1
  17. package/commands/ultra-test.md +1 -1
  18. package/commands/ultra-think.md +2 -2
  19. package/docs/AGENT-CONTEXT.md +38 -18
  20. package/docs/ARCHITECTURE.md +91 -13
  21. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  22. package/docs/PLAN.zh-CN.md +19 -12
  23. package/docs/ROADMAP.md +5 -3
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  25. package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
  26. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  27. package/docs/WORKFLOW-LIFECYCLE.md +179 -39
  28. package/hooks/pre_stop_check.py +3 -2
  29. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  30. package/mcp-server/lib/change-workflow.cjs +107 -39
  31. package/mcp-server/lib/context-spine.cjs +215 -87
  32. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  33. package/mcp-server/lib/doctor.cjs +9 -0
  34. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  35. package/mcp-server/lib/init-project.cjs +178 -41
  36. package/mcp-server/lib/project-breadcrumb.cjs +23 -14
  37. package/mcp-server/lib/state-db.cjs +279 -9
  38. package/mcp-server/lib/state-ops.cjs +64 -1
  39. package/mcp-server/lib/workflow-state.cjs +313 -205
  40. package/mcp-server/server.cjs +112 -34
  41. package/orchestrator/auto-merge.cjs +19 -0
  42. package/orchestrator/daemon.cjs +104 -0
  43. package/orchestrator/parallel-orchestrator.cjs +371 -49
  44. package/orchestrator/session-runner.cjs +354 -25
  45. package/package.json +2 -1
  46. package/skills/ultra-change/SKILL.md +69 -65
  47. package/skills/ultra-deliver/SKILL.md +37 -58
  48. package/skills/ultra-dev/SKILL.md +55 -49
  49. package/skills/ultra-doctor/SKILL.md +44 -49
  50. package/skills/ultra-init/SKILL.md +78 -100
  51. package/skills/ultra-plan/SKILL.md +78 -79
  52. package/skills/ultra-research/SKILL.md +99 -98
  53. package/skills/ultra-review/SKILL.md +38 -47
  54. package/skills/ultra-review/references/review-modes.md +5 -3
  55. package/skills/ultra-review/references/unified-schema.md +26 -6
  56. package/skills/ultra-review/scripts/review_wait.py +12 -0
  57. package/skills/ultra-status/SKILL.md +34 -40
  58. package/skills/ultra-test/SKILL.md +61 -48
  59. package/skills/ultra-think/SKILL.md +47 -47
  60. package/skills/ultra-think/references/decision-dialogue.md +106 -0
  61. package/spec/cli-protocol.md +46 -2
  62. package/spec/mcp-tools.yaml +444 -29
  63. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  64. package/spec/schemas/mcp-tools.schema.json +1 -1
  65. package/spec/schemas/state-db.sql +66 -2
  66. package/spec/schemas/test-report.v1.schema.json +45 -14
  67. package/ultra-tools/cli.cjs +1 -1
  68. package/ultra-tools/commands/session.cjs +10 -3
  69. package/ultra-tools/commands/status.cjs +34 -14
  70. package/ultra-tools/commands/system.cjs +2 -2
  71. package/ultra-tools/commands/task.cjs +3 -0
@@ -13,6 +13,7 @@ const providerRefs = require('./provider-refs.cjs');
13
13
  const baselines = require('./baseline-workflow.cjs');
14
14
  const archiveJournal = require('./archive-journal.cjs');
15
15
  const workflows = require('./workflow-state.cjs');
16
+ const decisions = require('./decision-dialogue.cjs');
16
17
  const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
17
18
 
18
19
  const reconciliationAjv = new Ajv({ allErrors: true, strict: false });
@@ -23,6 +24,10 @@ const CHANGE_PATCH_FIELDS = new Set([
23
24
  'title', 'intent', 'status', 'docs_impact', 'provider_refs',
24
25
  'contract', 'classification', 'research_disposition',
25
26
  ]);
27
+ const SEMANTIC_AUTHORITY_FIELDS = new Set([
28
+ 'intent', 'docs_impact', 'provider_refs', 'contract', 'classification',
29
+ 'research_disposition',
30
+ ]);
26
31
  const CHANGE_TRANSITIONS = Object.freeze({
27
32
  active: new Set(['active', 'blocked', 'cancelled']),
28
33
  blocked: new Set(['active', 'blocked', 'cancelled']),
@@ -437,6 +442,7 @@ function writeIntent(file, change) {
437
442
  `- Base commit: \`${change.base_commit || 'unavailable'}\``,
438
443
  `- Documentation impact: \`${impact.status}\``,
439
444
  `- Research disposition: \`${change.research_disposition.status}\``,
445
+ `- Alignment checkpoint: \`${change.alignment_thread_id || 'not-required'}\``,
440
446
  '',
441
447
  '## Intent',
442
448
  '',
@@ -542,9 +548,12 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
542
548
  ).all(change.id).map((task) => ops.readTask(db, task.id));
543
549
  const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
544
550
  const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
551
+ const recommendation = spine.recommendation;
552
+ const durableSpine = { ...spine };
553
+ delete durableSpine.recommendation;
545
554
  const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
546
555
  const manifest = {
547
- schema_version: '2.0',
556
+ schema_version: '3.0',
548
557
  snapshot_id: snapshotId,
549
558
  generated_at: nowIso(),
550
559
  change: {
@@ -564,7 +573,7 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
564
573
  specs,
565
574
  role: spine.role,
566
575
  gate: spine.gate,
567
- next_action: spine.next_action,
576
+ control: spine.control,
568
577
  readiness: spine.readiness,
569
578
  context: spine.context,
570
579
  execution_contract: spine.execution_contract,
@@ -586,14 +595,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
586
595
  db.prepare(
587
596
  `INSERT INTO context_snapshots
588
597
  (id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
589
- role, gate, next_action, readiness, blockers_json, context_json,
590
- token_estimate, token_budget)
591
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
598
+ role, gate, next_action, allowed_transitions_json, required_transition,
599
+ readiness, blockers_json, context_json, token_estimate, token_budget)
600
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
592
601
  ).run(
593
602
  snapshotId, change.id, input.task_id || null,
594
603
  spine.resume.git_head, JSON.stringify(providers), relative, hash,
595
- spine.role, spine.gate, spine.next_action, spine.readiness.status,
596
- JSON.stringify(spine.readiness.blockers), JSON.stringify(spine),
604
+ spine.role, spine.gate, '',
605
+ JSON.stringify(spine.control.allowed_transitions), spine.control.required_transition,
606
+ spine.readiness.status,
607
+ JSON.stringify(spine.readiness.blockers), JSON.stringify(durableSpine),
597
608
  spine.context.token_estimate, spine.context.budget.max_tokens,
598
609
  );
599
610
  upsertArtifact(db, {
@@ -621,34 +632,17 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
621
632
  manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
622
633
  worktree_digest: spine.resume.worktree_digest,
623
634
  role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
624
- next_action: spine.next_action,
635
+ allowed_transitions: spine.control.allowed_transitions,
636
+ required_transition: spine.control.required_transition,
625
637
  },
626
638
  });
627
- const linkedWorkflow = db.prepare(
628
- `SELECT id FROM workflow_runs
629
- WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
630
- AND status IN ('active', 'blocked')
631
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
632
- ).get(change.id);
633
- if (linkedWorkflow) {
634
- if (spine.readiness.status === 'ready') {
635
- workflows.recordWorkflowStep(db, {
636
- id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
637
- evidence: [{
638
- kind: 'context', ref: relative,
639
- summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
640
- }],
641
- outputs: [{ path: relative, kind: 'context-manifest' }],
642
- }, { rootDir });
643
- } else {
644
- workflows.recordWorkflowStep(db, {
645
- id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
646
- blockers: spine.readiness.blockers,
647
- }, { rootDir });
648
- }
649
- }
650
639
  });
651
- return { manifest, context_manifest_path: manifestPath, manifest_hash: hash };
640
+ return {
641
+ manifest,
642
+ recommendation,
643
+ context_manifest_path: manifestPath,
644
+ manifest_hash: hash,
645
+ };
652
646
  }
653
647
 
654
648
  function createChange(db, input, { rootDir = process.cwd() } = {}) {
@@ -685,11 +679,46 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
685
679
  'VALIDATION_ERROR', 'baseline_bypass is not valid while the project baseline is healthy',
686
680
  );
687
681
  }
682
+ const alignmentGate = decisions.decisionGate(
683
+ db, { baseline_id: baselineHealth.baseline?.id || null }, { rootDir },
684
+ );
685
+ if (!alignmentGate.ready) {
686
+ throw new ChangeWorkflowError(
687
+ 'CHANGE_ALIGNMENT_REQUIRED',
688
+ `resolve and confirm the active baseline alignment before creating a change: ${alignmentGate.blockers.join(', ')}`,
689
+ { blockers: alignmentGate.blockers, thread_id: alignmentGate.thread?.id || null },
690
+ );
691
+ }
688
692
  const docsImpact = normalizeDocsImpact(input.docs_impact);
689
693
  const providers = normalizeProviderRefs(input.provider_refs);
690
694
  const contract = normalizeChangeContract(input.contract);
691
695
  const classification = normalizeClassification(input.classification, input.kind);
692
696
  const researchDisposition = normalizeResearchDisposition(input.research_disposition, input.kind);
697
+ let alignmentThread = null;
698
+ if (input.alignment_thread_id !== undefined) {
699
+ try {
700
+ alignmentThread = decisions.assertConfirmedDecisionCheckpoint(
701
+ db, input.alignment_thread_id, { rootDir, requireArtifact: true },
702
+ );
703
+ } catch (error) {
704
+ throw new ChangeWorkflowError(
705
+ 'CHANGE_ALIGNMENT_REQUIRED',
706
+ `alignment_thread_id must name a current artifact-bound checkpoint: ${error.message}`,
707
+ );
708
+ }
709
+ if (alignmentThread.change_id) {
710
+ throw new ChangeWorkflowError(
711
+ 'CHANGE_ALIGNMENT_REQUIRED', `alignment thread already belongs to ${alignmentThread.change_id}`,
712
+ );
713
+ }
714
+ if (!baselineHealth.baseline?.id || alignmentThread.baseline_id !== baselineHealth.baseline.id
715
+ || alignmentThread.workflow_run_id) {
716
+ throw new ChangeWorkflowError(
717
+ 'CHANGE_ALIGNMENT_REQUIRED',
718
+ 'change alignment must be bound only to the current baseline before change creation',
719
+ );
720
+ }
721
+ }
693
722
  const artifactRoot = path.join('.ultra', 'changes', 'active', input.id);
694
723
  const artifactDir = path.resolve(rootDir, artifactRoot);
695
724
  if (fs.existsSync(artifactDir)) {
@@ -700,6 +729,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
700
729
  id: input.id, title, kind: input.kind, status: 'active',
701
730
  intent, docs_impact: docsImpact, provider_refs: providers, baseline_bypass: baselineBypass,
702
731
  contract, classification, research_disposition: researchDisposition,
732
+ alignment_thread_id: alignmentThread?.id || null,
703
733
  base_commit: input.base_commit || gitHead(rootDir), artifact_root: artifactRoot,
704
734
  created_at: ts, updated_at: ts,
705
735
  };
@@ -709,15 +739,22 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
709
739
  `INSERT INTO changes
710
740
  (id, title, kind, status, intent, docs_impact_json, provider_refs_json,
711
741
  baseline_bypass_json, contract_json, classification_json,
712
- research_disposition_json, base_commit, artifact_root, created_at, updated_at)
713
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
742
+ research_disposition_json, alignment_thread_id, base_commit, artifact_root,
743
+ created_at, updated_at)
744
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
714
745
  ).run(
715
746
  row.id, row.title, row.kind, row.status, row.intent, JSON.stringify(row.docs_impact),
716
747
  JSON.stringify(row.provider_refs), row.baseline_bypass
717
748
  ? JSON.stringify(row.baseline_bypass) : null,
718
749
  JSON.stringify(row.contract), JSON.stringify(row.classification),
719
- JSON.stringify(row.research_disposition), row.base_commit, row.artifact_root, ts, ts,
750
+ JSON.stringify(row.research_disposition), row.alignment_thread_id,
751
+ row.base_commit, row.artifact_root, ts, ts,
720
752
  );
753
+ if (row.alignment_thread_id) {
754
+ db.prepare(
755
+ 'UPDATE decision_threads SET change_id = ?, updated_at = ? WHERE id = ?',
756
+ ).run(row.id, ts, row.alignment_thread_id);
757
+ }
721
758
  fs.mkdirSync(artifactDir, { recursive: true });
722
759
  const intentPath = path.join(artifactDir, 'intent.md');
723
760
  writeIntent(intentPath, row);
@@ -738,14 +775,16 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
738
775
  }
739
776
  ops.appendEventInTx(db, {
740
777
  type: 'change_created', change_id: row.id,
741
- payload: { kind: row.kind, artifact_root: row.artifact_root },
778
+ payload: {
779
+ kind: row.kind, artifact_root: row.artifact_root,
780
+ alignment_thread_id: row.alignment_thread_id,
781
+ },
742
782
  });
743
783
  let workflow = workflows.startWorkflow(db, {
744
784
  kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
745
785
  change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
746
786
  metadata: { change_kind: row.kind, base_commit: row.base_commit },
747
787
  }, { rootDir });
748
- const context = compileContext(db, { id: row.id }, { rootDir });
749
788
  workflow = workflows.recordWorkflowStep(db, {
750
789
  id: workflow.id, step_id: 'bind-baseline', status: 'completed',
751
790
  evidence: [{
@@ -771,9 +810,10 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
771
810
  research_disposition: row.research_disposition,
772
811
  }],
773
812
  }, { rootDir });
813
+ workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
774
814
  return {
775
815
  change: readChange(db, row.id), intent_path: intentPath,
776
- context_manifest_path: context.context_manifest_path,
816
+ context_manifest_path: null,
777
817
  workflow,
778
818
  };
779
819
  });
@@ -866,6 +906,30 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
866
906
  sets.push('updated_at = ?'); values.push(nowIso(), id);
867
907
  db.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE id = ?`).run(...values);
868
908
  const updated = readChange(db, id);
909
+ const invalidatedTasks = [];
910
+ if (contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
911
+ const tasks = db.prepare(
912
+ 'SELECT id, stale FROM tasks WHERE change_id = ? ORDER BY created_at ASC, id ASC',
913
+ ).all(id);
914
+ const markStale = db.prepare(
915
+ 'UPDATE tasks SET stale = 1, updated_at = ? WHERE id = ?',
916
+ );
917
+ const invalidatedAt = nowIso();
918
+ for (const task of tasks) {
919
+ if (task.stale) continue;
920
+ markStale.run(invalidatedAt, task.id);
921
+ invalidatedTasks.push(task.id);
922
+ ops.appendEventInTx(db, {
923
+ type: 'task_stale_marked',
924
+ task_id: task.id,
925
+ change_id: id,
926
+ payload: {
927
+ reason: 'change_semantic_authority_updated',
928
+ fields: Object.keys(patch).filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field)),
929
+ },
930
+ });
931
+ }
932
+ }
869
933
  if (syncIntent) {
870
934
  fs.mkdirSync(path.dirname(intentPath), { recursive: true });
871
935
  writeIntent(intentPath, updated);
@@ -876,7 +940,11 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
876
940
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
877
941
  });
878
942
  }
879
- ops.appendEventInTx(db, { type: 'change_updated', change_id: id, payload: { fields: Object.keys(patch) } });
943
+ ops.appendEventInTx(db, {
944
+ type: 'change_updated',
945
+ change_id: id,
946
+ payload: { fields: Object.keys(patch), invalidated_tasks: invalidatedTasks },
947
+ });
880
948
  return updated;
881
949
  });
882
950
  } catch (error) {