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.
- package/CHANGELOG.md +43 -1
- package/README.md +7 -8
- package/adapters/_shared/codex-assets.cjs +23 -163
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
- package/adapters/opencode.js +30 -120
- package/adapters/tests/claude.test.cjs +3 -2
- package/adapters/tests/codex-hook.test.cjs +34 -7
- package/adapters/tests/codex.test.cjs +13 -6
- package/adapters/tests/kimi-hook.test.cjs +34 -7
- package/adapters/tests/opencode.test.cjs +70 -84
- package/docs/AGENT-CONTEXT.md +6 -3
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/ROADMAP.md +1 -1
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +19 -17
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +13 -19
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/prd-parser.test.cjs +49 -138
- package/mcp-server/lib/project-breadcrumb.cjs +139 -0
- package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/task-expander.test.cjs +54 -209
- package/mcp-server/server.cjs +21 -35
- package/mcp-server/tests/server-contract.test.cjs +41 -45
- package/package.json +2 -4
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-plan/SKILL.md +15 -5
- package/skills/ultra-verify/SKILL.md +21 -30
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/mcp-tools.yaml +38 -10
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- 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.
|
|
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
|
-
[](./CHANGELOG.md)
|
|
12
12
|
[](#verification)
|
|
13
13
|
[](./LICENSE)
|
|
14
14
|
[](./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
|
|
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
|
|
232
|
-
`
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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, {
|
|
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'));
|
|
@@ -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
|
});
|
package/adapters/opencode.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 =
|
|
460
|
-
const refresh = () => { active =
|
|
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(
|
|
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(
|
|
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,
|
|
74
|
-
assert.match(verify,
|
|
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
|
-
|
|
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,
|