ultra-builder-pro-cli 0.9.0 → 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 +47 -0
- package/README.md +11 -6
- package/adapters/_shared/codex-assets.cjs +10 -0
- package/adapters/_shared/handbook.cjs +2 -0
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/_shared/tests/handbook.test.cjs +3 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +1 -0
- package/adapters/kimi.js +2 -2
- 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.test.cjs +6 -2
- package/adapters/tests/opencode.test.cjs +46 -3
- package/docs/AGENT-CONTEXT.md +34 -12
- package/docs/ARCHITECTURE.md +31 -5
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +5 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +7 -5
- package/docs/USER-HANDBOOK-CONTRACT.md +9 -2
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -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 +72 -10
- package/mcp-server/lib/state-db.test.cjs +7 -2
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +1 -1
- 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 +133 -4
- package/spec/schemas/state-db.sql +31 -0
|
@@ -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,6 +96,48 @@ function applyCompatibleColumns(db) {
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
|
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
|
+
|
|
90
141
|
function tableSupportsKimi(db, table) {
|
|
91
142
|
const row = db.prepare(
|
|
92
143
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
@@ -94,11 +145,9 @@ function tableSupportsKimi(db, table) {
|
|
|
94
145
|
return typeof row?.sql === 'string' && row.sql.includes("'kimi'");
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
function upgradeRuntimeConstraints(db) {
|
|
148
|
+
function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
|
|
98
149
|
if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
|
|
99
|
-
const previousVersion =
|
|
100
|
-
'SELECT version FROM schema_version ORDER BY applied_at DESC LIMIT 1',
|
|
101
|
-
).get()?.version || 'unknown';
|
|
150
|
+
const previousVersion = fromVersion || 'unknown';
|
|
102
151
|
const foreignKeys = db.pragma('foreign_keys', { simple: true });
|
|
103
152
|
db.pragma('foreign_keys = OFF');
|
|
104
153
|
try {
|
|
@@ -155,7 +204,7 @@ function upgradeRuntimeConstraints(db) {
|
|
|
155
204
|
VALUES (?, ?, 'forward', 'success', ?)`,
|
|
156
205
|
).run(
|
|
157
206
|
previousVersion,
|
|
158
|
-
|
|
207
|
+
KIMI_SCHEMA_VERSION,
|
|
159
208
|
'Add Kimi to durable event and session runtime constraints',
|
|
160
209
|
);
|
|
161
210
|
const violations = db.pragma('foreign_key_check');
|
|
@@ -169,15 +218,27 @@ function upgradeRuntimeConstraints(db) {
|
|
|
169
218
|
return true;
|
|
170
219
|
}
|
|
171
220
|
|
|
172
|
-
function applyCompatibleUpgrades(db) {
|
|
173
|
-
upgradeRuntimeConstraints(db);
|
|
221
|
+
function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db)) {
|
|
222
|
+
const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
|
|
174
223
|
db.transaction(() => {
|
|
175
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
|
+
}
|
|
176
237
|
db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
|
|
177
238
|
db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
|
|
178
239
|
db.prepare(
|
|
179
240
|
'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
|
|
180
|
-
).run(EXPECTED_VERSION, '
|
|
241
|
+
).run(EXPECTED_VERSION, 'Role-scoped context snapshots, deterministic breadcrumbs, and approval-gated specification learning');
|
|
181
242
|
})();
|
|
182
243
|
}
|
|
183
244
|
|
|
@@ -199,12 +260,13 @@ function ensureSchemaVersion(db) {
|
|
|
199
260
|
function initStateDb(dbPath) {
|
|
200
261
|
const db = openStateDb(dbPath);
|
|
201
262
|
const existing = new Set(tableNames(db));
|
|
263
|
+
const fromVersion = latestSchemaVersion(db);
|
|
202
264
|
applyCompatibleColumns(db);
|
|
203
265
|
const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
|
|
204
266
|
if (missing.length > 0) {
|
|
205
267
|
applySchema(db);
|
|
206
268
|
}
|
|
207
|
-
applyCompatibleUpgrades(db);
|
|
269
|
+
applyCompatibleUpgrades(db, fromVersion);
|
|
208
270
|
const version = ensureSchemaVersion(db);
|
|
209
271
|
return {
|
|
210
272
|
db,
|
|
@@ -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
|
|
126
|
+
legacy.prepare("DELETE FROM schema_version WHERE version IN ('9.1', '10.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,11 +146,16 @@ 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, '10.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
|
+
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)));
|
|
154
159
|
|
|
155
160
|
upgraded.db.prepare(
|
|
156
161
|
"INSERT INTO tasks (id, title, type, priority) VALUES ('task-kimi', 'Kimi', 'feature', 'P1')",
|
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();
|
|
@@ -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.1 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
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",
|
package/skills/learn/SKILL.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra-change
|
|
3
|
-
description: "
|
|
3
|
+
description: "Open or resume a continuous change, capture its delta and decisions, compile bounded role context, and route one executable slice."
|
|
4
|
+
user-invocable: true
|
|
4
5
|
runtime: all
|
|
5
6
|
mcp_tools_required:
|
|
6
7
|
- change.create
|
|
@@ -8,142 +9,114 @@ mcp_tools_required:
|
|
|
8
9
|
- change.get
|
|
9
10
|
- change.list
|
|
10
11
|
- change.context
|
|
12
|
+
- change.breadcrumb
|
|
13
|
+
- change.learning_propose
|
|
14
|
+
- change.learning_resolve
|
|
11
15
|
- task.create
|
|
12
16
|
- task.list
|
|
13
17
|
---
|
|
14
18
|
|
|
15
|
-
# ultra-change —
|
|
19
|
+
# ultra-change — Delta Entry and Alignment Gate
|
|
16
20
|
|
|
17
|
-
Use this
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
that drifts away from specifications and verification evidence.
|
|
21
|
+
Use this after the first baseline delivery for fixes, small features, redesigns,
|
|
22
|
+
incidents, and maintenance. It keeps specifications live through a delta → implement
|
|
23
|
+
→ learn → converge loop.
|
|
21
24
|
|
|
22
|
-
## Authority
|
|
25
|
+
## Authority boundary
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
second state authority.
|
|
28
|
-
- Memory and code graph are external providers. Pass only metadata references:
|
|
29
|
-
`provider`, `project`, `reference`, `revision`, `indexed_head`, and `status`.
|
|
30
|
-
- Never paste recalled memories, graph nodes, embeddings, summaries, or provider
|
|
31
|
-
payloads into `provider_refs`. If provider data is unavailable, record
|
|
32
|
-
`status: unavailable` and continue with current-checkout evidence.
|
|
33
|
-
- If any required change/task MCP operation is unavailable, stop. Do not edit
|
|
34
|
-
`state.db`, `tasks.json`, or context projections directly.
|
|
27
|
+
All change and task lifecycle writes use the live MCP tools. Do not write raw SQLite
|
|
28
|
+
or mutate generated task/context projections. Provider payloads stay in their owning
|
|
29
|
+
memory or graph system; Ultra stores metadata references only.
|
|
35
30
|
|
|
36
|
-
##
|
|
31
|
+
## 1. Establish the change
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
- Optional flags/concepts: `quick`, `standard`, `major`, `incident` and an
|
|
40
|
-
explicit documentation impact.
|
|
33
|
+
Call `change.list` and `change.breadcrumb` first.
|
|
41
34
|
|
|
42
|
-
|
|
35
|
+
- Resume the single matching active/blocked change.
|
|
36
|
+
- If several may match, require an explicit id.
|
|
37
|
+
- Otherwise auto-discover repository facts, then ask only the highest-value unresolved
|
|
38
|
+
question. Do not ask the user for branch, stack, paths, or behavior already visible.
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
existing id, call `change.get` and resume it. If the request clearly belongs to
|
|
46
|
-
an active change, show the match and continue there rather than creating a
|
|
47
|
-
duplicate.
|
|
40
|
+
Capture:
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
- observable problem or requested outcome;
|
|
43
|
+
- acceptance and explicit non-goals;
|
|
44
|
+
- affected public seam or integration boundary;
|
|
45
|
+
- documentation impact: `required` paths, `none` with rationale, or blocking `unknown`;
|
|
46
|
+
- kind: `quick`, `standard`, `major`, or `incident`;
|
|
47
|
+
- decision inventory: accepted decisions, rejected alternatives, and unresolved choices.
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
Create with `change.create` using a stable kebab-case id. Update an existing packet
|
|
50
|
+
with `change.update`; never create a parallel packet for the same outcome.
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
## 2. Write only the delta
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| `quick` | localized fix or tiny behavior change | diff, tests, spec |
|
|
59
|
-
| `standard` | normal feature or cross-file change | diff, tests, spec, docs, review |
|
|
60
|
-
| `major` | architecture, public contract, migration, or broad redesign | diff, tests, spec, docs, review |
|
|
61
|
-
| `incident` | canonical debug lane for a production/runtime failure | diagnosis, diff, tests |
|
|
54
|
+
The bounded packet is:
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
changes
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- recovery or rollback concern when material.
|
|
74
|
-
|
|
75
|
-
Documentation impact is mandatory:
|
|
56
|
+
```text
|
|
57
|
+
.ultra/changes/active/<change-id>/
|
|
58
|
+
intent.md
|
|
59
|
+
delta/ # standard and major
|
|
60
|
+
plan.md # standard and major
|
|
61
|
+
diagnosis.md # incident
|
|
62
|
+
context-manifest.json # MCP projection
|
|
63
|
+
spec-learning.json # MCP projection
|
|
64
|
+
verification.md # convergence projection
|
|
65
|
+
```
|
|
76
66
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
`intent.md` records outcome, acceptance, non-goals, docs impact, and decisions.
|
|
68
|
+
Delta files describe only differences from the baseline. An incident diagnosis must
|
|
69
|
+
record symptom, earliest bad state, root-cause hypothesis, discriminating evidence,
|
|
70
|
+
and recovery boundary.
|
|
71
|
+
|
|
72
|
+
## 3. Create an executable slice
|
|
73
|
+
|
|
74
|
+
Prefer the smallest vertical `tracer_bullet` that crosses a live public seam. Split
|
|
75
|
+
only when ownership or verification is genuinely independent. Each task must declare:
|
|
76
|
+
|
|
77
|
+
- one observable outcome and linked `change_id`;
|
|
78
|
+
- bounded files/contracts and dependencies;
|
|
79
|
+
- `public_seam` that proves reachability;
|
|
80
|
+
- exact `verification_command`;
|
|
81
|
+
- expected red signal for a bug/incident;
|
|
82
|
+
- documentation impact.
|
|
83
|
+
|
|
84
|
+
Persist tasks with `task.create`. Generated `tasks.json` is never an input authority.
|
|
85
|
+
|
|
86
|
+
## 4. Compile Context Manifest v2
|
|
87
|
+
|
|
88
|
+
Call `change.context` for the next task with:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"id": "<change-id>",
|
|
93
|
+
"task_id": "<task-id>",
|
|
94
|
+
"role": "plan",
|
|
95
|
+
"gate": "planning",
|
|
96
|
+
"context_refs": [{"ref":"<path>","kind":"spec","reason":"defines acceptance","required":true}],
|
|
97
|
+
"budget": {"max_tokens":12000,"max_files":12},
|
|
98
|
+
"execution_contract": {
|
|
99
|
+
"slice_kind":"tracer_bullet",
|
|
100
|
+
"public_seam":"<reachable boundary>",
|
|
101
|
+
"verification_command":"<exact command>",
|
|
102
|
+
"context_budget_percent":40
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
|
|
107
|
+
Include only files needed for the current role and gate. Missing required references,
|
|
108
|
+
digest drift, file-count overflow, or a fresh-context budget over 40% blocks readiness.
|
|
109
|
+
Use `expand_contract` only for an explicitly approved wider slice.
|
|
83
110
|
|
|
84
|
-
##
|
|
111
|
+
## 5. Handle discoveries without silent spec drift
|
|
85
112
|
|
|
86
|
-
|
|
113
|
+
When implementation reveals a stable requirement, invariant, or public behavior not
|
|
114
|
+
in the baseline, call `change.learning_propose` with evidence and target document.
|
|
115
|
+
It is a candidate, not an automatic edit. A human/primary-agent decision must approve
|
|
116
|
+
or reject it; approved learning is marked applied only after the baseline was updated.
|
|
87
117
|
|
|
88
|
-
|
|
89
|
-
.ultra/changes/active/<change-id>/
|
|
90
|
-
intent.md
|
|
91
|
-
context-manifest.json
|
|
92
|
-
delta/ # required for standard and major
|
|
93
|
-
plan.md # required for standard and major
|
|
94
|
-
diagnosis.md # created and structurally required for incident
|
|
95
|
-
verification.md # generated by convergence
|
|
96
|
-
```
|
|
118
|
+
## Exit contract
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
and rollback. For `quick`, add only artifacts needed to make the decision and
|
|
102
|
-
evidence inspectable. For `incident`, complete the generated `diagnosis.md` with
|
|
103
|
-
non-empty `Reproduction`, `Hypotheses`, `Root cause`, `Regression test`, and
|
|
104
|
-
`Recovery` sections. Free-form convergence evidence never substitutes for this
|
|
105
|
-
durable diagnosis artifact.
|
|
106
|
-
|
|
107
|
-
Create at least one task through `task.create` with `change_id` set. Split only
|
|
108
|
-
when ownership, dependencies, or independent verification justify it. All task
|
|
109
|
-
status changes remain MCP-authoritative.
|
|
110
|
-
|
|
111
|
-
## 5. Compile task context
|
|
112
|
-
|
|
113
|
-
Call `change.context` before implementation and again whenever any of these
|
|
114
|
-
changes: git HEAD, task set, relevant specs, allowed paths, or provider revision.
|
|
115
|
-
Supply only the minimum task/spec/path references needed for the current slice.
|
|
116
|
-
|
|
117
|
-
Inspect `context-manifest.json` before handing work to an agent. A stale git head
|
|
118
|
-
or task set is a hard signal to recompile. Provider `status: stale` is visible
|
|
119
|
-
context, not permission to silently refresh or mutate the external provider.
|
|
120
|
-
|
|
121
|
-
## 6. Route execution
|
|
122
|
-
|
|
123
|
-
- For `incident`, diagnosis is the first execution slice. Route it to the bundled
|
|
124
|
-
`debugger` agent when the Host exposes native subagents; otherwise the primary
|
|
125
|
-
Host follows the same four-phase method. Supply only the compiled change/task
|
|
126
|
-
context. The primary Host owns the fix, updates `diagnosis.md`, and reruns the
|
|
127
|
-
discriminating regression test.
|
|
128
|
-
- Implement one linked task with `/ultra-dev <task-id>`.
|
|
129
|
-
- Run `/ultra-test` for project-level evidence.
|
|
130
|
-
- Run `/ultra-review` when the change kind requires review.
|
|
131
|
-
- Update baseline docs declared in `docs_impact` during the change, not after
|
|
132
|
-
memory has gone cold.
|
|
133
|
-
- Finish with `/ultra-deliver`; it runs deterministic change convergence and
|
|
134
|
-
archive reconciliation before release actions.
|
|
135
|
-
|
|
136
|
-
If work exposes a new requirement, update the change intent/delta and recompile
|
|
137
|
-
context before continuing. Never let code become the only record of the change.
|
|
138
|
-
|
|
139
|
-
## 7. Report
|
|
140
|
-
|
|
141
|
-
Return the change id, kind, artifact root, linked tasks, documentation impact,
|
|
142
|
-
context freshness, provider-reference status, and exact next workflow. Separate
|
|
143
|
-
provider unavailability from an Ultra runtime incident.
|
|
144
|
-
|
|
145
|
-
## What this skill does not do
|
|
146
|
-
|
|
147
|
-
- It does not implement, test, review, release, or repair runtime state.
|
|
148
|
-
- It does not own or synchronize Memory/code-graph content.
|
|
149
|
-
- It does not bypass user approval for destructive or external release actions.
|
|
120
|
+
Call `change.breadcrumb` and report the compiled task, readiness, blockers, and its
|
|
121
|
+
single next action. Do not claim alignment while documentation impact is unknown,
|
|
122
|
+
context is stale, or a material decision remains unresolved.
|