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
|
@@ -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, /
|
|
152
|
-
assert.match(verify, /
|
|
153
|
-
assert.match(verify, /
|
|
154
|
-
assert.doesNotMatch(verify, /codex-
|
|
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, /
|
|
158
|
-
assert.match(waiter, /
|
|
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
|
-
|
|
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
|
|
73
|
-
assert.match(verify, /
|
|
74
|
-
assert.match(verify, /
|
|
75
|
-
assert.
|
|
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, /
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
226
|
-
assert.match(
|
|
227
|
-
assert.match(
|
|
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
|
|
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
|
-
|
|
285
|
-
fs.writeFileSync(path.join(project, '.
|
|
286
|
-
|
|
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, /
|
|
292
|
-
assert.match(text, /
|
|
293
|
-
assert.
|
|
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
|
}
|
package/docs/AGENT-CONTEXT.md
CHANGED
|
@@ -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`,
|
|
117
|
-
|
|
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
|
-
├──
|
|
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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,
|
|
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,
|
|
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` |
|
|
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,
|
|
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 | `
|
|
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,
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
"
|
|
84
|
-
|
|
85
|
-
"
|
|
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
|
|