ultra-builder-pro-cli 0.13.0 → 0.14.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 (45) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +7 -8
  3. package/adapters/_shared/codex-assets.cjs +23 -163
  4. package/adapters/_shared/runtime-assets.cjs +1 -0
  5. package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
  6. package/adapters/opencode.js +30 -120
  7. package/adapters/tests/claude.test.cjs +3 -2
  8. package/adapters/tests/codex-hook.test.cjs +34 -7
  9. package/adapters/tests/codex.test.cjs +13 -6
  10. package/adapters/tests/kimi-hook.test.cjs +34 -7
  11. package/adapters/tests/opencode.test.cjs +70 -84
  12. package/docs/AGENT-CONTEXT.md +6 -3
  13. package/docs/ARCHITECTURE.md +8 -1
  14. package/docs/ROADMAP.md +1 -1
  15. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  16. package/hooks/active_task_context.py +22 -50
  17. package/hooks/context_spine.py +61 -332
  18. package/hooks/health_check.py +19 -17
  19. package/hooks/pre_stop_check.py +28 -33
  20. package/hooks/subagent_tracker.py +13 -19
  21. package/hooks/workflow_checkpoint.py +39 -47
  22. package/hooks/workflow_context.py +4 -9
  23. package/hooks/workflow_resume.py +48 -128
  24. package/mcp-server/breadcrumb.cjs +22 -0
  25. package/mcp-server/lib/prd-parser.cjs +14 -41
  26. package/mcp-server/lib/prd-parser.test.cjs +49 -138
  27. package/mcp-server/lib/project-breadcrumb.cjs +139 -0
  28. package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
  29. package/mcp-server/lib/task-expander.cjs +9 -38
  30. package/mcp-server/lib/task-expander.test.cjs +54 -209
  31. package/mcp-server/server.cjs +21 -35
  32. package/mcp-server/tests/server-contract.test.cjs +41 -45
  33. package/package.json +2 -4
  34. package/skills/cc-collab/SKILL.md +2 -2
  35. package/skills/codex-collab/SKILL.md +2 -3
  36. package/skills/ultra-plan/SKILL.md +15 -5
  37. package/skills/ultra-verify/SKILL.md +21 -30
  38. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  39. package/spec/mcp-tools.yaml +38 -10
  40. package/mcp-server/lib/llm-client.cjs +0 -157
  41. package/mcp-server/lib/llm-client.test.cjs +0 -156
  42. package/orchestrator/planner/expand-prompt.cjs +0 -62
  43. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  44. package/orchestrator/planner/prd-prompt.cjs +0 -44
  45. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
