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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.0] — 2026-07-19
11
+
12
+ ### Added
13
+
14
+ - Added one read-only JavaScript Context Spine reader and renderer for the
15
+ authoritative `.ultra/state.db`, plus a packaged breadcrumb CLI shared by
16
+ Claude Code, Codex, OpenCode, Kimi Code, and the MCP server.
17
+ - Added cross-host regression coverage proving stale JSON projections and
18
+ context manifests cannot override the active database change or task.
19
+
20
+ ### Changed
21
+
22
+ - Changed `task.parse_prd` and `task.expand` to validate and atomically persist
23
+ task objects derived by the active host model. Ultra MCP no longer starts a
24
+ second model client or requires an Anthropic, OpenAI, or other provider key.
25
+ - Reduced Python lifecycle hooks and the generated OpenCode plugin to thin
26
+ adapters over the canonical breadcrumb reader. Checkpoint and resume data is
27
+ advisory recovery evidence rather than a second workflow authority.
28
+ - Made collaboration and verification Skills host-neutral, kept the current
29
+ host responsible for delivery, and generated Codex metadata from the source
30
+ Skill instead of maintaining duplicated prompt bodies.
31
+
32
+ ### Fixed
33
+
34
+ - Fixed authoritative-state and injected-prompt divergence caused by hooks
35
+ reading `.ultra/workflow-state.json` or context manifests after
36
+ `.ultra/state.db` had advanced.
37
+ - Fixed stale or incomplete state routing: missing projects route to
38
+ `ultra-init`, prior schemas route to migration, and corrupt current schemas
39
+ route to `ultra-doctor` without fabricating active workflow context.
40
+ - Fixed installed OpenCode context injection so it reads the bundled runtime
41
+ with the installer-selected Node executable and remains portable outside the
42
+ source checkout.
43
+ - Fixed Codex collaboration invocation and generated metadata quoting while
44
+ preserving the bounded read-only advisor contract.
45
+
46
+ ### Removed
47
+
48
+ - Removed the private LLM client, planner prompt builders, and the
49
+ `@anthropic-ai/sdk` and `openai` runtime dependencies.
50
+
10
51
  ## [0.13.0] — 2026-07-18
11
52
 
12
53
  ### Added
@@ -541,7 +582,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
541
582
  skill manifest, CLI protocol + mapping table; 5 spec validators.
542
583
  - **Phase 0 — skeleton**: multi-runtime installer scaffolding.
543
584
 
