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.
- package/.ultra-template/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +120 -1
- package/README.md +67 -30
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +3 -2
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +38 -18
- package/docs/ARCHITECTURE.md +91 -13
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -12
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +179 -39
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +107 -39
- package/mcp-server/lib/context-spine.cjs +215 -87
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +9 -0
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +23 -14
- package/mcp-server/lib/state-db.cjs +279 -9
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +313 -205
- package/mcp-server/server.cjs +112 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -65
- package/skills/ultra-deliver/SKILL.md +37 -58
- package/skills/ultra-dev/SKILL.md +55 -49
- package/skills/ultra-doctor/SKILL.md +44 -49
- package/skills/ultra-init/SKILL.md +78 -100
- package/skills/ultra-plan/SKILL.md +78 -79
- package/skills/ultra-research/SKILL.md +99 -98
- package/skills/ultra-review/SKILL.md +38 -47
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -40
- package/skills/ultra-test/SKILL.md +61 -48
- package/skills/ultra-think/SKILL.md +47 -47
- package/skills/ultra-think/references/decision-dialogue.md +106 -0
- package/spec/cli-protocol.md +46 -2
- package/spec/mcp-tools.yaml +444 -29
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +66 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +34 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -12,6 +12,7 @@ const baselines = require('./baseline-workflow.cjs');
|
|
|
12
12
|
const changes = require('./change-workflow.cjs');
|
|
13
13
|
const archiveJournal = require('./archive-journal.cjs');
|
|
14
14
|
const workflows = require('./workflow-state.cjs');
|
|
15
|
+
const decisions = require('./decision-dialogue.cjs');
|
|
15
16
|
|
|
16
17
|
const SPEC_CONSUMER = 'spec-staleness';
|
|
17
18
|
|
|
@@ -63,6 +64,12 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
63
64
|
status: 'fail', active: 0, blocked: 0, ready: 0, stale_outputs: [],
|
|
64
65
|
historical_stale_outputs: [], terminal_authority_runs: [], untracked_active_changes: [],
|
|
65
66
|
};
|
|
67
|
+
const decisionHealth = missing.length === 0
|
|
68
|
+
? decisions.inspectDecisionHealth(db, { rootDir })
|
|
69
|
+
: {
|
|
70
|
+
status: 'fail', active: 0, awaiting_owner: 0, checkpoint_ready: 0,
|
|
71
|
+
deferred_blocking: 0, stale_artifacts: [], current: null, current_thread_id: null,
|
|
72
|
+
};
|
|
66
73
|
const baselineCheckStatus = baseline.status === 'pass'
|
|
67
74
|
? 'pass'
|
|
68
75
|
: (baseline.baseline?.status === 'ready' ? 'fail' : 'warning');
|
|
@@ -70,6 +77,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
70
77
|
|| pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
|
|
71
78
|
|| archiveIntents.length > 0
|
|
72
79
|
|| eventCursor > projectedCursor || workflowHealth.status === 'fail'
|
|
80
|
+
|| decisionHealth.status === 'fail'
|
|
73
81
|
|| baselineCheckStatus === 'fail';
|
|
74
82
|
return {
|
|
75
83
|
status: degraded ? 'degraded' : 'healthy',
|
|
@@ -115,6 +123,7 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
|
|
|
115
123
|
terminal_authority_runs: workflowHealth.terminal_authority_runs,
|
|
116
124
|
untracked_active_changes: workflowHealth.untracked_active_changes,
|
|
117
125
|
},
|
|
126
|
+
decisions: decisionHealth,
|
|
118
127
|
external_providers: {
|
|
119
128
|
status: 'pass', ownership: 'external',
|
|
120
129
|
note: 'Ultra stores provider metadata references only and never owns memory or code-graph content.',
|
|
@@ -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
|
-
|
|
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: `
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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:
|
|
484
|
-
|
|
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(
|
|
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',
|
|
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;
|