ultra-builder-pro-cli 0.16.0 → 0.17.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.
Files changed (66) hide show
  1. package/.ultra-template/reports/templates/delivery-report.json +17 -0
  2. package/.ultra-template/reports/templates/test-report.json +18 -0
  3. package/.ultra-template/specs/research-distillate.md +4 -3
  4. package/CHANGELOG.md +95 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +10 -5
  7. package/adapters/_shared/handbook.cjs +1 -1
  8. package/adapters/_shared/interaction-contract.cjs +61 -0
  9. package/adapters/claude.js +1 -1
  10. package/adapters/opencode.js +1 -1
  11. package/bin/orchestrator.js +175 -19
  12. package/commands/ultra-change.md +1 -1
  13. package/commands/ultra-deliver.md +3 -3
  14. package/commands/ultra-init.md +1 -1
  15. package/commands/ultra-plan.md +1 -1
  16. package/commands/ultra-research.md +2 -2
  17. package/commands/ultra-review.md +10 -0
  18. package/commands/ultra-status.md +1 -1
  19. package/commands/ultra-test.md +1 -1
  20. package/docs/AGENT-CONTEXT.md +11 -8
  21. package/docs/ARCHITECTURE.md +67 -9
  22. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  23. package/docs/PLAN.zh-CN.md +19 -13
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
  25. package/docs/WORKFLOW-LIFECYCLE.md +123 -59
  26. package/hooks/pre_stop_check.py +3 -2
  27. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  28. package/mcp-server/lib/change-workflow.cjs +55 -35
  29. package/mcp-server/lib/context-spine.cjs +171 -105
  30. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  31. package/mcp-server/lib/init-project.cjs +178 -41
  32. package/mcp-server/lib/project-breadcrumb.cjs +14 -14
  33. package/mcp-server/lib/state-db.cjs +257 -10
  34. package/mcp-server/lib/state-ops.cjs +64 -1
  35. package/mcp-server/lib/workflow-state.cjs +300 -205
  36. package/mcp-server/server.cjs +58 -34
  37. package/orchestrator/auto-merge.cjs +19 -0
  38. package/orchestrator/daemon.cjs +104 -0
  39. package/orchestrator/parallel-orchestrator.cjs +371 -49
  40. package/orchestrator/session-runner.cjs +354 -25
  41. package/package.json +2 -1
  42. package/skills/ultra-change/SKILL.md +69 -94
  43. package/skills/ultra-deliver/SKILL.md +37 -65
  44. package/skills/ultra-dev/SKILL.md +50 -52
  45. package/skills/ultra-doctor/SKILL.md +34 -49
  46. package/skills/ultra-init/SKILL.md +78 -105
  47. package/skills/ultra-plan/SKILL.md +78 -99
  48. package/skills/ultra-research/SKILL.md +99 -113
  49. package/skills/ultra-review/SKILL.md +38 -53
  50. package/skills/ultra-review/references/review-modes.md +5 -3
  51. package/skills/ultra-review/references/unified-schema.md +26 -6
  52. package/skills/ultra-review/scripts/review_wait.py +12 -0
  53. package/skills/ultra-status/SKILL.md +34 -44
  54. package/skills/ultra-test/SKILL.md +61 -54
  55. package/skills/ultra-think/SKILL.md +37 -51
  56. package/skills/ultra-think/references/decision-dialogue.md +87 -111
  57. package/spec/cli-protocol.md +35 -0
  58. package/spec/mcp-tools.yaml +145 -28
  59. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  60. package/spec/schemas/state-db.sql +8 -2
  61. package/spec/schemas/test-report.v1.schema.json +45 -14
  62. package/ultra-tools/cli.cjs +1 -1
  63. package/ultra-tools/commands/session.cjs +10 -3
  64. package/ultra-tools/commands/status.cjs +19 -14
  65. package/ultra-tools/commands/system.cjs +2 -2
  66. package/ultra-tools/commands/task.cjs +3 -0
@@ -40,20 +40,33 @@ function gitHead(rootDir) {
40
40
  return result.status === 0 ? result.stdout.trim() : null;
41
41
  }
42
42
 