544
- [Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.13.0...HEAD
585
+ [Unreleased]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.14.0...HEAD
586
+ [0.14.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.13.0...v0.14.0
545
587
  [0.13.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.12.1...v0.13.0
546
588
  [0.12.1]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.12.0...v0.12.1
547
589
  [0.12.0]: https://github.com/rocky2431/ultra-builder-pro-cli/compare/v0.11.0...v0.12.0
package/README.md CHANGED
@@ -8,7 +8,7 @@ authoritative `.ultra/state.db`.
8
8
 
9
9
  <div align="center">
10
10
 
11
- [![Version](https://img.shields.io/badge/version-0.13.0-blue)](./CHANGELOG.md)
11
+ [![Version](https://img.shields.io/badge/version-0.14.0-blue)](./CHANGELOG.md)
12
12
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](#verification)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
14
14
  [![Node](https://img.shields.io/badge/node-%E2%89%A522-informational)](./package.json)
@@ -17,8 +17,6 @@ authoritative `.ultra/state.db`.
17
17
 
18
18
  ---
19
19
 
20
- > *(30-second demo GIF placeholder — to be added before v1.0)*
21
-
22
20
  ## What it does
23
21
 
24
22
  - **Builds host-native plugins from one allowlist.** Claude Code, OpenCode,
@@ -33,7 +31,8 @@ authoritative `.ultra/state.db`.
33
31
  - **Keeps memory ownership explicit.** Ultra Builder Pro does not collect
34
32
  prompts, transcripts, observations, summaries, or cross-session memory.
35
33
  Install cloud-mem/claude-mem separately if persistent memory is wanted.
36
- - **Runs real PRDs end-to-end.** `task.parse_prd` → `lib/topo.cjs` waves →
34
+ - **Runs real PRDs end-to-end without a second model credential.** The current
35
+ host derives task objects; `task.parse_prd` validates them → `lib/topo.cjs` waves →
37
36
  `.ultra/execution-plan.json` → parallel worktree sessions → auto-merge back.
38
37
  - **Keeps daily work convergent.** `/ultra-change` creates a bounded delta and
39
38
  Context Manifest v2; the Context Spine assigns a role/gate, verifies required
@@ -228,10 +227,10 @@ Individual suites: `test:state`, `test:orch`, `test:spec`, `test:rest`.
228
227
  `ultra-tools legacy-memory inspect`, archive with
229
228
  `ultra-tools legacy-memory archive`, then prune only with the explicit
230
229
  confirmation token printed by the command. Nothing is deleted implicitly.
231
- - **MCP tool errors with `ANTHROPIC_API_KEY` missing**: `task.parse_prd` and
232
- `task.expand` need a real LLM key at runtime. Set `ANTHROPIC_API_KEY` or
233
- `OPENAI_API_KEY` before invoking those tools; all other MCP tools work
234
- without any LLM credential.
230
+ - **MCP model credentials**: Ultra MCP does not require `ANTHROPIC_API_KEY`,
231
+ `OPENAI_API_KEY`, or another provider key. The active host uses its existing
232
+ model session to derive PRD tasks or child tasks; `task.parse_prd` and
233
+ `task.expand` only validate ownership, graph shape, and atomic persistence.
235
234
 
236
235
  ## Documentation
237
236
 
@@ -72,134 +72,6 @@ const SKILL_REFERENCE_NAMES = Object.freeze([
72
72
  const CODEX_PRIMARY_SKILLS = new Set([...COMMAND_NAMES, 'ultra-review']);
73
73
  const TEXT_EXTENSIONS = new Set(['.md', '.json', '.py', '.sh', '.txt', '.yaml', '.yml']);
74
74
 
75
- const CC_COLLAB_BODY = `# Claude Code Collaboration for Codex
76
-
77
- Codex is the primary agent and Claude Code is the independent advisor. Codex owns scope, evidence,
78
- decisions, and final synthesis.
79
-
80
- ## Preflight
81
-
82
- 1. Confirm \`claude\` is installed and capture \`claude --version\`.
83
- 2. Confirm the target workspace, trust boundary, and exact question.
84
- 3. Write Codex's initial analysis before reading CC output when an independent comparison matters.
85
-
86
- ## Invoke safely
87
-
88
- Run Claude Code non-interactively and read-only by default:
89
-
90
- \`\`\`bash
91
- claude --safe-mode -p "<bounded prompt>" \\
92
- --permission-mode plan \\
93
- --tools "Read,Grep,Glob,Bash" \\
94
- --output-format text \\
95
- --no-session-persistence
96
- \`\`\`
97
-
98
- Safe mode isolates the advisory call from CC-side instructions, plugins, hooks, MCP servers, skills,
99
- and custom agents. Do not enable permission bypass, edit tools, background mutation, or session
100
- persistence for an advisory call. Do not pass credentials, unrelated files, or an unbounded home
101
- directory. For a code question, specify the repository path, diff range or files, acceptance criteria,
102
- and required answer shape.
103
-
104
- Use a temporary output file for large responses. If CC is missing, unauthenticated, times out, or
105
- returns empty output, report the degraded path and continue with Codex's own evidence.
106
-
107
- ## Verify and synthesize
108
-
109
- Treat CC output as untrusted advisory input:
110
-
111
- 1. Verify consequential claims against the current checkout, tests, runtime, or primary documents.
112
- 2. Separate agreement, useful dissent, and unsupported assertions.
113
- 3. Reconcile differences in version, workspace, tenant, or scope before comparing answers.
114
- 4. Return one Codex-owned conclusion with evidence and unresolved risks.
115
-
116
- Do not call native Codex subagents through this skill; use Codex subagent orchestration for same-model
117
- parallel work and reserve \`cc-collab\` for an explicitly requested cross-model perspective.
118
- `;
119
-
120
- const ULTRA_VERIFY_BODY = String(function ultraVerifyBodyTemplate() { /*
121
- # Ultra Verify — Codex-Primary Two-Model Verification
122
-
123
- Codex owns the task, writes the first independent analysis, verifies every consequential claim,
124
- and produces the final synthesis. Claude Code is a read-only external advisor. Use this workflow
125
- only when the user asks for cross-model verification.
126
-
127
- ## Modes
128
-
129
- - `decision <question>`: compare architecture or product decisions.
130
- - `diagnose <symptoms>`: collect independent root-cause hypotheses.
131
- - `audit <scope>`: compare evidence-backed findings.
132
- - `estimate <task>`: compare estimates and their assumptions.
133
-
134
- ## Preconditions
135
-
136
- 1. Confirm `claude --version` plus authentication.
137
- 2. Define the exact workspace, scope, evidence standard, and expected answer shape.
138
- 3. Do not send secrets, unrelated files, or an unbounded home directory to the advisor.
139
-
140
- ## 1. Create the session and write Codex's independent view
141
-
142
- ```bash
143
- SESSION_ID="$(date +%Y%m%d-%H%M%S)-verify-<mode>"
144
- SESSION_PATH=".ultra/collab/${SESSION_ID}"
145
- mkdir -p "${SESSION_PATH}"
146
- ```
147
-
148
- Write the evidence-backed Codex analysis to `${SESSION_PATH}/codex-analysis.md` before reading
149
- the external answer. Record the mode, scope, checkout, and evidence boundary.
150
-
151
- ## 2. Launch Claude Code read-only
152
-
153
- Give Claude Code the bounded raw question and evidence without Codex's conclusions.
154
-
155
- ```bash
156
- claude --safe-mode -p "<BOUNDED_PROMPT>" \
157
- --permission-mode plan \
158
- --tools "Read,Grep,Glob,Bash" \
159
- --output-format text \
160
- --no-session-persistence \
161
- > "${SESSION_PATH}/claude-output.md" \
162
- 2> "${SESSION_PATH}/claude-error.log"
163
- ```
164
-
165
- Run the command in a yielded shell session. Do not enable permission bypass, mutation tools,
166
- background writes, or session persistence.
167
-
168
- ## 3. Collect completed output
169
-
170
- The bundled `scripts/verify_wait.py` checks output stability. Poll yielded sessions in bounded
171
- intervals and never hold one blocking tool call longer than 60 seconds. Read the output only when
172
- it is non-empty; retain the error log when the advisor fails.
173
-
174
- ## 4. Verify and synthesize
175
-
176
- Compare `codex-analysis.md` and `claude-output.md`:
177
-
178
- 1. Verify claims against the current checkout, runtime, tests, or primary documentation.
179
- 2. Separate verified agreement, useful dissent, and unsupported assertions.
180
- 3. Explain scope, version, or assumption differences before judging the result.
181
- 4. Write `${SESSION_PATH}/synthesis.md` and `metadata.json`, then present one Codex-owned answer.
182
-
183
- Model agreement never overrides failing tests or authoritative runtime evidence.
184
-
185
- ## Degraded operation
186
-
187
- - Advisor failure: return Codex-only analysis with an explicit single-source warning.
188
- - Never block the user's task solely because the external CLI is absent, unauthenticated, or slow.
189
-
190
- ## Session files
191
-
192
- ```text
193
- .ultra/collab/<SESSION_ID>/
194
- codex-analysis.md
195
- claude-output.md
196
- claude-error.log
197
- metadata.json
198
- synthesis.md
199
- ```
200
-
201
- Read the bundled references for mode-specific evidence and confidence guidance.
202
- */ }).match(/\/\*([\s\S]*?)\*\//)[1].trim();
203
75
  function titleCase(name) {
204
76
  const special = { 'cc-collab': 'Claude Code Collaboration' };
205
77
  if (special[name]) return special[name];
@@ -226,34 +98,7 @@ function adaptCodexPrimaryText(input, skillName) {
226
98
  return text;
227
99
  }
228
100
 
229
- function adaptUltraVerifyAsset(input, rel) {
230
- let text = adaptHostText(String(input), '');
231
- if (rel === path.join('scripts', 'verify_wait.py')) {
232
- text = text.replaceAll('Codex', 'Claude Code');
233
- text = text.replaceAll('codex', 'claude');
234
- text = text.replaceAll('run_in_background', 'a yielded exec session');
235
- return text;
236
- }
237
-
238
- text = text.replaceAll('Claude', '__UBP_CODEX_PRIMARY__');
239
- text = text.replaceAll('Codex', 'Claude Code');
240
- text = text.replaceAll('__UBP_CODEX_PRIMARY__', 'Codex');
241
- text = text.replaceAll('claude-analysis.md', 'codex-analysis.md');
242
- text = text.replaceAll('codex-output.md', 'claude-output.md');
243
- text = text.replaceAll('codex-error.log', 'claude-error.log');
244
- text = text.replaceAll('codex-review', 'claude-review');
245
- text = text.replaceAll('"claude": "claude-opus-4-6"', '"codex": "<primary>"');
246
- text = text.replaceAll('"codex": "<model>"', '"claude": "<advisor>"');
247
- text = text.replaceAll('["claude"]', '["codex"]');
248
- text = text.replaceAll('Claude Code-only', 'Codex-only');
249
- text = text.replaceAll('run_in_background', 'native concurrent execution');
250
- text = text.replaceAll('Bash tool', '`exec_command`');
251
- text = text.replaceAll('Read tool', 'targeted file reads');
252
- return text;
253
- }
254
-
255
- function adaptSkillAsset(input, targetName, rel) {
256
- if (targetName === 'ultra-verify') return adaptUltraVerifyAsset(input, rel);
101
+ function adaptSkillAsset(input, targetName, _rel) {
257
102
  return adaptHostText(input, targetName);
258
103
  }
259
104
 
@@ -298,10 +143,7 @@ function adaptedDescription(sourceDescription, targetName) {
298
143
 
299
144
  function buildSkillMarkdown(sourceText, sourceName, targetName) {
300
145
  const { fm, body } = parseFrontmatter(sourceText);
301
- let adaptedBody;
302
- if (targetName === 'cc-collab') adaptedBody = CC_COLLAB_BODY;
303
- else if (targetName === 'ultra-verify') adaptedBody = ULTRA_VERIFY_BODY;
304
- else adaptedBody = adaptHostText(body, targetName);
146
+ const adaptedBody = adaptHostText(body, targetName);
305
147
  return yaml.dump({
306
148
  name: targetName,
307
149
  description: adaptedDescription(fm && fm.description, targetName),
@@ -309,13 +151,22 @@ function buildSkillMarkdown(sourceText, sourceName, targetName) {
309
151
  .replace(/^/, '---\n') + `\n---\n\n${adaptedBody.trim()}\n`;
310
152
  }
311
153
 
154
+ function shortDescription(description, name) {
155
+ const value = String(description || `${titleCase(name)} for Codex`).replace(/\s+/g, ' ').trim();
156
+ if (value.length <= 64) return value;
157
+ const candidate = value.slice(0, 65);
158
+ const boundary = candidate.lastIndexOf(' ');
159
+ const short = (boundary >= 25 ? candidate.slice(0, boundary) : value.slice(0, 64))
160
+ .replace(/[\s.,;:]+$/, '');
161
+ return short || `${titleCase(name)} for Codex`;
162
+ }
163
+
312
164
  function buildOpenAiYaml(name, description) {
313
165
  const policy = skillPolicy(name);
314
- const short = description.replace(/\s+/g, ' ').trim().slice(0, 63).replace(/[\s.,;:]+$/, '');
315
166
  const metadata = {
316
167
  interface: {
317
168
  display_name: titleCase(name),
318
- short_description: short || `${titleCase(name)} for Codex`,
169
+ short_description: shortDescription(description, name),
319
170
  default_prompt: `Use $ultra-builder-pro:${name} for this task and follow its workflow.`,
320
171
  },
321
172
  policy: { allow_implicit_invocation: policy.allowImplicitInvocation },
@@ -329,7 +180,12 @@ function buildOpenAiYaml(name, description) {
329
180
  }],
330
181
  };
331
182
  }
332
- return yaml.dump(metadata, { lineWidth: -1, noRefs: true });
183
+ return yaml.dump(metadata, {
184
+ lineWidth: -1,
185
+ noRefs: true,
186
+ forceQuotes: true,
187
+ quotingType: "'",
188
+ });
333
189
  }
334
190
 
335
191
  function isTextFile(rel) {
@@ -536,6 +392,10 @@ main().catch((error) => {
536
392
  });
537
393
  `;
538
394
  writeAtomic(path.join(runtimeRoot, 'launch.cjs'), launcher);
395
+ fs.copyFileSync(
396
+ path.join(repoRoot, 'mcp-server', 'breadcrumb.cjs'),
397
+ path.join(runtimeRoot, 'breadcrumb.cjs'),
398
+ );
539
399
 
540
400
  const sourceToolsFile = path.join(repoRoot, 'spec', 'mcp-tools.yaml');
541
401
  const upstreamManifest = yaml.load(fs.readFileSync(sourceToolsFile, 'utf8'));
@@ -40,6 +40,7 @@ const COLLAB_SKILLS_BY_RUNTIME = Object.freeze({
40
40
 
41
41
  const MCP_DEPENDENT_SKILLS = Object.freeze([
42
42
  'ultra-init',
43
+ 'ultra-research',
43
44
  'ultra-change',
44
45
  'ultra-plan',
45
46
  'ultra-dev',
@@ -85,6 +85,22 @@ test('host invocation and MCP metadata live outside source SKILL frontmatter', (
85
85
  assert.equal(policy.requiresUltraMcp, MCP_DEPENDENT_SKILLS.includes(name), name);
86
86
  }
87
87
  assert.throws(() => skillPolicy('not-packaged'), /unknown packaged Ultra skill/);
88
+ assert.ok(MCP_DEPENDENT_SKILLS.includes('ultra-research'));
89
+ });
90
+
91
+ test('packaged collaboration prompts use current CLI contracts and one source prompt', () => {
92
+ const skills = ['codex-collab', 'ultra-verify'];
93
+ for (const name of skills) {
94
+ const text = fs.readFileSync(path.join(REPO_ROOT, 'skills', name, 'SKILL.md'), 'utf8');
95
+ assert.doesNotMatch(text, /codex exec[^\n]*\s-a(?:\s|$)|^\s*-a\s+never/m, name);
96
+ assert.doesNotMatch(text, /The (?:OpenCode|Kimi Code) owns/, name);
97
+ }
98
+
99
+ const adapterSource = fs.readFileSync(
100
+ path.join(REPO_ROOT, 'adapters', '_shared', 'codex-assets.cjs'),
101
+ 'utf8',
102
+ );
103
+ assert.doesNotMatch(adapterSource, /const (?:CC_COLLAB_BODY|ULTRA_VERIFY_BODY)\s*=/);
88
104
  });
89
105
 
90
106
  test('workflow hook allowlist contains no memory, prompt capture, or generic policy hook', () => {
@@ -128,4 +144,6 @@ test('npm publish list uses the same explicit skill boundary', () => {
128
144
  assert.ok(!pkg.files.includes('skills'));
129
145
  assert.ok(!pkg.files.includes('CLAUDE.md'));
130
146
  assert.deepEqual(publishedSkills, expected);
147
+ assert.equal(pkg.dependencies['@anthropic-ai/sdk'], undefined);
148
+ assert.equal(pkg.dependencies.openai, undefined);
131
149
  });
@@ -304,135 +304,45 @@ function pluginSource() {
304
304
  import fs from "node:fs";
305
305
  import path from "node:path";
306
306
  import { execFileSync } from "node:child_process";
307
+ import { fileURLToPath } from "node:url";
307
308
 
308
- const TERMINAL = new Set(["committed", "completed", "done", "cancelled"]);
309
309
  const TASKS_PROJECTION = ".ultra/tasks/tasks.json";
310
+ const NODE_BINARY = ${JSON.stringify(process.execPath)};
311
+ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
312
+ const BREADCRUMB_CLI = path.resolve(
313
+ PLUGIN_DIR, "..", ${JSON.stringify(BUNDLE_DIR)}, "runtime", "breadcrumb.cjs",
314
+ );
310
315
 
311
- function gitHead(root) {
312
- try {
313
- return execFileSync("git", ["rev-parse", "HEAD"], {
314
- cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
315
- }).trim();
316
- } catch {
317
- return null;
318
- }
319
- }
320
-
321
- function findUltraContext(directory) {
316
+ function findUltraRoot(directory) {
322
317
  let current = path.resolve(directory);
323
318
  while (true) {
324
319
  const ultra = path.join(current, ".ultra");
325
- const currentHead = gitHead(current);
326
- const workflowFile = path.join(ultra, "workflow-state.json");
327
- const statePresent = fs.existsSync(path.join(ultra, "state.db"));
328
- let workflow = null;
329
- if (fs.existsSync(workflowFile)) {
330
- try {
331
- const state = JSON.parse(fs.readFileSync(workflowFile, "utf8"));
332
- workflow = state && !TERMINAL.has(state.status) ? state : null;
333
- } catch (error) {
334
- process.stderr.write(\`[ultra-builder-pro] cannot read \${workflowFile}: \${error.message}\\n\`);
335
- }
336
- }
337
- const changes = [];
338
- const changeRoot = path.join(ultra, "changes", "active");
339
- if (fs.existsSync(changeRoot)) {
340
- for (const entry of fs.readdirSync(changeRoot, { withFileTypes: true })) {
341
- if (!entry.isDirectory()) continue;
342
- const manifestPath = path.join(changeRoot, entry.name, "context-manifest.json");
343
- if (!fs.existsSync(manifestPath)) continue;
344
- try {
345
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
346
- if (manifest?.change) {
347
- const compiledHead = manifest.git?.head ?? null;
348
- const legacyContext = manifest.schema_version !== "2.0" || !manifest.baseline;
349
- const headStale = Boolean(compiledHead && currentHead && compiledHead !== currentHead);
350
- const blockers = Array.isArray(manifest.readiness?.blockers)
351
- ? [...manifest.readiness.blockers]
352
- : [];
353
- const warnings = Array.isArray(manifest.readiness?.warnings)
354
- ? [...manifest.readiness.warnings]
355
- : [];
356
- if (headStale && !blockers.includes("CONTEXT_HEAD_STALE")) blockers.push("CONTEXT_HEAD_STALE");
357
- if (legacyContext && !blockers.includes("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED")) {
358
- blockers.unshift("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED");
359
- }
360
- changes.push({
361
- id: manifest.change.id,
362
- kind: manifest.change.kind,
363
- status: manifest.change.status,
364
- role: manifest.role ?? "plan",
365
- gate: manifest.gate ?? "alignment",
366
- nextAction: legacyContext
367
- ? "Recompile change.context to upgrade this legacy context snapshot."
368
- : headStale
369
- ? "Git HEAD changed after context compilation; recompile change.context."
370
- : (manifest.next_action ?? "Inspect Ultra status."),
371
- readiness: legacyContext || headStale ? "blocked" : (manifest.readiness?.status ?? "ready"),
372
- blockers,
373
- warnings,
374
- baseline: manifest.baseline ?? null,
375
- taskId: manifest.selected_task?.id ?? manifest.resume?.task_id ?? null,
376
- taskStatus: manifest.selected_task?.status ?? manifest.resume?.task_status ?? null,
377
- tokenEstimate: manifest.context?.token_estimate ?? 0,
378
- manifestPath: path.relative(current, manifestPath),
379
- });
380
- }
381
- } catch (error) {
382
- process.stderr.write(\`[ultra-builder-pro] cannot read \${manifestPath}: \${error.message}\\n\`);
383
- }
384
- }
385
- }
386
- if (statePresent || workflow || changes.length > 0) {
387
- return { root: current, workflow, changes, statePresent };
388
- }
320
+ if (fs.existsSync(ultra) && fs.statSync(ultra).isDirectory()) return current;
389
321
  const parent = path.dirname(current);
390
322
  if (parent === current) return null;
391
323
  current = parent;
392
324
  }
393
325
  }
394
326
 
395
- function contextText(active) {
396
- const lines = [];
397
- if (active.workflow) {
398
- const state = active.workflow;
399
- lines.push(
400
- "[Ultra active workflow]",
401
- \`Project: \${active.root}\`,
402
- \`Command: \${state.command ?? "unknown"}\`,
403
- \`Task: \${state.task_id ?? state.task ?? "unknown"}\`,
404
- \`Step: \${state.step ?? "unknown"}\`,
405
- \`Status: \${state.status ?? "active"}\`,
406
- );
407
- }
408
- if (active.changes.length > 0) {
409
- const change = active.changes[0];
410
- lines.push(
411
- "[Ultra context spine]",
412
- \`Project: \${active.root}\`,
413
- \`Change: \${change.id} (\${change.kind ?? "unknown"}, \${change.status ?? "active"})\`,
414
- \`Task: \${change.taskId ?? "none"} (\${change.taskStatus ?? "none"})\`,
415
- \`Role: \${change.role}\`,
416
- \`Gate: \${change.gate}\`,
417
- \`Readiness: \${change.readiness}\`,
418
- ...(change.blockers.length > 0 ? [\`Blockers: \${change.blockers.join(", ")}\`] : []),
419
- ...(change.warnings.length > 0 ? [\`Warnings: \${change.warnings.join(", ")}\`] : []),
420
- \`Next: \${change.nextAction}\`,
421
- \`Context manifest: \${change.manifestPath}\`,
422
- );
423
- if (active.changes.length > 1) lines.push(
424
- \`Active change count: \${active.changes.length}; resolve scope with change.list before mutation.\`,
425
- );
426
- }
427
- if (!active.workflow && active.changes.length === 0) {
428
- lines.push(
429
- "[Ultra baseline]",
430
- \`Project: \${active.root}\`,
431
- "No active continuous change. Run ultra-status; resume ultra-init when the baseline is missing, incomplete, or stale, otherwise start ultra-change.",
432
- );
327
+ function readUltraContext(directory) {
328
+ const root = findUltraRoot(directory);
329
+ if (!root) return null;
330
+ try {
331
+ const raw = execFileSync(NODE_BINARY, [BREADCRUMB_CLI, root], {
332
+ cwd: root,
333
+ encoding: "utf8",
334
+ stdio: ["ignore", "pipe", "pipe"],
335
+ timeout: 5000,
336
+ });
337
+ const value = JSON.parse(raw);
338
+ if (!value?.breadcrumb || typeof value.text !== "string" || !value.text) {
339
+ throw new Error("canonical breadcrumb response is incomplete");
340
+ }
341
+ return { root, breadcrumb: value.breadcrumb, text: value.text };
342
+ } catch (error) {
343
+ process.stderr.write(\`[ultra-builder-pro] cannot read canonical breadcrumb: \${error.message}\\n\`);
344
+ return { root, breadcrumb: null, text: null };
433
345
  }
434
- lines.push("Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.");
435
- return lines.join("\\n");
436
346
  }
437
347
 
438
348
  function targetPaths(tool, args) {
@@ -456,19 +366,19 @@ function isTasksProjection(active, candidate) {
456
366
 
457
367
  export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
458
368
  const root = worktree || directory;
459
- let active = findUltraContext(root);
460
- const refresh = () => { active = findUltraContext(root); };
369
+ let active = readUltraContext(root);
370
+ const refresh = () => { active = readUltraContext(root); };
461
371
  return {
462
372
  event: async ({ event }) => {
463
373
  if (["session.created", "session.compacted", "session.idle"].includes(event?.type)) refresh();
464
374
  },
465
375
  "experimental.chat.system.transform": async (_input, output) => {
466
376
  refresh();
467
- if (active) output.system.push(contextText(active));
377
+ if (active?.text) output.system.push(active.text);
468
378
  },
469
379
  "experimental.session.compacting": async (_input, output) => {
470
380
  refresh();
471
- if (active) output.context.push(contextText(active));
381
+ if (active?.text) output.context.push(active.text);
472
382
  },
473
383
  "tool.execute.before": async (input, output) => {
474
384
  refresh();
@@ -70,8 +70,9 @@ test('Claude plugin collaboration and learn workflows are safe native plugin ass
70
70
  assert.match(codexCollab, /-s read-only/);
71
71
  assert.match(codexCollab, /--ephemeral/);
72
72
  assert.match(codexCollab, /--ignore-user-config/);
73
- assert.match(verify, /-s read-only/);
74
- assert.match(verify, /--ephemeral/);
73
+ assert.match(verify, /Keep the current host responsible/);
74
+ assert.match(verify, /installed collaboration companion/);
75
+ assert.doesNotMatch(verify, /codex exec|claude --safe-mode/);
75
76
  assert.match(verify, /scripts\/verify_wait\.py/);
76
77
  assert.match(review, /Claude Code Task workers/);
77
78
  assert.match(review, /scripts\/review_wait\.py/);
@@ -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', 'codex.py');
@@ -19,6 +23,35 @@ function run(feature, payload) {
19
23
  });
20
24
  }
21
25
 
26
+ function seedContext(project, taskId = 'task-7') {
27
+ const state = initStateDb(path.join(project, '.ultra', 'state.db'));
28
+ state.db.prepare(
29
+ `INSERT INTO baselines
30
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
31
+ VALUES ('baseline', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
32
+ ).run(new Date().toISOString());
33
+ const { change } = createChange(state.db, {
34
+ id: 'hook-change', title: 'Hook change', kind: 'quick',
35
+ intent: 'Inject the authoritative task.',
36
+ docs_impact: { status: 'none', rationale: 'fixture' },
37
+ }, { rootDir: project });
38
+ const task = createTask(state.db, {
39
+ id: taskId, title: 'Hook task', type: 'bugfix', priority: 'P0', change_id: change.id,
40
+ });
41
+ compileRoleContext(state.db, {
42
+ input: {
43
+ task_id: task.id, role: 'implement', gate: 'implementation',
44
+ execution_contract: {
45
+ slice_kind: 'tracer_bullet', public_seam: 'hook injection',
46
+ verification_command: 'node --test adapters/tests/codex-hook.test.cjs',
47
+ },
48
+ next_action: 'Continue the authoritative hook task.',
49
+ },
50
+ change, tasks: [task], rootDir: project,
51
+ });
52
+ closeStateDb(state.db);
53
+ }
54
+
22
55
  test('Codex hook adapter rejects retired generic policy hooks', () => {
23
56
  const result = run('block_dangerous_commands.py', {
24
57
  session_id: 'session-1',
@@ -36,10 +69,7 @@ test('Codex hook adapter rejects retired generic policy hooks', () => {
36
69
  test('Codex hook adapter injects only an active Ultra task boundary before edits', () => {
37
70
  const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-codex-hook-'));
38
71
  try {
39
- fs.mkdirSync(path.join(project, '.ultra'));
40
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
41
- command: 'ultra-dev', task_id: 'task-7', step: '4.5', status: 'review_pending',
42
- }));
72
+ seedContext(project);
43
73
  const result = run('active_task_context.py', {
44
74
  session_id: 'session-2',
45
75
  cwd: project,
@@ -78,9 +108,6 @@ test('Codex hook adapter denies apply_patch writes to tasks.json during an activ
78
108
  const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-codex-hook-deny-'));
79
109
  try {
80
110
  fs.mkdirSync(path.join(project, '.ultra'));
81
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
82
- command: 'ultra-plan', task_id: 'plan', step: 'persist', status: 'active',
83
- }));
84
111
  const result = run('active_task_context.py', {
85
112
  session_id: 'session-deny',
86
113
  cwd: project,