ultra-builder-pro-cli 0.8.1 → 0.10.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/CHANGELOG.md +79 -1
- package/README.md +45 -26
- package/adapters/_shared/codex-assets.cjs +38 -0
- package/adapters/_shared/handbook.cjs +10 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/runtime-assets.cjs +3 -1
- package/adapters/_shared/tests/handbook.test.cjs +9 -1
- package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
- package/adapters/_shared/tests/runtime-assets.test.cjs +5 -1
- package/adapters/kimi.js +642 -0
- package/adapters/opencode.js +46 -17
- package/adapters/tests/claude.test.cjs +4 -1
- package/adapters/tests/codex.test.cjs +4 -3
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +259 -0
- package/adapters/tests/opencode.test.cjs +46 -3
- package/bin/handbook.js +2 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +1 -1
- package/docs/AGENT-CONTEXT.md +39 -15
- package/docs/ARCHITECTURE.md +35 -9
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +8 -7
- package/docs/RUNTIME-COMPAT-MATRIX.md +110 -64
- package/docs/USER-HANDBOOK-CONTRACT.md +13 -3
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -0
- package/hooks/adapters/kimi.py +162 -0
- package/hooks/context_spine.py +218 -0
- package/hooks/health_check.py +1 -1
- package/hooks/workflow_context.py +16 -123
- package/hooks/workflow_resume.py +21 -0
- package/mcp-server/lib/change-workflow.cjs +89 -11
- package/mcp-server/lib/change-workflow.test.cjs +202 -7
- package/mcp-server/lib/context-spine.cjs +331 -0
- package/mcp-server/lib/context-spine.test.cjs +73 -0
- package/mcp-server/lib/spec-learning.cjs +191 -0
- package/mcp-server/lib/spec-learning.test.cjs +62 -0
- package/mcp-server/lib/state-db.cjs +149 -4
- package/mcp-server/lib/state-db.test.cjs +61 -0
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/init-project.test.cjs +2 -2
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +4 -2
- package/skills/learn/SKILL.md +1 -0
- package/skills/ultra-change/SKILL.md +91 -118
- package/skills/ultra-deliver/SKILL.md +93 -221
- package/skills/ultra-dev/SKILL.md +81 -310
- package/skills/ultra-doctor/SKILL.md +1 -0
- package/skills/ultra-init/SKILL.md +1 -0
- package/skills/ultra-plan/SKILL.md +72 -292
- package/skills/ultra-review/SKILL.md +79 -407
- package/skills/ultra-status/SKILL.md +55 -168
- package/skills/ultra-test/SKILL.md +64 -183
- package/skills/ultra-think/SKILL.md +1 -0
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +136 -7
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +35 -2
|
@@ -9,7 +9,8 @@ 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 = '10.0';
|
|
13
|
+
const KIMI_SCHEMA_VERSION = '9.1';
|
|
13
14
|
|
|
14
15
|
const REQUIRED_TABLES = Object.freeze([
|
|
15
16
|
'tasks',
|
|
@@ -23,6 +24,7 @@ const REQUIRED_TABLES = Object.freeze([
|
|
|
23
24
|
'changes',
|
|
24
25
|
'artifacts',
|
|
25
26
|
'context_snapshots',
|
|
27
|
+
'spec_learning_candidates',
|
|
26
28
|
'trace_links',
|
|
27
29
|
'incidents',
|
|
28
30
|
'projection_jobs',
|
|
@@ -71,6 +73,13 @@ function columnNames(db, table) {
|
|
|
71
73
|
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function latestSchemaVersion(db) {
|
|
77
|
+
if (!tableNames(db).includes('schema_version')) return null;
|
|
78
|
+
return db.prepare(
|
|
79
|
+
'SELECT version FROM schema_version ORDER BY applied_at DESC, rowid DESC LIMIT 1',
|
|
80
|
+
).get()?.version || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
function applyCompatibleColumns(db) {
|
|
75
84
|
const tables = new Set(tableNames(db));
|
|
76
85
|
if (!tables.has('tasks')) return;
|
|
@@ -87,14 +96,149 @@ function applyCompatibleColumns(db) {
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
function
|
|
99
|
+
function applyContextSpineUpgrade(db) {
|
|
100
|
+
const tables = new Set(tableNames(db));
|
|
101
|
+
if (!tables.has('context_snapshots')) return false;
|
|
102
|
+
let changed = !tables.has('spec_learning_candidates');
|
|
103
|
+
const columns = columnNames(db, 'context_snapshots');
|
|
104
|
+
const additions = [
|
|
105
|
+
['role', "TEXT NOT NULL DEFAULT 'plan' CHECK (role IN ('plan', 'implement', 'check', 'review'))"],
|
|
106
|
+
['gate', "TEXT NOT NULL DEFAULT 'alignment' CHECK (gate IN ('alignment', 'planning', 'implementation', 'verification', 'review', 'convergence', 'recovery'))"],
|
|
107
|
+
['next_action', "TEXT NOT NULL DEFAULT 'Resolve the next Ultra workflow action.'"],
|
|
108
|
+
['readiness', "TEXT NOT NULL DEFAULT 'ready' CHECK (readiness IN ('ready', 'blocked'))"],
|
|
109
|
+
['blockers_json', "TEXT NOT NULL DEFAULT '[]'"],
|
|
110
|
+
['context_json', "TEXT NOT NULL DEFAULT '{}'"],
|
|
111
|
+
['token_estimate', 'INTEGER NOT NULL DEFAULT 0 CHECK (token_estimate >= 0)'],
|
|
112
|
+
['token_budget', 'INTEGER NOT NULL DEFAULT 12000 CHECK (token_budget > 0)'],
|
|
113
|
+
];
|
|
114
|
+
for (const [name, definition] of additions) {
|
|
115
|
+
if (!columns.has(name)) {
|
|
116
|
+
db.exec(`ALTER TABLE context_snapshots ADD COLUMN ${name} ${definition}`);
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
db.exec(`
|
|
121
|
+
CREATE TABLE IF NOT EXISTS spec_learning_candidates (
|
|
122
|
+
id TEXT PRIMARY KEY,
|
|
123
|
+
change_id TEXT NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
124
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
125
|
+
target_ref TEXT NOT NULL,
|
|
126
|
+
summary TEXT NOT NULL,
|
|
127
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
128
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
129
|
+
CHECK (status IN ('proposed', 'approved', 'rejected', 'applied')),
|
|
130
|
+
resolution TEXT,
|
|
131
|
+
proposed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
132
|
+
resolved_at TEXT,
|
|
133
|
+
applied_at TEXT
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS spec_learning_change
|
|
136
|
+
ON spec_learning_candidates(change_id, status, proposed_at);
|
|
137
|
+
`);
|
|
138
|
+
return changed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tableSupportsKimi(db, table) {
|
|
142
|
+
const row = db.prepare(
|
|
143
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
144
|
+
).get(table);
|
|
145
|
+
return typeof row?.sql === 'string' && row.sql.includes("'kimi'");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
|
|
149
|
+
if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
|
|
150
|
+
const previousVersion = fromVersion || 'unknown';
|
|
151
|
+
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
152
|
+
db.pragma('foreign_keys = OFF');
|
|
153
|
+
try {
|
|
154
|
+
db.transaction(() => {
|
|
155
|
+
db.exec(`
|
|
156
|
+
DROP TABLE IF EXISTS events_runtime_upgrade;
|
|
157
|
+
CREATE TABLE events_runtime_upgrade (
|
|
158
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
159
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
160
|
+
type TEXT NOT NULL,
|
|
161
|
+
task_id TEXT,
|
|
162
|
+
change_id TEXT,
|
|
163
|
+
session_id TEXT,
|
|
164
|
+
runtime TEXT CHECK (runtime IS NULL OR runtime IN ('claude', 'opencode', 'codex', 'kimi')),
|
|
165
|
+
payload_json TEXT
|
|
166
|
+
);
|
|
167
|
+
INSERT INTO events_runtime_upgrade
|
|
168
|
+
(id, ts, type, task_id, change_id, session_id, runtime, payload_json)
|
|
169
|
+
SELECT id, ts, type, task_id, change_id, session_id, runtime, payload_json FROM events;
|
|
170
|
+
DROP TABLE events;
|
|
171
|
+
ALTER TABLE events_runtime_upgrade RENAME TO events;
|
|
172
|
+
CREATE INDEX events_ts_type ON events(ts, type);
|
|
173
|
+
CREATE INDEX events_task ON events(task_id, id);
|
|
174
|
+
CREATE INDEX events_session ON events(session_id, id);
|
|
175
|
+
CREATE INDEX events_change ON events(change_id, id);
|
|
176
|
+
|
|
177
|
+
DROP TABLE IF EXISTS sessions_runtime_upgrade;
|
|
178
|
+
CREATE TABLE sessions_runtime_upgrade (
|
|
179
|
+
sid TEXT PRIMARY KEY,
|
|
180
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
181
|
+
runtime TEXT NOT NULL CHECK (runtime IN ('claude', 'opencode', 'codex', 'kimi')),
|
|
182
|
+
pid INTEGER,
|
|
183
|
+
worktree_path TEXT NOT NULL,
|
|
184
|
+
artifact_dir TEXT NOT NULL,
|
|
185
|
+
status TEXT NOT NULL DEFAULT 'running'
|
|
186
|
+
CHECK (status IN ('running', 'completed', 'crashed', 'orphan')),
|
|
187
|
+
lease_expires_at TEXT NOT NULL,
|
|
188
|
+
heartbeat_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
189
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
190
|
+
);
|
|
191
|
+
INSERT INTO sessions_runtime_upgrade
|
|
192
|
+
(sid, task_id, runtime, pid, worktree_path, artifact_dir, status,
|
|
193
|
+
lease_expires_at, heartbeat_at, started_at)
|
|
194
|
+
SELECT sid, task_id, runtime, pid, worktree_path, artifact_dir, status,
|
|
195
|
+
lease_expires_at, heartbeat_at, started_at FROM sessions;
|
|
196
|
+
DROP TABLE sessions;
|
|
197
|
+
ALTER TABLE sessions_runtime_upgrade RENAME TO sessions;
|
|
198
|
+
CREATE INDEX sessions_active ON sessions(status, task_id);
|
|
199
|
+
CREATE INDEX sessions_lease ON sessions(lease_expires_at) WHERE status = 'running';
|
|
200
|
+
`);
|
|
201
|
+
db.prepare(
|
|
202
|
+
`INSERT INTO migration_history
|
|
203
|
+
(from_version, to_version, direction, status, notes)
|
|
204
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
205
|
+
).run(
|
|
206
|
+
previousVersion,
|
|
207
|
+
KIMI_SCHEMA_VERSION,
|
|
208
|
+
'Add Kimi to durable event and session runtime constraints',
|
|
209
|
+
);
|
|
210
|
+
const violations = db.pragma('foreign_key_check');
|
|
211
|
+
if (violations.length > 0) {
|
|
212
|
+
throw new Error(`runtime constraint migration produced ${violations.length} foreign key violation(s)`);
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
} finally {
|
|
216
|
+
db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db)) {
|
|
222
|
+
const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
|
|
91
223
|
db.transaction(() => {
|
|
92
224
|
applyCompatibleColumns(db);
|
|
225
|
+
const contextChanged = applyContextSpineUpgrade(db);
|
|
226
|
+
if (contextChanged || (fromVersion && fromVersion !== EXPECTED_VERSION)) {
|
|
227
|
+
db.prepare(
|
|
228
|
+
`INSERT INTO migration_history
|
|
229
|
+
(from_version, to_version, direction, status, notes)
|
|
230
|
+
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
231
|
+
).run(
|
|
232
|
+
runtimeChanged ? KIMI_SCHEMA_VERSION : (fromVersion || 'unknown'),
|
|
233
|
+
EXPECTED_VERSION,
|
|
234
|
+
'Add Context Spine role/gate readiness, execution contracts, breadcrumbs, and specification learning',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
93
237
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
94
238
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
95
239
|
db.prepare(
|
|
96
240
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
97
|
-
).run(EXPECTED_VERSION, '
|
|
241
|
+
).run(EXPECTED_VERSION, 'Role-scoped context snapshots, deterministic breadcrumbs, and approval-gated specification learning');
|
|
98
242
|
})();
|
|
99
243
|
}
|
|
100
244
|
|
|
@@ -116,12 +260,13 @@ function ensureSchemaVersion(db) {
|
|
|
116
260
|
function initStateDb(dbPath) {
|
|
117
261
|
const db = openStateDb(dbPath);
|
|
118
262
|
const existing = new Set(tableNames(db));
|
|
263
|
+
const fromVersion = latestSchemaVersion(db);
|
|
119
264
|
applyCompatibleColumns(db);
|
|
120
265
|
const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
|
|
121
266
|
if (missing.length > 0) {
|
|
122
267
|
applySchema(db);
|
|
123
268
|
}
|
|
124
|
-
applyCompatibleUpgrades(db);
|
|
269
|
+
applyCompatibleUpgrades(db, fromVersion);
|
|
125
270
|
const version = ensureSchemaVersion(db);
|
|
126
271
|
return {
|
|
127
272
|
db,
|
|
@@ -9,11 +9,13 @@ const path = require('node:path');
|
|
|
9
9
|
const {
|
|
10
10
|
EXPECTED_VERSION,
|
|
11
11
|
REQUIRED_TABLES,
|
|
12
|
+
SCHEMA_FILE,
|
|
12
13
|
initStateDb,
|
|
13
14
|
closeStateDb,
|
|
14
15
|
openStateDb,
|
|
15
16
|
tableNames,
|
|
16
17
|
} = require('./state-db.cjs');
|
|
18
|
+
const stateOps = require('./state-ops.cjs');
|
|
17
19
|
|
|
18
20
|
function tmpDbPath(prefix = 'ubp-state') {
|
|
19
21
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
@@ -114,3 +116,62 @@ test('Phase 8A.1 schema: tasks.parent_id column + tasks_parent partial index + s
|
|
|
114
116
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
115
117
|
}
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
test('initStateDb migrates existing runtime constraints to Kimi without losing references', () => {
|
|
121
|
+
const { dir, file } = tmpDbPath('ubp-kimi-runtime-upgrade');
|
|
122
|
+
try {
|
|
123
|
+
const legacy = openStateDb(file);
|
|
124
|
+
const legacySchema = fs.readFileSync(SCHEMA_FILE, 'utf8').replaceAll(", 'kimi'", '');
|
|
125
|
+
legacy.exec(legacySchema);
|
|
126
|
+
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.0')").run();
|
|
127
|
+
legacy.prepare(
|
|
128
|
+
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-old', 'Old', 'feature', 'P1')",
|
|
129
|
+
).run();
|
|
130
|
+
legacy.prepare(
|
|
131
|
+
`INSERT INTO sessions
|
|
132
|
+
(sid, task_id, runtime, worktree_path, artifact_dir, lease_expires_at)
|
|
133
|
+
VALUES ('session-old', 'task-old', 'codex', '/tmp/worktree', '/tmp/artifacts', '2099-01-01T00:00:00.000Z')`,
|
|
134
|
+
).run();
|
|
135
|
+
legacy.prepare(
|
|
136
|
+
"INSERT INTO telemetry (session_id, event_type, tool_name) VALUES ('session-old', 'tool_call', 'task.list')",
|
|
137
|
+
).run();
|
|
138
|
+
legacy.prepare(
|
|
139
|
+
`INSERT INTO incidents
|
|
140
|
+
(id, code, severity, message, session_id)
|
|
141
|
+
VALUES ('incident-old', 'OLD', 'warning', 'preserve me', 'session-old')`,
|
|
142
|
+
).run();
|
|
143
|
+
legacy.prepare(
|
|
144
|
+
"INSERT INTO events (type, session_id, runtime) VALUES ('session_spawned', 'session-old', 'codex')",
|
|
145
|
+
).run();
|
|
146
|
+
closeStateDb(legacy);
|
|
147
|
+
|
|
148
|
+
const upgraded = initStateDb(file);
|
|
149
|
+
assert.equal(upgraded.schema_version, '10.0');
|
|
150
|
+
assert.equal(upgraded.db.prepare("SELECT runtime FROM sessions WHERE sid = 'session-old'").get().runtime, 'codex');
|
|
151
|
+
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE session_id = 'session-old'").get().n, 1);
|
|
152
|
+
assert.equal(upgraded.db.prepare("SELECT COUNT(*) AS n FROM incidents WHERE session_id = 'session-old'").get().n, 1);
|
|
153
|
+
assert.deepEqual(upgraded.db.pragma('foreign_key_check'), []);
|
|
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",
|
|
156
|
+
).all();
|
|
157
|
+
assert.ok(migrations.some((row) => row.to_version === '9.1' && /Kimi/.test(row.notes)));
|
|
158
|
+
assert.ok(migrations.some((row) => row.to_version === '10.0' && /Context Spine/.test(row.notes)));
|
|
159
|
+
|
|
160
|
+
upgraded.db.prepare(
|
|
161
|
+
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
|
162
|
+
).run();
|
|
163
|
+
const session = stateOps.createSession(upgraded.db, {
|
|
164
|
+
sid: 'session-kimi',
|
|
165
|
+
task_id: 'task-kimi',
|
|
166
|
+
runtime: 'kimi',
|
|
167
|
+
worktree_path: '/tmp/kimi-worktree',
|
|
168
|
+
artifact_dir: '/tmp/kimi-artifacts',
|
|
169
|
+
});
|
|
170
|
+
assert.equal(session.runtime, 'kimi');
|
|
171
|
+
const event = stateOps.appendEvent(upgraded.db, { type: 'kimi-ready', runtime: 'kimi' });
|
|
172
|
+
assert.ok(event.event_id > 0);
|
|
173
|
+
closeStateDb(upgraded.db);
|
|
174
|
+
} finally {
|
|
175
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
package/mcp-server/server.cjs
CHANGED
|
@@ -73,6 +73,9 @@ const CHANGE_TOOLS = Object.freeze([
|
|
|
73
73
|
'change.get',
|
|
74
74
|
'change.list',
|
|
75
75
|
'change.context',
|
|
76
|
+
'change.breadcrumb',
|
|
77
|
+
'change.learning_propose',
|
|
78
|
+
'change.learning_resolve',
|
|
76
79
|
'change.converge',
|
|
77
80
|
'change.archive',
|
|
78
81
|
]);
|
|
@@ -95,6 +98,7 @@ const MUTATING_TOOLS = new Set([
|
|
|
95
98
|
'session.spawn', 'session.close', 'session.heartbeat',
|
|
96
99
|
'plan.export',
|
|
97
100
|
'change.create', 'change.update', 'change.context', 'change.converge', 'change.archive',
|
|
101
|
+
'change.learning_propose', 'change.learning_resolve',
|
|
98
102
|
]);
|
|
99
103
|
|
|
100
104
|
function loadRegisteredTools() {
|
|
@@ -368,7 +372,12 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
368
372
|
err.code = 'CHANGE_NOT_FOUND';
|
|
369
373
|
throw err;
|
|
370
374
|
}
|
|
371
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
change: {
|
|
377
|
+
...change,
|
|
378
|
+
learning_candidates: changes.listSpecLearning(db, change.id),
|
|
379
|
+
},
|
|
380
|
+
};
|
|
372
381
|
}
|
|
373
382
|
case 'change.list': {
|
|
374
383
|
const rows = changes.listChanges(db, input || {});
|
|
@@ -382,6 +391,27 @@ async function dispatchTool(name, input, db, ctx = {}) {
|
|
|
382
391
|
manifest: out.manifest,
|
|
383
392
|
};
|
|
384
393
|
}
|
|
394
|
+
case 'change.breadcrumb': {
|
|
395
|
+
return {
|
|
396
|
+
breadcrumb: changes.readBreadcrumb(
|
|
397
|
+
db, input || {}, { rootDir: ctx.rootDir || process.cwd() },
|
|
398
|
+
),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
case 'change.learning_propose': {
|
|
402
|
+
return {
|
|
403
|
+
candidate: changes.proposeSpecLearning(
|
|
404
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case 'change.learning_resolve': {
|
|
409
|
+
return {
|
|
410
|
+
candidate: changes.resolveSpecLearning(
|
|
411
|
+
db, input, { rootDir: ctx.rootDir || process.cwd() },
|
|
412
|
+
),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
385
415
|
case 'change.converge': {
|
|
386
416
|
return changes.convergeChange(db, input, { rootDir: ctx.rootDir || process.cwd() });
|
|
387
417
|
}
|
|
@@ -11,7 +11,7 @@ const Database = require('better-sqlite3');
|
|
|
11
11
|
|
|
12
12
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
13
|
const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
|
|
14
|
-
const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
14
|
+
const { EXPECTED_VERSION, initStateDb, closeStateDb } = require('../lib/state-db.cjs');
|
|
15
15
|
const ops = require('../lib/state-ops.cjs');
|
|
16
16
|
|
|
17
17
|
function tmpDir() {
|
|
@@ -97,8 +97,8 @@ test('db backup writes a file that opens independently with the same schema', ()
|
|
|
97
97
|
|
|
98
98
|
// Independently open the backup with a fresh connection
|
|
99
99
|
const snap = new Database(backupPath);
|
|
100
|
-
const v = snap.prepare(
|
|
101
|
-
assert.equal(v.version,
|
|
100
|
+
const v = snap.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
|
|
101
|
+
assert.equal(v.version, EXPECTED_VERSION);
|
|
102
102
|
const t = snap.prepare("SELECT id FROM tasks WHERE id = 'b-1'").get();
|
|
103
103
|
assert.equal(t.id, 'b-1');
|
|
104
104
|
snap.close();
|
|
@@ -36,8 +36,8 @@ test('initProject copies bundled template into .ultra/', () => {
|
|
|
36
36
|
assert.ok(fs.existsSync(r.state_db_path));
|
|
37
37
|
const db = new Database(r.state_db_path, { readonly: true });
|
|
38
38
|
try {
|
|
39
|
-
const version = db.prepare("SELECT version FROM schema_version WHERE version = '9.
|
|
40
|
-
assert.equal(version.version, '9.
|
|
39
|
+
const version = db.prepare("SELECT version FROM schema_version WHERE version = '9.1'").get();
|
|
40
|
+
assert.equal(version.version, '9.1');
|
|
41
41
|
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
|
|
42
42
|
} finally {
|
|
43
43
|
db.close();
|
|
@@ -75,10 +75,13 @@ test('listTools returns workflow tools and exposes no Ultra memory API', async (
|
|
|
75
75
|
const names = list.tools.map((t) => t.name).sort();
|
|
76
76
|
assert.deepEqual(names, [
|
|
77
77
|
'change.archive',
|
|
78
|
+
'change.breadcrumb',
|
|
78
79
|
'change.context',
|
|
79
80
|
'change.converge',
|
|
80
81
|
'change.create',
|
|
81
82
|
'change.get',
|
|
83
|
+
'change.learning_propose',
|
|
84
|
+
'change.learning_resolve',
|
|
82
85
|
'change.list',
|
|
83
86
|
'change.update',
|
|
84
87
|
'plan.export',
|
|
@@ -142,9 +145,19 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
142
145
|
name: 'change.context', arguments: { id: 'mcp-change' },
|
|
143
146
|
}));
|
|
144
147
|
assert.equal(context.manifest.change.id, 'mcp-change');
|
|
148
|
+
assert.equal(context.manifest.schema_version, '2.0');
|
|
149
|
+
assert.equal(context.manifest.role, 'plan');
|
|
150
|
+
assert.equal(context.manifest.readiness.status, 'ready');
|
|
145
151
|
assert.equal(context.manifest.providers.memory.provider, 'cloud-mem');
|
|
146
152
|
assert.equal(context.manifest.provider_boundary.includes('content remain external'), true);
|
|
147
153
|
|
|
154
|
+
const breadcrumb = readToolPayload(await client.callTool({
|
|
155
|
+
name: 'change.breadcrumb', arguments: { id: 'mcp-change' },
|
|
156
|
+
}));
|
|
157
|
+
assert.equal(breadcrumb.breadcrumb.change_id, 'mcp-change');
|
|
158
|
+
assert.equal(breadcrumb.breadcrumb.recommended_workflow, 'ultra-change');
|
|
159
|
+
assert.match(breadcrumb.breadcrumb.context_manifest_hash, /^[0-9a-f]{64}$/);
|
|
160
|
+
|
|
148
161
|
const listed = readToolPayload(await client.callTool({
|
|
149
162
|
name: 'change.list', arguments: { status: 'active' },
|
|
150
163
|
}));
|
|
@@ -156,6 +169,67 @@ test('change.create + change.context expose a continuous change unit with extern
|
|
|
156
169
|
}
|
|
157
170
|
});
|
|
158
171
|
|
|
172
|
+
test('spec-learning MCP tools persist an approval-gated candidate and projection', async () => {
|
|
173
|
+
const proj = tmpProject();
|
|
174
|
+
try {
|
|
175
|
+
await withClient(proj, async (client) => {
|
|
176
|
+
await client.callTool({
|
|
177
|
+
name: 'change.create',
|
|
178
|
+
arguments: {
|
|
179
|
+
id: 'learning-change', title: 'Learn stable contract', kind: 'quick',
|
|
180
|
+
intent: 'Persist only approved specification discoveries.',
|
|
181
|
+
docs_impact: { status: 'none', files: [], rationale: 'Contract fixture.' },
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
await client.callTool({
|
|
185
|
+
name: 'task.create',
|
|
186
|
+
arguments: {
|
|
187
|
+
id: 'learning-task', title: 'Discover contract', type: 'feature', priority: 'P1',
|
|
188
|
+
change_id: 'learning-change',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const proposed = readToolPayload(await client.callTool({
|
|
192
|
+
name: 'change.learning_propose',
|
|
193
|
+
arguments: {
|
|
194
|
+
id: 'learning-contract', change_id: 'learning-change', task_id: 'learning-task',
|
|
195
|
+
target_ref: 'README.md#contract', summary: 'Context compilation fails closed on stale refs.',
|
|
196
|
+
evidence: ['node --test context-spine.test.cjs'],
|
|
197
|
+
},
|
|
198
|
+
}));
|
|
199
|
+
assert.equal(proposed.candidate.status, 'proposed');
|
|
200
|
+
const withLearning = readToolPayload(await client.callTool({
|
|
201
|
+
name: 'change.get', arguments: { id: 'learning-change' },
|
|
202
|
+
}));
|
|
203
|
+
assert.deepEqual(
|
|
204
|
+
withLearning.change.learning_candidates.map((candidate) => candidate.id),
|
|
205
|
+
['learning-contract'],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const approved = readToolPayload(await client.callTool({
|
|
209
|
+
name: 'change.learning_resolve',
|
|
210
|
+
arguments: {
|
|
211
|
+
change_id: 'learning-change', candidate_id: 'learning-contract', decision: 'approve',
|
|
212
|
+
resolution: 'Stable behavior contract.',
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
assert.equal(approved.candidate.status, 'approved');
|
|
216
|
+
const applied = readToolPayload(await client.callTool({
|
|
217
|
+
name: 'change.learning_resolve',
|
|
218
|
+
arguments: {
|
|
219
|
+
change_id: 'learning-change', candidate_id: 'learning-contract', decision: 'apply',
|
|
220
|
+
resolution: 'Applied to README.md#contract.',
|
|
221
|
+
},
|
|
222
|
+
}));
|
|
223
|
+
assert.equal(applied.candidate.status, 'applied');
|
|
224
|
+
assert.ok(fs.existsSync(path.join(
|
|
225
|
+
proj.dir, '.ultra', 'changes', 'active', 'learning-change', 'spec-learning.json',
|
|
226
|
+
)));
|
|
227
|
+
});
|
|
228
|
+
} finally {
|
|
229
|
+
fs.rmSync(proj.dir, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
159
233
|
test('system.doctor returns structured provider ownership and projection health', async () => {
|
|
160
234
|
const proj = tmpProject();
|
|
161
235
|
try {
|
|
@@ -8,7 +8,7 @@ const path = require('node:path');
|
|
|
8
8
|
|
|
9
9
|
const stateDb = require('../lib/state-db.cjs');
|
|
10
10
|
|
|
11
|
-
test('initStateDb upgrades a pre-
|
|
11
|
+
test('initStateDb upgrades a pre-10.0 database in place with Context Spine state', () => {
|
|
12
12
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-upgrade-'));
|
|
13
13
|
const file = path.join(dir, 'state.db');
|
|
14
14
|
try {
|
|
@@ -59,8 +59,25 @@ test('initStateDb upgrades a pre-9.0 database in place with task/event change li
|
|
|
59
59
|
assert.ok(columns.includes('change_id'));
|
|
60
60
|
const eventColumns = upgraded.db.prepare('PRAGMA table_info(events)').all().map((row) => row.name);
|
|
61
61
|
assert.ok(eventColumns.includes('change_id'));
|
|
62
|
-
assert.equal(upgraded.schema_version, '
|
|
63
|
-
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '
|
|
62
|
+
assert.equal(upgraded.schema_version, '10.0');
|
|
63
|
+
assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '10.0'").get());
|
|
64
|
+
const contextColumns = upgraded.db.prepare('PRAGMA table_info(context_snapshots)').all()
|
|
65
|
+
.map((row) => row.name);
|
|
66
|
+
for (const column of ['role', 'gate', 'next_action', 'readiness', 'context_json', 'token_budget']) {
|
|
67
|
+
assert.ok(contextColumns.includes(column), column);
|
|
68
|
+
}
|
|
69
|
+
assert.ok(upgraded.db.prepare(
|
|
70
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'spec_learning_candidates'",
|
|
71
|
+
).get());
|
|
72
|
+
const contextMigration = upgraded.db.prepare(
|
|
73
|
+
"SELECT from_version, to_version, notes FROM migration_history WHERE to_version = '10.0' ORDER BY id DESC LIMIT 1",
|
|
74
|
+
).get();
|
|
75
|
+
assert.equal(contextMigration.from_version, '9.1');
|
|
76
|
+
assert.equal(contextMigration.to_version, '10.0');
|
|
77
|
+
assert.match(contextMigration.notes, /Context Spine/);
|
|
78
|
+
assert.ok(upgraded.db.prepare(
|
|
79
|
+
"SELECT 1 FROM migration_history WHERE from_version = '8A.1' AND to_version = '9.1' AND notes LIKE '%Kimi%'",
|
|
80
|
+
).get());
|
|
64
81
|
stateDb.closeStateDb(upgraded.db);
|
|
65
82
|
} finally {
|
|
66
83
|
fs.rmSync(dir, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, and
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Multi-runtime installer for the Ultra Builder Pro agent engineering system — deploys commands, agents, skills, hooks, and MCP server to Claude Code, OpenCode, Codex CLI, and Kimi Code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
7
7
|
"ubp": "bin/install.js",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"claude-code",
|
|
61
61
|
"opencode",
|
|
62
62
|
"codex",
|
|
63
|
+
"kimi-code",
|
|
63
64
|
"agent",
|
|
64
65
|
"cli",
|
|
65
66
|
"multi-runtime",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"verify:release": "npm run test:all && npm run test:hooks && npm audit --omit=dev --audit-level=high",
|
|
93
94
|
"prepublishOnly": "npm run verify:release",
|
|
94
95
|
"install:claude": "node bin/install.js --claude --local",
|
|
96
|
+
"install:kimi": "node bin/install.js --kimi --local",
|
|
95
97
|
"install:all": "node bin/install.js --all --local"
|
|
96
98
|
},
|
|
97
99
|
"dependencies": {
|