ultra-builder-pro-cli 0.12.1 → 0.13.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/docs/research/README.md +21 -44
- package/.ultra-template/specs/discovery.md +10 -0
- package/CHANGELOG.md +53 -1
- package/README.md +25 -9
- package/adapters/_shared/handbook.cjs +2 -2
- package/adapters/tests/codex.test.cjs +8 -0
- package/commands/ultra-init.md +1 -1
- package/docs/AGENT-CONTEXT.md +4 -3
- package/docs/ARCHITECTURE.md +32 -16
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +1 -1
- package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
- package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
- package/hooks/context_spine.py +37 -4
- package/mcp-server/lib/archive-journal.cjs +192 -0
- package/mcp-server/lib/archive-journal.test.cjs +35 -0
- package/mcp-server/lib/baseline-workflow.cjs +404 -38
- package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
- package/mcp-server/lib/change-workflow.cjs +249 -75
- package/mcp-server/lib/change-workflow.test.cjs +235 -1
- package/mcp-server/lib/context-spine.test.cjs +5 -0
- package/mcp-server/lib/doctor.cjs +14 -1
- package/mcp-server/lib/doctor.test.cjs +42 -4
- package/mcp-server/lib/init-project.cjs +292 -9
- package/mcp-server/lib/spec-learning.test.cjs +1 -1
- package/mcp-server/lib/state-authority.cjs +9 -3
- package/mcp-server/lib/state-db.cjs +154 -32
- package/mcp-server/lib/state-db.test.cjs +136 -4
- package/mcp-server/lib/state-ops.cjs +68 -6
- package/mcp-server/lib/task-expander.cjs +7 -1
- package/mcp-server/lib/task-expander.test.cjs +71 -0
- package/mcp-server/server.cjs +14 -3
- package/mcp-server/tests/init-project.test.cjs +162 -1
- package/mcp-server/tests/migrate.test.cjs +33 -0
- package/mcp-server/tests/server-contract.test.cjs +384 -6
- package/mcp-server/tests/state-db.test.cjs +14 -2
- package/mcp-server/tests/state-ops.test.cjs +108 -0
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +18 -10
- package/skills/ultra-deliver/SKILL.md +13 -9
- package/skills/ultra-dev/SKILL.md +6 -4
- package/skills/ultra-doctor/SKILL.md +22 -7
- package/skills/ultra-init/SKILL.md +104 -59
- package/skills/ultra-init/references/brownfield-adoption.md +65 -37
- package/skills/ultra-plan/SKILL.md +8 -5
- package/skills/ultra-research/SKILL.md +12 -5
- package/skills/ultra-status/SKILL.md +6 -4
- package/spec/cli-protocol.md +11 -1
- package/spec/mcp-tools.yaml +95 -9
- package/spec/schemas/state-db.sql +11 -0
- package/ultra-tools/cli.cjs +9 -5
- package/ultra-tools/commands/migrate.cjs +62 -36
- package/ultra-tools/commands/status.cjs +18 -9
- package/ultra-tools/commands/system.cjs +340 -6
- package/ultra-tools/commands/task.cjs +7 -1
- package/ultra-tools/status-command.test.cjs +29 -0
- package/ultra-tools/system-command.test.cjs +252 -1
|
@@ -15,21 +15,26 @@ const fs = require('node:fs');
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
|
|
17
17
|
const { initStateDb } = require('./state-db.cjs');
|
|
18
|
+
const { assertStateAuthority, inspectProjection } = require('./state-authority.cjs');
|
|
18
19
|
const baselines = require('./baseline-workflow.cjs');
|
|
19
20
|
const runtimeState = require('./runtime-state.cjs');
|
|
21
|
+
const ops = require('./state-ops.cjs');
|
|
20
22
|
|
|
21
23
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
22
24
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
23
25
|
: path.resolve(__dirname, '..', '..');
|
|
24
|
-
const
|
|
26
|
+
const SOURCE_TEMPLATE = path.join(REPO_ROOT, 'templates', '.ultra');
|
|
27
|
+
const PACKAGED_TEMPLATE = path.join(REPO_ROOT, '.ultra-template');
|
|
28
|
+
const DEFAULT_TEMPLATE = fs.existsSync(SOURCE_TEMPLATE) ? SOURCE_TEMPLATE : PACKAGED_TEMPLATE;
|
|
25
29
|
|
|
26
30
|
class InitProjectError extends Error {
|
|
27
|
-
constructor(code, message, { retriable = false, cause } = {}) {
|
|
31
|
+
constructor(code, message, { retriable = false, cause, details } = {}) {
|
|
28
32
|
super(message);
|
|
29
33
|
this.name = 'InitProjectError';
|
|
30
34
|
this.code = code;
|
|
31
35
|
this.retriable = retriable;
|
|
32
36
|
if (cause) this.cause = cause;
|
|
37
|
+
if (details) this.details = details;
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -97,6 +102,18 @@ function copyTemplate(source, dest) {
|
|
|
97
102
|
return files;
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
function copyMissingTemplate(source, dest) {
|
|
106
|
+
const copied = [];
|
|
107
|
+
for (const rel of walkRelative(source)) {
|
|
108
|
+
const to = path.join(dest, rel);
|
|
109
|
+
if (fs.existsSync(to)) continue;
|
|
110
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
111
|
+
fs.copyFileSync(path.join(source, rel), to);
|
|
112
|
+
copied.push(rel);
|
|
113
|
+
}
|
|
114
|
+
return copied;
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
function validateTasksTemplate(ultraDir) {
|
|
101
118
|
const tasksFile = path.join(ultraDir, 'tasks', 'tasks.json');
|
|
102
119
|
if (!fs.existsSync(tasksFile)) return;
|
|
@@ -112,15 +129,150 @@ const IGNORED_ADOPTION_ENTRIES = new Set([
|
|
|
112
129
|
'.DS_Store', '.git', '.gitignore', '.ultra', 'node_modules', 'vendor', '.venv', 'venv',
|
|
113
130
|
]);
|
|
114
131
|
|
|
132
|
+
const IGNORED_SCAN_DIRS = new Set([
|
|
133
|
+
...IGNORED_ADOPTION_ENTRIES, 'dist', 'build', 'coverage', '.next', '.turbo', 'target',
|
|
134
|
+
]);
|
|
135
|
+
const MANIFEST_NAMES = new Set([
|
|
136
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', 'go.work',
|
|
137
|
+
'pom.xml', 'build.gradle', 'build.gradle.kts', 'Gemfile', 'composer.json',
|
|
138
|
+
]);
|
|
139
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
140
|
+
'.c', '.cc', '.cpp', '.cs', '.go', '.java', '.js', '.jsx', '.kt', '.kts', '.mjs',
|
|
141
|
+
'.move', '.php', '.py', '.rb', '.rs', '.sol', '.swift', '.ts', '.tsx', '.vue',
|
|
142
|
+
]);
|
|
143
|
+
const SOURCE_DIR_SEGMENTS = new Set([
|
|
144
|
+
'src', 'app', 'apps', 'lib', 'server', 'backend', 'frontend', 'api', 'cmd', 'internal',
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
function scanRepositoryFiles(rootDir, { maxFiles = 5000, maxDepth = 8 } = {}) {
|
|
148
|
+
const files = [];
|
|
149
|
+
function walk(dir, depth) {
|
|
150
|
+
if (files.length >= maxFiles || depth > maxDepth) return;
|
|
151
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
152
|
+
if (files.length >= maxFiles) break;
|
|
153
|
+
if (entry.name === '.DS_Store') continue;
|
|
154
|
+
const full = path.join(dir, entry.name);
|
|
155
|
+
const rel = path.relative(rootDir, full).split(path.sep).join('/');
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
if (!IGNORED_SCAN_DIRS.has(entry.name) && !entry.name.startsWith('.ultra.backup.')) {
|
|
158
|
+
walk(full, depth + 1);
|
|
159
|
+
}
|
|
160
|
+
} else if (entry.isFile()) files.push(rel);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
walk(rootDir, 0);
|
|
164
|
+
return { files: files.sort(), truncated: files.length >= maxFiles };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function expandWorkspacePattern(rootDir, pattern) {
|
|
168
|
+
const normalized = String(pattern || '').trim().replace(/^['"]|['"]$/g, '').replace(/\\/g, '/');
|
|
169
|
+
if (!normalized) return [];
|
|
170
|
+
if (!normalized.includes('*')) {
|
|
171
|
+
const candidate = path.join(rootDir, normalized);
|
|
172
|
+
return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory() ? [normalized] : [];
|
|
173
|
+
}
|
|
174
|
+
if (!normalized.endsWith('/*') || normalized.slice(0, -2).includes('*')) return [];
|
|
175
|
+
const parentRel = normalized.slice(0, -2);
|
|
176
|
+
const parent = path.join(rootDir, parentRel);
|
|
177
|
+
if (!fs.existsSync(parent) || !fs.statSync(parent).isDirectory()) return [];
|
|
178
|
+
return fs.readdirSync(parent, { withFileTypes: true })
|
|
179
|
+
.filter((entry) => entry.isDirectory())
|
|
180
|
+
.map((entry) => `${parentRel}/${entry.name}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function packageProfile(rootDir) {
|
|
184
|
+
const file = path.join(rootDir, 'package.json');
|
|
185
|
+
if (!fs.existsSync(file)) return { workspacePatterns: [], verificationCommands: [] };
|
|
186
|
+
try {
|
|
187
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
188
|
+
const rawWorkspaces = Array.isArray(pkg.workspaces)
|
|
189
|
+
? pkg.workspaces
|
|
190
|
+
: (Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : []);
|
|
191
|
+
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
192
|
+
const verificationCommands = ['build', 'test', 'lint', 'typecheck']
|
|
193
|
+
.filter((name) => typeof scripts[name] === 'string' && scripts[name].trim())
|
|
194
|
+
.map((name) => name === 'test' ? 'npm test' : `npm run ${name}`);
|
|
195
|
+
return { workspacePatterns: rawWorkspaces, verificationCommands };
|
|
196
|
+
} catch {
|
|
197
|
+
return { workspacePatterns: [], verificationCommands: [] };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function classifyRepository(absTarget) {
|
|
202
|
+
const { files, truncated } = scanRepositoryFiles(absTarget);
|
|
203
|
+
const packageData = packageProfile(absTarget);
|
|
204
|
+
const manifestSignals = files.filter((file) => MANIFEST_NAMES.has(path.basename(file))).slice(0, 100);
|
|
205
|
+
const testSignals = files.filter((file) => {
|
|
206
|
+
const lower = file.toLowerCase();
|
|
207
|
+
return /(^|\/)(test|tests|__tests__|spec)(\/|$)/.test(lower)
|
|
208
|
+
|| /\.(test|spec)\.[^.]+$/.test(lower);
|
|
209
|
+
}).slice(0, 100);
|
|
210
|
+
const deploymentSignals = files.filter((file) => {
|
|
211
|
+
const lower = file.toLowerCase();
|
|
212
|
+
return /(^|\/)(dockerfile|railway\.json|railway\.toml|vercel\.json|fly\.toml)$/.test(lower)
|
|
213
|
+
|| lower.startsWith('.github/workflows/')
|
|
214
|
+
|| /(^|\/)docker-compose[^/]*\.ya?ml$/.test(lower);
|
|
215
|
+
}).slice(0, 100);
|
|
216
|
+
const databaseSignals = files.filter((file) => {
|
|
217
|
+
const lower = file.toLowerCase();
|
|
218
|
+
return /(^|\/)(migrations?|prisma|schema)(\/|$)/.test(lower)
|
|
219
|
+
|| /(^|\/)schema\.(sql|prisma)$/.test(lower);
|
|
220
|
+
}).slice(0, 100);
|
|
221
|
+
const sourceSignals = files.filter((file) => {
|
|
222
|
+
const lower = file.toLowerCase();
|
|
223
|
+
if (testSignals.includes(file) || lower.endsWith('.config.js') || lower.endsWith('.config.ts')) return false;
|
|
224
|
+
const segments = file.split('/');
|
|
225
|
+
const ext = path.extname(file);
|
|
226
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return false;
|
|
227
|
+
return segments.some((segment) => SOURCE_DIR_SEGMENTS.has(segment))
|
|
228
|
+
|| /(^|\/)(main|index|server|app)\.[^.]+$/.test(lower);
|
|
229
|
+
}).slice(0, 200);
|
|
230
|
+
const workspaceRoots = [...new Set(packageData.workspacePatterns
|
|
231
|
+
.flatMap((pattern) => expandWorkspacePattern(absTarget, pattern)))]
|
|
232
|
+
.sort();
|
|
233
|
+
const workspaceMarkers = [];
|
|
234
|
+
if (packageData.workspacePatterns.length > 0) workspaceMarkers.push('package.json#workspaces');
|
|
235
|
+
for (const marker of ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'go.work']) {
|
|
236
|
+
if (files.includes(marker)) workspaceMarkers.push(marker);
|
|
237
|
+
}
|
|
238
|
+
if (workspaceRoots.length === 0 && files.some((file) => /^packages\/[^/]+\/package\.json$/.test(file))) {
|
|
239
|
+
workspaceRoots.push(...files
|
|
240
|
+
.filter((file) => /^packages\/[^/]+\/package\.json$/.test(file))
|
|
241
|
+
.map((file) => path.posix.dirname(file)));
|
|
242
|
+
workspaceRoots.sort();
|
|
243
|
+
}
|
|
244
|
+
const reasons = [];
|
|
245
|
+
if (sourceSignals.length > 0) reasons.push('SOURCE_PRESENT');
|
|
246
|
+
if (testSignals.length > 0) reasons.push('TESTS_PRESENT');
|
|
247
|
+
if (deploymentSignals.length > 0) reasons.push('DEPLOYMENT_PRESENT');
|
|
248
|
+
if (databaseSignals.length > 0) reasons.push('PERSISTED_STATE_PRESENT');
|
|
249
|
+
if (workspaceMarkers.length > 0 || workspaceRoots.length > 0) reasons.push('MONOREPO_PRESENT');
|
|
250
|
+
if (reasons.length === 0) reasons.push('SKELETON_ONLY');
|
|
251
|
+
const mode = sourceSignals.length > 0 || testSignals.length > 0
|
|
252
|
+
|| deploymentSignals.length > 0 || databaseSignals.length > 0
|
|
253
|
+
? 'brownfield' : 'greenfield';
|
|
254
|
+
return {
|
|
255
|
+
mode,
|
|
256
|
+
reasons,
|
|
257
|
+
repository_kind: workspaceMarkers.length > 0 || workspaceRoots.length > 0 ? 'monorepo' : 'single',
|
|
258
|
+
workspace_markers: workspaceMarkers,
|
|
259
|
+
workspace_roots: [...new Set(workspaceRoots)],
|
|
260
|
+
manifest_signals: manifestSignals,
|
|
261
|
+
source_signals: sourceSignals,
|
|
262
|
+
test_signals: testSignals,
|
|
263
|
+
deployment_signals: deploymentSignals,
|
|
264
|
+
database_signals: databaseSignals,
|
|
265
|
+
verification_commands: packageData.verificationCommands,
|
|
266
|
+
scan_truncated: truncated,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
115
270
|
function detectProjectMode(absTarget, requested = 'auto') {
|
|
116
271
|
if (!['auto', 'greenfield', 'brownfield'].includes(requested)) {
|
|
117
272
|
throw new InitProjectError('VALIDATION_ERROR', `unsupported initialization mode: ${requested}`);
|
|
118
273
|
}
|
|
119
274
|
if (requested !== 'auto') return requested;
|
|
120
|
-
|
|
121
|
-
.some((entry) => !IGNORED_ADOPTION_ENTRIES.has(entry.name)
|
|
122
|
-
&& !entry.name.startsWith('.ultra.backup.'));
|
|
123
|
-
return meaningful ? 'brownfield' : 'greenfield';
|
|
275
|
+
return classifyRepository(absTarget).mode;
|
|
124
276
|
}
|
|
125
277
|
|
|
126
278
|
function rollbackInitialization(ultraDir, backupPath) {
|
|
@@ -137,25 +289,144 @@ function rollbackInitialization(ultraDir, backupPath) {
|
|
|
137
289
|
return errors;
|
|
138
290
|
}
|
|
139
291
|
|
|
292
|
+
function projectionMigrationError(projection) {
|
|
293
|
+
const targets = { '4.4': '4.5', '4.5': '12.0' };
|
|
294
|
+
const target = targets[projection.version];
|
|
295
|
+
if (target) {
|
|
296
|
+
return new InitProjectError(
|
|
297
|
+
'LEGACY_STATE_MIGRATION_REQUIRED',
|
|
298
|
+
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} v${projection.version} tasks; `
|
|
299
|
+
+ `run ultra-tools migrate --from=${projection.version} --to=${target} --source-dir <project-root> before resuming initialization`,
|
|
300
|
+
{ details: { projection_path: projection.path, projection_version: projection.version, legacy_task_count: projection.taskCount } },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return new InitProjectError(
|
|
304
|
+
'STATE_AUTHORITY_CONFLICT',
|
|
305
|
+
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} tasks with unsupported projection version ${projection.version}`,
|
|
306
|
+
{ details: { projection_path: projection.path, projection_version: projection.version, legacy_task_count: projection.taskCount } },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function resumeProject({
|
|
311
|
+
absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
|
|
312
|
+
scope, repositoryProfile, projection,
|
|
313
|
+
}) {
|
|
314
|
+
const stateDbPath = path.join(ultraDir, 'state.db');
|
|
315
|
+
const stateExisted = fs.existsSync(stateDbPath);
|
|
316
|
+
let initialized;
|
|
317
|
+
let copiedFiles = [];
|
|
318
|
+
let baselineResult;
|
|
319
|
+
try {
|
|
320
|
+
initialized = initStateDb(stateDbPath);
|
|
321
|
+
const { db } = initialized;
|
|
322
|
+
if (projection && projection.taskCount > 0) assertStateAuthority(db, absTarget);
|
|
323
|
+
copiedFiles = copyMissingTemplate(source, ultraDir);
|
|
324
|
+
validateTasksTemplate(ultraDir);
|
|
325
|
+
baselineResult = baselines.readBaseline(db);
|
|
326
|
+
if (!baselineResult) {
|
|
327
|
+
baselineResult = baselines.startBaseline(db, {
|
|
328
|
+
id: 'project-baseline', project_name, project_type, stack,
|
|
329
|
+
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
|
|
330
|
+
scope: scope === undefined ? ['.'] : scope,
|
|
331
|
+
classification: repositoryProfile,
|
|
332
|
+
}, { rootDir: absTarget });
|
|
333
|
+
}
|
|
334
|
+
ops.appendEvent(db, {
|
|
335
|
+
type: 'project_resumed',
|
|
336
|
+
payload: {
|
|
337
|
+
project_name, mode: baselineResult.mode, baseline_id: baselineResult.id,
|
|
338
|
+
installed_missing_files: copiedFiles.length,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
|
|
342
|
+
const projectionResult = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
|
|
343
|
+
const projected = projectionResult.jobs.find((item) => item.id === job.id);
|
|
344
|
+
if (!projected || projected.status !== 'completed') {
|
|
345
|
+
throw new Error(projected?.error || 'resume projection did not complete');
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (initialized?.db?.open) initialized.db.close();
|
|
349
|
+
for (const rel of copiedFiles.reverse()) {
|
|
350
|
+
try { fs.rmSync(path.join(ultraDir, rel), { force: true }); } catch { /* best effort */ }
|
|
351
|
+
}
|
|
352
|
+
if (!stateExisted) {
|
|
353
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
354
|
+
try { fs.rmSync(`${stateDbPath}${suffix}`, { force: true }); } catch { /* best effort */ }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (error.code && ['LEGACY_STATE_MIGRATION_REQUIRED', 'STATE_AUTHORITY_CONFLICT', 'STATE_PROJECTION_INVALID'].includes(error.code)) {
|
|
358
|
+
throw new InitProjectError(error.code, error.message, { cause: error, details: error.details });
|
|
359
|
+
}
|
|
360
|
+
throw new InitProjectError(
|
|
361
|
+
'IO_ERROR', `project resume failed: ${error.message}; existing .ultra assets were preserved`,
|
|
362
|
+
{ retriable: true, cause: error },
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
initialized.db.close();
|
|
366
|
+
const result = {
|
|
367
|
+
created_path: ultraDir,
|
|
368
|
+
state_db_path: stateDbPath,
|
|
369
|
+
status: 'resumed',
|
|
370
|
+
mode: resolvedMode,
|
|
371
|
+
baseline: {
|
|
372
|
+
id: baselineResult.id,
|
|
373
|
+
mode: baselineResult.mode,
|
|
374
|
+
status: baselineResult.status,
|
|
375
|
+
repository_revision: baselineResult.repository_revision,
|
|
376
|
+
scope: baselineResult.scope,
|
|
377
|
+
},
|
|
378
|
+
copied_files: copiedFiles,
|
|
379
|
+
repository_profile: repositoryProfile,
|
|
380
|
+
};
|
|
381
|
+
if (initialized.backup_path) result.migration_backup_path = initialized.backup_path;
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
140
385
|
function initProject({
|
|
141
386
|
target_dir,
|
|
142
387
|
project_name,
|
|
143
388
|
project_type,
|
|
144
389
|
stack,
|
|
145
390
|
mode = 'auto',
|
|
391
|
+
scope,
|
|
392
|
+
resume = false,
|
|
146
393
|
overwrite = false,
|
|
147
394
|
source_template,
|
|
148
395
|
} = {}) {
|
|
149
396
|
if (typeof project_name !== 'string' || project_name.length === 0) {
|
|
150
397
|
throw new InitProjectError('VALIDATION_ERROR', 'project_name must be non-empty');
|
|
151
398
|
}
|
|
399
|
+
if (resume && overwrite) {
|
|
400
|
+
throw new InitProjectError('VALIDATION_ERROR', 'resume and overwrite are mutually exclusive');
|
|
401
|
+
}
|
|
152
402
|
|
|
153
403
|
const absTarget = resolveTargetDir(target_dir);
|
|
154
404
|
ensureTargetDir(absTarget);
|
|
155
|
-
const
|
|
405
|
+
const repositoryProfile = classifyRepository(absTarget);
|
|
406
|
+
const resolvedMode = mode === 'auto' ? repositoryProfile.mode : detectProjectMode(absTarget, mode);
|
|
407
|
+
repositoryProfile.detected_mode = repositoryProfile.mode;
|
|
408
|
+
repositoryProfile.mode = resolvedMode;
|
|
156
409
|
const source = ensureTemplate(source_template);
|
|
157
410
|
const ultraDir = path.join(absTarget, '.ultra');
|
|
158
411
|
|
|
412
|
+
if (fs.existsSync(ultraDir) && resume) {
|
|
413
|
+
let projection;
|
|
414
|
+
try { projection = inspectProjection(absTarget); }
|
|
415
|
+
catch (error) {
|
|
416
|
+
throw new InitProjectError(error.code || 'STATE_PROJECTION_INVALID', error.message, {
|
|
417
|
+
cause: error, details: error.details,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (projection && projection.taskCount > 0
|
|
421
|
+
&& !fs.existsSync(path.join(ultraDir, 'state.db'))) {
|
|
422
|
+
throw projectionMigrationError(projection);
|
|
423
|
+
}
|
|
424
|
+
return resumeProject({
|
|
425
|
+
absTarget, ultraDir, source, project_name, project_type, stack, resolvedMode,
|
|
426
|
+
scope, repositoryProfile, projection,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
159
430
|
let status = 'created';
|
|
160
431
|
let backupPath;
|
|
161
432
|
if (fs.existsSync(ultraDir)) {
|
|
@@ -179,8 +450,16 @@ function initProject({
|
|
|
179
450
|
try {
|
|
180
451
|
baselineResult = baselines.startBaseline(db, {
|
|
181
452
|
id: 'project-baseline', project_name, project_type, stack,
|
|
182
|
-
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
|
|
453
|
+
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget),
|
|
454
|
+
scope: scope === undefined ? ['.'] : scope,
|
|
455
|
+
classification: repositoryProfile,
|
|
183
456
|
}, { rootDir: absTarget });
|
|
457
|
+
ops.appendEvent(db, {
|
|
458
|
+
type: 'project_initialized',
|
|
459
|
+
payload: {
|
|
460
|
+
project_name, mode: resolvedMode, baseline_id: baselineResult.id,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
184
463
|
const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
|
|
185
464
|
const projection = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
|
|
186
465
|
const projected = projection.jobs.find((item) => item.id === job.id);
|
|
@@ -213,11 +492,15 @@ function initProject({
|
|
|
213
492
|
mode: baselineResult.mode,
|
|
214
493
|
status: baselineResult.status,
|
|
215
494
|
repository_revision: baselineResult.repository_revision,
|
|
495
|
+
scope: baselineResult.scope,
|
|
216
496
|
},
|
|
217
497
|
copied_files: copiedFiles,
|
|
498
|
+
repository_profile: repositoryProfile,
|
|
218
499
|
};
|
|
219
500
|
if (backupPath) result.backup_path = backupPath;
|
|
220
501
|
return result;
|
|
221
502
|
}
|
|
222
503
|
|
|
223
|
-
module.exports = {
|
|
504
|
+
module.exports = {
|
|
505
|
+
initProject, InitProjectError, DEFAULT_TEMPLATE, detectProjectMode, classifyRepository,
|
|
506
|
+
};
|
|
@@ -23,7 +23,7 @@ function fixture() {
|
|
|
23
23
|
db.prepare(
|
|
24
24
|
`INSERT INTO baselines
|
|
25
25
|
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
26
|
-
VALUES ('test-baseline', 'fixture', '
|
|
26
|
+
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
27
27
|
).run(new Date().toISOString());
|
|
28
28
|
changes.createChange(db, {
|
|
29
29
|
id: 'learning-change', title: 'Learning change', kind: 'quick',
|
|
@@ -51,11 +51,17 @@ function assertStateAuthority(db, rootDir) {
|
|
|
51
51
|
projection_path: projection.path,
|
|
52
52
|
projection_version: projection.version,
|
|
53
53
|
};
|
|
54
|
-
|
|
54
|
+
const migrationTargets = {
|
|
55
|
+
'4.4': '4.5',
|
|
56
|
+
'4.5': '12.0',
|
|
57
|
+
};
|
|
58
|
+
const migrationTarget = migrationTargets[projection.version];
|
|
59
|
+
if (migrationTarget) {
|
|
55
60
|
throw new StateAuthorityError(
|
|
56
61
|
'LEGACY_STATE_MIGRATION_REQUIRED',
|
|
57
|
-
`state.db has no tasks but ${projection.path} contains ${projection.taskCount}
|
|
58
|
-
+
|
|
62
|
+
`state.db has no tasks but ${projection.path} contains ${projection.taskCount} v${projection.version} tasks; `
|
|
63
|
+
+ `run ultra-tools migrate --from=${projection.version} --to=${migrationTarget} `
|
|
64
|
+
+ '--source-dir <project-root> before using Ultra MCP tools',
|
|
59
65
|
details,
|
|
60
66
|
);
|
|
61
67
|
}
|