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
|
@@ -14,6 +14,11 @@ const CLI = path.join(__dirname, 'cli.cjs');
|
|
|
14
14
|
function fixture() {
|
|
15
15
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-cli-'));
|
|
16
16
|
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
17
|
+
db.prepare(
|
|
18
|
+
`INSERT INTO baselines
|
|
19
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
20
|
+
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
21
|
+
).run(new Date().toISOString());
|
|
17
22
|
closeStateDb(db);
|
|
18
23
|
return rootDir;
|
|
19
24
|
}
|
|
@@ -51,3 +56,254 @@ test('system doctor --repair creates a backup before recovery', () => {
|
|
|
51
56
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
52
57
|
}
|
|
53
58
|
});
|
|
59
|
+
|
|
60
|
+
test('system doctor --repair migrates an older schema only after preserving a pre-migration backup', () => {
|
|
61
|
+
const rootDir = fixture();
|
|
62
|
+
try {
|
|
63
|
+
const dbPath = path.join(rootDir, '.ultra', 'state.db');
|
|
64
|
+
const Database = require('better-sqlite3');
|
|
65
|
+
const db = new Database(dbPath);
|
|
66
|
+
db.prepare("DELETE FROM schema_version WHERE version = '12.0'").run();
|
|
67
|
+
db.close();
|
|
68
|
+
|
|
69
|
+
const result = invoke(rootDir, ['doctor', '--repair']);
|
|
70
|
+
assert.equal(result.status, 0, result.stderr);
|
|
71
|
+
const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
|
|
72
|
+
assert.equal(envelope.ok, true);
|
|
73
|
+
assert.equal(envelope.data.schema_version, '12.0');
|
|
74
|
+
assert.ok(fs.existsSync(envelope.data.schema_migration_backup_path));
|
|
75
|
+
assert.ok(fs.existsSync(envelope.data.backup_path));
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('system doctor reports a corrupt database as structured recovery guidance', () => {
|
|
82
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-corrupt-'));
|
|
83
|
+
try {
|
|
84
|
+
fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
|
|
85
|
+
fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'not a sqlite database');
|
|
86
|
+
const result = invoke(rootDir, ['doctor']);
|
|
87
|
+
assert.equal(result.status, 2);
|
|
88
|
+
const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
|
|
89
|
+
assert.equal(envelope.ok, false);
|
|
90
|
+
assert.equal(envelope.error.code, 'STATE_DB_CORRUPT');
|
|
91
|
+
assert.match(envelope.error.message, /restore.*backup|rebaseline/i);
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('system restore replaces only a corrupt state database from a verified managed backup', () => {
|
|
98
|
+
const rootDir = fixture();
|
|
99
|
+
try {
|
|
100
|
+
const dbPath = path.join(rootDir, '.ultra', 'state.db');
|
|
101
|
+
const backupsDir = path.join(rootDir, '.ultra', 'backups');
|
|
102
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
103
|
+
const sourceBackup = path.join(backupsDir, 'verified.db');
|
|
104
|
+
fs.copyFileSync(dbPath, sourceBackup);
|
|
105
|
+
fs.writeFileSync(dbPath, 'not a sqlite database');
|
|
106
|
+
|
|
107
|
+
const refused = invoke(rootDir, ['restore', '--backup', sourceBackup]);
|
|
108
|
+
assert.equal(refused.status, 1);
|
|
109
|
+
assert.equal(JSON.parse(refused.stdout.trim()).error.code, 'CONFIRMATION_REQUIRED');
|
|
110
|
+
assert.equal(fs.readFileSync(dbPath, 'utf8'), 'not a sqlite database');
|
|
111
|
+
|
|
112
|
+
const restored = invoke(rootDir, [
|
|
113
|
+
'restore', '--backup', sourceBackup,
|
|
114
|
+
'--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
|
|
115
|
+
]);
|
|
116
|
+
assert.equal(restored.status, 0, restored.stderr);
|
|
117
|
+
const envelope = JSON.parse(restored.stdout.trim());
|
|
118
|
+
assert.equal(envelope.ok, true);
|
|
119
|
+
assert.equal(envelope.data.schema_version, '12.0');
|
|
120
|
+
assert.equal(envelope.data.source_backup, fs.realpathSync(sourceBackup));
|
|
121
|
+
assert.ok(fs.existsSync(envelope.data.quarantined_state_path));
|
|
122
|
+
assert.equal(
|
|
123
|
+
fs.readFileSync(envelope.data.quarantined_state_path, 'utf8'),
|
|
124
|
+
'not a sqlite database',
|
|
125
|
+
);
|
|
126
|
+
const restoredDb = require('better-sqlite3')(dbPath, { readonly: true });
|
|
127
|
+
try {
|
|
128
|
+
assert.equal(restoredDb.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
|
|
129
|
+
} finally { restoredDb.close(); }
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('system rebaseline preserves corrupt authority and legacy projection before starting brownfield adoption', () => {
|
|
136
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-rebaseline-'));
|
|
137
|
+
try {
|
|
138
|
+
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
|
|
139
|
+
fs.mkdirSync(path.join(rootDir, '.ultra', 'tasks', 'contexts'), { recursive: true });
|
|
140
|
+
fs.writeFileSync(path.join(rootDir, 'src', 'index.js'), 'module.exports = true;\n');
|
|
141
|
+
fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'broken authority');
|
|
142
|
+
const tasksPath = path.join(rootDir, '.ultra', 'tasks', 'tasks.json');
|
|
143
|
+
fs.writeFileSync(tasksPath, `${JSON.stringify({
|
|
144
|
+
schema_version: '4.5', source: '.ultra/state.db',
|
|
145
|
+
tasks: [{ id: 'legacy-task', title: 'Preserved task', type: 'feature', priority: 'P1', status: 'pending' }],
|
|
146
|
+
}, null, 2)}\n`);
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(rootDir, '.ultra', 'tasks', 'contexts', 'legacy-task.md'),
|
|
149
|
+
'# Preserved task context\n',
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const result = invoke(rootDir, [
|
|
153
|
+
'rebaseline', '--project-name', 'legacy-app',
|
|
154
|
+
'--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
|
|
155
|
+
]);
|
|
156
|
+
assert.equal(result.status, 0, result.stderr);
|
|
157
|
+
const envelope = JSON.parse(result.stdout.trim());
|
|
158
|
+
assert.equal(envelope.ok, true);
|
|
159
|
+
assert.equal(envelope.data.baseline.mode, 'brownfield');
|
|
160
|
+
assert.equal(envelope.data.baseline.status, 'adopting');
|
|
161
|
+
assert.ok(fs.existsSync(envelope.data.quarantined_state_path));
|
|
162
|
+
assert.ok(fs.existsSync(envelope.data.legacy_projection_path));
|
|
163
|
+
assert.equal(
|
|
164
|
+
JSON.parse(fs.readFileSync(envelope.data.legacy_projection_path, 'utf8')).tasks[0].id,
|
|
165
|
+
'legacy-task',
|
|
166
|
+
);
|
|
167
|
+
assert.ok(fs.existsSync(path.join(
|
|
168
|
+
envelope.data.recovery_backup_dir, 'tasks', 'contexts', 'legacy-task.md',
|
|
169
|
+
)));
|
|
170
|
+
assert.equal(
|
|
171
|
+
fs.existsSync(path.join(rootDir, '.ultra', 'tasks', 'contexts', 'legacy-task.md')),
|
|
172
|
+
false,
|
|
173
|
+
);
|
|
174
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(tasksPath, 'utf8')).tasks, []);
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('system restore rolls back the corrupt database and sidecars when replacement initialization fails', () => {
|
|
181
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-restore-rollback-'));
|
|
182
|
+
try {
|
|
183
|
+
const ultraDir = path.join(rootDir, '.ultra');
|
|
184
|
+
const backupsDir = path.join(ultraDir, 'backups');
|
|
185
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
186
|
+
const statePath = path.join(ultraDir, 'state.db');
|
|
187
|
+
const originals = {
|
|
188
|
+
'': Buffer.from('broken authority'),
|
|
189
|
+
'-wal': Buffer.from('broken wal'),
|
|
190
|
+
'-shm': Buffer.from('broken shm'),
|
|
191
|
+
};
|
|
192
|
+
for (const [suffix, content] of Object.entries(originals)) {
|
|
193
|
+
fs.writeFileSync(`${statePath}${suffix}`, content);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const incompatibleBackup = path.join(backupsDir, 'structurally-incompatible.db');
|
|
197
|
+
const backupDb = require('better-sqlite3')(incompatibleBackup);
|
|
198
|
+
backupDb.exec("CREATE TABLE schema_version (version TEXT NOT NULL); INSERT INTO schema_version VALUES ('11.0');");
|
|
199
|
+
backupDb.close();
|
|
200
|
+
|
|
201
|
+
const result = invoke(rootDir, [
|
|
202
|
+
'restore', '--backup', incompatibleBackup,
|
|
203
|
+
'--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
|
|
204
|
+
]);
|
|
205
|
+
assert.notEqual(result.status, 0, result.stderr);
|
|
206
|
+
const envelope = JSON.parse(result.stdout.trim());
|
|
207
|
+
assert.equal(envelope.ok, false);
|
|
208
|
+
assert.equal(envelope.error.code, 'STATE_RESTORE_FAILED');
|
|
209
|
+
for (const [suffix, content] of Object.entries(originals)) {
|
|
210
|
+
assert.deepEqual(fs.readFileSync(`${statePath}${suffix}`), content);
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('system rebaseline rolls back authority, sidecars, and the full task projection when adoption fails', () => {
|
|
218
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-rebaseline-rollback-'));
|
|
219
|
+
try {
|
|
220
|
+
const ultraDir = path.join(rootDir, '.ultra');
|
|
221
|
+
const tasksDir = path.join(ultraDir, 'tasks');
|
|
222
|
+
fs.mkdirSync(path.join(tasksDir, 'contexts'), { recursive: true });
|
|
223
|
+
fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
|
|
224
|
+
fs.writeFileSync(path.join(rootDir, 'src', 'index.js'), 'module.exports = true;\n');
|
|
225
|
+
const statePath = path.join(ultraDir, 'state.db');
|
|
226
|
+
const originals = {
|
|
227
|
+
'': Buffer.from('broken authority'),
|
|
228
|
+
'-wal': Buffer.from('broken wal'),
|
|
229
|
+
'-shm': Buffer.from('broken shm'),
|
|
230
|
+
};
|
|
231
|
+
for (const [suffix, content] of Object.entries(originals)) {
|
|
232
|
+
fs.writeFileSync(`${statePath}${suffix}`, content);
|
|
233
|
+
}
|
|
234
|
+
const tasksPath = path.join(tasksDir, 'tasks.json');
|
|
235
|
+
const contextPath = path.join(tasksDir, 'contexts', 'legacy.md');
|
|
236
|
+
const tasksContent = Buffer.from('{"schema_version":"4.5","source":".ultra/state.db","tasks":[]}\n');
|
|
237
|
+
const contextContent = Buffer.from('# Legacy context\n');
|
|
238
|
+
fs.writeFileSync(tasksPath, tasksContent);
|
|
239
|
+
fs.writeFileSync(contextPath, contextContent);
|
|
240
|
+
|
|
241
|
+
const result = invoke(rootDir, [
|
|
242
|
+
'rebaseline', '--project-name', 'legacy-app', '--scope', 'missing-workspace',
|
|
243
|
+
'--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
|
|
244
|
+
]);
|
|
245
|
+
assert.notEqual(result.status, 0, result.stderr);
|
|
246
|
+
const envelope = JSON.parse(result.stdout.trim());
|
|
247
|
+
assert.equal(envelope.ok, false);
|
|
248
|
+
assert.equal(envelope.error.code, 'IO_ERROR');
|
|
249
|
+
for (const [suffix, content] of Object.entries(originals)) {
|
|
250
|
+
assert.deepEqual(fs.readFileSync(`${statePath}${suffix}`), content);
|
|
251
|
+
}
|
|
252
|
+
assert.deepEqual(fs.readFileSync(tasksPath), tasksContent);
|
|
253
|
+
assert.deepEqual(fs.readFileSync(contextPath), contextContent);
|
|
254
|
+
} finally {
|
|
255
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('system restore rejects backups outside the managed project backup directory', () => {
|
|
260
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-external-backup-'));
|
|
261
|
+
const externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-external-backup-'));
|
|
262
|
+
try {
|
|
263
|
+
fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
|
|
264
|
+
fs.mkdirSync(path.join(rootDir, '.ultra', 'backups'), { recursive: true });
|
|
265
|
+
fs.writeFileSync(path.join(rootDir, '.ultra', 'state.db'), 'broken authority');
|
|
266
|
+
const external = path.join(externalDir, 'state.db');
|
|
267
|
+
fs.writeFileSync(external, 'not trusted');
|
|
268
|
+
const result = invoke(rootDir, [
|
|
269
|
+
'restore', '--backup', external,
|
|
270
|
+
'--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
|
|
271
|
+
]);
|
|
272
|
+
assert.equal(result.status, 1);
|
|
273
|
+
assert.equal(JSON.parse(result.stdout.trim()).error.code, 'BACKUP_OUTSIDE_MANAGED_ROOT');
|
|
274
|
+
} finally {
|
|
275
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
276
|
+
fs.rmSync(externalDir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('system corrupt-state recovery commands refuse to replace readable authority', () => {
|
|
281
|
+
const rootDir = fixture();
|
|
282
|
+
try {
|
|
283
|
+
const backupsDir = path.join(rootDir, '.ultra', 'backups');
|
|
284
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
285
|
+
const backup = path.join(backupsDir, 'healthy.db');
|
|
286
|
+
fs.copyFileSync(path.join(rootDir, '.ultra', 'state.db'), backup);
|
|
287
|
+
|
|
288
|
+
const restore = invoke(rootDir, [
|
|
289
|
+
'restore', '--backup', backup,
|
|
290
|
+
'--confirm', 'REPLACE_CORRUPT_ULTRA_STATE',
|
|
291
|
+
]);
|
|
292
|
+
assert.equal(restore.status, 2);
|
|
293
|
+
assert.equal(JSON.parse(restore.stdout.trim()).error.code, 'STATE_DB_NOT_CORRUPT');
|
|
294
|
+
|
|
295
|
+
const rebaseline = invoke(rootDir, [
|
|
296
|
+
'rebaseline', '--project-name', 'fixture',
|
|
297
|
+
'--confirm', 'REBASELINE_CORRUPT_ULTRA_STATE',
|
|
298
|
+
]);
|
|
299
|
+
assert.equal(rebaseline.status, 2);
|
|
300
|
+
assert.equal(JSON.parse(rebaseline.stdout.trim()).error.code, 'STATE_DB_NOT_CORRUPT');
|
|
301
|
+
|
|
302
|
+
const db = require('better-sqlite3')(path.join(rootDir, '.ultra', 'state.db'), { readonly: true });
|
|
303
|
+
try {
|
|
304
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
|
|
305
|
+
} finally { db.close(); }
|
|
306
|
+
} finally {
|
|
307
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
});
|