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.
Files changed (52) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +11 -6
  3. package/adapters/_shared/codex-assets.cjs +10 -0
  4. package/adapters/_shared/handbook.cjs +2 -0
  5. package/adapters/_shared/runtime-assets.cjs +1 -0
  6. package/adapters/_shared/tests/handbook.test.cjs +3 -0
  7. package/adapters/_shared/tests/runtime-assets.test.cjs +1 -0
  8. package/adapters/kimi.js +2 -2
  9. package/adapters/opencode.js +46 -17
  10. package/adapters/tests/claude.test.cjs +4 -1
  11. package/adapters/tests/codex.test.cjs +4 -3
  12. package/adapters/tests/kimi.test.cjs +6 -2
  13. package/adapters/tests/opencode.test.cjs +46 -3
  14. package/docs/AGENT-CONTEXT.md +34 -12
  15. package/docs/ARCHITECTURE.md +31 -5
  16. package/docs/PLAN.zh-CN.md +3 -2
  17. package/docs/ROADMAP.md +5 -4
  18. package/docs/RUNTIME-COMPAT-MATRIX.md +7 -5
  19. package/docs/USER-HANDBOOK-CONTRACT.md +9 -2
  20. package/hooks/README.md +6 -4
  21. package/hooks/active_task_context.py +14 -0
  22. package/hooks/context_spine.py +218 -0
  23. package/hooks/health_check.py +1 -1
  24. package/hooks/workflow_context.py +16 -123
  25. package/hooks/workflow_resume.py +21 -0
  26. package/mcp-server/lib/change-workflow.cjs +89 -11
  27. package/mcp-server/lib/change-workflow.test.cjs +202 -7
  28. package/mcp-server/lib/context-spine.cjs +331 -0
  29. package/mcp-server/lib/context-spine.test.cjs +73 -0
  30. package/mcp-server/lib/spec-learning.cjs +191 -0
  31. package/mcp-server/lib/spec-learning.test.cjs +62 -0
  32. package/mcp-server/lib/state-db.cjs +72 -10
  33. package/mcp-server/lib/state-db.test.cjs +7 -2
  34. package/mcp-server/server.cjs +31 -1
  35. package/mcp-server/tests/db-ops.test.cjs +3 -3
  36. package/mcp-server/tests/server-contract.test.cjs +74 -0
  37. package/mcp-server/tests/state-db.test.cjs +20 -3
  38. package/package.json +1 -1
  39. package/skills/learn/SKILL.md +1 -0
  40. package/skills/ultra-change/SKILL.md +91 -118
  41. package/skills/ultra-deliver/SKILL.md +93 -221
  42. package/skills/ultra-dev/SKILL.md +81 -310
  43. package/skills/ultra-doctor/SKILL.md +1 -0
  44. package/skills/ultra-init/SKILL.md +1 -0
  45. package/skills/ultra-plan/SKILL.md +72 -292
  46. package/skills/ultra-review/SKILL.md +79 -407
  47. package/skills/ultra-status/SKILL.md +55 -168
  48. package/skills/ultra-test/SKILL.md +64 -183
  49. package/skills/ultra-think/SKILL.md +1 -0
  50. package/spec/cli-protocol.md +3 -0
  51. package/spec/mcp-tools.yaml +133 -4
  52. 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 = '9.1';
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 = db.prepare(
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
- EXPECTED_VERSION,
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, 'Kimi runtime support with durable event and session state');
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 = '9.1'").run();
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, '9.1');
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')",
@@ -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 { change };
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("SELECT version FROM schema_version").get();
101
- assert.equal(v.version, '4.5');
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-9.1 database in place with task/event change linkage', () => {
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, '9.1');
63
- assert.ok(upgraded.db.prepare("SELECT 1 FROM schema_version WHERE version = '9.1'").get());
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.9.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",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: learn
3
3
  description: "Extract one reusable pattern from the current session into a valid user skill, with explicit user approval before writing."
4
+ user-invocable: true
4
5
  ---
5
6
 
6
7
  # learn — Phase 3.6
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: ultra-change
3
- description: "Start or resume a continuous post-delivery change, compile bounded context, declare documentation impact, and route the change through implementation and convergence."
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 — Continuous Change Entry
19
+ # ultra-change — Delta Entry and Alignment Gate
16
20
 
17
- Use this workflow for daily fixes, small features, redesigns, and incidents after
18
- the initial `init → research → plan → dev → test → deliver` baseline exists.
19
- Each piece of work becomes one durable change unit instead of an informal edit
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 and provider boundary
25
+ ## Authority boundary
23
26
 
24
- - `.ultra/state.db` is authoritative for the change, its tasks, events, incidents,
25
- context snapshots, and projection status.
26
- - `.ultra/changes/active/<change-id>/` contains inspectable artifacts, not a
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
- ## Arguments
31
+ ## 1. Establish the change
37
32
 
38
- - `$1`: existing change id, or a short title for a new change.
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
- ## 1. Resume before creating
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
- Call `change.list` for `active` and `blocked` changes. If `$1` matches an
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
- Do not resume `ready`, `archived`, or `cancelled` work as a new mutation. Create
50
- a follow-up change that links to the prior archive in its intent.
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
- ## 2. Classify the change
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
- Choose the smallest honest kind:
52
+ ## 2. Write only the delta
55
53
 
56
- | Kind | Use when | Required convergence evidence |
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
- When uncertain between two kinds, choose the higher-risk kind. Classification
64
- changes the proof required; it must not be used to bypass evidence.
65
-
66
- ## 3. Declare intent and documentation impact
67
-
68
- Before implementation, write a concise intent with:
69
-
70
- - observed problem or requested outcome;
71
- - acceptance behavior and non-goals;
72
- - affected contract/surface;
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
- - `required`: list every baseline document expected to change;
78
- - `none`: provide a concrete rationale;
79
- - `unknown`: allowed only while investigating and blocks convergence.
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
- Create the change with `change.create`. Use a stable kebab-case id. External
82
- provider references are optional and metadata-only.
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
- ## 4. Build the delta packet
111
+ ## 5. Handle discoveries without silent spec drift
85
112
 
86
- The change directory is the bounded execution packet:
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
- ```text
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
- For `standard` and `major`, write delta documents under `delta/` describing
99
- what changes relative to the current baseline. Do not copy the whole baseline.
100
- Write `plan.md` with live entry points, files/contracts, tests, documentation,
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.