ultra-builder-pro-cli 0.16.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 (64) hide show
  1. package/.ultra-template/specs/research-distillate.md +4 -3
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +54 -24
  4. package/adapters/_shared/codex-assets.cjs +10 -5
  5. package/adapters/_shared/handbook.cjs +1 -1
  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/docs/AGENT-CONTEXT.md +11 -8
  19. package/docs/ARCHITECTURE.md +67 -9
  20. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  21. package/docs/PLAN.zh-CN.md +19 -13
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
  23. package/docs/WORKFLOW-LIFECYCLE.md +123 -59
  24. package/hooks/pre_stop_check.py +3 -2
  25. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  26. package/mcp-server/lib/change-workflow.cjs +55 -35
  27. package/mcp-server/lib/context-spine.cjs +171 -105
  28. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  29. package/mcp-server/lib/init-project.cjs +178 -41
  30. package/mcp-server/lib/project-breadcrumb.cjs +14 -14
  31. package/mcp-server/lib/state-db.cjs +257 -10
  32. package/mcp-server/lib/state-ops.cjs +64 -1
  33. package/mcp-server/lib/workflow-state.cjs +300 -205
  34. package/mcp-server/server.cjs +58 -34
  35. package/orchestrator/auto-merge.cjs +19 -0
  36. package/orchestrator/daemon.cjs +104 -0
  37. package/orchestrator/parallel-orchestrator.cjs +371 -49
  38. package/orchestrator/session-runner.cjs +354 -25
  39. package/package.json +2 -1
  40. package/skills/ultra-change/SKILL.md +69 -94
  41. package/skills/ultra-deliver/SKILL.md +37 -65
  42. package/skills/ultra-dev/SKILL.md +50 -52
  43. package/skills/ultra-doctor/SKILL.md +34 -49
  44. package/skills/ultra-init/SKILL.md +78 -105
  45. package/skills/ultra-plan/SKILL.md +78 -99
  46. package/skills/ultra-research/SKILL.md +99 -113
  47. package/skills/ultra-review/SKILL.md +38 -53
  48. package/skills/ultra-review/references/review-modes.md +5 -3
  49. package/skills/ultra-review/references/unified-schema.md +26 -6
  50. package/skills/ultra-review/scripts/review_wait.py +12 -0
  51. package/skills/ultra-status/SKILL.md +34 -44
  52. package/skills/ultra-test/SKILL.md +61 -54
  53. package/skills/ultra-think/SKILL.md +37 -51
  54. package/skills/ultra-think/references/decision-dialogue.md +87 -111
  55. package/spec/cli-protocol.md +35 -0
  56. package/spec/mcp-tools.yaml +145 -28
  57. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  58. package/spec/schemas/state-db.sql +8 -2
  59. package/spec/schemas/test-report.v1.schema.json +45 -14
  60. package/ultra-tools/cli.cjs +1 -1
  61. package/ultra-tools/commands/session.cjs +10 -3
  62. package/ultra-tools/commands/status.cjs +19 -14
  63. package/ultra-tools/commands/system.cjs +2 -2
  64. package/ultra-tools/commands/task.cjs +3 -0
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ const GIT_MODES = new Set(['auto', 'initialize', 'skip']);
8
+ // No trailing slash: isolated session worktrees expose `.ultra` as a symlink
9
+ // to the central authority, and Git's directory-only pattern does not ignore
10
+ // that symlink reliably.
11
+ const ULTRA_IGNORE_LINE = '.ultra';
12
+
13
+ class GitBootstrapError extends Error {
14
+ constructor(code, message, { cause } = {}) {
15
+ super(message);
16
+ this.name = 'GitBootstrapError';
17
+ this.code = code;
18
+ if (cause) this.cause = cause;
19
+ }
20
+ }
21
+
22
+ function runGit(rootDir, args) {
23
+ return spawnSync('git', args, {
24
+ cwd: rootDir,
25
+ encoding: 'utf8',
26
+ stdio: ['ignore', 'pipe', 'pipe'],
27
+ });
28
+ }
29
+
30
+ function gitValue(rootDir, args) {
31
+ const result = runGit(rootDir, args);
32
+ return result.status === 0 ? result.stdout.trim() : null;
33
+ }
34
+
35
+ function inspectGitRepository(rootDir) {
36
+ const repositoryRoot = gitValue(rootDir, ['rev-parse', '--show-toplevel']);
37
+ if (!repositoryRoot) {
38
+ return {
39
+ present: false,
40
+ repository_root: null,
41
+ branch: null,
42
+ head: null,
43
+ };
44
+ }
45
+ return {
46
+ present: true,
47
+ repository_root: path.resolve(repositoryRoot),
48
+ branch: gitValue(rootDir, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
49
+ || gitValue(rootDir, ['branch', '--show-current']),
50
+ head: gitValue(rootDir, ['rev-parse', '--verify', 'HEAD']),
51
+ };
52
+ }
53
+
54
+ function initializeRepository(rootDir) {
55
+ let result = runGit(rootDir, ['init', '-q', '-b', 'main']);
56
+ if (result.status !== 0) {
57
+ result = runGit(rootDir, ['init', '-q']);
58
+ if (result.status === 0) {
59
+ const branch = runGit(rootDir, ['symbolic-ref', 'HEAD', 'refs/heads/main']);
60
+ if (branch.status !== 0) result = branch;
61
+ }
62
+ }
63
+ if (result.status !== 0) {
64
+ throw new GitBootstrapError(
65
+ 'GIT_INIT_FAILED',
66
+ `git init failed: ${(result.stderr || result.stdout || 'unknown error').trim()}`,
67
+ );
68
+ }
69
+ }
70
+
71
+ function containsUltraIgnore(contents, { requireSymlinkSafe = false } = {}) {
72
+ const accepted = requireSymlinkSafe
73
+ ? ['.ultra', '/.ultra']
74
+ : ['.ultra', '.ultra/', '/.ultra', '/.ultra/'];
75
+ return contents.split(/\r?\n/).some((line) => {
76
+ const value = line.trim();
77
+ return accepted.includes(value);
78
+ });
79
+ }
80
+
81
+ function effectiveUltraIgnore(rootDir) {
82
+ return runGit(rootDir, ['check-ignore', '--quiet', '--no-index', '--', '.ultra']).status === 0;
83
+ }
84
+
85
+ function ensureUltraIgnored(rootDir, { requireSymlinkSafe = false } = {}) {
86
+ const file = path.join(rootDir, '.gitignore');
87
+ let stat = null;
88
+ try { stat = fs.lstatSync(file); }
89
+ catch (error) {
90
+ if (error.code !== 'ENOENT') throw error;
91
+ }
92
+ const existed = stat !== null;
93
+ if (stat) {
94
+ if (stat.isSymbolicLink() || !stat.isFile()) {
95
+ throw new GitBootstrapError(
96
+ 'GITIGNORE_UNSAFE',
97
+ `refusing to update non-regular root .gitignore: ${file}`,
98
+ );
99
+ }
100
+ }
101
+ const before = existed ? fs.readFileSync(file, 'utf8') : null;
102
+ if (!requireSymlinkSafe && effectiveUltraIgnore(rootDir)) {
103
+ return {
104
+ changed: false,
105
+ path: file,
106
+ rollback: { existed, before },
107
+ };
108
+ }
109
+ if (before !== null && containsUltraIgnore(before, { requireSymlinkSafe })) {
110
+ return {
111
+ changed: false,
112
+ path: file,
113
+ rollback: { existed, before },
114
+ };
115
+ }
116
+ const prefix = before && !before.endsWith('\n') ? `${before}\n` : (before || '');
117
+ const separator = prefix && !prefix.endsWith('\n\n') ? '\n' : '';
118
+ const next = `${prefix}${separator}# Ultra Builder Pro local authority\n${ULTRA_IGNORE_LINE}\n`;
119
+ fs.writeFileSync(file, next);
120
+ return {
121
+ changed: true,
122
+ path: file,
123
+ rollback: { existed, before },
124
+ };
125
+ }
126
+
127
+ function assertUltraIgnored(rootDir) {
128
+ const result = runGit(rootDir, ['check-ignore', '--quiet', '--no-index', '--', '.ultra']);
129
+ if (result.status === 0) return;
130
+ const detail = result.status === 1
131
+ ? 'the effective Git rules re-include .ultra'
132
+ : (result.stderr || result.stdout || 'Git could not evaluate the rule').trim();
133
+ throw new GitBootstrapError(
134
+ 'GITIGNORE_INEFFECTIVE',
135
+ `Ultra authority is not ignored by Git: ${detail}`,
136
+ );
137
+ }
138
+
139
+ function rollbackGitBootstrap(rootDir, internal) {
140
+ const errors = [];
141
+ if (!internal) return errors;
142
+ const gitignore = internal.gitignore;
143
+ if (gitignore?.changed) {
144
+ try {
145
+ if (gitignore.rollback.existed) {
146
+ fs.writeFileSync(gitignore.path, gitignore.rollback.before);
147
+ } else {
148
+ fs.rmSync(gitignore.path, { force: true });
149
+ }
150
+ } catch (error) {
151
+ errors.push(`gitignore restore failed: ${error.message}`);
152
+ }
153
+ }
154
+ if (internal.created_repository) {
155
+ try {
156
+ fs.rmSync(path.join(rootDir, '.git'), { recursive: true, force: true });
157
+ } catch (error) {
158
+ errors.push(`git repository rollback failed: ${error.message}`);
159
+ }
160
+ }
161
+ return errors;
162
+ }
163
+
164
+ function bootstrapGit(rootDir, { mode = 'auto' } = {}) {
165
+ if (!GIT_MODES.has(mode)) {
166
+ throw new GitBootstrapError(
167
+ 'VALIDATION_ERROR',
168
+ `git_mode must be one of: ${[...GIT_MODES].join(', ')}`,
169
+ );
170
+ }
171
+ const before = inspectGitRepository(rootDir);
172
+ if (!before.present && mode === 'skip') {
173
+ return {
174
+ result: {
175
+ requested_mode: mode,
176
+ status: 'skipped',
177
+ repository_root: null,
178
+ branch: null,
179
+ head: null,
180
+ gitignore_path: null,
181
+ gitignore_changed: false,
182
+ initial_commit_required: false,
183
+ },
184
+ internal: null,
185
+ };
186
+ }
187
+
188
+ const internal = {
189
+ created_repository: false,
190
+ gitignore: null,
191
+ };
192
+ try {
193
+ if (!before.present) {
194
+ initializeRepository(rootDir);
195
+ internal.created_repository = true;
196
+ }
197
+ internal.gitignore = ensureUltraIgnored(rootDir, {
198
+ // A repository created in this call has no tracked baseline to preserve,
199
+ // so its ignore rule can be made safe for the session authority symlink.
200
+ requireSymlinkSafe: internal.created_repository,
201
+ });
202
+ assertUltraIgnored(rootDir);
203
+ const after = inspectGitRepository(rootDir);
204
+ if (!after.present) {
205
+ throw new GitBootstrapError(
206
+ 'GIT_INIT_FAILED',
207
+ 'git initialization completed without a discoverable repository',
208
+ );
209
+ }
210
+ return {
211
+ result: {
212
+ requested_mode: mode,
213
+ status: internal.created_repository ? 'initialized' : 'existing',
214
+ repository_root: after.repository_root,
215
+ branch: after.branch,
216
+ head: after.head,
217
+ gitignore_path: path.relative(rootDir, internal.gitignore.path) || '.gitignore',
218
+ gitignore_changed: internal.gitignore.changed,
219
+ initial_commit_required: after.head === null,
220
+ },
221
+ internal,
222
+ };
223
+ } catch (error) {
224
+ rollbackGitBootstrap(rootDir, internal);
225
+ if (error instanceof GitBootstrapError) throw error;
226
+ throw new GitBootstrapError(
227
+ 'GIT_INIT_FAILED',
228
+ `Git bootstrap failed: ${error.message}`,
229
+ { cause: error },
230
+ );
231
+ }
232
+ }
233
+
234
+ module.exports = {
235
+ GitBootstrapError,
236
+ GIT_MODES,
237
+ bootstrapGit,
238
+ inspectGitRepository,
239
+ rollbackGitBootstrap,
240
+ };
@@ -17,6 +17,7 @@ const path = require('node:path');
17
17
  const { EXPECTED_VERSION, initStateDb } = require('./state-db.cjs');
18
18
  const { assertStateAuthority, inspectProjection } = require('./state-authority.cjs');
19
19
  const baselines = require('./baseline-workflow.cjs');
20
+ const gitBootstrap = require('./git-bootstrap.cjs');
20
21
  const runtimeState = require('./runtime-state.cjs');
21
22
  const ops = require('./state-ops.cjs');
22
23
  const workflows = require('./workflow-state.cjs');
@@ -406,6 +407,80 @@ function rollbackInitialization(ultraDir, backupPath) {
406
407
  return errors;
407
408
  }
408
409
 
410
+ function createResumeSnapshot(db, ultraDir, stateDbPath) {
411
+ const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
412
+ const statePath = path.join(ultraDir, `.resume-state-${token}.db`);
413
+ const projectionDir = path.join(ultraDir, `.resume-projections-${token}`);
414
+ const projectionTargets = [
415
+ path.join('tasks', 'tasks.json'),
416
+ path.join('tasks', 'contexts'),
417
+ ];
418
+ try {
419
+ db.prepare('VACUUM INTO ?').run(statePath);
420
+ const projections = projectionTargets.map((relative) => {
421
+ const source = path.join(ultraDir, relative);
422
+ const existed = fs.existsSync(source);
423
+ if (existed) {
424
+ const destination = path.join(projectionDir, relative);
425
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
426
+ fs.cpSync(source, destination, { recursive: true });
427
+ }
428
+ return { relative, existed };
429
+ });
430
+ return { statePath, projectionDir, projections, stateDbPath, ultraDir };
431
+ } catch (error) {
432
+ for (const target of [statePath, projectionDir]) {
433
+ try { fs.rmSync(target, { recursive: true, force: true }); } catch { /* best effort */ }
434
+ }
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ function discardResumeSnapshot(snapshot) {
440
+ if (!snapshot) return [];
441
+ const errors = [];
442
+ for (const target of [snapshot.statePath, snapshot.projectionDir]) {
443
+ try { fs.rmSync(target, { recursive: true, force: true }); }
444
+ catch (error) { errors.push(`resume snapshot cleanup failed for ${target}: ${error.message}`); }
445
+ }
446
+ return errors;
447
+ }
448
+
449
+ function restoreResumeSnapshot(snapshot, { stateSourcePath = null } = {}) {
450
+ if (!snapshot) return [];
451
+ const errors = [];
452
+ const source = stateSourcePath && fs.existsSync(stateSourcePath)
453
+ ? stateSourcePath
454
+ : snapshot.statePath;
455
+ try {
456
+ for (const suffix of ['', '-wal', '-shm']) {
457
+ fs.rmSync(`${snapshot.stateDbPath}${suffix}`, { force: true });
458
+ }
459
+ if (source === snapshot.statePath) {
460
+ fs.renameSync(source, snapshot.stateDbPath);
461
+ } else {
462
+ fs.copyFileSync(source, snapshot.stateDbPath);
463
+ }
464
+ } catch (error) {
465
+ errors.push(`state.db restore failed: ${error.message}`);
466
+ }
467
+ for (const item of snapshot.projections) {
468
+ const target = path.join(snapshot.ultraDir, item.relative);
469
+ try {
470
+ fs.rmSync(target, { recursive: true, force: true });
471
+ if (item.existed) {
472
+ const source = path.join(snapshot.projectionDir, item.relative);
473
+ fs.mkdirSync(path.dirname(target), { recursive: true });
474
+ fs.cpSync(source, target, { recursive: true });
475
+ }
476
+ } catch (error) {
477
+ errors.push(`projection restore failed for ${item.relative}: ${error.message}`);
478
+ }
479
+ }
480
+ errors.push(...discardResumeSnapshot(snapshot));
481
+ return errors;
482
+ }
483
+
409
484
  function projectionMigrationError(projection) {
410
485
  const targets = { '4.4': '4.5', '4.5': EXPECTED_VERSION };
411
486
  const target = targets[projection.version];
@@ -428,32 +503,13 @@ function ensureInitializationWorkflows(db, baseline, { rootDir }) {
428
503
  let initRun = workflows.listWorkflows(
429
504
  db, { kind: 'init', baseline_id: baseline.id, limit: 1 }, { rootDir },
430
505
  )[0];
431
- let researchRun = workflows.listWorkflows(
506
+ const researchRun = workflows.listWorkflows(
432
507
  db, { kind: 'research', baseline_id: baseline.id, limit: 1 }, { rootDir },
433
508
  )[0];
434
- if (baseline.status === 'ready') {
435
- const compatibilityOnly = baseline.mode === 'migrated';
436
- const provenanceComplete = !compatibilityOnly
437
- && initRun?.status === 'completed'
438
- && researchRun?.status === 'completed'
439
- && baseline.research_run_id === researchRun.id;
440
- return {
441
- init_id: initRun?.id || null,
442
- init_status: initRun?.status || 'missing',
443
- research_id: researchRun?.id || null,
444
- research_mode: researchRun?.mode || null,
445
- research_status: compatibilityOnly
446
- ? 'not_required' : (researchRun?.status || 'missing'),
447
- provenance_status: compatibilityOnly
448
- ? 'compatibility_only' : (provenanceComplete ? 'complete' : 'incomplete'),
449
- next_action: compatibilityOnly
450
- ? 'ultra-init' : (provenanceComplete ? 'ultra-change' : 'ultra-doctor'),
451
- };
452
- }
453
509
  if (!initRun) {
454
510
  initRun = workflows.startWorkflow(db, {
455
511
  kind: 'init', baseline_id: baseline.id,
456
- subject: `Establish the ${baseline.mode} baseline for ${baseline.project_name}.`,
512
+ subject: `Initialize Ultra authority for ${baseline.project_name}.`,
457
513
  metadata: { repository_revision: baseline.repository_revision, scope: baseline.scope },
458
514
  }, { rootDir });
459
515
  for (const stepId of ['inspect-authority', 'classify-repository', 'scaffold-authority']) {
@@ -461,33 +517,49 @@ function ensureInitializationWorkflows(db, baseline, { rootDir }) {
461
517
  id: initRun.id, step_id: stepId, status: 'completed',
462
518
  }, { rootDir });
463
519
  }
464
- }
465
-
466
- if (baseline.mode !== 'migrated') {
467
- if (!researchRun) {
468
- const researchMode = baseline.mode === 'brownfield' ? 'adoption' : 'full';
469
- researchRun = workflows.startWorkflow(db, {
470
- kind: 'research', mode: researchMode, baseline_id: baseline.id,
471
- subject: baseline.mode === 'brownfield'
472
- ? 'Adopt the existing system as an evidence-backed product and architecture baseline.'
473
- : 'Validate the complete product and architecture baseline before planning.',
474
- }, { rootDir });
475
- }
476
- }
520
+ initRun = workflows.recordWorkflowStep(db, {
521
+ id: initRun.id,
522
+ step_id: 'verify-initialization',
523
+ status: 'completed',
524
+ evidence: [{
525
+ kind: 'authority',
526
+ ref: '.ultra/state.db',
527
+ summary: 'State authority, scaffold, projection, and repository classification were verified.',
528
+ }],
529
+ }, { rootDir });
530
+ initRun = workflows.completeWorkflow(db, { id: initRun.id }, { rootDir });
531
+ }
532
+ const compatibilityOnly = baseline.mode === 'migrated';
533
+ const provenanceComplete = !compatibilityOnly
534
+ && baseline.status === 'ready'
535
+ && initRun.status === 'completed'
536
+ && researchRun?.status === 'completed'
537
+ && baseline.research_run_id === researchRun.id;
538
+ const readyWithoutProvenance = baseline.status === 'ready' && !provenanceComplete;
539
+ const allowedTransitions = provenanceComplete
540
+ ? ['ultra-change', 'ultra-research', 'ultra-status', 'ultra-doctor']
541
+ : (readyWithoutProvenance
542
+ ? ['ultra-doctor', 'ultra-research', 'ultra-status']
543
+ : ['ultra-research', 'ultra-status', 'ultra-doctor']);
477
544
  return {
478
545
  init_id: initRun.id,
479
546
  init_status: initRun.status,
480
547
  research_id: researchRun?.id || null,
481
548
  research_mode: researchRun?.mode || null,
482
549
  research_status: researchRun?.status || 'not_started',
483
- provenance_status: 'in_progress',
484
- next_action: baseline.mode === 'migrated' ? 'ultra-init' : 'ultra-research',
550
+ provenance_status: compatibilityOnly
551
+ ? 'compatibility_only'
552
+ : (provenanceComplete ? 'complete' : (readyWithoutProvenance ? 'incomplete' : 'pending_research')),
553
+ allowed_transitions: allowedTransitions,
554
+ required_transition: readyWithoutProvenance
555
+ ? 'ultra-doctor'
556
+ : (compatibilityOnly ? 'ultra-research' : null),
485
557
  };
486
558
  }
487
559
 
488
560
  function resumeProject({
489
561
  absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
490
- scope, repositoryProfile, projection,
562
+ scope, repositoryProfile, projection, git_mode,
491
563
  }) {
492
564
  const stateDbPath = path.join(ultraDir, 'state.db');
493
565
  const stateExisted = fs.existsSync(stateDbPath);
@@ -495,12 +567,19 @@ function resumeProject({
495
567
  let copiedFiles = [];
496
568
  let baselineResult;
497
569
  let workflowResult;
570
+ let gitSetup;
571
+ let resumeSnapshot;
498
572
  try {
499
573
  initialized = initStateDb(stateDbPath);
500
574
  const { db } = initialized;
575
+ if (stateExisted) {
576
+ resumeSnapshot = createResumeSnapshot(db, ultraDir, stateDbPath);
577
+ }
501
578
  if (projection && projection.taskCount > 0) assertStateAuthority(db, absTarget);
502
579
  copiedFiles = copyMissingTemplate(source, ultraDir);
503
580
  validateTasksTemplate(ultraDir);
581
+ gitSetup = gitBootstrap.bootstrapGit(absTarget, { mode: git_mode });
582
+ repositoryProfile.git = gitSetup.result;
504
583
  baselineResult = baselines.readBaseline(db);
505
584
  if (!baselineResult) {
506
585
  baselineResult = baselines.startBaseline(db, {
@@ -509,23 +588,42 @@ function resumeProject({
509
588
  scope: scope === undefined ? ['.'] : scope,
510
589
  classification: repositoryProfile,
511
590
  }, { rootDir: absTarget });
591
+ } else {
592
+ baselineResult = baselines.refreshInProgressBaseline(db, {
593
+ id: baselineResult.id,
594
+ project_name,
595
+ project_type,
596
+ stack,
597
+ classification: repositoryProfile,
598
+ }, { rootDir: absTarget });
512
599
  }
513
600
  workflowResult = ensureInitializationWorkflows(db, baselineResult, { rootDir: absTarget });
514
601
  ops.appendEvent(db, {
515
602
  type: 'project_resumed',
516
603
  payload: {
517
604
  project_name, mode: baselineResult.mode, baseline_id: baselineResult.id,
518
- installed_missing_files: copiedFiles.length,
605
+ installed_missing_files: copiedFiles.length, git: gitSetup.result,
519
606
  },
520
607
  });
521
608
  const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
522
609
  const projectionResult = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
523
610
  const projected = projectionResult.jobs.find((item) => item.id === job.id);
524
611
  if (!projected || projected.status !== 'completed') {
525
- throw new Error(projected?.error || 'resume projection did not complete');
612
+ throw new Error(
613
+ `resume projection did not complete: ${projected?.error || 'projection job missing'}`,
614
+ );
526
615
  }
527
616
  } catch (error) {
528
617
  if (initialized?.db?.open) initialized.db.close();
618
+ const stateRollbackErrors = stateExisted
619
+ ? restoreResumeSnapshot(resumeSnapshot, {
620
+ // initStateDb creates this durable snapshot before a schema migration.
621
+ // A later resume failure must return to the exact entry authority, not
622
+ // merely to the successfully migrated intermediate database.
623
+ stateSourcePath: initialized?.backup_path || null,
624
+ })
625
+ : [];
626
+ const gitRollbackErrors = gitBootstrap.rollbackGitBootstrap(absTarget, gitSetup?.internal);
529
627
  for (const rel of copiedFiles.reverse()) {
530
628
  try { fs.rmSync(path.join(ultraDir, rel), { force: true }); } catch { /* best effort */ }
531
629
  }
@@ -534,15 +632,27 @@ function resumeProject({
534
632
  try { fs.rmSync(`${stateDbPath}${suffix}`, { force: true }); } catch { /* best effort */ }
535
633
  }
536
634
  }
635
+ const rollbackErrors = [...stateRollbackErrors, ...gitRollbackErrors];
537
636
  if (error.code && ['LEGACY_STATE_MIGRATION_REQUIRED', 'STATE_AUTHORITY_CONFLICT', 'STATE_PROJECTION_INVALID'].includes(error.code)) {
538
637
  throw new InitProjectError(error.code, error.message, { cause: error, details: error.details });
539
638
  }
639
+ if (error instanceof gitBootstrap.GitBootstrapError) {
640
+ throw new InitProjectError(
641
+ error.code,
642
+ `${error.message}${rollbackErrors.length ? `; ${rollbackErrors.join('; ')}` : ''}`,
643
+ { cause: error },
644
+ );
645
+ }
540
646
  throw new InitProjectError(
541
- 'IO_ERROR', `project resume failed: ${error.message}; existing .ultra assets were preserved`,
647
+ 'IO_ERROR',
648
+ `project resume failed: ${error.message}; `
649
+ + `${rollbackErrors.length ? 'resume rollback was incomplete' : 'existing authority and projections were restored'}`
650
+ + `${rollbackErrors.length ? `; ${rollbackErrors.join('; ')}` : ''}`,
542
651
  { retriable: true, cause: error },
543
652
  );
544
653
  }
545
654
  initialized.db.close();
655
+ discardResumeSnapshot(resumeSnapshot);
546
656
  const result = {
547
657
  created_path: ultraDir,
548
658
  state_db_path: stateDbPath,
@@ -552,13 +662,17 @@ function resumeProject({
552
662
  id: baselineResult.id,
553
663
  mode: baselineResult.mode,
554
664
  status: baselineResult.status,
665
+ project_name: baselineResult.project_name,
555
666
  project_type: baselineResult.project_type,
556
667
  stack: baselineResult.stack,
557
668
  repository_revision: baselineResult.repository_revision,
669
+ repository_branch: baselineResult.repository_branch,
670
+ worktree_state: baselineResult.worktree_state,
558
671
  scope: baselineResult.scope,
559
672
  },
560
673
  copied_files: copiedFiles,
561
674
  repository_profile: repositoryProfile,
675
+ git: gitSetup.result,
562
676
  workflow: workflowResult,
563
677
  };
564
678
  if (initialized.backup_path) result.migration_backup_path = initialized.backup_path;
@@ -574,6 +688,7 @@ function initProject({
574
688
  scope,
575
689
  resume = false,
576
690
  overwrite = false,
691
+ git_mode = 'auto',
577
692
  source_template,
578
693
  } = {}) {
579
694
  if (typeof project_name !== 'string' || project_name.length === 0) {
@@ -582,6 +697,12 @@ function initProject({
582
697
  if (resume && overwrite) {
583
698
  throw new InitProjectError('VALIDATION_ERROR', 'resume and overwrite are mutually exclusive');
584
699
  }
700
+ if (!gitBootstrap.GIT_MODES.has(git_mode)) {
701
+ throw new InitProjectError(
702
+ 'VALIDATION_ERROR',
703
+ `git_mode must be one of: ${[...gitBootstrap.GIT_MODES].join(', ')}`,
704
+ );
705
+ }
585
706
 
586
707
  const absTarget = resolveTargetDir(target_dir);
587
708
  ensureTargetDir(absTarget);
@@ -611,7 +732,7 @@ function initProject({
611
732
  return resumeProject({
612
733
  absTarget, ultraDir, source, project_name, project_type: resolvedProjectType,
613
734
  stack: resolvedStack, resolvedMode,
614
- scope, repositoryProfile, projection,
735
+ scope, repositoryProfile, projection, git_mode,
615
736
  });
616
737
  }
617
738
 
@@ -630,10 +751,13 @@ function initProject({
630
751
  let copiedFiles;
631
752
  let baselineResult;
632
753
  let workflowResult;
754
+ let gitSetup;
633
755
  try {
634
756
  fs.mkdirSync(ultraDir, { recursive: true });
635
757
  copiedFiles = copyTemplate(source, ultraDir);
636
758
  validateTasksTemplate(ultraDir);
759
+ gitSetup = gitBootstrap.bootstrapGit(absTarget, { mode: git_mode });
760
+ repositoryProfile.git = gitSetup.result;
637
761
  const stateDbPath = path.join(ultraDir, 'state.db');
638
762
  const { db } = initStateDb(stateDbPath);
639
763
  try {
@@ -649,6 +773,7 @@ function initProject({
649
773
  type: 'project_initialized',
650
774
  payload: {
651
775
  project_name, mode: resolvedMode, baseline_id: baselineResult.id,
776
+ git: gitSetup.result,
652
777
  },
653
778
  });
654
779
  const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
@@ -662,9 +787,17 @@ function initProject({
662
787
  }
663
788
  } catch (err) {
664
789
  const rollbackErrors = rollbackInitialization(ultraDir, backupPath);
790
+ rollbackErrors.push(...gitBootstrap.rollbackGitBootstrap(absTarget, gitSetup?.internal));
665
791
  const rollbackNote = rollbackErrors.length > 0
666
792
  ? `; rollback incomplete: ${rollbackErrors.join('; ')}`
667
793
  : '; prior state restored';
794
+ if (err instanceof gitBootstrap.GitBootstrapError) {
795
+ throw new InitProjectError(
796
+ err.code,
797
+ `project initialization failed: ${err.message}${rollbackNote}`,
798
+ { cause: err },
799
+ );
800
+ }
668
801
  throw new InitProjectError(
669
802
  'IO_ERROR',
670
803
  `project initialization failed: ${err.message}${rollbackNote}`,
@@ -682,13 +815,17 @@ function initProject({
682
815
  id: baselineResult.id,
683
816
  mode: baselineResult.mode,
684
817
  status: baselineResult.status,
818
+ project_name: baselineResult.project_name,
685
819
  project_type: baselineResult.project_type,
686
820
  stack: baselineResult.stack,
687
821
  repository_revision: baselineResult.repository_revision,
822
+ repository_branch: baselineResult.repository_branch,
823
+ worktree_state: baselineResult.worktree_state,
688
824
  scope: baselineResult.scope,
689
825
  },
690
826
  copied_files: copiedFiles,
691
827
  repository_profile: repositoryProfile,
828
+ git: gitSetup.result,
692
829
  workflow: workflowResult,
693
830
  };
694
831
  if (backupPath) result.backup_path = backupPath;
@@ -9,8 +9,8 @@ const { EXPECTED_VERSION, REQUIRED_TABLES } = require('./state-db.cjs');
9
9
  const { readBreadcrumb } = require('./context-spine.cjs');
10
10
 
11
11
  function blockedBreadcrumb(code, {
12
- nextAction,
13
- workflow,
12
+ allowedTransitions = [],
13
+ requiredTransition = null,
14
14
  } = {}) {
15
15
  return {
16
16
  change_id: null,
@@ -20,8 +20,8 @@ function blockedBreadcrumb(code, {
20
20
  readiness: 'blocked',
21
21
  blockers: [code],
22
22
  warnings: [],
23
- next_action: nextAction,
24
- recommended_workflow: workflow,
23
+ allowed_transitions: allowedTransitions,
24
+ required_transition: requiredTransition,
25
25
  context_manifest_path: null,
26
26
  context_manifest_hash: null,
27
27
  git_head: null,
@@ -31,15 +31,15 @@ function blockedBreadcrumb(code, {
31
31
 
32
32
  function migrationBreadcrumb(code) {
33
33
  return blockedBreadcrumb(code, {
34
- nextAction: 'Resume project initialization to migrate and re-establish the Ultra baseline.',
35
- workflow: 'ultra-init',
34
+ allowedTransitions: ['ultra-init', 'ultra-status'],
35
+ requiredTransition: 'ultra-init',
36
36
  });
37
37
  }
38
38
 
39
39
  function doctorBreadcrumb(code) {
40
40
  return blockedBreadcrumb(code, {
41
- nextAction: 'Run ultra-doctor to inspect and recover the authoritative Ultra state database.',
42
- workflow: 'ultra-doctor',
41
+ allowedTransitions: ['ultra-doctor', 'ultra-status'],
42
+ requiredTransition: 'ultra-doctor',
43
43
  });
44
44
  }
45
45
 
@@ -62,8 +62,8 @@ function readProjectBreadcrumb(rootDir) {
62
62
  const dbPath = path.join(ultraDir, 'state.db');
63
63
  if (!fs.existsSync(dbPath)) {
64
64
  return blockedBreadcrumb('STATE_DB_MISSING', {
65
- nextAction: 'Initialize or resume this Ultra project through ultra-init.',
66
- workflow: 'ultra-init',
65
+ allowedTransitions: ['ultra-init', 'ultra-status'],
66
+ requiredTransition: 'ultra-init',
67
67
  });
68
68
  }
69
69
 
@@ -135,10 +135,10 @@ function renderProjectBreadcrumb(rootDir, breadcrumb) {
135
135
  : 'Decision: checkpoint confirmation required',
136
136
  );
137
137
  }
138
- lines.push(
139
- `Next: ${breadcrumb.next_action || 'Inspect Ultra status.'}`,
140
- `Route: ${breadcrumb.recommended_workflow || 'ultra-doctor'}`,
141
- );
138
+ lines.push(`Allowed transitions: ${(breadcrumb.allowed_transitions || []).join(', ') || 'none'}`);
139
+ if (breadcrumb.required_transition) {
140
+ lines.push(`Required transition: ${breadcrumb.required_transition}`);
141
+ }
142
142
  if (breadcrumb.context_manifest_path) {
143
143
  lines.push(
144
144
  `Context: ${breadcrumb.context_manifest_path} sha256=${breadcrumb.context_manifest_hash || 'unknown'}`,