ultra-builder-pro-cli 0.6.0 → 0.8.1
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/.ultra-template/changes/active/.gitkeep +1 -0
- package/.ultra-template/changes/archive/.gitkeep +1 -0
- package/CHANGELOG.md +90 -11
- package/README.md +39 -19
- package/adapters/_shared/handbook.cjs +8 -7
- package/adapters/_shared/provenance.cjs +286 -0
- package/adapters/_shared/runtime-assets.cjs +2 -0
- package/adapters/_shared/tests/handbook.test.cjs +4 -1
- package/adapters/_shared/tests/provenance.test.cjs +133 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +2 -0
- package/adapters/claude.js +33 -0
- package/adapters/codex.js +87 -0
- package/adapters/opencode.js +147 -20
- package/adapters/tests/codex.test.cjs +42 -3
- package/adapters/tests/opencode.test.cjs +16 -9
- package/agents/debugger.md +12 -0
- package/bin/install.js +63 -27
- package/bin/orchestrator.js +1 -26
- package/commands/ultra-change.md +48 -0
- package/commands/ultra-deliver.md +14 -7
- package/commands/ultra-dev.md +6 -3
- package/commands/ultra-doctor.md +36 -0
- package/commands/ultra-init.md +4 -3
- package/commands/ultra-status.md +7 -5
- package/commands/ultra-test.md +2 -1
- package/docs/AGENT-CONTEXT.md +31 -23
- package/docs/ARCHITECTURE.md +80 -17
- package/docs/PLAN.zh-CN.md +37 -29
- package/docs/ROADMAP.md +14 -14
- package/docs/RUNTIME-COMPAT-MATRIX.md +19 -12
- package/hooks/README.md +14 -9
- package/hooks/active_task_context.py +22 -15
- package/hooks/health_check.py +136 -38
- package/hooks/workflow_checkpoint.py +15 -1
- package/hooks/workflow_context.py +120 -30
- package/hooks/workflow_resume.py +96 -7
- package/mcp-server/lib/change-workflow.cjs +625 -0
- package/mcp-server/lib/change-workflow.test.cjs +346 -0
- package/mcp-server/lib/doctor.cjs +112 -0
- package/mcp-server/lib/doctor.test.cjs +86 -0
- package/mcp-server/lib/init-project.cjs +32 -6
- package/mcp-server/lib/runtime-state.cjs +222 -0
- package/mcp-server/lib/runtime-state.test.cjs +92 -0
- package/mcp-server/lib/state-db.cjs +31 -8
- package/mcp-server/lib/state-ops.cjs +17 -3
- package/mcp-server/server.cjs +102 -10
- package/mcp-server/tests/db-ops.test.cjs +5 -1
- package/mcp-server/tests/init-project.test.cjs +45 -0
- package/mcp-server/tests/server-contract.test.cjs +123 -1
- package/mcp-server/tests/state-db.test.cjs +44 -7
- package/mcp-server/tests/state-ops.test.cjs +19 -0
- package/orchestrator/daemon.cjs +39 -0
- package/orchestrator/tests/daemon.test.cjs +50 -0
- package/package.json +3 -3
- package/settings.json +1 -2
- package/skills/ultra-change/SKILL.md +149 -0
- package/skills/ultra-deliver/SKILL.md +83 -15
- package/skills/ultra-dev/SKILL.md +32 -12
- package/skills/ultra-doctor/SKILL.md +97 -0
- package/skills/ultra-init/SKILL.md +8 -5
- package/skills/ultra-review/SKILL.md +16 -0
- package/skills/ultra-status/SKILL.md +30 -7
- package/skills/ultra-test/SKILL.md +14 -2
- package/spec/cli-protocol.md +12 -1
- package/spec/mcp-tools.yaml +337 -1
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +117 -0
- package/ultra-tools/cli.cjs +3 -0
- package/ultra-tools/commands/system.cjs +57 -0
- package/ultra-tools/system-command.test.cjs +53 -0
- package/adapters/_shared/rtk-detect.cjs +0 -86
- package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
- package/docs/LEGACY-HERMES.md +0 -1099
- package/orchestrator/code-graph-watcher.cjs +0 -121
- package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const provenance = require('../provenance.cjs');
|
|
11
|
+
|
|
12
|
+
function fixture() {
|
|
13
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-'));
|
|
14
|
+
fs.mkdirSync(path.join(root, 'runtime'), { recursive: true });
|
|
15
|
+
fs.mkdirSync(path.join(root, 'hooks'), { recursive: true });
|
|
16
|
+
fs.writeFileSync(path.join(root, 'runtime', 'launch.cjs'), 'module.exports = true;\n');
|
|
17
|
+
fs.writeFileSync(path.join(root, 'hooks', 'hooks.json'), '{}\n');
|
|
18
|
+
const file = path.join(root, 'provenance.json');
|
|
19
|
+
provenance.writeProvenance({
|
|
20
|
+
file,
|
|
21
|
+
adapter: 'claude',
|
|
22
|
+
packageInfo: { name: 'ultra-builder-pro-cli', version: '1.2.3' },
|
|
23
|
+
repository: 'https://example.test/ultra-builder-pro',
|
|
24
|
+
sourceCommit: '0123456789abcdef0123456789abcdef01234567',
|
|
25
|
+
roots: { plugin: root },
|
|
26
|
+
assets: [
|
|
27
|
+
{ root: 'plugin', path: 'hooks/hooks.json' },
|
|
28
|
+
{ root: 'plugin', path: 'runtime/launch.cjs' },
|
|
29
|
+
],
|
|
30
|
+
contracts: {
|
|
31
|
+
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
32
|
+
mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return { root, file };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('writeProvenance + inspectProvenance produce a normalized healthy report', () => {
|
|
39
|
+
const fx = fixture();
|
|
40
|
+
try {
|
|
41
|
+
const manifest = JSON.parse(fs.readFileSync(fx.file, 'utf8'));
|
|
42
|
+
assert.equal(manifest.schema_version, 1);
|
|
43
|
+
assert.equal(manifest.adapter, 'claude');
|
|
44
|
+
assert.equal(manifest.package.version, '1.2.3');
|
|
45
|
+
assert.equal(manifest.source.repository, 'https://example.test/ultra-builder-pro');
|
|
46
|
+
assert.match(manifest.content.digest, /^[0-9a-f]{64}$/);
|
|
47
|
+
assert.deepEqual(manifest.assets.map((asset) => asset.path), [
|
|
48
|
+
'hooks/hooks.json', 'runtime/launch.cjs',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const report = provenance.inspectProvenance({
|
|
52
|
+
file: fx.file,
|
|
53
|
+
expectedAdapter: 'claude',
|
|
54
|
+
expectedPackageVersion: '1.2.3',
|
|
55
|
+
});
|
|
56
|
+
assert.equal(report.status, 'healthy');
|
|
57
|
+
assert.deepEqual(report.issues, []);
|
|
58
|
+
assert.equal(report.checks.assets.checked, 2);
|
|
59
|
+
assert.equal(report.checks.contracts.checked, 2);
|
|
60
|
+
} finally {
|
|
61
|
+
fs.rmSync(fx.root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('inspectProvenance detects content drift and missing contract targets', () => {
|
|
66
|
+
const fx = fixture();
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(path.join(fx.root, 'runtime', 'launch.cjs'), 'tampered\n');
|
|
69
|
+
fs.unlinkSync(path.join(fx.root, 'hooks', 'hooks.json'));
|
|
70
|
+
const report = provenance.inspectProvenance({
|
|
71
|
+
file: fx.file,
|
|
72
|
+
expectedAdapter: 'claude',
|
|
73
|
+
expectedPackageVersion: '1.2.3',
|
|
74
|
+
});
|
|
75
|
+
assert.equal(report.status, 'degraded');
|
|
76
|
+
assert.ok(report.issues.some((issue) => issue.code === 'ASSET_HASH_MISMATCH'));
|
|
77
|
+
assert.ok(report.issues.some(
|
|
78
|
+
(issue) => issue.code === 'CONTRACT_TARGET_MISSING' && issue.contract === 'hooks_manifest',
|
|
79
|
+
));
|
|
80
|
+
} finally {
|
|
81
|
+
fs.rmSync(fx.root, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('inspectProvenance fails closed for missing, corrupt, or mismatched manifests', () => {
|
|
86
|
+
const fx = fixture();
|
|
87
|
+
try {
|
|
88
|
+
const mismatch = provenance.inspectProvenance({
|
|
89
|
+
file: fx.file,
|
|
90
|
+
expectedAdapter: 'codex',
|
|
91
|
+
expectedPackageVersion: '9.9.9',
|
|
92
|
+
});
|
|
93
|
+
assert.equal(mismatch.status, 'degraded');
|
|
94
|
+
assert.ok(mismatch.issues.some((issue) => issue.code === 'ADAPTER_MISMATCH'));
|
|
95
|
+
assert.ok(mismatch.issues.some((issue) => issue.code === 'PACKAGE_VERSION_MISMATCH'));
|
|
96
|
+
|
|
97
|
+
fs.writeFileSync(fx.file, 'not-json');
|
|
98
|
+
const corrupt = provenance.inspectProvenance({ file: fx.file });
|
|
99
|
+
assert.equal(corrupt.status, 'degraded');
|
|
100
|
+
assert.equal(corrupt.issues[0].code, 'PROVENANCE_INVALID');
|
|
101
|
+
|
|
102
|
+
fs.unlinkSync(fx.file);
|
|
103
|
+
const missing = provenance.inspectProvenance({ file: fx.file });
|
|
104
|
+
assert.equal(missing.status, 'missing');
|
|
105
|
+
assert.equal(missing.issues[0].code, 'PROVENANCE_MISSING');
|
|
106
|
+
} finally {
|
|
107
|
+
fs.rmSync(fx.root, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('packageSource never attributes an enclosing consumer repository commit to the package', () => {
|
|
112
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-source-'));
|
|
113
|
+
try {
|
|
114
|
+
execFileSync('git', ['init', '-q'], { cwd: root });
|
|
115
|
+
execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: root });
|
|
116
|
+
execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: root });
|
|
117
|
+
fs.writeFileSync(path.join(root, 'README.md'), '# consumer\n');
|
|
118
|
+
execFileSync('git', ['add', 'README.md'], { cwd: root });
|
|
119
|
+
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: root });
|
|
120
|
+
const packageRoot = path.join(root, 'node_modules', 'ultra-builder-pro-cli');
|
|
121
|
+
fs.mkdirSync(packageRoot, { recursive: true });
|
|
122
|
+
fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({
|
|
123
|
+
name: 'ultra-builder-pro-cli',
|
|
124
|
+
version: '1.2.3',
|
|
125
|
+
repository: { url: 'https://example.test/ultra-builder-pro' },
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const source = provenance.packageSource(packageRoot);
|
|
129
|
+
assert.equal(source.sourceCommit, null);
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
package/adapters/claude.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
writeAtomic,
|
|
16
16
|
} = require('./_shared/file-ops.cjs');
|
|
17
17
|
const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
|
|
18
|
+
const provenance = require('./_shared/provenance.cjs');
|
|
18
19
|
const {
|
|
19
20
|
CORE_PUBLIC_SKILLS,
|
|
20
21
|
WORKFLOW_HOOK_FILES,
|
|
@@ -26,6 +27,7 @@ const MCP_SERVER_NAME = PLUGIN_NAME;
|
|
|
26
27
|
const SOURCE_TAG = 'ubp';
|
|
27
28
|
const SENTINEL_KEY = '_ubp_manifest';
|
|
28
29
|
const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
|
|
30
|
+
const PROVENANCE_FILE = 'provenance.json';
|
|
29
31
|
|
|
30
32
|
function resolveTarget(ctx = {}) {
|
|
31
33
|
if (ctx.configDir) return ctx.configDir;
|
|
@@ -155,9 +157,39 @@ function install(ctx = {}) {
|
|
|
155
157
|
license: pkg.license || 'MIT',
|
|
156
158
|
}, null, 2) + '\n');
|
|
157
159
|
markManaged(target, { adapter: 'claude', plugin: PLUGIN_NAME });
|
|
160
|
+
const source = provenance.packageSource(repoRoot);
|
|
161
|
+
const provenanceFile = path.join(target, PROVENANCE_FILE);
|
|
162
|
+
report.provenance = provenance.writeProvenance({
|
|
163
|
+
file: provenanceFile,
|
|
164
|
+
adapter: 'claude',
|
|
165
|
+
...source,
|
|
166
|
+
roots: { plugin: target },
|
|
167
|
+
assets: provenance.assetRefsForTree('plugin', target, {
|
|
168
|
+
exclude: ['.ubp-managed', PROVENANCE_FILE],
|
|
169
|
+
}),
|
|
170
|
+
contracts: {
|
|
171
|
+
plugin_manifest: { root: 'plugin', path: '.claude-plugin/plugin.json' },
|
|
172
|
+
mcp_registration: { root: 'plugin', path: '.mcp.json' },
|
|
173
|
+
mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
|
|
174
|
+
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
175
|
+
checkpoint_hook: { root: 'plugin', path: 'hooks/workflow_checkpoint.py' },
|
|
176
|
+
resume_hook: { root: 'plugin', path: 'hooks/workflow_resume.py' },
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
report.provenance.file = provenanceFile;
|
|
158
180
|
return report;
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
function doctor(ctx = {}) {
|
|
184
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
185
|
+
const source = provenance.packageSource(repoRoot);
|
|
186
|
+
return provenance.inspectProvenance({
|
|
187
|
+
file: path.join(resolvePluginRoot(ctx), PROVENANCE_FILE),
|
|
188
|
+
expectedAdapter: 'claude',
|
|
189
|
+
expectedPackageVersion: source.packageInfo.version,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
161
193
|
function uninstall(ctx = {}) {
|
|
162
194
|
const target = resolvePluginRoot(ctx);
|
|
163
195
|
const report = { target, removed: {}, config: { updated: false } };
|
|
@@ -179,4 +211,5 @@ module.exports = {
|
|
|
179
211
|
resolvePluginRoot,
|
|
180
212
|
install,
|
|
181
213
|
uninstall,
|
|
214
|
+
doctor,
|
|
182
215
|
};
|
package/adapters/codex.js
CHANGED
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
buildPlugin,
|
|
34
34
|
installAgents,
|
|
35
35
|
} = require('./_shared/codex-assets.cjs');
|
|
36
|
+
const provenance = require('./_shared/provenance.cjs');
|
|
36
37
|
|
|
37
38
|
const MCP_SERVER_NAME = PLUGIN_NAME;
|
|
38
39
|
const SOURCE_TAG = 'ubp';
|
|
@@ -40,6 +41,7 @@ const MARKER_BEGIN = '# >>> ultra-builder-pro managed block — do not edit by h
|
|
|
40
41
|
const MARKER_END = '# <<< ultra-builder-pro managed block';
|
|
41
42
|
const RUNTIME_MANIFEST_DIR = 'ultra-builder-pro';
|
|
42
43
|
const RUNTIME_MANIFEST_FILE = 'install-manifest.json';
|
|
44
|
+
const PROVENANCE_FILE = 'provenance.json';
|
|
43
45
|
const HOOK_ADAPTER_RELATIVE = path.join('hooks', 'adapters', 'codex.py');
|
|
44
46
|
|
|
45
47
|
function resolveTarget(ctx = {}) {
|
|
@@ -355,6 +357,30 @@ function install(ctx = {}) {
|
|
|
355
357
|
agents: agents.installed,
|
|
356
358
|
hookCacheVersions,
|
|
357
359
|
});
|
|
360
|
+
const source = provenance.packageSource(repoRoot);
|
|
361
|
+
const provenanceFile = path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE);
|
|
362
|
+
const provenanceManifest = provenance.writeProvenance({
|
|
363
|
+
file: provenanceFile,
|
|
364
|
+
adapter: 'codex',
|
|
365
|
+
...source,
|
|
366
|
+
roots: { plugin: pluginRoot, config: configDir },
|
|
367
|
+
assets: [
|
|
368
|
+
...provenance.assetRefsForTree('plugin', pluginRoot, {
|
|
369
|
+
exclude: ['.ubp-managed'],
|
|
370
|
+
}),
|
|
371
|
+
...agents.installed.map((file) => ({ root: 'config', path: path.join('agents', file) })),
|
|
372
|
+
{ root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
|
|
373
|
+
],
|
|
374
|
+
contracts: {
|
|
375
|
+
plugin_manifest: { root: 'plugin', path: '.codex-plugin/plugin.json' },
|
|
376
|
+
mcp_registration: { root: 'plugin', path: '.mcp.json' },
|
|
377
|
+
mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
|
|
378
|
+
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
379
|
+
hook_adapter: { root: 'plugin', path: HOOK_ADAPTER_RELATIVE },
|
|
380
|
+
runtime_manifest: { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
provenanceManifest.file = provenanceFile;
|
|
358
384
|
|
|
359
385
|
return {
|
|
360
386
|
target: configDir,
|
|
@@ -365,10 +391,70 @@ function install(ctx = {}) {
|
|
|
365
391
|
hookCompatibility,
|
|
366
392
|
legacy,
|
|
367
393
|
manifestFile,
|
|
394
|
+
provenance: provenanceManifest,
|
|
368
395
|
config: { updated: legacy.configUpdated },
|
|
369
396
|
};
|
|
370
397
|
}
|
|
371
398
|
|
|
399
|
+
function doctor(ctx = {}) {
|
|
400
|
+
const configDir = resolveTarget(ctx);
|
|
401
|
+
const pluginRoot = resolvePluginRoot(ctx);
|
|
402
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
403
|
+
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
404
|
+
const source = provenance.packageSource(repoRoot);
|
|
405
|
+
const report = provenance.inspectProvenance({
|
|
406
|
+
file: path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE),
|
|
407
|
+
expectedAdapter: 'codex',
|
|
408
|
+
expectedPackageVersion: source.packageInfo.version,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
let runtimeManifest = null;
|
|
412
|
+
try {
|
|
413
|
+
runtimeManifest = readRuntimeManifest(configDir);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', message: error.message });
|
|
416
|
+
}
|
|
417
|
+
const runtimeManifestOk = runtimeManifest?.source === SOURCE_TAG
|
|
418
|
+
&& runtimeManifest.adapter === 'codex'
|
|
419
|
+
&& runtimeManifest.plugin?.root === pluginRoot
|
|
420
|
+
&& typeof runtimeManifest.plugin?.version === 'string';
|
|
421
|
+
if (!runtimeManifestOk && !report.issues.some((entry) => entry.code === 'RUNTIME_MANIFEST_INVALID')) {
|
|
422
|
+
report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', path: runtimeManifestFile(configDir) });
|
|
423
|
+
}
|
|
424
|
+
report.checks.runtime_manifest = { status: runtimeManifestOk ? 'pass' : 'fail' };
|
|
425
|
+
|
|
426
|
+
let marketplaceOk = false;
|
|
427
|
+
try {
|
|
428
|
+
const marketplace = loadMarketplace(marketplaceFile);
|
|
429
|
+
const entry = marketplace.plugins.find((plugin) => plugin?.name === PLUGIN_NAME);
|
|
430
|
+
marketplaceOk = entry?.source?.source === 'local'
|
|
431
|
+
&& entry.source.path === `./plugins/${PLUGIN_NAME}`;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', message: error.message });
|
|
434
|
+
}
|
|
435
|
+
if (!marketplaceOk && !report.issues.some((entry) => entry.code === 'MARKETPLACE_REGISTRATION_INVALID')) {
|
|
436
|
+
report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', path: marketplaceFile });
|
|
437
|
+
}
|
|
438
|
+
report.checks.marketplace = { status: marketplaceOk ? 'pass' : 'fail' };
|
|
439
|
+
|
|
440
|
+
let hookTargetsOk = runtimeManifestOk;
|
|
441
|
+
if (runtimeManifestOk) {
|
|
442
|
+
const cacheRoot = pluginCacheRoot(configDir, runtimeManifest.marketplace?.name || 'personal');
|
|
443
|
+
const currentCacheRequired = shouldRunPluginCli(ctx);
|
|
444
|
+
for (const version of runtimeManifest.hook_cache_versions || []) {
|
|
445
|
+
if (version === runtimeManifest.plugin.version && !currentCacheRequired) continue;
|
|
446
|
+
const adapter = path.join(cacheRoot, version, HOOK_ADAPTER_RELATIVE);
|
|
447
|
+
if (!fs.existsSync(adapter)) {
|
|
448
|
+
hookTargetsOk = false;
|
|
449
|
+
report.issues.push({ code: 'HOOK_TARGET_MISSING', version, path: adapter });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
report.checks.hook_targets = { status: hookTargetsOk ? 'pass' : 'fail' };
|
|
454
|
+
if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
455
|
+
return report;
|
|
456
|
+
}
|
|
457
|
+
|
|
372
458
|
function removeManagedAgents(configDir, manifest) {
|
|
373
459
|
const agentRoot = path.join(configDir, 'agents');
|
|
374
460
|
const requested = manifest && Array.isArray(manifest.agents)
|
|
@@ -440,6 +526,7 @@ module.exports = {
|
|
|
440
526
|
resolveMarketplaceFile,
|
|
441
527
|
install,
|
|
442
528
|
uninstall,
|
|
529
|
+
doctor,
|
|
443
530
|
_internal: {
|
|
444
531
|
stripManagedBlock,
|
|
445
532
|
hasManagedBlock,
|
package/adapters/opencode.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
writeAtomic,
|
|
16
16
|
} = require('./_shared/file-ops.cjs');
|
|
17
17
|
const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
|
|
18
|
+
const provenance = require('./_shared/provenance.cjs');
|
|
18
19
|
const {
|
|
19
20
|
CORE_PUBLIC_SKILLS,
|
|
20
21
|
skillsForRuntime,
|
|
@@ -34,6 +35,7 @@ const SOURCE_TAG = 'ubp';
|
|
|
34
35
|
const BUNDLE_DIR = '.ultra-builder-pro';
|
|
35
36
|
const MANAGED_TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
36
37
|
const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
38
|
+
const PROVENANCE_FILE = 'provenance.json';
|
|
37
39
|
const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
|
|
38
40
|
|
|
39
41
|
function resolveTarget(ctx = {}) {
|
|
@@ -337,20 +339,55 @@ import path from "node:path";
|
|
|
337
339
|
|
|
338
340
|
const TERMINAL = new Set(["committed", "completed", "done", "cancelled"]);
|
|
339
341
|
const TASKS_PROJECTION = ".ultra/tasks/tasks.json";
|
|
342
|
+
const PROVIDER_FIELDS = new Set(["provider", "project", "reference", "revision", "indexed_head", "status"]);
|
|
343
|
+
|
|
344
|
+
function providerMetadata(value) {
|
|
345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
346
|
+
const clean = {};
|
|
347
|
+
for (const [kind, ref] of Object.entries(value)) {
|
|
348
|
+
if (!["memory", "code_graph"].includes(kind) || !ref || typeof ref !== "object" || Array.isArray(ref)) continue;
|
|
349
|
+
clean[kind] = Object.fromEntries(Object.entries(ref).filter(([key]) => PROVIDER_FIELDS.has(key)));
|
|
350
|
+
}
|
|
351
|
+
return clean;
|
|
352
|
+
}
|
|
340
353
|
|
|
341
|
-
function
|
|
354
|
+
function findUltraContext(directory) {
|
|
342
355
|
let current = path.resolve(directory);
|
|
343
356
|
while (true) {
|
|
344
|
-
const
|
|
345
|
-
|
|
357
|
+
const ultra = path.join(current, ".ultra");
|
|
358
|
+
const workflowFile = path.join(ultra, "workflow-state.json");
|
|
359
|
+
const statePresent = fs.existsSync(path.join(ultra, "state.db"));
|
|
360
|
+
let workflow = null;
|
|
361
|
+
if (fs.existsSync(workflowFile)) {
|
|
346
362
|
try {
|
|
347
|
-
const state = JSON.parse(fs.readFileSync(
|
|
348
|
-
|
|
363
|
+
const state = JSON.parse(fs.readFileSync(workflowFile, "utf8"));
|
|
364
|
+
workflow = state && !TERMINAL.has(state.status) ? state : null;
|
|
349
365
|
} catch (error) {
|
|
350
|
-
process.stderr.write(\`[ultra-builder-pro] cannot read \${
|
|
351
|
-
|
|
366
|
+
process.stderr.write(\`[ultra-builder-pro] cannot read \${workflowFile}: \${error.message}\\n\`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const changes = [];
|
|
370
|
+
const changeRoot = path.join(ultra, "changes", "active");
|
|
371
|
+
if (fs.existsSync(changeRoot)) {
|
|
372
|
+
for (const entry of fs.readdirSync(changeRoot, { withFileTypes: true })) {
|
|
373
|
+
if (!entry.isDirectory()) continue;
|
|
374
|
+
const manifestPath = path.join(changeRoot, entry.name, "context-manifest.json");
|
|
375
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
376
|
+
try {
|
|
377
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
378
|
+
if (manifest?.change) changes.push({
|
|
379
|
+
...manifest.change,
|
|
380
|
+
providers: providerMetadata(manifest.providers),
|
|
381
|
+
manifestPath: path.relative(current, manifestPath),
|
|
382
|
+
});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
process.stderr.write(\`[ultra-builder-pro] cannot read \${manifestPath}: \${error.message}\\n\`);
|
|
385
|
+
}
|
|
352
386
|
}
|
|
353
387
|
}
|
|
388
|
+
if (statePresent || workflow || changes.length > 0) {
|
|
389
|
+
return { root: current, workflow, changes, statePresent };
|
|
390
|
+
}
|
|
354
391
|
const parent = path.dirname(current);
|
|
355
392
|
if (parent === current) return null;
|
|
356
393
|
current = parent;
|
|
@@ -358,16 +395,42 @@ function findActiveWorkflow(directory) {
|
|
|
358
395
|
}
|
|
359
396
|
|
|
360
397
|
function contextText(active) {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
398
|
+
const lines = [];
|
|
399
|
+
if (active.workflow) {
|
|
400
|
+
const state = active.workflow;
|
|
401
|
+
lines.push(
|
|
402
|
+
"[Ultra active workflow]",
|
|
403
|
+
\`Project: \${active.root}\`,
|
|
404
|
+
\`Command: \${state.command ?? "unknown"}\`,
|
|
405
|
+
\`Task: \${state.task_id ?? state.task ?? "unknown"}\`,
|
|
406
|
+
\`Step: \${state.step ?? "unknown"}\`,
|
|
407
|
+
\`Status: \${state.status ?? "active"}\`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (active.changes.length > 0) {
|
|
411
|
+
const change = active.changes[0];
|
|
412
|
+
lines.push(
|
|
413
|
+
"[Ultra continuous change]",
|
|
414
|
+
\`Project: \${active.root}\`,
|
|
415
|
+
\`Change: \${change.id} (\${change.kind ?? "unknown"}, \${change.status ?? "active"})\`,
|
|
416
|
+
\`Title: \${change.title ?? "unknown"}\`,
|
|
417
|
+
\`Intent: \${change.intent ?? "unknown"}\`,
|
|
418
|
+
\`Context manifest: \${change.manifestPath}\`,
|
|
419
|
+
\`External providers (metadata references only): \${JSON.stringify(change.providers)}\`,
|
|
420
|
+
);
|
|
421
|
+
if (active.changes.length > 1) lines.push(
|
|
422
|
+
\`Active change count: \${active.changes.length}; resolve scope with change.list before mutation.\`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (!active.workflow && active.changes.length === 0) {
|
|
426
|
+
lines.push(
|
|
427
|
+
"[Ultra baseline]",
|
|
428
|
+
\`Project: \${active.root}\`,
|
|
429
|
+
"No active continuous change. Start daily work with the ultra-change workflow.",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
lines.push("Authority: .ultra/state.db; generated JSON/Markdown are projections or artifacts.");
|
|
433
|
+
return lines.join("\\n");
|
|
371
434
|
}
|
|
372
435
|
|
|
373
436
|
function targetPaths(tool, args) {
|
|
@@ -391,8 +454,8 @@ function isTasksProjection(active, candidate) {
|
|
|
391
454
|
|
|
392
455
|
export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
|
|
393
456
|
const root = worktree || directory;
|
|
394
|
-
let active =
|
|
395
|
-
const refresh = () => { active =
|
|
457
|
+
let active = findUltraContext(root);
|
|
458
|
+
const refresh = () => { active = findUltraContext(root); };
|
|
396
459
|
return {
|
|
397
460
|
event: async ({ event }) => {
|
|
398
461
|
if (["session.created", "session.compacted", "session.idle"].includes(event?.type)) refresh();
|
|
@@ -413,7 +476,7 @@ export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
|
|
|
413
476
|
if (targetPaths(tool, output?.args).some((candidate) => isTasksProjection(active, candidate))) {
|
|
414
477
|
throw new Error(
|
|
415
478
|
"Ultra Builder Pro refused a direct write to .ultra/tasks/tasks.json. " +
|
|
416
|
-
".ultra/state.db is authoritative; use MCP task tools
|
|
479
|
+
".ultra/state.db is authoritative; use MCP task tools and run ultra-doctor when state or projection health is degraded."
|
|
417
480
|
);
|
|
418
481
|
}
|
|
419
482
|
},
|
|
@@ -454,6 +517,69 @@ function install(ctx = {}) {
|
|
|
454
517
|
delete next[LEGACY_SENTINEL_KEY];
|
|
455
518
|
writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
|
|
456
519
|
report.config.updated = true;
|
|
520
|
+
const source = provenance.packageSource(repoRoot);
|
|
521
|
+
const bundleAssets = provenance.assetRefsForTree('config', bundleRoot, {
|
|
522
|
+
exclude: [PROVENANCE_FILE],
|
|
523
|
+
}).map((asset) => ({ ...asset, path: path.join(BUNDLE_DIR, asset.path) }));
|
|
524
|
+
const skillMarkers = skillsForRuntime('opencode').map((name) => ({
|
|
525
|
+
root: 'config', path: path.join('skills', name, '.ubp-managed'),
|
|
526
|
+
}));
|
|
527
|
+
const provenanceFile = path.join(bundleRoot, PROVENANCE_FILE);
|
|
528
|
+
report.provenance = provenance.writeProvenance({
|
|
529
|
+
file: provenanceFile,
|
|
530
|
+
adapter: 'opencode',
|
|
531
|
+
...source,
|
|
532
|
+
roots: { config: target },
|
|
533
|
+
assets: [
|
|
534
|
+
...report.copied.commands.map((file) => ({ root: 'config', path: path.join('commands', file) })),
|
|
535
|
+
...report.copied.skills.map((file) => ({ root: 'config', path: path.join('skills', file) })),
|
|
536
|
+
...skillMarkers,
|
|
537
|
+
...report.copied.agents.map((file) => ({ root: 'config', path: path.join('agents', file) })),
|
|
538
|
+
{ root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
539
|
+
...bundleAssets,
|
|
540
|
+
],
|
|
541
|
+
contracts: {
|
|
542
|
+
host_plugin: { root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
543
|
+
mcp_config: { root: 'config', path: 'opencode.json' },
|
|
544
|
+
mcp_launcher: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'launch.cjs') },
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
report.provenance.file = provenanceFile;
|
|
548
|
+
return report;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function doctor(ctx = {}) {
|
|
552
|
+
const target = resolveTarget(ctx);
|
|
553
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
554
|
+
const source = provenance.packageSource(repoRoot);
|
|
555
|
+
const report = provenance.inspectProvenance({
|
|
556
|
+
file: path.join(target, BUNDLE_DIR, PROVENANCE_FILE),
|
|
557
|
+
expectedAdapter: 'opencode',
|
|
558
|
+
expectedPackageVersion: source.packageInfo.version,
|
|
559
|
+
});
|
|
560
|
+
const configFile = path.join(target, 'opencode.json');
|
|
561
|
+
const expectedLauncher = path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs');
|
|
562
|
+
let registrationOk = false;
|
|
563
|
+
try {
|
|
564
|
+
const config = readJsonSafe(configFile);
|
|
565
|
+
const entry = config.mcp && config.mcp[MCP_SERVER_NAME];
|
|
566
|
+
registrationOk = entry?.type === 'local'
|
|
567
|
+
&& entry.enabled === true
|
|
568
|
+
&& Array.isArray(entry.command)
|
|
569
|
+
&& entry.command[0] === process.execPath
|
|
570
|
+
&& entry.command[1] === expectedLauncher;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
report.issues.push({ code: 'MCP_REGISTRATION_INVALID', message: error.message });
|
|
573
|
+
}
|
|
574
|
+
if (!registrationOk && !report.issues.some((entry) => entry.code === 'MCP_REGISTRATION_INVALID')) {
|
|
575
|
+
report.issues.push({
|
|
576
|
+
code: 'MCP_REGISTRATION_INVALID',
|
|
577
|
+
path: configFile,
|
|
578
|
+
expected_launcher: expectedLauncher,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
|
|
582
|
+
if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
457
583
|
return report;
|
|
458
584
|
}
|
|
459
585
|
|
|
@@ -529,4 +655,5 @@ module.exports = {
|
|
|
529
655
|
resolveTarget,
|
|
530
656
|
install,
|
|
531
657
|
uninstall,
|
|
658
|
+
doctor,
|
|
532
659
|
};
|
|
@@ -21,8 +21,10 @@ const {
|
|
|
21
21
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
22
22
|
const COMMANDS = [
|
|
23
23
|
'learn',
|
|
24
|
+
'ultra-change',
|
|
24
25
|
'ultra-deliver',
|
|
25
26
|
'ultra-dev',
|
|
27
|
+
'ultra-doctor',
|
|
26
28
|
'ultra-init',
|
|
27
29
|
'ultra-plan',
|
|
28
30
|
'ultra-research',
|
|
@@ -89,7 +91,7 @@ test('install builds one Codex-native plugin with complete skill and command cov
|
|
|
89
91
|
|
|
90
92
|
const expectedSkills = skillsForRuntime('codex').sort();
|
|
91
93
|
assert.deepEqual(skillNames(layout.pluginRoot), expectedSkills);
|
|
92
|
-
assert.equal(expectedSkills.length,
|
|
94
|
+
assert.equal(expectedSkills.length, 18);
|
|
93
95
|
assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'codex-collab')));
|
|
94
96
|
assert.ok(!fs.existsSync(path.join(layout.pluginRoot, 'skills', 'learned')));
|
|
95
97
|
|
|
@@ -229,8 +231,8 @@ test('plugin declares current Codex hooks and a project-local Ultra MCP server',
|
|
|
229
231
|
const liveSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'mcp-tools.yaml'), 'utf8'));
|
|
230
232
|
const upstreamSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'upstream-mcp-tools.yaml'), 'utf8'));
|
|
231
233
|
const capabilityMap = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'codex-capability-map.json'), 'utf8'));
|
|
232
|
-
assert.equal(liveSpec.tools.length,
|
|
233
|
-
assert.equal(upstreamSpec.tools.length,
|
|
234
|
+
assert.equal(liveSpec.tools.length, 29);
|
|
235
|
+
assert.equal(upstreamSpec.tools.length, 29);
|
|
234
236
|
assert.deepEqual(upstreamSpec.tools.map((tool) => tool.name).sort(), liveSpec.tools.map((tool) => tool.name).sort());
|
|
235
237
|
assert.deepEqual(capabilityMap.live_mcp_tools.sort(), liveSpec.tools.map((tool) => tool.name).sort());
|
|
236
238
|
assert.equal(Object.keys(capabilityMap.codex_native_replacements).length, 9);
|
|
@@ -360,3 +362,40 @@ test('uninstall removes only UBP-managed plugin and agents', () => {
|
|
|
360
362
|
cleanup(layout);
|
|
361
363
|
}
|
|
362
364
|
});
|
|
365
|
+
|
|
366
|
+
test('doctor checks the current Codex cache hook target for a CLI-managed global install', () => {
|
|
367
|
+
const layout = mkLayout();
|
|
368
|
+
try {
|
|
369
|
+
install(layout);
|
|
370
|
+
const runtimeManifest = JSON.parse(fs.readFileSync(
|
|
371
|
+
path.join(layout.configDir, 'ultra-builder-pro', 'install-manifest.json'),
|
|
372
|
+
'utf8',
|
|
373
|
+
));
|
|
374
|
+
const version = runtimeManifest.plugin.version;
|
|
375
|
+
const cacheAdapter = path.join(
|
|
376
|
+
codex._internal.pluginCacheRoot(layout.configDir, 'personal'),
|
|
377
|
+
version,
|
|
378
|
+
'hooks',
|
|
379
|
+
'adapters',
|
|
380
|
+
'codex.py',
|
|
381
|
+
);
|
|
382
|
+
const doctorCtx = {
|
|
383
|
+
homeDir: layout.homeDir,
|
|
384
|
+
scope: 'global',
|
|
385
|
+
repoRoot: REPO_ROOT,
|
|
386
|
+
runPluginCli: true,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const degraded = codex.doctor(doctorCtx);
|
|
390
|
+
assert.equal(degraded.status, 'degraded');
|
|
391
|
+
assert.ok(degraded.issues.some((entry) => (
|
|
392
|
+
entry.code === 'HOOK_TARGET_MISSING' && entry.version === version
|
|
393
|
+
)));
|
|
394
|
+
|
|
395
|
+
fs.mkdirSync(path.dirname(cacheAdapter), { recursive: true });
|
|
396
|
+
fs.copyFileSync(path.join(layout.pluginRoot, 'hooks', 'adapters', 'codex.py'), cacheAdapter);
|
|
397
|
+
assert.equal(codex.doctor(doctorCtx).status, 'healthy');
|
|
398
|
+
} finally {
|
|
399
|
+
cleanup(layout);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
@@ -171,13 +171,14 @@ test('install writes a schema-safe opencode.json and keeps ownership outside hos
|
|
|
171
171
|
assert.match(plugin, /\.ultra[\\/]tasks[\\/]tasks\.json/);
|
|
172
172
|
assert.match(plugin, /throw new Error/);
|
|
173
173
|
assert.match(plugin, /session\.compacted/);
|
|
174
|
-
assert.doesNotMatch(plugin, /memory|recall|journal|observation/);
|
|
174
|
+
assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
|
|
175
|
+
assert.match(plugin, /metadata references only/);
|
|
175
176
|
} finally {
|
|
176
177
|
fs.rmSync(target, { recursive: true, force: true });
|
|
177
178
|
}
|
|
178
179
|
});
|
|
179
180
|
|
|
180
|
-
test('OpenCode
|
|
181
|
+
test('OpenCode context covers active changes and projection writes are blocked for Ultra projects', async () => {
|
|
181
182
|
const target = mkTarget();
|
|
182
183
|
const project = mkTarget();
|
|
183
184
|
try {
|
|
@@ -186,15 +187,21 @@ test('OpenCode before-tool hook blocks tasks.json writes only during an active w
|
|
|
186
187
|
const module = await import(pathToFileURL(pluginFile).href);
|
|
187
188
|
const plugin = await module.UltraBuilderProPlugin({ directory: project });
|
|
188
189
|
|
|
189
|
-
await plugin['tool.execute.before'](
|
|
190
|
-
{ tool: 'edit' },
|
|
191
|
-
{ args: { filePath: '.ultra/tasks/tasks.json' } },
|
|
192
|
-
);
|
|
193
|
-
|
|
194
190
|
fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
|
|
195
|
-
fs.writeFileSync(path.join(project, '.ultra', '
|
|
196
|
-
|
|
191
|
+
fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
|
|
192
|
+
const changeRoot = path.join(project, '.ultra', 'changes', 'active', 'daily-fix');
|
|
193
|
+
fs.mkdirSync(changeRoot, { recursive: true });
|
|
194
|
+
fs.writeFileSync(path.join(changeRoot, 'context-manifest.json'), JSON.stringify({
|
|
195
|
+
change: { id: 'daily-fix', title: 'Daily fix', kind: 'quick', status: 'active', intent: 'Fix drift' },
|
|
196
|
+
providers: { memory: { provider: 'cloud-mem', status: 'available' } },
|
|
197
197
|
}));
|
|
198
|
+
|
|
199
|
+
const output = { system: [] };
|
|
200
|
+
await plugin['experimental.chat.system.transform']({}, output);
|
|
201
|
+
assert.match(output.system.join('\n'), /Ultra continuous change/);
|
|
202
|
+
assert.match(output.system.join('\n'), /daily-fix/);
|
|
203
|
+
assert.match(output.system.join('\n'), /metadata references only/);
|
|
204
|
+
|
|
198
205
|
await assert.rejects(
|
|
199
206
|
plugin['tool.execute.before'](
|
|
200
207
|
{ tool: 'apply_patch' },
|