ultra-builder-pro-cli 0.10.0 → 0.12.1
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/architecture.md +54 -223
- package/.ultra-template/specs/discovery.md +34 -231
- 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 +86 -1
- package/README.md +21 -9
- package/adapters/_shared/codex-assets.cjs +24 -342
- package/adapters/_shared/handbook.cjs +4 -3
- package/adapters/_shared/runtime-assets.cjs +24 -0
- package/adapters/_shared/tests/handbook.test.cjs +4 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +11 -4
- package/adapters/claude.js +28 -5
- package/adapters/kimi.js +15 -138
- package/adapters/opencode.js +22 -49
- package/adapters/tests/claude.test.cjs +22 -8
- package/adapters/tests/codex.test.cjs +21 -8
- package/adapters/tests/kimi-hook.test.cjs +1 -1
- package/adapters/tests/kimi.test.cjs +11 -11
- package/adapters/tests/opencode.test.cjs +47 -8
- package/agents/code-reviewer.md +24 -85
- package/agents/debugger.md +30 -132
- package/agents/review-code.md +24 -104
- package/agents/review-comments.md +20 -83
- package/agents/review-coordinator.md +40 -137
- package/agents/review-design.md +21 -118
- package/agents/review-errors.md +22 -70
- package/agents/review-spec.md +36 -0
- package/agents/review-tests.md +20 -76
- package/agents/tdd-runner.md +26 -84
- package/commands/learn.md +5 -36
- package/commands/ultra-change.md +5 -43
- package/commands/ultra-deliver.md +4 -46
- package/commands/ultra-dev.md +4 -48
- package/commands/ultra-doctor.md +4 -30
- package/commands/ultra-init.md +5 -47
- package/commands/ultra-plan.md +5 -48
- package/commands/ultra-research.md +5 -54
- package/commands/ultra-status.md +5 -44
- package/commands/ultra-test.md +5 -45
- package/commands/ultra-think.md +5 -40
- package/docs/AGENT-CONTEXT.md +16 -8
- package/docs/ARCHITECTURE.md +32 -5
- package/docs/PLAN.zh-CN.md +8 -6
- package/docs/ROADMAP.md +5 -3
- package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
- package/docs/SKILL-AUTHORING.md +75 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -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 +120 -16
- package/hooks/health_check.py +11 -0
- package/hooks/pre_stop_check.py +6 -5
- package/mcp-server/lib/baseline-workflow.cjs +415 -0
- package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
- package/mcp-server/lib/change-workflow.cjs +24 -27
- package/mcp-server/lib/change-workflow.test.cjs +202 -6
- package/mcp-server/lib/context-spine.cjs +74 -9
- package/mcp-server/lib/context-spine.test.cjs +15 -0
- package/mcp-server/lib/doctor.cjs +14 -0
- package/mcp-server/lib/doctor.test.cjs +25 -1
- package/mcp-server/lib/init-project.cjs +49 -14
- package/mcp-server/lib/provider-refs.cjs +42 -0
- package/mcp-server/lib/spec-learning.test.cjs +5 -0
- package/mcp-server/lib/state-db.cjs +63 -4
- package/mcp-server/lib/state-db.test.cjs +8 -3
- package/mcp-server/server.cjs +48 -3
- package/mcp-server/tests/init-project.test.cjs +54 -8
- package/mcp-server/tests/server-contract.test.cjs +99 -2
- package/mcp-server/tests/state-db.test.cjs +11 -2
- package/package.json +3 -2
- package/skills/cc-collab/SKILL.md +16 -18
- package/skills/code-review-expert/SKILL.md +39 -114
- package/skills/code-review-expert/references/correctness-reliability.md +30 -0
- package/skills/code-review-expert/references/design-boundaries.md +26 -0
- package/skills/code-review-expert/references/removal-plan.md +17 -45
- package/skills/codex-collab/SKILL.md +23 -28
- package/skills/codex-collab/evals/evals.json +18 -23
- package/skills/integration-rules/SKILL.md +28 -67
- package/skills/learn/SKILL.md +46 -106
- package/skills/security-rules/SKILL.md +34 -56
- package/skills/testing-rules/SKILL.md +28 -52
- package/skills/ultra-change/SKILL.md +68 -114
- package/skills/ultra-deliver/SKILL.md +58 -86
- package/skills/ultra-dev/SKILL.md +46 -105
- package/skills/ultra-doctor/SKILL.md +45 -72
- package/skills/ultra-init/SKILL.md +62 -147
- package/skills/ultra-init/references/brownfield-adoption.md +65 -0
- package/skills/ultra-plan/SKILL.md +56 -89
- package/skills/ultra-research/SKILL.md +64 -152
- package/skills/ultra-research/references/architecture.md +38 -0
- package/skills/ultra-research/references/problem-product.md +39 -0
- package/skills/ultra-research/references/quality-delivery.md +34 -0
- package/skills/ultra-research/references/synthesis.md +38 -0
- package/skills/ultra-research/references/users-scope.md +40 -0
- package/skills/ultra-review/SKILL.md +54 -98
- package/skills/ultra-review/references/unified-schema.md +76 -101
- package/skills/ultra-review/scripts/review_wait.py +242 -77
- package/skills/ultra-status/SKILL.md +41 -56
- package/skills/ultra-test/SKILL.md +37 -72
- package/skills/ultra-think/SKILL.md +43 -102
- package/skills/ultra-verify/SKILL.md +44 -84
- package/skills/ultra-verify/evals/evals.json +24 -27
- package/spec/cli-protocol.md +4 -0
- package/spec/mcp-tools.yaml +223 -4
- package/spec/schemas/command-manifest.schema.json +7 -25
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/skill-manifest.schema.json +9 -79
- package/spec/schemas/state-db.sql +33 -2
- package/ultra-tools/commands/status.cjs +20 -2
- package/ultra-tools/commands/task.cjs +4 -1
- package/ultra-tools/status-command.test.cjs +20 -0
- package/ultra-tools/system-command.test.cjs +5 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
- package/skills/code-review-expert/references/integration-checklist.md +0 -46
- package/skills/code-review-expert/references/security-checklist.md +0 -100
- package/skills/code-review-expert/references/solid-checklist.md +0 -65
- package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
- package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
- package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
- package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
- package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
- package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
- package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
- package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
- package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
- package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
- package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
- package/skills/ultra-research/steps/step-40-deployment.md +0 -123
- package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
- package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
- package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Implementation for MCP tool `task.init_project` and the CLI fallback
|
|
4
|
-
// `ultra-tools init`. Copies the bundled templates/.ultra/ skeleton
|
|
5
|
-
// target project root
|
|
4
|
+
// `ultra-tools task init-project`. Copies the bundled templates/.ultra/ skeleton
|
|
5
|
+
// to the target project root and records project metadata in authoritative state.
|
|
6
6
|
//
|
|
7
7
|
// Error contract matches spec/mcp-tools.yaml#task.init_project:
|
|
8
8
|
// ULTRA_DIR_EXISTS | TEMPLATE_MISSING | TARGET_NOT_DIR | IO_ERROR
|
|
@@ -15,6 +15,8 @@ const fs = require('node:fs');
|
|
|
15
15
|
const path = require('node:path');
|
|
16
16
|
|
|
17
17
|
const { initStateDb } = require('./state-db.cjs');
|
|
18
|
+
const baselines = require('./baseline-workflow.cjs');
|
|
19
|
+
const runtimeState = require('./runtime-state.cjs');
|
|
18
20
|
|
|
19
21
|
const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
20
22
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
@@ -95,20 +97,30 @@ function copyTemplate(source, dest) {
|
|
|
95
97
|
return files;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
function
|
|
100
|
+
function validateTasksTemplate(ultraDir) {
|
|
99
101
|
const tasksFile = path.join(ultraDir, 'tasks', 'tasks.json');
|
|
100
102
|
if (!fs.existsSync(tasksFile)) return;
|
|
101
103
|
let data;
|
|
102
104
|
try { data = JSON.parse(fs.readFileSync(tasksFile, 'utf8')); }
|
|
103
105
|
catch (err) { throw new InitProjectError('IO_ERROR', `tasks.json malformed: ${err.message}`, { cause: err }); }
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
if (!data || typeof data !== 'object' || !Array.isArray(data.tasks)) {
|
|
107
|
+
throw new InitProjectError('IO_ERROR', 'tasks.json template must contain a tasks array');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const IGNORED_ADOPTION_ENTRIES = new Set([
|
|
112
|
+
'.DS_Store', '.git', '.gitignore', '.ultra', 'node_modules', 'vendor', '.venv', 'venv',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
function detectProjectMode(absTarget, requested = 'auto') {
|
|
116
|
+
if (!['auto', 'greenfield', 'brownfield'].includes(requested)) {
|
|
117
|
+
throw new InitProjectError('VALIDATION_ERROR', `unsupported initialization mode: ${requested}`);
|
|
118
|
+
}
|
|
119
|
+
if (requested !== 'auto') return requested;
|
|
120
|
+
const meaningful = fs.readdirSync(absTarget, { withFileTypes: true })
|
|
121
|
+
.some((entry) => !IGNORED_ADOPTION_ENTRIES.has(entry.name)
|
|
122
|
+
&& !entry.name.startsWith('.ultra.backup.'));
|
|
123
|
+
return meaningful ? 'brownfield' : 'greenfield';
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
function rollbackInitialization(ultraDir, backupPath) {
|
|
@@ -130,6 +142,7 @@ function initProject({
|
|
|
130
142
|
project_name,
|
|
131
143
|
project_type,
|
|
132
144
|
stack,
|
|
145
|
+
mode = 'auto',
|
|
133
146
|
overwrite = false,
|
|
134
147
|
source_template,
|
|
135
148
|
} = {}) {
|
|
@@ -139,6 +152,7 @@ function initProject({
|
|
|
139
152
|
|
|
140
153
|
const absTarget = resolveTargetDir(target_dir);
|
|
141
154
|
ensureTargetDir(absTarget);
|
|
155
|
+
const resolvedMode = detectProjectMode(absTarget, mode);
|
|
142
156
|
const source = ensureTemplate(source_template);
|
|
143
157
|
const ultraDir = path.join(absTarget, '.ultra');
|
|
144
158
|
|
|
@@ -155,13 +169,27 @@ function initProject({
|
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
let copiedFiles;
|
|
172
|
+
let baselineResult;
|
|
158
173
|
try {
|
|
159
174
|
fs.mkdirSync(ultraDir, { recursive: true });
|
|
160
175
|
copiedFiles = copyTemplate(source, ultraDir);
|
|
161
|
-
|
|
176
|
+
validateTasksTemplate(ultraDir);
|
|
162
177
|
const stateDbPath = path.join(ultraDir, 'state.db');
|
|
163
178
|
const { db } = initStateDb(stateDbPath);
|
|
164
|
-
|
|
179
|
+
try {
|
|
180
|
+
baselineResult = baselines.startBaseline(db, {
|
|
181
|
+
id: 'project-baseline', project_name, project_type, stack,
|
|
182
|
+
mode: resolvedMode, repository_revision: baselines.gitHead(absTarget), scope: ['.'],
|
|
183
|
+
}, { rootDir: absTarget });
|
|
184
|
+
const job = runtimeState.enqueueProjection(db, { tool_name: 'task.init_project' });
|
|
185
|
+
const projection = runtimeState.processProjectionJobs(db, { rootDir: absTarget, limit: 1 });
|
|
186
|
+
const projected = projection.jobs.find((item) => item.id === job.id);
|
|
187
|
+
if (!projected || projected.status !== 'completed') {
|
|
188
|
+
throw new Error(projected?.error || 'initial projection did not complete');
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
db.close();
|
|
192
|
+
}
|
|
165
193
|
} catch (err) {
|
|
166
194
|
const rollbackErrors = rollbackInitialization(ultraDir, backupPath);
|
|
167
195
|
const rollbackNote = rollbackErrors.length > 0
|
|
@@ -179,10 +207,17 @@ function initProject({
|
|
|
179
207
|
created_path: ultraDir,
|
|
180
208
|
state_db_path: stateDbPath,
|
|
181
209
|
status,
|
|
210
|
+
mode: resolvedMode,
|
|
211
|
+
baseline: {
|
|
212
|
+
id: baselineResult.id,
|
|
213
|
+
mode: baselineResult.mode,
|
|
214
|
+
status: baselineResult.status,
|
|
215
|
+
repository_revision: baselineResult.repository_revision,
|
|
216
|
+
},
|
|
182
217
|
copied_files: copiedFiles,
|
|
183
218
|
};
|
|
184
219
|
if (backupPath) result.backup_path = backupPath;
|
|
185
220
|
return result;
|
|
186
221
|
}
|
|
187
222
|
|
|
188
|
-
module.exports = { initProject, InitProjectError, DEFAULT_TEMPLATE };
|
|
223
|
+
module.exports = { initProject, InitProjectError, DEFAULT_TEMPLATE, detectProjectMode };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PROVIDER_KINDS = new Set(['memory', 'code_graph']);
|
|
4
|
+
const PROVIDER_FIELDS = new Set([
|
|
5
|
+
'provider', 'project', 'reference', 'revision', 'indexed_head', 'status',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
class ProviderRefsError extends Error {
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ProviderRefsError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeProviderRefs(value, ErrorClass = ProviderRefsError) {
|
|
17
|
+
if (value == null) return {};
|
|
18
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
19
|
+
throw new ErrorClass('VALIDATION_ERROR', 'provider_refs must be an object');
|
|
20
|
+
}
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [kind, ref] of Object.entries(value)) {
|
|
23
|
+
if (!PROVIDER_KINDS.has(kind) || !ref || typeof ref !== 'object' || Array.isArray(ref)) {
|
|
24
|
+
throw new ErrorClass('VALIDATION_ERROR', `invalid external provider reference: ${kind}`);
|
|
25
|
+
}
|
|
26
|
+
for (const key of Object.keys(ref)) {
|
|
27
|
+
if (!PROVIDER_FIELDS.has(key)) {
|
|
28
|
+
throw new ErrorClass(
|
|
29
|
+
'PROVIDER_CONTENT_FORBIDDEN',
|
|
30
|
+
`provider_refs.${kind}.${key} is not metadata; provider content remains external`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof ref.provider !== 'string' || !ref.provider.trim()) {
|
|
35
|
+
throw new ErrorClass('VALIDATION_ERROR', `provider_refs.${kind}.provider required`);
|
|
36
|
+
}
|
|
37
|
+
out[kind] = { ...ref, provider: ref.provider.trim() };
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { ProviderRefsError, normalizeProviderRefs };
|
|
@@ -20,6 +20,11 @@ function fixture() {
|
|
|
20
20
|
execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
|
|
21
21
|
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
|
|
22
22
|
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
23
|
+
db.prepare(
|
|
24
|
+
`INSERT INTO baselines
|
|
25
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
26
|
+
VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
|
|
27
|
+
).run(new Date().toISOString());
|
|
23
28
|
changes.createChange(db, {
|
|
24
29
|
id: 'learning-change', title: 'Learning change', kind: 'quick',
|
|
25
30
|
intent: 'Exercise guarded specification-learning transitions.',
|
|
@@ -9,10 +9,11 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
|
|
|
9
9
|
? path.resolve(process.env.UBP_RUNTIME_ROOT)
|
|
10
10
|
: path.resolve(__dirname, '..', '..');
|
|
11
11
|
const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
|
|
12
|
-
const EXPECTED_VERSION = '
|
|
12
|
+
const EXPECTED_VERSION = '11.0';
|
|
13
13
|
const KIMI_SCHEMA_VERSION = '9.1';
|
|
14
14
|
|
|
15
15
|
const REQUIRED_TABLES = Object.freeze([
|
|
16
|
+
'baselines',
|
|
16
17
|
'tasks',
|
|
17
18
|
'events',
|
|
18
19
|
'sessions',
|
|
@@ -31,6 +32,43 @@ const REQUIRED_TABLES = Object.freeze([
|
|
|
31
32
|
'event_consumers',
|
|
32
33
|
]);
|
|
33
34
|
|
|
35
|
+
function applyBaselineUpgrade(db, fromVersion) {
|
|
36
|
+
db.exec(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS baselines (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
project_name TEXT NOT NULL,
|
|
40
|
+
project_type TEXT,
|
|
41
|
+
stack TEXT,
|
|
42
|
+
mode TEXT NOT NULL CHECK (mode IN ('greenfield', 'brownfield', 'migrated')),
|
|
43
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
44
|
+
CHECK (status IN ('draft', 'adopting', 'blocked', 'ready', 'superseded')),
|
|
45
|
+
repository_root TEXT NOT NULL DEFAULT '.',
|
|
46
|
+
scope_json TEXT NOT NULL DEFAULT '["."]',
|
|
47
|
+
repository_revision TEXT,
|
|
48
|
+
spec_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
49
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
50
|
+
verification_json TEXT NOT NULL DEFAULT '[]',
|
|
51
|
+
unknowns_json TEXT NOT NULL DEFAULT '[]',
|
|
52
|
+
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
53
|
+
approved_by TEXT,
|
|
54
|
+
approval_note TEXT,
|
|
55
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
56
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
57
|
+
converged_at TEXT
|
|
58
|
+
);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS baselines_status ON baselines(status, updated_at);
|
|
60
|
+
`);
|
|
61
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION) {
|
|
62
|
+
db.prepare(
|
|
63
|
+
`INSERT OR IGNORE INTO baselines
|
|
64
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
65
|
+
VALUES ('migrated-baseline', 'Migrated Ultra project', 'migrated', 'ready',
|
|
66
|
+
'schema-migration', 'Legacy project preserved during schema 11.0 migration',
|
|
67
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
68
|
+
).run();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
34
72
|
function readSchemaSql() {
|
|
35
73
|
if (!fs.existsSync(SCHEMA_FILE)) {
|
|
36
74
|
throw new Error(`state-db schema missing at ${SCHEMA_FILE}`);
|
|
@@ -223,22 +261,43 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db)) {
|
|
|
223
261
|
db.transaction(() => {
|
|
224
262
|
applyCompatibleColumns(db);
|
|
225
263
|
const contextChanged = applyContextSpineUpgrade(db);
|
|
226
|
-
|
|
264
|
+
const contextMigration = db.prepare(
|
|
265
|
+
"SELECT 1 FROM migration_history WHERE to_version = '10.0' AND status = 'success' LIMIT 1",
|
|
266
|
+
).get();
|
|
267
|
+
const needsContextMigration = Boolean(
|
|
268
|
+
fromVersion && !['10.0', EXPECTED_VERSION].includes(fromVersion) && !contextMigration,
|
|
269
|
+
);
|
|
270
|
+
if (contextChanged || needsContextMigration) {
|
|
227
271
|
db.prepare(
|
|
228
272
|
`INSERT INTO migration_history
|
|
229
273
|
(from_version, to_version, direction, status, notes)
|
|
230
274
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
231
275
|
).run(
|
|
232
276
|
runtimeChanged ? KIMI_SCHEMA_VERSION : (fromVersion || 'unknown'),
|
|
233
|
-
|
|
277
|
+
'10.0',
|
|
234
278
|
'Add Context Spine role/gate readiness, execution contracts, breadcrumbs, and specification learning',
|
|
235
279
|
);
|
|
236
280
|
}
|
|
281
|
+
applyBaselineUpgrade(db, fromVersion);
|
|
282
|
+
const baselineMigration = db.prepare(
|
|
283
|
+
"SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
|
|
284
|
+
).get();
|
|
285
|
+
if (fromVersion && fromVersion !== EXPECTED_VERSION && !baselineMigration) {
|
|
286
|
+
db.prepare(
|
|
287
|
+
`INSERT INTO migration_history
|
|
288
|
+
(from_version, to_version, direction, status, notes)
|
|
289
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
290
|
+
).run(
|
|
291
|
+
fromVersion === '10.0' ? fromVersion : '10.0',
|
|
292
|
+
EXPECTED_VERSION,
|
|
293
|
+
'Add authoritative greenfield and brownfield baseline adoption, evidence, convergence, and drift state',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
237
296
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
238
297
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
239
298
|
db.prepare(
|
|
240
299
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
241
|
-
).run(EXPECTED_VERSION, '
|
|
300
|
+
).run(EXPECTED_VERSION, 'Greenfield and brownfield baseline adoption, convergence, and drift authority');
|
|
242
301
|
})();
|
|
243
302
|
}
|
|
244
303
|
|
|
@@ -123,7 +123,7 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
|
|
|
123
123
|
const legacy = openStateDb(file);
|
|
124
124
|
const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
|
|
125
125
|
legacy.exec(legacySchema);
|
|
126
|
-
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0')").run();
|
|
126
|
+
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0', '11.0')").run();
|
|
127
127
|
legacy.prepare(
|
|
128
128
|
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
|
|
129
129
|
).run();
|
|
@@ -146,16 +146,21 @@ test('initStateDb migrates existing runtime constraints to Kimi without losing r
|
|
|
146
146
|
closeStateDb(legacy);
|
|
147
147
|
|
|
148
148
|
const upgraded = initStateDb(file);
|
|
149
|
-
assert.equal(upgraded.schema_version, '
|
|
149
|
+
assert.equal(upgraded.schema_version, '11.0');
|
|
150
150
|
assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
|
|
151
151
|
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
|
|
152
152
|
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
|
|
153
153
|
assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
|
|
154
154
|
const migrations = upgraded.db.prepare(
|
|
155
|
-
"SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0') ORDER BY id",
|
|
155
|
+
"SELECT to_version, notes FROM migration_history WHERE to_version IN ('9.1', '10.0', '11.0') ORDER BY id",
|
|
156
156
|
).all();
|
|
157
157
|
assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
|
|
158
158
|
assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
|
|
159
|
+
assert.ok(migrations.some((row) => row.to_version === '11.0' && /baseline adoption/i.test(row.notes)));
|
|
160
|
+
assert.deepEqual(
|
|
161
|
+
upgraded.db.prepare("SELECT mode, status FROM baselines WHERE id = 'migrated-baseline'").get(),
|
|
162
|
+
{ mode: 'migrated', status: 'ready' },
|
|
163
|
+
);
|
|
159
164
|
|
|
160
165
|
upgraded.db.prepare(
|
|
161
166
|
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
package/mcp-server/server.cjs
CHANGED
|
@@ -28,6 +28,7 @@ const parser = require('./lib/prd-parser.cjs');
|
|
|
28
28
|
const expander = require('./lib/task-expander.cjs');
|
|
29
29
|
const planStore = require('./lib/plan-store.cjs');
|
|
30
30
|
const { initProject } = require('./lib/init-project.cjs');
|
|
31
|
+
const baselines = require('./lib/baseline-workflow.cjs');
|
|
31
32
|
const changes = require('./lib/change-workflow.cjs');
|
|
32
33
|
const runtimeState = require('./lib/runtime-state.cjs');
|
|
33
34
|
const doctor = require('./lib/doctor.cjs');
|
|
@@ -67,6 +68,13 @@ const PLAN_TOOLS = Object.freeze([
|
|
|
67
68
|
'plan.get',
|
|
68
69
|
]);
|
|
69
70
|
|
|
71
|
+
const BASELINE_TOOLS = Object.freeze([
|
|
72
|
+
'baseline.start',
|
|
73
|
+
'baseline.record',
|
|
74
|
+
'baseline.get',
|
|
75
|
+
'baseline.converge',
|
|
76
|
+
]);
|
|
77
|
+
|
|
70
78
|
const CHANGE_TOOLS = Object.freeze([
|
|
71
79
|
'change.create',
|
|
72
80
|
'change.update',
|
|
@@ -85,18 +93,20 @@ const SYSTEM_TOOLS = Object.freeze([
|
|
|
85
93
|
]);
|
|
86
94
|
|
|
87
95
|
const REGISTERED_TOOLS = Object.freeze([
|
|
88
|
-
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...
|
|
96
|
+
...TASK_TOOLS, ...SESSION_TOOLS, ...PLAN_TOOLS, ...BASELINE_TOOLS, ...CHANGE_TOOLS,
|
|
97
|
+
...SYSTEM_TOOLS,
|
|
89
98
|
]);
|
|
90
99
|
|
|
91
100
|
const STATELESS_TOOLS = new Set(['task.init_project']);
|
|
92
101
|
|
|
93
|
-
// init_project
|
|
94
|
-
//
|
|
102
|
+
// init_project owns its target project's schema, baseline seed, and first
|
|
103
|
+
// projection internally. Do not enqueue a second projection on the caller DB.
|
|
95
104
|
const MUTATING_TOOLS = new Set([
|
|
96
105
|
'task.create', 'task.update', 'task.delete', 'task.append_event', 'task.switch_tag',
|
|
97
106
|
'task.parse_prd', 'task.expand',
|
|
98
107
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
99
108
|
'plan.export',
|
|
109
|
+
'baseline.start', 'baseline.record', 'baseline.converge',
|
|
100
110
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
101
111
|
'change.learning_propose', 'change.learning_resolve',
|
|
102
112
|
]);
|
|
@@ -352,6 +362,39 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
352
362
|
}
|
|
353
363
|
return { plan: planStore.selectSection(loaded, input.section) };
|
|
354
364
|
}
|
|
365
|
+
case 'baseline.start': {
|
|
366
|
+
return {
|
|
367
|
+
baseline: baselines.startBaseline(
|
|
368
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case 'baseline.record': {
|
|
373
|
+
return {
|
|
374
|
+
baseline: baselines.recordBaseline(
|
|
375
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
376
|
+
),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case 'baseline.get': {
|
|
380
|
+
const baseline = baselines.readBaseline(db, input.id);
|
|
381
|
+
if (!baseline) {
|
|
382
|
+
const err = new Error(input.id ? `baseline ${input.id} not found` : 'current baseline not found');
|
|
383
|
+
err.code = 'BASELINE_NOT_FOUND';
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
baseline,
|
|
388
|
+
health: baselines.inspectBaseline(db, {
|
|
389
|
+
rootDir: ctx.rootDir || process.cwd(), id: input.id,
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
case 'baseline.converge': {
|
|
394
|
+
return baselines.convergeBaseline(
|
|
395
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
355
398
|
case 'change.create': {
|
|
356
399
|
const { randomUUID } = require('node:crypto');
|
|
357
400
|
const id = input.id || `chg-${randomUUID().slice(0, 12)}`;
|
|
@@ -598,6 +641,8 @@ module.exports = {
|
|
|
598
641
|
dispatchTool,
|
|
599
642
|
TASK_TOOLS,
|
|
600
643
|
SESSION_TOOLS,
|
|
644
|
+
PLAN_TOOLS,
|
|
645
|
+
BASELINE_TOOLS,
|
|
601
646
|
CHANGE_TOOLS,
|
|
602
647
|
SYSTEM_TOOLS,
|
|
603
648
|
REGISTERED_TOOLS,
|
|
@@ -5,9 +5,11 @@ const assert = require('node:assert/strict');
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
8
9
|
const Database = require('better-sqlite3');
|
|
9
10
|
|
|
10
11
|
const { initProject, InitProjectError, DEFAULT_TEMPLATE } = require('../lib/init-project.cjs');
|
|
12
|
+
const { EXPECTED_VERSION } = require('../lib/state-db.cjs');
|
|
11
13
|
|
|
12
14
|
function mkTempDir(prefix = 'ubp-init-test-') {
|
|
13
15
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -27,6 +29,7 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
27
29
|
stack: 'node',
|
|
28
30
|
});
|
|
29
31
|
assert.equal(r.status, 'created');
|
|
32
|
+
assert.equal(r.mode, 'greenfield');
|
|
30
33
|
assert.equal(r.created_path, path.join(target, '.ultra'));
|
|
31
34
|
assert.ok(r.copied_files.includes('tasks/tasks.json'));
|
|
32
35
|
assert.ok(r.copied_files.includes('specs/product.md'));
|
|
@@ -36,9 +39,11 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
36
39
|
assert.ok(fs.existsSync(r.state_db_path));
|
|
37
40
|
const db = new Database(r.state_db_path, { readonly: true });
|
|
38
41
|
try {
|
|
39
|
-
const version = db.prepare(
|
|
40
|
-
assert.equal(version.version,
|
|
42
|
+
const version = db.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
|
|
43
|
+
assert.equal(version.version, EXPECTED_VERSION);
|
|
41
44
|
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
|
|
45
|
+
const baseline = db.prepare('SELECT mode, status, project_name FROM baselines').get();
|
|
46
|
+
assert.deepEqual(baseline, { mode: 'greenfield', status: 'draft', project_name: 'demo' });
|
|
42
47
|
} finally {
|
|
43
48
|
db.close();
|
|
44
49
|
}
|
|
@@ -46,7 +51,42 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
46
51
|
} finally { cleanup(target); }
|
|
47
52
|
});
|
|
48
53
|
|
|
49
|
-
test('initProject
|
|
54
|
+
test('initProject auto-detects existing source as brownfield and starts adoption', () => {
|
|
55
|
+
const target = mkTempDir();
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(path.join(target, 'src'));
|
|
58
|
+
fs.writeFileSync(path.join(target, 'package.json'), '{"name":"legacy-app"}\n');
|
|
59
|
+
fs.writeFileSync(path.join(target, 'src', 'index.js'), 'module.exports = true;\n');
|
|
60
|
+
const result = initProject({ target_dir: target, project_name: 'legacy-app' });
|
|
61
|
+
assert.equal(result.mode, 'brownfield');
|
|
62
|
+
const db = new Database(result.state_db_path, { readonly: true });
|
|
63
|
+
try {
|
|
64
|
+
const baseline = db.prepare('SELECT mode, status, scope_json FROM baselines').get();
|
|
65
|
+
assert.equal(baseline.mode, 'brownfield');
|
|
66
|
+
assert.equal(baseline.status, 'adopting');
|
|
67
|
+
assert.deepEqual(JSON.parse(baseline.scope_json), ['.']);
|
|
68
|
+
} finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
} finally { cleanup(target); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('maintenance CLI forwards an explicit initialization mode', () => {
|
|
75
|
+
const target = mkTempDir();
|
|
76
|
+
try {
|
|
77
|
+
fs.writeFileSync(path.join(target, 'README.md'), '# Intentional greenfield notes\n');
|
|
78
|
+
const output = execFileSync(process.execPath, [
|
|
79
|
+
path.resolve(__dirname, '..', '..', 'ultra-tools', 'cli.cjs'),
|
|
80
|
+
'task', 'init-project', '--target-dir', target, '--project-name', 'explicit-mode',
|
|
81
|
+
'--mode', 'greenfield',
|
|
82
|
+
], { encoding: 'utf8' });
|
|
83
|
+
const envelope = JSON.parse(output);
|
|
84
|
+
assert.equal(envelope.ok, true);
|
|
85
|
+
assert.equal(envelope.data.mode, 'greenfield');
|
|
86
|
+
} finally { cleanup(target); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('initProject keeps project metadata in authoritative baseline state and tasks.json as a projection', () => {
|
|
50
90
|
const target = mkTempDir();
|
|
51
91
|
try {
|
|
52
92
|
initProject({
|
|
@@ -56,11 +96,17 @@ test('initProject injects project metadata into tasks.json', () => {
|
|
|
56
96
|
stack: 'next',
|
|
57
97
|
});
|
|
58
98
|
const tasksJson = JSON.parse(fs.readFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
59
|
-
assert.equal(tasksJson.
|
|
60
|
-
assert.equal(tasksJson.
|
|
61
|
-
assert.
|
|
62
|
-
assert.
|
|
63
|
-
|
|
99
|
+
assert.equal(tasksJson.schema_version, '4.5');
|
|
100
|
+
assert.equal(tasksJson.source, '.ultra/state.db');
|
|
101
|
+
assert.deepEqual(tasksJson.tasks, []);
|
|
102
|
+
assert.equal('project' in tasksJson, false);
|
|
103
|
+
const db = new Database(path.join(target, '.ultra', 'state.db'), { readonly: true });
|
|
104
|
+
try {
|
|
105
|
+
const baseline = db.prepare('SELECT project_name, project_type, stack FROM baselines').get();
|
|
106
|
+
assert.deepEqual(baseline, { project_name: 'meta-demo', project_type: 'web', stack: 'next' });
|
|
107
|
+
} finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
64
110
|
} finally { cleanup(target); }
|
|
65
111
|
});
|
|
66
112
|
|
|
@@ -14,12 +14,23 @@ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio
|
|
|
14
14
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
15
15
|
const SERVER = path.join(REPO_ROOT, 'mcp-server', 'server.cjs');
|
|
16
16
|
const PACKAGE_VERSION = require(path.join(REPO_ROOT, 'package.json')).version;
|
|
17
|
+
const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
17
18
|
|
|
18
19
|
function tmpProject() {
|
|
19
20
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mcp-'));
|
|
20
21
|
return { dir, dbPath: path.join(dir, '.ultra', 'state.db') };
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function seedMigratedBaseline(project) {
|
|
25
|
+
const { db } = initStateDb(project.dbPath);
|
|
26
|
+
db.prepare(
|
|
27
|
+
`INSERT OR IGNORE INTO baselines
|
|
28
|
+
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
29
|
+
VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
|
|
30
|
+
).run(new Date().toISOString());
|
|
31
|
+
closeStateDb(db);
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
async function withClient({ dir, dbPath }, fn, extraEnv = {}) {
|
|
24
35
|
const transport = new StdioClientTransport({
|
|
25
36
|
command: process.execPath,
|
|
@@ -74,6 +85,10 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
74
85
|
const list = await client.listTools();
|
|
75
86
|
const names = list.tools.map((t) => t.name).sort();
|
|
76
87
|
assert.deepEqual(names, [
|
|
88
|
+
'baseline.converge',
|
|
89
|
+
'baseline.get',
|
|
90
|
+
'baseline.record',
|
|
91
|
+
'baseline.start',
|
|
77
92
|
'change.archive',
|
|
78
93
|
'change.breadcrumb',
|
|
79
94
|
'change.context',
|
|
@@ -118,9 +133,78 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
118
133
|
}
|
|
119
134
|
});
|
|
120
135
|
|
|
136
|
+
test('baseline MCP tools adopt and converge an existing checkout without storing provider payloads', async () => {
|
|
137
|
+
const proj = tmpProject();
|
|
138
|
+
try {
|
|
139
|
+
fs.mkdirSync(path.join(proj.dir, '.ultra', 'specs'), { recursive: true });
|
|
140
|
+
fs.mkdirSync(path.join(proj.dir, 'src'), { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(proj.dir, '.ultra', 'specs', 'product.md'), '# Product\n\nObserved behavior.\n');
|
|
142
|
+
fs.writeFileSync(path.join(proj.dir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nCurrent boundary.\n');
|
|
143
|
+
fs.writeFileSync(path.join(proj.dir, 'src', 'index.js'), 'module.exports = true;\n');
|
|
144
|
+
require('node:child_process').execFileSync('git', ['init', '-q'], { cwd: proj.dir });
|
|
145
|
+
require('node:child_process').execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: proj.dir });
|
|
146
|
+
require('node:child_process').execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: proj.dir });
|
|
147
|
+
require('node:child_process').execFileSync('git', ['add', '.'], { cwd: proj.dir });
|
|
148
|
+
require('node:child_process').execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: proj.dir });
|
|
149
|
+
const revision = require('node:child_process').execFileSync(
|
|
150
|
+
'git', ['rev-parse', 'HEAD'], { cwd: proj.dir, encoding: 'utf8' },
|
|
151
|
+
).trim();
|
|
152
|
+
|
|
153
|
+
await withClient(proj, async (client) => {
|
|
154
|
+
const started = readToolPayload(await client.callTool({
|
|
155
|
+
name: 'baseline.start',
|
|
156
|
+
arguments: {
|
|
157
|
+
id: 'mcp-baseline', project_name: 'fixture', mode: 'brownfield',
|
|
158
|
+
repository_revision: revision, scope: ['.'],
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
assert.equal(started.baseline.status, 'adopting');
|
|
162
|
+
|
|
163
|
+
const recorded = readToolPayload(await client.callTool({
|
|
164
|
+
name: 'baseline.record',
|
|
165
|
+
arguments: {
|
|
166
|
+
id: 'mcp-baseline', repository_revision: revision,
|
|
167
|
+
spec_refs: [
|
|
168
|
+
{ kind: 'product', path: '.ultra/specs/product.md' },
|
|
169
|
+
{ kind: 'architecture', path: '.ultra/specs/architecture.md' },
|
|
170
|
+
],
|
|
171
|
+
evidence: [{ kind: 'source', ref: 'src/index.js', summary: 'Current entry point.' }],
|
|
172
|
+
verification: [{
|
|
173
|
+
name: 'smoke', command: 'node -e "require(\'./src\')"', status: 'pass', evidence: 'exit 0',
|
|
174
|
+
}],
|
|
175
|
+
unknowns: [],
|
|
176
|
+
provider_refs: {
|
|
177
|
+
code_graph: { provider: 'codebase-memory-mcp', project: 'fixture', status: 'fresh' },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
assert.match(recorded.baseline.spec_refs[0].digest, /^[0-9a-f]{64}$/);
|
|
182
|
+
|
|
183
|
+
const converged = readToolPayload(await client.callTool({
|
|
184
|
+
name: 'baseline.converge',
|
|
185
|
+
arguments: {
|
|
186
|
+
id: 'mcp-baseline', expected_revision: revision, approved_by: 'owner',
|
|
187
|
+
approval_note: 'The baseline matches the current checkout.', accept_known_red: false,
|
|
188
|
+
},
|
|
189
|
+
}));
|
|
190
|
+
assert.equal(converged.ready, true);
|
|
191
|
+
assert.equal(converged.baseline.status, 'ready');
|
|
192
|
+
|
|
193
|
+
const got = readToolPayload(await client.callTool({
|
|
194
|
+
name: 'baseline.get', arguments: {},
|
|
195
|
+
}));
|
|
196
|
+
assert.equal(got.baseline.id, 'mcp-baseline');
|
|
197
|
+
assert.equal(JSON.stringify(got.baseline).includes('captured prompt'), false);
|
|
198
|
+
});
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
121
204
|
test('change.create + change.context expose a continuous change unit with external provider references', async () => {
|
|
122
205
|
const proj = tmpProject();
|
|
123
206
|
try {
|
|
207
|
+
seedMigratedBaseline(proj);
|
|
124
208
|
fs.writeFileSync(path.join(proj.dir, 'README.md'), '# project\n');
|
|
125
209
|
await withClient(proj, async (client) => {
|
|
126
210
|
const created = await client.callTool({
|
|
@@ -172,6 +256,7 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
172
256
|
test('spec-learning MCP tools persist an approval-gated candidate and projection', async () => {
|
|
173
257
|
const proj = tmpProject();
|
|
174
258
|
try {
|
|
259
|
+
seedMigratedBaseline(proj);
|
|
175
260
|
await withClient(proj, async (client) => {
|
|
176
261
|
await client.callTool({
|
|
177
262
|
name: 'change.create',
|
|
@@ -233,6 +318,7 @@ test('spec-learning MCP tools persist an approval-gated candidate and projection
|
|
|
233
318
|
test('system.doctor returns structured provider ownership and projection health', async () => {
|
|
234
319
|
const proj = tmpProject();
|
|
235
320
|
try {
|
|
321
|
+
seedMigratedBaseline(proj);
|
|
236
322
|
await withClient(proj, async (client) => {
|
|
237
323
|
await client.callTool({
|
|
238
324
|
name: 'task.create',
|
|
@@ -515,11 +601,22 @@ test('task.init_project creates .ultra/ skeleton in a fresh target directory', a
|
|
|
515
601
|
assert.equal(payload.status, 'created');
|
|
516
602
|
assert.equal(payload.created_path, path.join(freshTarget, '.ultra'));
|
|
517
603
|
assert.equal(payload.state_db_path, path.join(freshTarget, '.ultra', 'state.db'));
|
|
604
|
+
assert.equal(payload.mode, 'greenfield');
|
|
605
|
+
assert.equal(payload.baseline.status, 'draft');
|
|
518
606
|
assert.ok(fs.existsSync(payload.state_db_path));
|
|
519
607
|
assert.ok(payload.copied_files.includes('tasks/tasks.json'));
|
|
520
608
|
const tasksJson = JSON.parse(fs.readFileSync(path.join(payload.created_path, 'tasks', 'tasks.json'), 'utf8'));
|
|
521
|
-
assert.equal(tasksJson.
|
|
522
|
-
assert.
|
|
609
|
+
assert.equal(tasksJson.source, '.ultra/state.db');
|
|
610
|
+
assert.deepEqual(tasksJson.tasks, []);
|
|
611
|
+
const state = new Database(payload.state_db_path, { readonly: true });
|
|
612
|
+
try {
|
|
613
|
+
assert.deepEqual(
|
|
614
|
+
state.prepare('SELECT project_name, project_type, mode FROM baselines').get(),
|
|
615
|
+
{ project_name: 'mcp-init', project_type: 'cli', mode: 'greenfield' },
|
|
616
|
+
);
|
|
617
|
+
} finally {
|
|
618
|
+
state.close();
|
|
619
|
+
}
|
|
523
620
|
});
|
|
524
621
|
} finally {
|
|
525
622
|
fs.rmSync(proj.dir, { recursive: true, force: true });
|