ultra-builder-pro-cli 0.11.0 → 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/architecture.md +54 -223
- package/.ultra-template/specs/discovery.md +41 -228
- package/.ultra-template/specs/product.md +44 -200
- package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
- package/.ultra-template/tasks/tasks.json +6 -0
- package/CHANGELOG.md +94 -1
- package/README.md +37 -9
- package/adapters/_shared/codex-assets.cjs +1 -1
- package/adapters/_shared/handbook.cjs +5 -4
- package/adapters/_shared/tests/handbook.test.cjs +4 -0
- package/adapters/opencode.js +8 -2
- package/adapters/tests/codex.test.cjs +10 -2
- package/adapters/tests/kimi-hook.test.cjs +1 -1
- package/adapters/tests/opencode.test.cjs +36 -1
- package/commands/ultra-init.md +3 -3
- package/docs/AGENT-CONTEXT.md +17 -8
- package/docs/ARCHITECTURE.md +51 -8
- package/docs/PLAN.zh-CN.md +9 -6
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
- package/docs/STATE-DB-ACCESS-POLICY.md +19 -13
- package/docs/USER-HANDBOOK-CONTRACT.md +9 -5
- package/hooks/README.md +4 -3
- package/hooks/adapters/codex.py +1 -1
- package/hooks/adapters/kimi.py +1 -1
- package/hooks/context_spine.py +153 -16
- package/hooks/health_check.py +11 -0
- package/hooks/pre_stop_check.py +6 -5
- 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 +781 -0
- package/mcp-server/lib/baseline-workflow.test.cjs +477 -0
- package/mcp-server/lib/change-workflow.cjs +256 -85
- package/mcp-server/lib/change-workflow.test.cjs +436 -6
- package/mcp-server/lib/context-spine.cjs +74 -9
- package/mcp-server/lib/context-spine.test.cjs +20 -0
- package/mcp-server/lib/doctor.cjs +28 -1
- package/mcp-server/lib/doctor.test.cjs +63 -1
- package/mcp-server/lib/init-project.cjs +334 -16
- package/mcp-server/lib/provider-refs.cjs +42 -0
- package/mcp-server/lib/spec-learning.test.cjs +5 -0
- package/mcp-server/lib/state-authority.cjs +9 -3
- package/mcp-server/lib/state-db.cjs +202 -21
- package/mcp-server/lib/state-db.test.cjs +140 -3
- 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 +62 -6
- package/mcp-server/tests/init-project.test.cjs +216 -9
- package/mcp-server/tests/migrate.test.cjs +33 -0
- package/mcp-server/tests/server-contract.test.cjs +478 -3
- package/mcp-server/tests/state-db.test.cjs +23 -2
- package/mcp-server/tests/state-ops.test.cjs +108 -0
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +32 -13
- package/skills/ultra-deliver/SKILL.md +19 -5
- package/skills/ultra-dev/SKILL.md +8 -2
- package/skills/ultra-doctor/SKILL.md +30 -9
- package/skills/ultra-init/SKILL.md +107 -66
- package/skills/ultra-init/references/brownfield-adoption.md +93 -0
- package/skills/ultra-plan/SKILL.md +15 -5
- package/skills/ultra-research/SKILL.md +17 -5
- package/skills/ultra-status/SKILL.md +15 -6
- package/spec/cli-protocol.md +15 -1
- package/spec/mcp-tools.yaml +316 -11
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +44 -2
- package/ultra-tools/cli.cjs +9 -5
- package/ultra-tools/commands/migrate.cjs +62 -36
- package/ultra-tools/commands/status.cjs +29 -2
- package/ultra-tools/commands/system.cjs +340 -6
- package/ultra-tools/commands/task.cjs +11 -2
- package/ultra-tools/status-command.test.cjs +49 -0
- package/ultra-tools/system-command.test.cjs +256 -0
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
initStateDb, openStateDb, closeStateDb, MIGRATED_GAPS,
|
|
8
|
+
} = require('../../mcp-server/lib/state-db.cjs');
|
|
7
9
|
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
8
10
|
const projector = require('../../mcp-server/lib/projector.cjs');
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
+
const DEFAULT_FROM = '4.4';
|
|
13
|
+
const SUPPORTED_TRANSITIONS = Object.freeze({
|
|
14
|
+
'4.4': '4.5',
|
|
15
|
+
'4.5': '12.0',
|
|
16
|
+
});
|
|
12
17
|
|
|
13
18
|
// Frozen SQL — values flow through parameter bindings.
|
|
14
19
|
const RECORD_MIGRATION_SQL = "INSERT INTO migration_history (from_version, to_version, direction, status, notes) VALUES (@from, @to, @direction, @status, @notes)";
|
|
@@ -73,7 +78,7 @@ function copyDirSync(src, dst) {
|
|
|
73
78
|
const s = path.join(src, entry.name);
|
|
74
79
|
const d = path.join(dst, entry.name);
|
|
75
80
|
if (entry.name === 'state.db' || entry.name === 'state.db-wal' || entry.name === 'state.db-shm') continue;
|
|
76
|
-
if (entry.name
|
|
81
|
+
if (/^backup-v[^/]+-/.test(entry.name)) continue;
|
|
77
82
|
if (entry.isDirectory()) copyDirSync(s, d);
|
|
78
83
|
else fs.copyFileSync(s, d);
|
|
79
84
|
}
|
|
@@ -148,15 +153,15 @@ function normalizeLegacyTask(task, tasksJson, contextHeaders, sourceDir) {
|
|
|
148
153
|
};
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
function planForward(sourceDir) {
|
|
156
|
+
function planForward(sourceDir, expectedFrom = DEFAULT_FROM) {
|
|
152
157
|
const tasksPath = path.join(sourceDir, '.ultra', 'tasks', 'tasks.json');
|
|
153
158
|
const tasksJson = readJsonOptional(tasksPath);
|
|
154
159
|
if (!tasksJson || !Array.isArray(tasksJson.tasks)) {
|
|
155
160
|
throw new Error(`migrate: tasks.json missing or malformed at ${tasksPath}`);
|
|
156
161
|
}
|
|
157
162
|
const version = String(tasksJson.version || tasksJson.schema_version || '');
|
|
158
|
-
if (version !==
|
|
159
|
-
throw new Error(`migrate: expected v${
|
|
163
|
+
if (version !== expectedFrom) {
|
|
164
|
+
throw new Error(`migrate: expected v${expectedFrom} tasks.json, found ${version || '(missing version)'}`);
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
const contextHeaders = {};
|
|
@@ -173,7 +178,7 @@ function planForward(sourceDir) {
|
|
|
173
178
|
task_id: task.id,
|
|
174
179
|
json_status: task.status,
|
|
175
180
|
context_status: ctx.status,
|
|
176
|
-
resolution:
|
|
181
|
+
resolution: `tasks.json wins (v${expectedFrom} projection migration rule)`,
|
|
177
182
|
});
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -190,6 +195,7 @@ function planForward(sourceDir) {
|
|
|
190
195
|
events: eventList,
|
|
191
196
|
contextHeaders,
|
|
192
197
|
warnings,
|
|
198
|
+
projectName: tasksJson.project?.name || path.basename(sourceDir),
|
|
193
199
|
};
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -241,16 +247,17 @@ function recordMigration(db, { from, to, direction, status, notes }) {
|
|
|
241
247
|
db.prepare(RECORD_MIGRATION_SQL).run({ from, to, direction, status, notes });
|
|
242
248
|
}
|
|
243
249
|
|
|
244
|
-
function ensureBackupName(sourceDir) {
|
|
250
|
+
function ensureBackupName(sourceDir, fromVersion = DEFAULT_FROM) {
|
|
245
251
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
246
|
-
return path.join(sourceDir, '.ultra', `backup-
|
|
252
|
+
return path.join(sourceDir, '.ultra', `backup-v${fromVersion}-${ts}`);
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
function findLatestBackup(sourceDir) {
|
|
255
|
+
function findLatestBackup(sourceDir, fromVersion = DEFAULT_FROM) {
|
|
250
256
|
const ultra = path.join(sourceDir, '.ultra');
|
|
251
257
|
if (!fs.existsSync(ultra)) return null;
|
|
258
|
+
const prefix = `backup-v${fromVersion}-`;
|
|
252
259
|
const candidates = fs.readdirSync(ultra)
|
|
253
|
-
.filter((n) => n.startsWith(
|
|
260
|
+
.filter((n) => n.startsWith(prefix))
|
|
254
261
|
.sort();
|
|
255
262
|
return candidates.length === 0 ? null : path.join(ultra, candidates[candidates.length - 1]);
|
|
256
263
|
}
|
|
@@ -258,9 +265,11 @@ function findLatestBackup(sourceDir) {
|
|
|
258
265
|
function cmdForward(flags) {
|
|
259
266
|
const sourceDir = path.resolve(flags.sourceDir || '.');
|
|
260
267
|
const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
|
|
268
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
269
|
+
const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
|
|
261
270
|
let plan;
|
|
262
271
|
try {
|
|
263
|
-
plan = planForward(sourceDir);
|
|
272
|
+
plan = planForward(sourceDir, fromVersion);
|
|
264
273
|
} catch (err) {
|
|
265
274
|
emit({ ok: false, error: { code: 'MIGRATE_FAILED', message: err.message, retriable: false } });
|
|
266
275
|
return 2;
|
|
@@ -271,8 +280,8 @@ function cmdForward(flags) {
|
|
|
271
280
|
ok: true,
|
|
272
281
|
data: {
|
|
273
282
|
mode: 'dry',
|
|
274
|
-
from:
|
|
275
|
-
to:
|
|
283
|
+
from: fromVersion,
|
|
284
|
+
to: toVersion,
|
|
276
285
|
source_dir: sourceDir,
|
|
277
286
|
db_path: dbPath,
|
|
278
287
|
tasks_to_insert: plan.tasks.length,
|
|
@@ -284,7 +293,7 @@ function cmdForward(flags) {
|
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
// Real run: backup the entire .ultra subtree first.
|
|
287
|
-
const backupDir = ensureBackupName(sourceDir);
|
|
296
|
+
const backupDir = ensureBackupName(sourceDir, fromVersion);
|
|
288
297
|
const ultraSource = path.join(sourceDir, '.ultra');
|
|
289
298
|
copyDirSync(ultraSource, backupDir);
|
|
290
299
|
|
|
@@ -297,9 +306,18 @@ function cmdForward(flags) {
|
|
|
297
306
|
throw new Error(`migrate: refusing to merge into non-empty state.db (tasks=${existing})`);
|
|
298
307
|
}
|
|
299
308
|
const inserted = applyForward(db, plan);
|
|
309
|
+
db.prepare(
|
|
310
|
+
`INSERT INTO baselines
|
|
311
|
+
(id, project_name, mode, status, gaps_json, approval_note)
|
|
312
|
+
VALUES ('migrated-baseline', ?, 'migrated', 'adopting', ?, ?)`,
|
|
313
|
+
).run(
|
|
314
|
+
plan.projectName,
|
|
315
|
+
JSON.stringify(MIGRATED_GAPS),
|
|
316
|
+
`Legacy v${fromVersion} projection imported; evidence-backed owner re-adoption is required`,
|
|
317
|
+
);
|
|
300
318
|
recordMigration(db, {
|
|
301
|
-
from:
|
|
302
|
-
to:
|
|
319
|
+
from: fromVersion,
|
|
320
|
+
to: toVersion,
|
|
303
321
|
direction: 'forward',
|
|
304
322
|
status: 'success',
|
|
305
323
|
notes: `tasks=${inserted.taskInserted} events=${inserted.eventsInserted} warnings=${plan.warnings.length}`,
|
|
@@ -312,8 +330,8 @@ function cmdForward(flags) {
|
|
|
312
330
|
ok: true,
|
|
313
331
|
data: {
|
|
314
332
|
mode: 'apply',
|
|
315
|
-
from:
|
|
316
|
-
to:
|
|
333
|
+
from: fromVersion,
|
|
334
|
+
to: toVersion,
|
|
317
335
|
source_dir: sourceDir,
|
|
318
336
|
db_path: dbPath,
|
|
319
337
|
backup_dir: backupDir,
|
|
@@ -328,8 +346,8 @@ function cmdForward(flags) {
|
|
|
328
346
|
if (db) {
|
|
329
347
|
try {
|
|
330
348
|
recordMigration(db, {
|
|
331
|
-
from:
|
|
332
|
-
to:
|
|
349
|
+
from: fromVersion,
|
|
350
|
+
to: toVersion,
|
|
333
351
|
direction: 'forward',
|
|
334
352
|
status: 'failed',
|
|
335
353
|
notes: err.message,
|
|
@@ -346,9 +364,11 @@ function cmdForward(flags) {
|
|
|
346
364
|
function cmdRollback(flags) {
|
|
347
365
|
const sourceDir = path.resolve(flags.sourceDir || '.');
|
|
348
366
|
const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
|
|
349
|
-
const
|
|
367
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
368
|
+
const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
|
|
369
|
+
const backupDir = findLatestBackup(sourceDir, fromVersion);
|
|
350
370
|
if (!backupDir) {
|
|
351
|
-
emit({ ok: false, error: { code: 'NO_BACKUP', message:
|
|
371
|
+
emit({ ok: false, error: { code: 'NO_BACKUP', message: `no backup-v${fromVersion}-* directory found` } });
|
|
352
372
|
return 2;
|
|
353
373
|
}
|
|
354
374
|
|
|
@@ -358,8 +378,8 @@ function cmdRollback(flags) {
|
|
|
358
378
|
if (fs.existsSync(dbPath)) {
|
|
359
379
|
db = openStateDb(dbPath);
|
|
360
380
|
recordMigration(db, {
|
|
361
|
-
from:
|
|
362
|
-
to:
|
|
381
|
+
from: toVersion,
|
|
382
|
+
to: fromVersion,
|
|
363
383
|
direction: 'rollback',
|
|
364
384
|
status: 'success',
|
|
365
385
|
notes: `restored from ${backupDir}`,
|
|
@@ -376,7 +396,7 @@ function cmdRollback(flags) {
|
|
|
376
396
|
// Restore .ultra contents from backup, leaving the backup itself in place.
|
|
377
397
|
const ultraDir = path.join(sourceDir, '.ultra');
|
|
378
398
|
for (const entry of fs.readdirSync(ultraDir, { withFileTypes: true })) {
|
|
379
|
-
if (entry.name
|
|
399
|
+
if (/^backup-v[^/]+-/.test(entry.name)) continue;
|
|
380
400
|
const target = path.join(ultraDir, entry.name);
|
|
381
401
|
fs.rmSync(target, { recursive: true, force: true });
|
|
382
402
|
}
|
|
@@ -386,8 +406,8 @@ function cmdRollback(flags) {
|
|
|
386
406
|
ok: true,
|
|
387
407
|
data: {
|
|
388
408
|
mode: 'rollback',
|
|
389
|
-
from:
|
|
390
|
-
to:
|
|
409
|
+
from: toVersion,
|
|
410
|
+
to: fromVersion,
|
|
391
411
|
backup_dir: backupDir,
|
|
392
412
|
source_dir: sourceDir,
|
|
393
413
|
},
|
|
@@ -406,29 +426,35 @@ function dispatch(args) {
|
|
|
406
426
|
process.stdout.write(USAGE);
|
|
407
427
|
return 0;
|
|
408
428
|
}
|
|
409
|
-
|
|
410
|
-
|
|
429
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
430
|
+
const expectedTo = SUPPORTED_TRANSITIONS[fromVersion];
|
|
431
|
+
if (!expectedTo) {
|
|
432
|
+
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${fromVersion} unsupported (supported: ${Object.keys(SUPPORTED_TRANSITIONS).join(', ')})` } });
|
|
411
433
|
return 1;
|
|
412
434
|
}
|
|
413
|
-
if (flags.to && flags.to !==
|
|
414
|
-
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported (
|
|
435
|
+
if (flags.to && flags.to !== expectedTo) {
|
|
436
|
+
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported for --from ${fromVersion} (expected ${expectedTo})` } });
|
|
415
437
|
return 1;
|
|
416
438
|
}
|
|
417
439
|
return flags.rollback ? cmdRollback(flags) : cmdForward(flags);
|
|
418
440
|
}
|
|
419
441
|
|
|
420
|
-
const USAGE = `ultra-tools migrate --from
|
|
442
|
+
const USAGE = `ultra-tools migrate --from=<version> --to=<version> [flags]
|
|
443
|
+
|
|
444
|
+
Supported transitions:
|
|
445
|
+
4.4 -> 4.5 import the legacy task projection into authoritative state
|
|
446
|
+
4.5 -> 12.0 import a projection-only project into current authoritative state
|
|
421
447
|
|
|
422
448
|
Flags:
|
|
423
449
|
--source-dir <dir> project root containing .ultra/ (default: .)
|
|
424
450
|
--db-path <path> state.db destination (default: <source-dir>/.ultra/state.db)
|
|
425
451
|
--dry print the migration plan without writing
|
|
426
|
-
--rollback restore the most recent backup-
|
|
452
|
+
--rollback restore the most recent matching backup-v<from>-* and drop state.db
|
|
427
453
|
|
|
428
454
|
The forward flow: backup .ultra/ → init state.db → insert tasks from
|
|
429
455
|
tasks.json → merge context md status (tasks.json wins on conflict, warnings
|
|
430
456
|
recorded) → insert activity-log events → record migration_history.
|
|
431
|
-
Rollback restores from the latest backup-
|
|
457
|
+
Rollback restores from the latest matching backup-v<from>-* directory and writes a
|
|
432
458
|
matching migration_history rollback row before dropping state.db.
|
|
433
459
|
`;
|
|
434
460
|
|
|
@@ -10,6 +10,7 @@ const fs = require('node:fs');
|
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
|
|
12
12
|
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
13
|
+
const baselines = require('../../mcp-server/lib/baseline-workflow.cjs');
|
|
13
14
|
|
|
14
15
|
const USAGE = `ultra-tools status [flags]
|
|
15
16
|
|
|
@@ -64,12 +65,32 @@ function parseFlags(args) {
|
|
|
64
65
|
return out;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function buildCostPanel(db, { since = null, limit = 3 } = {}) {
|
|
68
|
+
function buildCostPanel(db, { since = null, limit = 3, rootDir = process.cwd() } = {}) {
|
|
68
69
|
const by_runtime = ops.aggregateTelemetryByRuntime(db, { since });
|
|
69
70
|
const top_tasks = ops.aggregateTelemetryByTask(db, { since, limit });
|
|
70
71
|
const total_cost_usd = by_runtime.reduce((acc, r) => acc + (r.cost_usd || 0), 0);
|
|
72
|
+
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
73
|
+
const current = baselineHealth.baseline;
|
|
74
|
+
const baselineStatus = current?.status
|
|
75
|
+
|| (baselineHealth.blockers.includes('BASELINE_SCHEMA_MIGRATION_REQUIRED')
|
|
76
|
+
? 'migration_required' : 'missing');
|
|
77
|
+
const baseline = {
|
|
78
|
+
id: current?.id || null,
|
|
79
|
+
mode: current?.mode || null,
|
|
80
|
+
status: baselineStatus,
|
|
81
|
+
health: baselineHealth.status,
|
|
82
|
+
repository_revision: current?.repository_revision || null,
|
|
83
|
+
blockers: baselineHealth.blockers,
|
|
84
|
+
warnings: baselineHealth.warnings,
|
|
85
|
+
};
|
|
86
|
+
if (current) {
|
|
87
|
+
baseline.repository_branch = current.repository_branch || null;
|
|
88
|
+
baseline.worktree_state = current.worktree_state || 'unavailable';
|
|
89
|
+
baseline.gaps = baselines.summarizeGaps(current.gaps);
|
|
90
|
+
}
|
|
71
91
|
return {
|
|
72
92
|
period: { since: since || 'all-time' },
|
|
93
|
+
baseline,
|
|
73
94
|
by_runtime,
|
|
74
95
|
top_tasks,
|
|
75
96
|
total_cost_usd,
|
|
@@ -84,6 +105,11 @@ function formatCost(n) {
|
|
|
84
105
|
|
|
85
106
|
function renderHuman(panel) {
|
|
86
107
|
const lines = [];
|
|
108
|
+
const baseline = panel.baseline;
|
|
109
|
+
lines.push(
|
|
110
|
+
`Baseline: ${baseline.mode || 'none'}/${baseline.status} · ${baseline.health === 'pass' ? 'ready' : 'blocked'}`,
|
|
111
|
+
);
|
|
112
|
+
if (baseline.blockers.length > 0) lines.push(`Baseline blockers: ${baseline.blockers.join(', ')}`);
|
|
87
113
|
lines.push(`Period: ${panel.period.since}`);
|
|
88
114
|
lines.push(`Total cost: ${formatCost(panel.total_cost_usd)}`);
|
|
89
115
|
lines.push('');
|
|
@@ -136,7 +162,8 @@ function dispatch(args) {
|
|
|
136
162
|
const Database = require('better-sqlite3');
|
|
137
163
|
const db = new Database(dbPath, { readonly: true });
|
|
138
164
|
try {
|
|
139
|
-
const
|
|
165
|
+
const rootDir = path.dirname(path.dirname(dbPath));
|
|
166
|
+
const panel = buildCostPanel(db, { since, limit: flags.limit, rootDir });
|
|
140
167
|
if (flags.json) {
|
|
141
168
|
emit({ ok: true, data: panel });
|
|
142
169
|
} else {
|