@@ -128,8 +128,13 @@ test('every generated skill is Codex-valid, UI-visible, and free of Claude host
128
128
  assert.doesNotMatch(text, /[\u3400-\u9fff]|ultra-review-findings-v1|Context7|Exa MCP|confidence\s*>=?\s*\d+/iu);
129
129
 
130
130
  const openai = yaml.load(fs.readFileSync(path.join(skillDir, 'agents', 'openai.yaml'), 'utf8'));
131
+ const openaiSource = fs.readFileSync(path.join(skillDir, 'agents', 'openai.yaml'), 'utf8');
131
132
  assert.ok(openai.interface.display_name);
132
133
  assert.ok(openai.interface.short_description);
134
+ assert.ok(openai.interface.short_description.length >= 25, name);
135
+ assert.ok(openai.interface.short_description.length <= 64, name);
136
+ assert.match(openaiSource, /short_description:\s+['"]/);
137
+ assert.match(openaiSource, /default_prompt:\s+['"]/);
133
138
  assert.match(openai.interface.default_prompt, new RegExp(`\\$ultra-builder-pro:${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
134
139
  assert.equal(openai.policy.allow_implicit_invocation, false);
135
140
  if (MCP_DEPENDENT_SKILLS.includes(name)) {
@@ -148,14 +153,15 @@ test('every generated skill is Codex-valid, UI-visible, and free of Claude host
148
153
  assert.match(cc, /claude --safe-mode/);
149
154
 
150
155
  const verify = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-verify', 'SKILL.md'), 'utf8');
151
- assert.match(verify, /Codex owns the task/);
152
- assert.match(verify, /claude --safe-mode/);
153
- assert.match(verify, /codex-analysis\.md/);
154
- assert.doesNotMatch(verify, /codex-output\.md|Claude synthesizes|Claude-only/);
156
+ assert.match(verify, /Keep the current host responsible/);
157
+ assert.match(verify, /installed collaboration companion/);
158
+ assert.match(verify, /host-analysis\.md/);
159
+ assert.doesNotMatch(verify, /codex exec|claude --safe-mode|Claude synthesizes|Claude-only/);
155
160
 
156
161
  const waiter = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-verify', 'scripts', 'verify_wait.py'), 'utf8');
157
- assert.match(waiter, /ADVISOR = "claude"/);
158
- assert.match(waiter, /OUTPUT_FILE = "claude-output\.md"/);
162
+ assert.match(waiter, /--advisor/);
163
+ assert.match(waiter, /advisor-output\.md/);
164
+ assert.doesNotMatch(waiter, /^ADVISOR\s*=|^OUTPUT_FILE\s*=/m);
159
165
 
160
166
  const review = fs.readFileSync(path.join(layout.pluginRoot, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
161
167
  assert.match(review, /native Codex custom agents/);
@@ -231,6 +237,7 @@ test('plugin declares current Codex hooks and a project-local Ultra MCP server',
231
237
 
232
238
  for (const rel of [
233
239
  'runtime/index.cjs',
240
+ 'runtime/breadcrumb.cjs',
234
241
  'runtime/launch.cjs',
235
242
  'runtime/ultra-tools.cjs',
236
243
  'runtime/build/Release/better_sqlite3.node',
@@ -6,6 +6,10 @@ const fs = require('node:fs');
6
6
  const os = require('node:os');
7
7
  const path = require('node:path');
8
8
  const { spawnSync } = require('node:child_process');
9
+ const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
10
+ const { createChange } = require('../../mcp-server/lib/change-workflow.cjs');
11
+ const { createTask } = require('../../mcp-server/lib/state-ops.cjs');
12
+ const { compileRoleContext } = require('../../mcp-server/lib/context-spine.cjs');
9
13
 
10
14
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
11
15
  const ADAPTER = path.join(REPO_ROOT, 'hooks', 'adapters', 'kimi.py');
@@ -18,6 +22,35 @@ function run(feature, payload, args = []) {
18
22
  });
19
23
  }
20
24
 
25
+ function seedContext(project, taskId = 'task-7') {
26
+ const state = initStateDb(path.join(project, '.ultra', 'state.db'));
27
+ state.db.prepare(
28
+ `INSERT INTO baselines
29
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
30
+ VALUES ('baseline', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
31
+ ).run(new Date().toISOString());
32
+ const { change } = createChange(state.db, {
33
+ id: 'hook-change', title: 'Hook change', kind: 'quick',
34
+ intent: 'Inject the authoritative task.',
35
+ docs_impact: { status: 'none', rationale: 'fixture' },
36
+ }, { rootDir: project });
37
+ const task = createTask(state.db, {
38
+ id: taskId, title: 'Hook task', type: 'bugfix', priority: 'P0', change_id: change.id,
39
+ });
40
+ compileRoleContext(state.db, {
41
+ input: {
42
+ task_id: task.id, role: 'implement', gate: 'implementation',
43
+ execution_contract: {
44
+ slice_kind: 'tracer_bullet', public_seam: 'hook injection',
45
+ verification_command: 'node --test adapters/tests/kimi-hook.test.cjs',
46
+ },
47
+ next_action: 'Continue the authoritative hook task.',
48
+ },
49
+ change, tasks: [task], rootDir: project,
50
+ });
51
+ closeStateDb(state.db);
52
+ }
53
+
21
54
  test('Kimi hook adapter rejects non-workflow policy hooks without blocking the host', () => {
22
55
  const result = run('block_dangerous_commands.py', {
23
56
  session_id: 'session-1',
@@ -33,10 +66,7 @@ test('Kimi hook adapter rejects non-workflow policy hooks without blocking the h
33
66
  test('Kimi hook adapter translates active edit context to the native message field', () => {
34
67
  const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-'));
35
68
  try {
36
- fs.mkdirSync(path.join(project, '.ultra'));
37
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
38
- command: 'ultra-dev', task_id: 'task-7', step: '4.5', status: 'review_pending',
39
- }));
69
+ seedContext(project);
40
70
  const result = run('active_task_context.py', {
41
71
  session_id: 'session-2',
42
72
  cwd: project,
@@ -57,9 +87,6 @@ test('Kimi hook adapter emits the native deny contract for projection writes', (
57
87
  const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-deny-'));
58
88
  try {
59
89
  fs.mkdirSync(path.join(project, '.ultra'));
60
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
61
- command: 'ultra-plan', task_id: 'plan', step: 'persist', status: 'active',
62
- }));
63
90
  const result = run('active_task_context.py', {
64
91
  session_id: 'session-deny',
65
92
  cwd: project,
@@ -11,6 +11,9 @@ const { pathToFileURL } = require('node:url');
11
11
  const opencode = require('../opencode.js');
12
12
  const { parse: parseFm } = require('../_shared/frontmatter.cjs');
13
13
  const { skillsForRuntime } = require('../_shared/runtime-assets.cjs');
14
+ const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
15
+ const changes = require('../../mcp-server/lib/change-workflow.cjs');
16
+ const { createTask } = require('../../mcp-server/lib/state-ops.cjs');
14
17
 
15
18
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
16
19
 
@@ -69,10 +72,10 @@ test('install performs content-level OpenCode adaptation for commands, skills, r
69
72
  assert.match(review, /OpenCode `task` tool/);
70
73
  assert.match(review, /scripts\/review_wait\.py/);
71
74
  assert.match(codexCollab, /OpenCode remains primary/);
72
- assert.match(verify, /OpenCode writes the first analysis/);
73
- assert.match(verify, /opencode-analysis\.md/);
74
- assert.match(verify, /codex exec -s read-only/);
75
- assert.match(verify, /--ignore-user-config/);
75
+ assert.match(verify, /Keep OpenCode responsible/);
76
+ assert.match(verify, /host-analysis\.md/);
77
+ assert.match(verify, /installed collaboration companion/);
78
+ assert.doesNotMatch(verify, /codex exec|claude --safe-mode/);
76
79
  assert.doesNotMatch(plan, /LEGACY_STATE_MIGRATION_REQUIRED|v4\.4|v4\.5/);
77
80
  assert.match(plan, /never read or\s+write .*tasks\.json/i);
78
81
  assert.doesNotMatch(plan, /ultra-tools task create/);
@@ -176,93 +179,29 @@ test('install writes a schema-safe opencode.json and keeps ownership outside hos
176
179
  assert.match(plugin, /\.ultra[\\/]tasks[\\/]tasks\.json/);
177
180
  assert.match(plugin, /throw new Error/);
178
181
  assert.match(plugin, /session\.compacted/);
182
+ assert.match(plugin, /breadcrumb\.cjs/);
183
+ assert.doesNotMatch(plugin, /workflow-state\.json|context-manifest\.json/);
179
184
  assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
180
- assert.match(plugin, /Ultra context spine/);
185
+ assert.match(plugin, /canonical breadcrumb/);
181
186
  assert.doesNotMatch(plugin, /providerMetadata|External providers/);
182
187
  } finally {
183
188
  fs.rmSync(target, { recursive: true, force: true });
184
189
  }
185
190
  });
186
191
 
187
- test('OpenCode context covers active changes and projection writes are blocked for Ultra projects', async () => {
192
+ test('OpenCode routes a missing authority to init and always protects task projections', async () => {
188
193
  const target = mkTarget();
189
194
  const project = mkTarget();
190
195
  try {
191
196
  opencode.install({ configDir: target, repoRoot: REPO_ROOT });
192
- const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
193
- const module = await import(pathToFileURL(pluginFile).href);
194
- const plugin = await module.UltraBuilderProPlugin({ directory: project });
195
-
196
197
  fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
197
- fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
198
- execFileSync('git', ['init'], { cwd: project, stdio: 'ignore' });
199
- fs.writeFileSync(path.join(project, 'app.txt'), 'initial\n');
200
- execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
201
- execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'initial'], {
202
- cwd: project, stdio: 'ignore',
203
- });
204
- const compiledHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: project, encoding: 'utf8' }).trim();
205
- const changeRoot = path.join(project, '.ultra', 'changes', 'active', 'daily-fix');
206
- fs.mkdirSync(changeRoot, { recursive: true });
207
- fs.writeFileSync(path.join(changeRoot, 'context-manifest.json'), JSON.stringify({
208
- schema_version: '2.0',
209
- baseline: { id: 'project-baseline', mode: 'brownfield', status: 'ready', repository_revision: compiledHead },
210
- change: { id: 'daily-fix', title: 'Daily fix', kind: 'quick', status: 'active', intent: 'Fix drift' },
211
- role: 'implement',
212
- gate: 'implementation',
213
- next_action: 'Run the exact regression test.',
214
- readiness: {
215
- status: 'ready', blockers: [], warnings: ['CONTEXT_FILE_BUDGET_EXCEEDED'],
216
- },
217
- git: { head: compiledHead },
218
- selected_task: { id: 'daily-task', status: 'in_progress' },
219
- context: { token_estimate: 1200, budget: { max_tokens: 8000, max_files: 8 } },
220
- providers: { memory: { provider: 'cloud-mem', status: 'available' } },
221
- }));
222
-
198
+ const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?baseline`);
199
+ const plugin = await module.UltraBuilderProPlugin({ directory: project });
223
200
  const output = { system: [] };
224
201
  await plugin['experimental.chat.system.transform']({}, output);
225
- assert.match(output.system.join('\n'), /Ultra context spine/);
226
- assert.match(output.system.join('\n'), /daily-fix/);
227
- assert.match(output.system.join('\n'), /daily-task/);
228
- assert.match(output.system.join('\n'), /Role: implement/);
229
- assert.match(output.system.join('\n'), /Gate: implementation/);
230
- assert.match(output.system.join('\n'), /Readiness: ready/);
231
- assert.match(output.system.join('\n'), /Warnings: CONTEXT_FILE_BUDGET_EXCEEDED/);
232
- assert.match(output.system.join('\n'), /Run the exact regression test/);
233
- assert.doesNotMatch(output.system.join('\n'), /Fix drift|cloud-mem/);
234
-
235
- const manifestFile = path.join(changeRoot, 'context-manifest.json');
236
- const legacyManifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
237
- legacyManifest.schema_version = '1.0';
238
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
239
- const legacyOutput = { system: [] };
240
- await plugin['experimental.chat.system.transform']({}, legacyOutput);
241
- assert.match(legacyOutput.system.join('\n'), /CONTEXT_SNAPSHOT_UPGRADE_REQUIRED/);
242
- legacyManifest.schema_version = '2.0';
243
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
244
-
245
- delete legacyManifest.baseline;
246
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
247
- const missingBaselineOutput = { system: [] };
248
- await plugin['experimental.chat.system.transform']({}, missingBaselineOutput);
249
- assert.match(missingBaselineOutput.system.join('\n'), /CONTEXT_SNAPSHOT_UPGRADE_REQUIRED/);
250
- legacyManifest.baseline = {
251
- id: 'project-baseline', mode: 'brownfield', status: 'ready', repository_revision: compiledHead,
252
- };
253
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
254
-
255
- fs.writeFileSync(path.join(project, 'app.txt'), 'changed\n');
256
- execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
257
- execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'change'], {
258
- cwd: project, stdio: 'ignore',
259
- });
260
- const staleOutput = { system: [] };
261
- await plugin['experimental.chat.system.transform']({}, staleOutput);
262
- assert.match(staleOutput.system.join('\n'), /Readiness: blocked/);
263
- assert.match(staleOutput.system.join('\n'), /CONTEXT_HEAD_STALE/);
264
- assert.match(staleOutput.system.join('\n'), /recompile change\.context/);
265
-
202
+ const text = output.system.join('\n');
203
+ assert.match(text, /STATE_DB_MISSING/);
204
+ assert.match(text, /ultra-init/);
266
205
  await assert.rejects(
267
206
  plugin['tool.execute.before'](
268
207
  { tool: 'apply_patch' },
@@ -276,22 +215,69 @@ test('OpenCode context covers active changes and projection writes are blocked f
276
215
  }
277
216
  });
278
217
 
279
- test('OpenCode does not assume a project baseline is ready when no active change manifest exists', async () => {
218
+ test('OpenCode injects the DB breadcrumb and ignores conflicting workflow projections', async () => {
280
219
  const target = mkTarget();
281
220
  const project = mkTarget();
221
+ let state;
282
222
  try {
283
223
  opencode.install({ configDir: target, repoRoot: REPO_ROOT });
284
- fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
285
- fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
286
- const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?baseline`);
224
+ execFileSync('git', ['init'], { cwd: project, stdio: 'ignore' });
225
+ fs.writeFileSync(path.join(project, 'app.txt'), 'initial\n');
226
+ execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
227
+ execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'initial'], {
228
+ cwd: project, stdio: 'ignore',
229
+ });
230
+ state = initStateDb(path.join(project, '.ultra', 'state.db'));
231
+ state.db.prepare(
232
+ `INSERT INTO baselines
233
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
234
+ VALUES ('baseline-db', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
235
+ ).run(new Date().toISOString());
236
+ const { change } = changes.createChange(state.db, {
237
+ id: 'db-authority-change', title: 'DB authority', kind: 'quick',
238
+ intent: 'Use one authoritative injected state.',
239
+ docs_impact: { status: 'none', rationale: 'test fixture' },
240
+ }, { rootDir: project });
241
+ const task = createTask(state.db, {
242
+ id: 'db-authority-task', title: 'Use DB authority', type: 'bugfix', priority: 'P0',
243
+ change_id: change.id,
244
+ });
245
+ changes.compileContext(state.db, {
246
+ id: change.id,
247
+ task_id: task.id,
248
+ role: 'implement',
249
+ gate: 'implementation',
250
+ execution_contract: {
251
+ slice_kind: 'tracer_bullet', public_seam: 'OpenCode injection',
252
+ verification_command: 'node --test adapters/tests/opencode.test.cjs',
253
+ },
254
+ next_action: 'Verify the DB-only OpenCode injection.',
255
+ }, { rootDir: project });
256
+
257
+ fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
258
+ command: 'ultra-dev', task_id: 'projection-task', status: 'active', step: 'wrong',
259
+ }));
260
+ const projectionRoot = path.join(project, '.ultra', 'changes', 'active', 'projection-change');
261
+ fs.mkdirSync(projectionRoot, { recursive: true });
262
+ fs.writeFileSync(path.join(projectionRoot, 'context-manifest.json'), JSON.stringify({
263
+ schema_version: '2.0',
264
+ baseline: { id: 'projection-baseline', mode: 'brownfield', status: 'ready' },
265
+ change: { id: 'projection-change', kind: 'quick', status: 'active' },
266
+ selected_task: { id: 'projection-task', status: 'in_progress' },
267
+ readiness: { status: 'ready', blockers: [], warnings: [] },
268
+ }));
269
+
270
+ const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?db-authority`);
287
271
  const plugin = await module.UltraBuilderProPlugin({ directory: project });
288
272
  const output = { system: [] };
289
273
  await plugin['experimental.chat.system.transform']({}, output);
290
274
  const text = output.system.join('\n');
291
- assert.match(text, /ultra-status/);
292
- assert.match(text, /ultra-init/);
293
- assert.doesNotMatch(text, /Start daily work with the ultra-change workflow/);
275
+ assert.match(text, /Change: db-authority-change/);
276
+ assert.match(text, /Task: db-authority-task/);
277
+ assert.match(text, /Role: implement/);
278
+ assert.doesNotMatch(text, /projection-change|projection-task|projection-baseline/);
294
279
  } finally {
280
+ if (state) closeStateDb(state.db);
295
281
  fs.rmSync(target, { recursive: true, force: true });
296
282
  fs.rmSync(project, { recursive: true, force: true });
297
283
  }
@@ -113,8 +113,10 @@ the shared read-only `context_spine.py` breadcrumb helper:
113
113
  - `pre_stop_check.py` reports unfinished workflow position at stop without denial;
114
114
  - `subagent_tracker.py` for bounded worker lifecycle evidence.
115
115
 
116
- `workflow_context.py`, `active_task_context.py`, and `workflow_resume.py` read the
117
- same DB-derived breadcrumb through `context_spine.py`. `health_check.py` and
116
+ `workflow_context.py`, `active_task_context.py`, `workflow_resume.py`, and the
117
+ OpenCode plugin invoke the same bundled JavaScript `change.breadcrumb` reader
118
+ through the thin `context_spine.py` bridge. No hook reimplements the state query.
119
+ `health_check.py` and
118
120
  `workflow_context.py` may inspect any initialized project;
119
121
  `active_task_context.py` always protects the task projection but limits ordinary
120
122
  edit guidance to an active workflow. Compact/stop/subagent hooks remain
@@ -149,7 +151,8 @@ Archive before prune; the confirmation token is intentionally required.
149
151
  ```text
150
152
  .ultra/
151
153
  ├── state.db # authoritative SQLite store
152
- ├── workflow-state.json # active workflow/recovery checkpoint
154
+ ├── runtime/
155
+ │ └── checkpoint.json # advisory breadcrumb recovery projection
153
156
  ├── changes/
154
157
  │ ├── active/<id>/ # intent, delta, plan, context v2, learning, verification
155
158
  │ └── archive/ # converged packets after baseline reconciliation
@@ -190,10 +190,17 @@ checking, review, convergence, and recovery. Each snapshot records:
190
190
  - one deterministic next action.
191
191
 
192
192
  `change.breadcrumb` derives the compact current position from state.db. Session,
193
- edit, and resume hooks inject only this breadcrumb, never the intent body,
193
+ edit, resume, and OpenCode lifecycle hooks invoke one bundled read-only reader
194
+ and inject only this breadcrumb, never the intent body,
194
195
  provider content, or a conversation summary. A changed git HEAD marks the
195
196
  snapshot stale and routes back to `change.context`.
196
197
 
198
+ PRD decomposition and complex-task subdivision use the active host model. The
199
+ MCP accepts the resulting structured tasks, validates schema, topology,
200
+ baseline/change ownership, and transaction boundaries, then persists them. It
201
+ does not call Anthropic, OpenAI, or another model provider and needs no separate
202
+ model API credential.
203
+
197
204
  File count, token estimate, and context-share overflow produce warnings, never a
198
205
  refusal. The agent may narrow reads, load files lazily, or split a slice when that
199
206
  preserves correctness; it must not raise a threshold merely to clear a gate or
package/docs/ROADMAP.md CHANGED
@@ -101,5 +101,5 @@ npm audit
101
101
  ## Out of scope for v1.0 (deferred)
102
102
 
103
103
  PLAN §13 lists Copilot / Cursor / Windsurf / 7 more runtimes, web
104
- dashboard / TUI, LLM provider abstraction, team-collab server, plugin
104
+ dashboard / TUI, team-collab server, plugin
105
105
  marketplace.
@@ -46,7 +46,7 @@ not presented as nonexistent custom agents.
46
46
 
47
47
  | Lifecycle | Claude Code | OpenCode | Codex | Kimi Code 0.26+ |
48
48
  |---|---|---|---|---|
49
- | Session context/health | FULL, DB-derived Context Spine breadcrumb on native `SessionStart` | DEGRADED, projected v2 breadcrumb; health via `system.doctor` | FULL, DB-derived breadcrumb on native `SessionStart` | FULL, DB-derived breadcrumb via session-start skill + hooks |
49
+ | Session context/health | FULL, DB-derived Context Spine breadcrumb on native `SessionStart` | FULL, the native JS plugin invokes the same bundled DB breadcrumb reader; health via `system.doctor` | FULL, DB-derived breadcrumb on native `SessionStart` | FULL, DB-derived breadcrumb via session-start skill + hooks |
50
50
  | Active edit boundary | FULL, `PreToolUse Edit|Write` | FULL, `tool.execute.before` rejects projection writes | FULL, `PreToolUse Edit|Write|apply_patch` | FULL, native `PreToolUse Edit|Write` deny contract |
51
51
  | Pre-compact checkpoint | FULL | FULL | FULL | FULL, native `PreCompact` |
52
52
  | Post-compact context injection | FULL | FULL, native compacting context | FULL, native `PostCompact` restore | DEGRADED; checkpoint restoration runs, but Kimi 0.26/0.27 does not reinject fire-and-forget hook text |
@@ -73,7 +73,7 @@ they do not deny edits or stop active incident work.
73
73
  | stdio MCP registration | plugin `.mcp.json` | `opencode.json` local MCP entry | plugin `.mcp.json` | plugin `mcpServers` entry |
74
74
  | Live/declared contracts | 36 | 36 | 36 | 36 |
75
75
  | Greenfield/brownfield baseline adoption | FULL | FULL | FULL | FULL |
76
- | Context Spine v2 / breadcrumb | FULL | FULL, DB-generated projection consumed by native JS | FULL | FULL |
76
+ | Context Spine v2 / breadcrumb | FULL | FULL, same bundled read-only DB reader as every hook | FULL | FULL |
77
77
  | Approval-gated spec learning | FULL | FULL | FULL | FULL |
78
78
  | Host-native review/discovery/ask | native | native | documented in `codex-capability-map.json` | native Kimi tools and workers |
79
79
  | Durable authority | project `.ultra/state.db` | project `.ultra/state.db` | project `.ultra/state.db` | project `.ultra/state.db` |
@@ -126,9 +126,9 @@ records. Uninstall refuses an unmanaged or conflicting root.
126
126
 
127
127
  | Capability | Contract |
128
128
  |---|---|
129
- | Current state schema | `11.0` |
129
+ | Current state schema | `12.0` |
130
130
  | Runtime values | `claude`, `opencode`, `codex`, `kimi` |
131
- | Upgrade from earlier schema | preserves runtime rows, adds Context Spine state through 10.0, then adds authoritative baseline adoption state transactionally in 11.0 |
131
+ | Upgrade from earlier schema | preserves runtime rows, adds Context Spine state through 10.0, authoritative baseline adoption in 11.0, then runtime constraint compatibility in 12.0 |
132
132
  | Preservation gate | rows, IDs, indexes, foreign keys, telemetry, incidents, and migration history remain intact |
133
133
  | Failure behavior | rollback; no partially upgraded authority database |
134
134
 
@@ -2,13 +2,11 @@
2
2
  """Protect Ultra projections and restate an active task boundary before edits."""
3
3
 
4
4
  import json
5
- import sqlite3
6
5
  import sys
7
6
  from pathlib import Path
8
7
 
9
- from context_spine import read_breadcrumb, render_breadcrumb
8
+ from context_spine import ContextSpineError, find_root, read_breadcrumb, render_breadcrumb
10
9
 
11
- TERMINAL = {"committed", "completed", "done", "cancelled"}
12
10
  TASKS_PROJECTION = Path(".ultra/tasks/tasks.json")
13
11
 
14
12
 
@@ -34,58 +32,32 @@ def main() -> None:
34
32
  except json.JSONDecodeError as exc:
35
33
  print(f"[active_task_context] invalid hook input: {exc}", file=sys.stderr)
36
34
  data = {}
37
- start = Path(data.get("cwd") or Path.cwd()).resolve()
38
- for root in (start, *start.parents):
39
- ultra = root / ".ultra"
40
- state_file = ultra / "workflow-state.json"
41
- initialized = (
42
- (ultra / "state.db").is_file()
43
- or state_file.is_file()
44
- or (ultra / "changes" / "active").is_dir()
45
- )
46
- if not initialized:
47
- continue
48
- if targets_tasks_projection(data, root):
49
- print(json.dumps({"hookSpecificOutput": {
50
- "hookEventName": "PreToolUse",
51
- "permissionDecision": "deny",
52
- "permissionDecisionReason": (
53
- "Refusing a direct write to the .ultra/tasks/tasks.json projection. "
54
- ".ultra/state.db is authoritative; use the Ultra MCP task tools and "
55
- "run ultra-doctor when state or projection health is degraded."
56
- ),
57
- }}))
58
- return
59
- try:
60
- breadcrumb = read_breadcrumb(root)
61
- except (sqlite3.Error, OSError) as exc:
62
- print(f"[active_task_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
63
- breadcrumb = None
64
- if breadcrumb and breadcrumb.get("task_id"):
65
- print(json.dumps({"hookSpecificOutput": {
66
- "hookEventName": "PreToolUse",
67
- "additionalContext": render_breadcrumb(root, breadcrumb),
68
- }}))
69
- return
70
- if not state_file.is_file():
71
- break
72
- try:
73
- state = json.loads(state_file.read_text(encoding="utf-8"))
74
- except (OSError, json.JSONDecodeError) as exc:
75
- print(f"[active_task_context] cannot read {state_file}: {exc}", file=sys.stderr)
76
- break
77
- if not isinstance(state, dict) or state.get("status") in TERMINAL:
78
- break
79
- task = state.get("task_id", state.get("task", "unknown"))
80
- step = state.get("step", "unknown")
35
+ root = find_root(Path(data.get("cwd") or Path.cwd()).resolve())
36
+ if root is None:
37
+ print(json.dumps({}))
38
+ return
39
+ if targets_tasks_projection(data, root):
81
40
  print(json.dumps({"hookSpecificOutput": {
82
41
  "hookEventName": "PreToolUse",
83
- "additionalContext": (
84
- f"Active Ultra task {task}, step {step}. Keep this edit inside the approved "
85
- "task and preserve .ultra/state.db as workflow authority."
42
+ "permissionDecision": "deny",
43
+ "permissionDecisionReason": (
44
+ "Refusing a direct write to the .ultra/tasks/tasks.json projection. "
45
+ ".ultra/state.db is authoritative; use the Ultra MCP task tools and "
46
+ "run ultra-doctor when state or projection health is degraded."
86
47
  ),
87
48
  }}))
88
49
  return
50
+ try:
51
+ breadcrumb = read_breadcrumb(root)
52
+ except ContextSpineError as exc:
53
+ print(f"[active_task_context] cannot inspect Context Spine: {exc}", file=sys.stderr)
54
+ breadcrumb = None
55
+ if breadcrumb and breadcrumb.get("change_id"):
56
+ print(json.dumps({"hookSpecificOutput": {
57
+ "hookEventName": "PreToolUse",
58
+ "additionalContext": render_breadcrumb(root, breadcrumb),
59
+ }}))
60
+ return
89
61
  print(json.dumps({}))
90
62
 
91
63