43
+ function gitRepositoryRoot(rootDir) {
44
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
45
+ cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
46
+ });
47
+ return result.status === 0 ? result.stdout.trim() : null;
48
+ }
49
+
43
50
  function gitBranch(rootDir) {
44
- const result = spawnSync('git', ['branch', '--show-current'], {
51
+ let result = spawnSync('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], {
45
52
  cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
46
53
  });
54
+ if (result.status !== 0) {
55
+ result = spawnSync('git', ['branch', '--show-current'], {
56
+ cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
57
+ });
58
+ }
47
59
  return result.status === 0 ? (result.stdout.trim() || null) : null;
48
60
  }
49
61
 
50
62
  function gitWorktreeSnapshot(rootDir, scope = ['.']) {
51
- const head = gitHead(rootDir);
52
- if (!head) {
63
+ const repositoryRoot = gitRepositoryRoot(rootDir);
64
+ if (!repositoryRoot) {
53
65
  return {
54
66
  head: null, branch: null, state: 'unavailable', digest: null, files: [],
55
67
  };
56
68
  }
69
+ const head = gitHead(rootDir);
57
70
  const selectedScope = Array.isArray(scope) && scope.length > 0 ? scope : ['.'];
58
71
  const pathspecs = selectedScope.map((item) => (
59
72
  item === '.' ? '.' : `:(literal)${item}`
@@ -63,10 +76,12 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
63
76
  'git', ['status', '--porcelain=v1', '-z', '--untracked-files=all', ...args],
64
77
  { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
65
78
  );
66
- const diff = spawnSync(
67
- 'git', ['diff', '--binary', '--no-ext-diff', 'HEAD', ...args],
68
- { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
69
- );
79
+ const diff = head
80
+ ? spawnSync(
81
+ 'git', ['diff', '--binary', '--no-ext-diff', 'HEAD', ...args],
82
+ { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
83
+ )
84
+ : { status: 0, stdout: Buffer.alloc(0) };
70
85
  const untracked = spawnSync(
71
86
  'git', ['ls-files', '--others', '--exclude-standard', '-z', ...args],
72
87
  { cwd: rootDir, encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 * 1024 },
@@ -79,7 +94,7 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
79
94
  const statusText = status.stdout.toString('utf8');
80
95
  const files = statusText.split('\0').filter(Boolean);
81
96
  const digest = crypto.createHash('sha256');
82
- digest.update(head).update('\0').update(gitBranch(rootDir) || '').update('\0');
97
+ digest.update(head || 'unborn').update('\0').update(gitBranch(rootDir) || '').update('\0');
83
98
  digest.update(status.stdout).update(diff.stdout);
84
99
  for (const relative of untracked.stdout.toString('utf8').split('\0').filter(Boolean).sort()) {
85
100
  digest.update(relative).update('\0');
@@ -93,7 +108,7 @@ function gitWorktreeSnapshot(rootDir, scope = ['.']) {
93
108
  return {
94
109
  head,
95
110
  branch: gitBranch(rootDir),
96
- state: files.length > 0 ? 'dirty' : 'clean',
111
+ state: head ? (files.length > 0 ? 'dirty' : 'clean') : 'unborn',
97
112
  digest: digest.digest('hex'),
98
113
  files,
99
114
  };
@@ -464,6 +479,55 @@ function startBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = tr
464
479
  });
465
480
  }
466
481
 
482
+ function refreshInProgressBaseline(db, input = {}, {
483
+ rootDir = process.cwd(), emitEvent = true,
484
+ } = {}) {
485
+ const current = readBaseline(db, input.id);
486
+ if (!current) {
487
+ throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id || '(current)'} not found`);
488
+ }
489
+ if (!['draft', 'adopting', 'blocked'].includes(current.status)) return current;
490
+ const projectName = input.project_name === undefined
491
+ ? current.project_name
492
+ : nonEmpty(input.project_name, 'project_name');
493
+ const projectType = input.project_type === undefined ? current.project_type : input.project_type;
494
+ const stack = input.stack === undefined ? current.stack : input.stack;
495
+ const classification = input.classification === undefined
496
+ ? current.classification
497
+ : normalizeClassification(input.classification);
498
+ const snapshot = gitWorktreeSnapshot(rootDir, current.scope);
499
+ const repositoryRevision = snapshot.head
500
+ || (snapshot.state === 'unborn' ? null : current.repository_revision);
501
+ const ts = nowIso();
502
+ return ops.tx(db, () => {
503
+ db.prepare(
504
+ `UPDATE baselines SET project_name = ?, project_type = ?, stack = ?,
505
+ repository_revision = ?, repository_branch = ?, worktree_state = ?,
506
+ worktree_digest = ?, worktree_files_json = ?, classification_json = ?,
507
+ updated_at = ? WHERE id = ?`,
508
+ ).run(
509
+ projectName, projectType || null, stack || null,
510
+ repositoryRevision, snapshot.branch, snapshot.state, snapshot.digest,
511
+ JSON.stringify(snapshot.files), JSON.stringify(classification), ts, current.id,
512
+ );
513
+ if (emitEvent) {
514
+ ops.appendEventInTx(db, {
515
+ type: 'baseline_metadata_refreshed',
516
+ payload: {
517
+ baseline_id: current.id,
518
+ project_name: projectName,
519
+ project_type: projectType || null,
520
+ stack: stack || null,
521
+ repository_revision: repositoryRevision,
522
+ repository_branch: snapshot.branch,
523
+ worktree_state: snapshot.state,
524
+ },
525
+ });
526
+ }
527
+ return readBaseline(db, current.id);
528
+ });
529
+ }
530
+
467
531
  function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
468
532
  const current = readBaseline(db, input.id);
469
533
  if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
@@ -484,6 +548,12 @@ function recordBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = t
484
548
  ? current.provider_refs
485
549
  : providerRefs.normalizeProviderRefs(input.provider_refs, BaselineWorkflowError);
486
550
  const snapshot = gitWorktreeSnapshot(rootDir, scope);
551
+ if (snapshot.state === 'unborn') {
552
+ throw new BaselineWorkflowError(
553
+ 'BASELINE_GIT_HEAD_REQUIRED',
554
+ 'Git is initialized but has no commit; create an owner-authorized local checkpoint commit before recording the baseline',
555
+ );
556
+ }
487
557
  if (snapshot.head && input.repository_revision && input.repository_revision !== snapshot.head) {
488
558
  throw new BaselineWorkflowError(
489
559
  'BASELINE_REVISION_MISMATCH', 'repository_revision does not match current Git HEAD',
@@ -667,6 +737,7 @@ function convergenceBlockers(db, baseline, input, rootDir) {
667
737
  if (!expected) blockers.add('BASELINE_REVISION_REQUIRED');
668
738
  else if (baseline.repository_revision !== expected) blockers.add('BASELINE_REVISION_MISMATCH');
669
739
  const snapshot = gitWorktreeSnapshot(rootDir, baseline.scope);
740
+ if (snapshot.state === 'unborn') blockers.add('BASELINE_GIT_HEAD_REQUIRED');
670
741
  if (snapshot.head && expected && snapshot.head !== expected) blockers.add('BASELINE_HEAD_STALE');
671
742
  if (baseline.repository_branch && snapshot.branch
672
743
  && baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
@@ -725,7 +796,6 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
725
796
  type: 'baseline_blocked', payload: { baseline_id: current.id, blockers },
726
797
  });
727
798
  }
728
- workflows.blockInitializationInTx(db, current.id, blockers);
729
799
  });
730
800
  return { ready: false, status: 'blocked', blockers, baseline: readBaseline(db, current.id) };
731
801
  }
@@ -745,12 +815,6 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
745
815
  type: 'baseline_converged', payload: { baseline_id: current.id, revision: current.repository_revision },
746
816
  });
747
817
  }
748
- workflows.completeInitializationInTx(db, {
749
- ...current,
750
- approved_by: String(input.approved_by).trim(),
751
- research_run_id: research.id,
752
- converged_at: ts,
753
- });
754
818
  });
755
819
  return { ready: true, status: 'ready', blockers: [], baseline: readBaseline(db, current.id) };
756
820
  }
@@ -786,6 +850,9 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
786
850
  const snapshot = gitWorktreeSnapshot(
787
851
  rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
788
852
  );
853
+ if (snapshot.state === 'unborn') {
854
+ blockers.push('BASELINE_GIT_HEAD_REQUIRED');
855
+ }
789
856
  if (snapshot.head && baseline.repository_revision && snapshot.head !== baseline.repository_revision) {
790
857
  blockers.push('BASELINE_HEAD_STALE');
791
858
  } else if (!snapshot.head && baseline.repository_revision?.startsWith('workspace:')) {
@@ -905,6 +972,7 @@ function appendGap(db, input = {}, options = {}) {
905
972
  module.exports = {
906
973
  BaselineWorkflowError,
907
974
  startBaseline,
975
+ refreshInProgressBaseline,
908
976
  recordBaseline,
909
977
  convergeBaseline,
910
978
  readBaseline,
@@ -24,6 +24,10 @@ const CHANGE_PATCH_FIELDS = new Set([
24
24
  'title', 'intent', 'status', 'docs_impact', 'provider_refs',
25
25
  'contract', 'classification', 'research_disposition',
26
26
  ]);
27
+ const SEMANTIC_AUTHORITY_FIELDS = new Set([
28
+ 'intent', 'docs_impact', 'provider_refs', 'contract', 'classification',
29
+ 'research_disposition',
30
+ ]);
27
31
  const CHANGE_TRANSITIONS = Object.freeze({
28
32
  active: new Set(['active', 'blocked', 'cancelled']),
29
33
  blocked: new Set(['active', 'blocked', 'cancelled']),
@@ -544,9 +548,12 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
544
548
  ).all(change.id).map((task) => ops.readTask(db, task.id));
545
549
  const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
546
550
  const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
551
+ const recommendation = spine.recommendation;
552
+ const durableSpine = { ...spine };
553
+ delete durableSpine.recommendation;
547
554
  const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
548
555
  const manifest = {
549
- schema_version: '2.0',
556
+ schema_version: '3.0',
550
557
  snapshot_id: snapshotId,
551
558
  generated_at: nowIso(),
552
559
  change: {
@@ -566,7 +573,7 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
566
573
  specs,
567
574
  role: spine.role,
568
575
  gate: spine.gate,
569
- next_action: spine.next_action,
576
+ control: spine.control,
570
577
  readiness: spine.readiness,
571
578
  context: spine.context,
572
579
  execution_contract: spine.execution_contract,
@@ -588,14 +595,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
588
595
  db.prepare(
589
596
  `INSERT INTO context_snapshots
590
597
  (id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
591
- role, gate, next_action, readiness, blockers_json, context_json,
592
- token_estimate, token_budget)
593
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
598
+ role, gate, next_action, allowed_transitions_json, required_transition,
599
+ readiness, blockers_json, context_json, token_estimate, token_budget)
600
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
594
601
  ).run(
595
602
  snapshotId, change.id, input.task_id || null,
596
603
  spine.resume.git_head, JSON.stringify(providers), relative, hash,
597
- spine.role, spine.gate, spine.next_action, spine.readiness.status,
598
- 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),
599
608
  spine.context.token_estimate, spine.context.budget.max_tokens,
600
609
  );
601
610
  upsertArtifact(db, {
@@ -623,34 +632,17 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
623
632
  manifest_path: relative, manifest_hash: hash, git_head: spine.resume.git_head,
624
633
  worktree_digest: spine.resume.worktree_digest,
625
634
  role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
626
- next_action: spine.next_action,
635
+ allowed_transitions: spine.control.allowed_transitions,
636
+ required_transition: spine.control.required_transition,
627
637
  },
628
638
  });
629
- const linkedWorkflow = db.prepare(
630
- `SELECT id FROM workflow_runs
631
- WHERE kind = 'change' AND change_id = ? AND current_step = 'compile-context'
632
- AND status IN ('active', 'blocked')
633
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
634
- ).get(change.id);
635
- if (linkedWorkflow) {
636
- if (spine.readiness.status === 'ready') {
637
- workflows.recordWorkflowStep(db, {
638
- id: linkedWorkflow.id, step_id: 'compile-context', status: 'completed',
639
- evidence: [{
640
- kind: 'context', ref: relative,
641
- summary: `Role ${spine.role} context is ready for ${input.task_id || 'change planning'}.`,
642
- }],
643
- outputs: [{ path: relative, kind: 'context-manifest' }],
644
- }, { rootDir });
645
- } else {
646
- workflows.recordWorkflowStep(db, {
647
- id: linkedWorkflow.id, step_id: 'compile-context', status: 'blocked',
648
- blockers: spine.readiness.blockers,
649
- }, { rootDir });
650
- }
651
- }
652
639
  });
653
- 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
+ };
654
646
  }
655
647
 
656
648
  function createChange(db, input, { rootDir = process.cwd() } = {}) {
@@ -793,7 +785,6 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
793
785
  change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
794
786
  metadata: { change_kind: row.kind, base_commit: row.base_commit },
795
787
  }, { rootDir });
796
- const context = compileContext(db, { id: row.id }, { rootDir });
797
788
  workflow = workflows.recordWorkflowStep(db, {
798
789
  id: workflow.id, step_id: 'bind-baseline', status: 'completed',
799
790
  evidence: [{
@@ -819,9 +810,10 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
819
810
  research_disposition: row.research_disposition,
820
811
  }],
821
812
  }, { rootDir });
813
+ workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
822
814
  return {
823
815
  change: readChange(db, row.id), intent_path: intentPath,
824
- context_manifest_path: context.context_manifest_path,
816
+ context_manifest_path: null,
825
817
  workflow,
826
818
  };
827
819
  });
@@ -914,6 +906,30 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
914
906
  sets.push('updated_at = ?'); values.push(nowIso(), id);
915
907
  db.prepare(`UPDATE changes SET ${sets.join(', ')} WHERE id = ?`).run(...values);
916
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
+ }
917
933
  if (syncIntent) {
918
934
  fs.mkdirSync(path.dirname(intentPath), { recursive: true });
919
935
  writeIntent(intentPath, updated);
@@ -924,7 +940,11 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
924
940
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
925
941
  });
926
942
  }
927
- 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
+ });
928
948
  return updated;
929
949
  });
930
950
  } catch (error) {