ultra-builder-pro-cli 0.24.2 → 0.25.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 +42 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/docs/AGENT-CONTEXT.md +14 -0
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/mcp-server/lib/artifact-registry.cjs +181 -51
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-workflow.cjs +21 -16
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +2 -2
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1786 -169
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +23 -4
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- package/spec/schemas/tasks.v4.5.schema.json +34 -12
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.25.0] — 2026-07-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `task_outcome / attest_commit` as the sole model-facing path for recording
|
|
15
|
+
checkout-local integrated commit proof after durable Task completion.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Published exact Task identifier, numeric, array, object, digest, and runtime-field
|
|
20
|
+
boundaries in the seven-tool MCP contract while keeping repository-defined Task,
|
|
21
|
+
Change, priority, risk, research, and slice labels under model ownership.
|
|
22
|
+
- Applied the same exact-input boundary to Baseline, Change, Decision, Artifact, Event,
|
|
23
|
+
Context, Checkpoint, Sync, Session, Archive, and Doctor calls. Unknown fields and
|
|
24
|
+
wrong scalar, enum, array, object, digest, or scope types are rejected before SQLite
|
|
25
|
+
affinity or downstream consumers can reinterpret them.
|
|
26
|
+
- Made Dev delivery publish durable completion before one integration commit, then
|
|
27
|
+
attest that commit locally without changing the Git-facing team ledger.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Removed arbitrary Task patches and model-supplied commit hashes from
|
|
32
|
+
`task_outcome / complete`, eliminating the remaining self-referential or
|
|
33
|
+
bookkeeping-commit path.
|
|
34
|
+
- Fixed malformed semantic input and idempotency attempts escaping as transport or raw
|
|
35
|
+
SQLite errors. Correctable requests now return mutable diagnostics and enter bounded
|
|
36
|
+
rejected-attempt audit history without creating semantic authority.
|
|
37
|
+
- Fixed record, checkpoint, team-checkpoint publication, Session lease, Worker Packet,
|
|
38
|
+
and archive receipt failures leaving database rows, managed files, worktrees, or Git
|
|
39
|
+
checkpoint bytes partially committed. Each public mutation now commits its durable
|
|
40
|
+
receipt with the owned state or compensates the external effect before retry.
|
|
41
|
+
- Fixed Task deletion reporting a generic draft-state error before identifying the
|
|
42
|
+
exact durable Session, Artifact, checkpoint, Context, workflow-history, or recovery
|
|
43
|
+
reference that must be resolved.
|
|
44
|
+
- Added a linked active successor path for immutable archived or cancelled Changes
|
|
45
|
+
instead of requiring history mutation or direct SQLite repair.
|
|
46
|
+
- Removed the public Kernel's runtime dependency on the retired workflow supervisor;
|
|
47
|
+
the legacy Change implementation remains compatibility-only and is absent from all
|
|
48
|
+
five bundled host runtimes.
|
|
49
|
+
- Revoked an assigned Worker Packet when a parallel reservation is unwound, ensuring
|
|
50
|
+
the abandoned capability cannot later complete a Task without a live Session.
|
|
51
|
+
|
|
10
52
|
## [0.24.2] — 2026-07-31
|
|
11
53
|
|
|
12
54
|
### Changed
|
package/adapters/claude.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
8
9
|
|
|
9
10
|
const {
|
|
10
11
|
copyTree,
|
|
@@ -53,6 +54,77 @@ function resolveRepoRoot(ctx = {}) {
|
|
|
53
54
|
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function shouldRunHostCli(ctx = {}) {
|
|
58
|
+
if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
|
|
59
|
+
return ctx.scope === 'global' && !ctx.configDir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runClaudeCli(args, ctx = {}) {
|
|
63
|
+
const result = spawnSync(ctx.claudeBin || 'claude', args, {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
timeout: ctx.hostCliTimeoutMs || 30000,
|
|
66
|
+
killSignal: 'SIGKILL',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
CLAUDE_CONFIG_DIR: resolveTarget(ctx),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (result.error) {
|
|
74
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
75
|
+
const error = new Error(`claude ${args.join(' ')} timed out`);
|
|
76
|
+
error.code = 'HOST_CLI_TIMEOUT';
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
throw result.error;
|
|
80
|
+
}
|
|
81
|
+
if (result.status !== 0) {
|
|
82
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
83
|
+
throw new Error(`claude ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
84
|
+
}
|
|
85
|
+
return result.stdout;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function claudeMcpRow(output, identity) {
|
|
89
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
90
|
+
const line = rawLine.trim();
|
|
91
|
+
if (line.slice(0, identity.length) !== identity) continue;
|
|
92
|
+
if (line[identity.length] !== ':') continue;
|
|
93
|
+
return line;
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function inspectClaudeHost(ctx = {}) {
|
|
99
|
+
const target = resolvePluginRoot(ctx);
|
|
100
|
+
const manifest = JSON.parse(
|
|
101
|
+
fs.readFileSync(path.join(target, '.claude-plugin', 'plugin.json'), 'utf8'),
|
|
102
|
+
);
|
|
103
|
+
const plugins = JSON.parse(runClaudeCli(['plugin', 'list', '--json'], ctx));
|
|
104
|
+
if (!Array.isArray(plugins)) throw new Error('claude plugin list --json did not return an array');
|
|
105
|
+
const plugin = plugins.find((entry) => entry?.id === `${PLUGIN_NAME}@skills-dir`);
|
|
106
|
+
const pluginOk = !!plugin
|
|
107
|
+
&& plugin.enabled === true
|
|
108
|
+
&& plugin.version === manifest.version
|
|
109
|
+
&& path.resolve(plugin.installPath || '') === path.resolve(target);
|
|
110
|
+
const mcpOutput = pluginOk
|
|
111
|
+
? runClaudeCli(['mcp', 'list'], ctx).replace(/\u001b\[[0-9;]*m/g, '')
|
|
112
|
+
: '';
|
|
113
|
+
const mcpLine = claudeMcpRow(
|
|
114
|
+
mcpOutput,
|
|
115
|
+
`plugin:${PLUGIN_NAME}:${MCP_SERVER_NAME}`,
|
|
116
|
+
);
|
|
117
|
+
const mcpOk = pluginOk
|
|
118
|
+
&& !!plugin.mcpServers?.[MCP_SERVER_NAME]
|
|
119
|
+
&& /\bConnected\b/i.test(mcpLine);
|
|
120
|
+
let detailsOk = false;
|
|
121
|
+
if (pluginOk) {
|
|
122
|
+
runClaudeCli(['plugin', 'details', `${PLUGIN_NAME}@skills-dir`], ctx);
|
|
123
|
+
detailsOk = true;
|
|
124
|
+
}
|
|
125
|
+
return { plugin, pluginOk, mcpOk, mcpOutput, detailsOk };
|
|
126
|
+
}
|
|
127
|
+
|
|
56
128
|
function copySkills(repoRoot, target, names) {
|
|
57
129
|
const copied = [];
|
|
58
130
|
for (const name of names) {
|
|
@@ -245,6 +317,37 @@ function doctor(ctx = {}) {
|
|
|
245
317
|
expectedAdapter: 'claude',
|
|
246
318
|
expectedPackageVersion: source.packageInfo.version,
|
|
247
319
|
});
|
|
320
|
+
if (shouldRunHostCli(ctx)) {
|
|
321
|
+
try {
|
|
322
|
+
const host = inspectClaudeHost(ctx);
|
|
323
|
+
report.checks.host_plugin = {
|
|
324
|
+
status: host.pluginOk && host.detailsOk ? 'pass' : 'fail',
|
|
325
|
+
};
|
|
326
|
+
report.checks.host_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
|
|
327
|
+
if (!host.pluginOk || !host.detailsOk) {
|
|
328
|
+
report.issues.push({
|
|
329
|
+
code: 'HOST_PLUGIN_NOT_DISCOVERED',
|
|
330
|
+
plugin_id: `${PLUGIN_NAME}@skills-dir`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (!host.mcpOk) {
|
|
334
|
+
report.issues.push({
|
|
335
|
+
code: 'HOST_MCP_NOT_DISCOVERED',
|
|
336
|
+
plugin_id: `${PLUGIN_NAME}@skills-dir`,
|
|
337
|
+
server: MCP_SERVER_NAME,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
report.checks.host_plugin = { status: 'fail' };
|
|
342
|
+
report.checks.host_mcp = { status: 'fail' };
|
|
343
|
+
report.issues.push({
|
|
344
|
+
code: error.code === 'HOST_CLI_TIMEOUT'
|
|
345
|
+
? 'HOST_CLI_TIMEOUT'
|
|
346
|
+
: 'HOST_CLI_INSPECTION_FAILED',
|
|
347
|
+
message: error.message,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
248
351
|
return applyNativeDoctor(report, path.join(resolvePluginRoot(ctx), 'runtime'));
|
|
249
352
|
}
|
|
250
353
|
|
|
@@ -265,6 +368,7 @@ module.exports = {
|
|
|
265
368
|
SOURCE_TAG,
|
|
266
369
|
MCP_SERVER_NAME,
|
|
267
370
|
buildHooksManifest,
|
|
371
|
+
inspectClaudeHost,
|
|
268
372
|
resolveTarget,
|
|
269
373
|
resolvePluginRoot,
|
|
270
374
|
install,
|
package/adapters/codex.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const path = require('node:path');
|
|
22
|
+
const crypto = require('node:crypto');
|
|
22
23
|
const { spawnSync } = require('node:child_process');
|
|
23
24
|
|
|
24
25
|
const {
|
|
@@ -177,19 +178,127 @@ function shouldRunPluginCli(ctx = {}) {
|
|
|
177
178
|
return ctx.scope === 'global' && !ctx.configDir;
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
function shouldRunHostCli(ctx = {}) {
|
|
182
|
+
if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
|
|
183
|
+
return shouldRunPluginCli(ctx);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function runCodexCli(args, ctx = {}) {
|
|
187
|
+
const result = spawnSync(ctx.codexBin || 'codex', args, {
|
|
188
|
+
encoding: 'utf8',
|
|
189
|
+
timeout: ctx.hostCliTimeoutMs || 30000,
|
|
190
|
+
killSignal: 'SIGKILL',
|
|
191
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
192
|
+
env: {
|
|
193
|
+
...process.env,
|
|
194
|
+
HOME: resolveHomeDir(ctx),
|
|
195
|
+
CODEX_HOME: resolveTarget(ctx),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
if (result.error) {
|
|
199
|
+
if (result.error.code === 'ETIMEDOUT') {
|
|
200
|
+
const error = new Error(`codex ${args.join(' ')} timed out`);
|
|
201
|
+
error.code = 'HOST_CLI_TIMEOUT';
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
throw result.error;
|
|
205
|
+
}
|
|
206
|
+
if (result.status !== 0) {
|
|
207
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
208
|
+
throw new Error(`codex ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
209
|
+
}
|
|
210
|
+
return result.stdout;
|
|
211
|
+
}
|
|
212
|
+
|
|
180
213
|
function runPluginCli(action, marketplaceName, ctx = {}) {
|
|
181
|
-
const codexBin = ctx.codexBin || 'codex';
|
|
182
214
|
const selector = `${PLUGIN_NAME}@${marketplaceName}`;
|
|
183
215
|
const args = action === 'add'
|
|
184
216
|
? ['plugin', 'add', selector, '--json']
|
|
185
|
-
: ['plugin', 'remove', selector];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
: ['plugin', 'remove', selector, '--json'];
|
|
218
|
+
return { selector, stdout: runCodexCli(args, ctx).trim() };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function snapshotPluginRegistration(marketplaceName, ctx = {}) {
|
|
222
|
+
const selector = `${PLUGIN_NAME}@${marketplaceName}`;
|
|
223
|
+
const payload = JSON.parse(runCodexCli(['plugin', 'list', '--json'], ctx));
|
|
224
|
+
if (!payload || !Array.isArray(payload.installed)) {
|
|
225
|
+
throw new Error('codex plugin list --json did not return an installed array');
|
|
226
|
+
}
|
|
227
|
+
const entry = payload.installed.find((plugin) => (
|
|
228
|
+
plugin?.pluginId === selector
|
|
229
|
+
|| (plugin?.name === PLUGIN_NAME && plugin?.marketplaceName === marketplaceName)
|
|
230
|
+
)) || null;
|
|
231
|
+
return {
|
|
232
|
+
selector,
|
|
233
|
+
marketplaceName,
|
|
234
|
+
installed: !!entry && entry.installed !== false,
|
|
235
|
+
enabled: !!entry && entry.enabled === true,
|
|
236
|
+
entry,
|
|
237
|
+
config: captureFiles([path.join(resolveTarget(ctx), 'config.toml')])[0],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function restorePluginRegistration(snapshot, ctx = {}) {
|
|
242
|
+
const action = snapshot.installed ? 'add' : 'remove';
|
|
243
|
+
let actionError = null;
|
|
244
|
+
let result = null;
|
|
245
|
+
try {
|
|
246
|
+
result = runPluginCli(action, snapshot.marketplaceName, ctx);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
actionError = error;
|
|
249
|
+
}
|
|
250
|
+
let configError = null;
|
|
251
|
+
try {
|
|
252
|
+
restoreFiles([snapshot.config]);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
configError = error;
|
|
255
|
+
}
|
|
256
|
+
if (actionError || configError) {
|
|
257
|
+
throw new AggregateError(
|
|
258
|
+
[actionError, configError].filter(Boolean),
|
|
259
|
+
`Codex ${action} registration rollback failed`,
|
|
260
|
+
);
|
|
191
261
|
}
|
|
192
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
...result,
|
|
264
|
+
restored: {
|
|
265
|
+
installed: snapshot.installed,
|
|
266
|
+
enabled: snapshot.enabled,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function inspectCodexHost(ctx = {}, runtimeManifest = null, expectedPluginVersion = null) {
|
|
272
|
+
const payload = JSON.parse(runCodexCli(['plugin', 'list', '--json'], ctx));
|
|
273
|
+
if (!payload || !Array.isArray(payload.installed)) {
|
|
274
|
+
throw new Error('codex plugin list --json did not return an installed array');
|
|
275
|
+
}
|
|
276
|
+
const marketplaceName = runtimeManifest?.marketplace?.name || 'personal';
|
|
277
|
+
const pluginId = `${PLUGIN_NAME}@${marketplaceName}`;
|
|
278
|
+
const plugin = payload.installed.find((entry) => (
|
|
279
|
+
entry?.pluginId === pluginId
|
|
280
|
+
|| (entry?.name === PLUGIN_NAME && entry?.marketplaceName === marketplaceName)
|
|
281
|
+
));
|
|
282
|
+
const pluginOk = !!plugin
|
|
283
|
+
&& plugin.installed === true
|
|
284
|
+
&& plugin.enabled === true
|
|
285
|
+
&& (!(expectedPluginVersion || runtimeManifest?.plugin?.version)
|
|
286
|
+
|| plugin.version === (expectedPluginVersion || runtimeManifest.plugin.version));
|
|
287
|
+
const mcpPayload = JSON.parse(runCodexCli(['mcp', 'list', '--json'], ctx));
|
|
288
|
+
if (!Array.isArray(mcpPayload)) {
|
|
289
|
+
throw new Error('codex mcp list --json did not return an array');
|
|
290
|
+
}
|
|
291
|
+
const expected = JSON.parse(
|
|
292
|
+
fs.readFileSync(path.join(resolvePluginRoot(ctx), '.mcp.json'), 'utf8'),
|
|
293
|
+
).mcpServers?.[MCP_SERVER_NAME];
|
|
294
|
+
const mcp = mcpPayload.find((entry) => entry?.name === MCP_SERVER_NAME);
|
|
295
|
+
const mcpOk = !!mcp
|
|
296
|
+
&& mcp.enabled === true
|
|
297
|
+
&& mcp.disabled_reason === null
|
|
298
|
+
&& mcp.transport?.type === 'stdio'
|
|
299
|
+
&& mcp.transport.command === expected?.command
|
|
300
|
+
&& JSON.stringify(mcp.transport.args) === JSON.stringify(expected?.args);
|
|
301
|
+
return { pluginId, plugin, pluginOk, mcp, mcpOk };
|
|
193
302
|
}
|
|
194
303
|
|
|
195
304
|
function pluginCacheRoot(configDir, marketplaceName) {
|
|
@@ -211,14 +320,18 @@ function normalizeHookCacheVersions(versions) {
|
|
|
211
320
|
return [...normalized].sort();
|
|
212
321
|
}
|
|
213
322
|
|
|
214
|
-
function
|
|
323
|
+
function listHookCacheVersionsOnDisk(configDir, marketplaceName) {
|
|
215
324
|
const root = pluginCacheRoot(configDir, marketplaceName);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
325
|
+
if (!fs.existsSync(root)) return [];
|
|
326
|
+
return normalizeHookCacheVersions(
|
|
327
|
+
fs.readdirSync(root, { withFileTypes: true })
|
|
219
328
|
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
220
|
-
.map((entry) => entry.name)
|
|
221
|
-
|
|
329
|
+
.map((entry) => entry.name),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function listKnownHookCacheVersions(configDir, marketplaceName, manifest = null) {
|
|
334
|
+
const versions = listHookCacheVersionsOnDisk(configDir, marketplaceName);
|
|
222
335
|
|
|
223
336
|
if (manifest && manifest.source === SOURCE_TAG && manifest.adapter === 'codex') {
|
|
224
337
|
if (manifest.plugin && manifest.plugin.version !== undefined) {
|
|
@@ -234,6 +347,94 @@ function listKnownHookCacheVersions(configDir, marketplaceName, manifest = null)
|
|
|
234
347
|
return normalizeHookCacheVersions(versions);
|
|
235
348
|
}
|
|
236
349
|
|
|
350
|
+
function readPluginManifest(pluginRoot) {
|
|
351
|
+
const file = path.join(pluginRoot, '.codex-plugin', 'plugin.json');
|
|
352
|
+
const contents = fs.readFileSync(file);
|
|
353
|
+
const manifest = JSON.parse(contents.toString('utf8'));
|
|
354
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)
|
|
355
|
+
|| manifest.name !== PLUGIN_NAME) {
|
|
356
|
+
throw new Error(`invalid Codex plugin manifest: ${file}`);
|
|
357
|
+
}
|
|
358
|
+
normalizeHookCacheVersions([manifest.version]);
|
|
359
|
+
return { file, contents, manifest };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function codexVersionParts(version) {
|
|
363
|
+
if (typeof version !== 'string') return null;
|
|
364
|
+
const match = /^(.+)\+codex\.([A-Za-z0-9][A-Za-z0-9._-]*)$/.exec(version);
|
|
365
|
+
return match ? { base: match[1], cachebuster: match[2] } : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function bindCachebusterProvenance({
|
|
369
|
+
report,
|
|
370
|
+
runtimeManifest,
|
|
371
|
+
sourceManifest,
|
|
372
|
+
host,
|
|
373
|
+
configDir,
|
|
374
|
+
marketplaceName,
|
|
375
|
+
}) {
|
|
376
|
+
const previousVersion = runtimeManifest?.plugin?.version;
|
|
377
|
+
const currentVersion = sourceManifest?.manifest?.version;
|
|
378
|
+
const previous = codexVersionParts(previousVersion);
|
|
379
|
+
const current = codexVersionParts(currentVersion);
|
|
380
|
+
if (!previous || !current || previous.base !== current.base
|
|
381
|
+
|| previousVersion === currentVersion) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const mismatch = report.issues.filter((issue) => (
|
|
386
|
+
issue.code === 'ASSET_HASH_MISMATCH'
|
|
387
|
+
&& issue.root === 'plugin'
|
|
388
|
+
&& issue.path === path.join('.codex-plugin', 'plugin.json')
|
|
389
|
+
));
|
|
390
|
+
if (mismatch.length !== 1
|
|
391
|
+
|| !host?.pluginOk
|
|
392
|
+
|| host.plugin?.version !== currentVersion) {
|
|
393
|
+
report.checks.cachebuster_binding = { status: 'fail' };
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const originalManifest = {
|
|
398
|
+
...sourceManifest.manifest,
|
|
399
|
+
version: previousVersion,
|
|
400
|
+
};
|
|
401
|
+
const originalHash = crypto.createHash('sha256')
|
|
402
|
+
.update(`${JSON.stringify(originalManifest, null, 2)}\n`)
|
|
403
|
+
.digest('hex');
|
|
404
|
+
const cacheManifestFile = path.join(
|
|
405
|
+
pluginCacheRoot(configDir, marketplaceName),
|
|
406
|
+
currentVersion,
|
|
407
|
+
'.codex-plugin',
|
|
408
|
+
'plugin.json',
|
|
409
|
+
);
|
|
410
|
+
let cacheMatchesSource = false;
|
|
411
|
+
try {
|
|
412
|
+
cacheMatchesSource = crypto.createHash('sha256')
|
|
413
|
+
.update(fs.readFileSync(cacheManifestFile))
|
|
414
|
+
.digest('hex')
|
|
415
|
+
=== crypto.createHash('sha256')
|
|
416
|
+
.update(sourceManifest.contents)
|
|
417
|
+
.digest('hex');
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error.code !== 'ENOENT') throw error;
|
|
420
|
+
}
|
|
421
|
+
if (originalHash !== mismatch[0].expected || !cacheMatchesSource) {
|
|
422
|
+
report.checks.cachebuster_binding = { status: 'fail' };
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
report.issues = report.issues.filter((issue) => issue !== mismatch[0]);
|
|
427
|
+
report.checks.assets.failed -= 1;
|
|
428
|
+
if (report.checks.assets.failed === 0) report.checks.assets.status = 'pass';
|
|
429
|
+
report.checks.cachebuster_binding = {
|
|
430
|
+
status: 'pass',
|
|
431
|
+
previous_version: previousVersion,
|
|
432
|
+
current_version: currentVersion,
|
|
433
|
+
cache_manifest: cacheManifestFile,
|
|
434
|
+
};
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
237
438
|
function hookAdaptersForVersions(configDir, marketplaceName, versions) {
|
|
238
439
|
const root = pluginCacheRoot(configDir, marketplaceName);
|
|
239
440
|
return normalizeHookCacheVersions(versions)
|
|
@@ -379,6 +580,8 @@ function install(ctx = {}) {
|
|
|
379
580
|
let movedPrevious = false;
|
|
380
581
|
let published = false;
|
|
381
582
|
let registration = null;
|
|
583
|
+
let registrationSnapshot = null;
|
|
584
|
+
let registrationAttempted = false;
|
|
382
585
|
let hookCompatibility = null;
|
|
383
586
|
let legacy = { configUpdated: false, removed: [] };
|
|
384
587
|
try {
|
|
@@ -418,7 +621,16 @@ function install(ctx = {}) {
|
|
|
418
621
|
writeAtomic(marketplaceFile, JSON.stringify(marketplacePayload, null, 2) + '\n');
|
|
419
622
|
|
|
420
623
|
if (shouldRunPluginCli(ctx)) {
|
|
624
|
+
registrationSnapshot = snapshotPluginRegistration(marketplace.name, ctx);
|
|
625
|
+
registrationAttempted = true;
|
|
421
626
|
registration = runPluginCli('add', marketplace.name, ctx);
|
|
627
|
+
if (typeof ctx.afterPluginRegistration === 'function') {
|
|
628
|
+
ctx.afterPluginRegistration({
|
|
629
|
+
action: 'add',
|
|
630
|
+
before: registrationSnapshot,
|
|
631
|
+
registration,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
422
634
|
hookCompatibility = restoreCachedHookAdapters(
|
|
423
635
|
previousAdapters,
|
|
424
636
|
currentHookAdapter(configDir, marketplace.name, plugin),
|
|
@@ -480,17 +692,29 @@ function install(ctx = {}) {
|
|
|
480
692
|
if (published && fs.existsSync(pluginRoot)) removeTree(pluginRoot);
|
|
481
693
|
else if (fs.existsSync(stagingRoot)) removeTree(stagingRoot);
|
|
482
694
|
if (movedPrevious && fs.existsSync(backupRoot)) fs.renameSync(backupRoot, pluginRoot);
|
|
695
|
+
let rollbackError = null;
|
|
483
696
|
try {
|
|
484
|
-
restoreFiles(snapshots);
|
|
485
697
|
const currentCache = marketplace && plugin
|
|
486
698
|
? path.join(pluginCacheRoot(configDir, marketplace.name), plugin.version)
|
|
487
699
|
: null;
|
|
488
700
|
if (currentCache && !previousVersions.includes(plugin.version) && fs.existsSync(currentCache)) {
|
|
489
701
|
removeTree(currentCache);
|
|
490
702
|
}
|
|
703
|
+
if (registrationAttempted && registrationSnapshot) {
|
|
704
|
+
restorePluginRegistration(registrationSnapshot, ctx);
|
|
705
|
+
}
|
|
706
|
+
restoreFiles(snapshots);
|
|
707
|
+
} catch (caught) {
|
|
708
|
+
rollbackError = caught;
|
|
491
709
|
} finally {
|
|
492
710
|
if (fs.existsSync(agentStagingConfig)) removeTree(agentStagingConfig);
|
|
493
711
|
}
|
|
712
|
+
if (rollbackError) {
|
|
713
|
+
throw new AggregateError(
|
|
714
|
+
[error, rollbackError],
|
|
715
|
+
'Codex install and rollback both failed',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
494
718
|
throw error;
|
|
495
719
|
}
|
|
496
720
|
}
|
|
@@ -536,12 +760,82 @@ function doctor(ctx = {}) {
|
|
|
536
760
|
}
|
|
537
761
|
report.checks.marketplace = { status: marketplaceOk ? 'pass' : 'fail' };
|
|
538
762
|
|
|
763
|
+
let sourceManifest = null;
|
|
764
|
+
try {
|
|
765
|
+
sourceManifest = readPluginManifest(pluginRoot);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
report.issues.push({ code: 'PLUGIN_MANIFEST_INVALID', message: error.message });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
let host = null;
|
|
771
|
+
if (shouldRunHostCli(ctx)) {
|
|
772
|
+
try {
|
|
773
|
+
host = inspectCodexHost(
|
|
774
|
+
ctx,
|
|
775
|
+
runtimeManifest,
|
|
776
|
+
sourceManifest?.manifest?.version || null,
|
|
777
|
+
);
|
|
778
|
+
report.checks.host_plugin = { status: host.pluginOk ? 'pass' : 'fail' };
|
|
779
|
+
report.checks.host_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
|
|
780
|
+
if (!host.pluginOk) {
|
|
781
|
+
report.issues.push({
|
|
782
|
+
code: 'HOST_PLUGIN_NOT_DISCOVERED',
|
|
783
|
+
plugin_id: host.pluginId,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (!host.mcpOk) {
|
|
787
|
+
report.issues.push({
|
|
788
|
+
code: 'HOST_MCP_NOT_DISCOVERED',
|
|
789
|
+
plugin_id: host.pluginId,
|
|
790
|
+
server: MCP_SERVER_NAME,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
} catch (error) {
|
|
794
|
+
report.checks.host_plugin = { status: 'fail' };
|
|
795
|
+
report.checks.host_mcp = { status: 'fail' };
|
|
796
|
+
report.issues.push({
|
|
797
|
+
code: error.code === 'HOST_CLI_TIMEOUT'
|
|
798
|
+
? 'HOST_CLI_TIMEOUT'
|
|
799
|
+
: 'HOST_CLI_INSPECTION_FAILED',
|
|
800
|
+
message: error.message,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const marketplaceName = runtimeManifest?.marketplace?.name || 'personal';
|
|
806
|
+
if (runtimeManifestOk && sourceManifest && host) {
|
|
807
|
+
bindCachebusterProvenance({
|
|
808
|
+
report,
|
|
809
|
+
runtimeManifest,
|
|
810
|
+
sourceManifest,
|
|
811
|
+
host,
|
|
812
|
+
configDir,
|
|
813
|
+
marketplaceName,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
539
817
|
let hookTargetsOk = runtimeManifestOk;
|
|
540
818
|
if (runtimeManifestOk) {
|
|
541
|
-
const cacheRoot = pluginCacheRoot(configDir,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
819
|
+
const cacheRoot = pluginCacheRoot(configDir, marketplaceName);
|
|
820
|
+
let requiredVersions = [];
|
|
821
|
+
try {
|
|
822
|
+
requiredVersions = listHookCacheVersionsOnDisk(configDir, marketplaceName);
|
|
823
|
+
const activeVersion = host?.plugin?.installed === true
|
|
824
|
+
? host.plugin.version
|
|
825
|
+
: (shouldRunPluginCli(ctx)
|
|
826
|
+
? sourceManifest?.manifest?.version || runtimeManifest.plugin.version
|
|
827
|
+
: null);
|
|
828
|
+
if (activeVersion) {
|
|
829
|
+
requiredVersions = normalizeHookCacheVersions([
|
|
830
|
+
...requiredVersions,
|
|
831
|
+
activeVersion,
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
} catch (error) {
|
|
835
|
+
hookTargetsOk = false;
|
|
836
|
+
report.issues.push({ code: 'HOOK_CACHE_INVALID', message: error.message });
|
|
837
|
+
}
|
|
838
|
+
for (const version of requiredVersions) {
|
|
545
839
|
const adapter = path.join(cacheRoot, version, HOOK_ADAPTER_RELATIVE);
|
|
546
840
|
if (!fs.existsSync(adapter)) {
|
|
547
841
|
hookTargetsOk = false;
|
|
@@ -586,30 +880,55 @@ function uninstall(ctx = {}) {
|
|
|
586
880
|
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
587
881
|
const manifest = readRuntimeManifest(configDir);
|
|
588
882
|
const marketplaceBefore = loadMarketplace(marketplaceFile);
|
|
883
|
+
let registration = null;
|
|
884
|
+
let registrationSnapshot = null;
|
|
885
|
+
let registrationAttempted = false;
|
|
886
|
+
try {
|
|
887
|
+
if (shouldRunPluginCli(ctx)) {
|
|
888
|
+
registrationSnapshot = snapshotPluginRegistration(marketplaceBefore.name, ctx);
|
|
889
|
+
registrationAttempted = true;
|
|
890
|
+
registration = runPluginCli('remove', marketplaceBefore.name, ctx);
|
|
891
|
+
if (typeof ctx.afterPluginRegistration === 'function') {
|
|
892
|
+
ctx.afterPluginRegistration({
|
|
893
|
+
action: 'remove',
|
|
894
|
+
before: registrationSnapshot,
|
|
895
|
+
registration,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const agents = removeManagedAgents(configDir, manifest);
|
|
589
900
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
901
|
+
let pluginRemoved = false;
|
|
902
|
+
if (fs.existsSync(pluginRoot) && fs.existsSync(path.join(pluginRoot, '.ubp-managed'))) {
|
|
903
|
+
removeTree(pluginRoot);
|
|
904
|
+
pluginRemoved = true;
|
|
905
|
+
}
|
|
906
|
+
const marketplace = removeMarketplaceEntry(marketplaceFile);
|
|
907
|
+
const legacy = cleanupLegacyConfig(configDir);
|
|
908
|
+
const manifestDir = path.join(configDir, RUNTIME_MANIFEST_DIR);
|
|
909
|
+
if (fs.existsSync(manifestDir)) removeTree(manifestDir);
|
|
594
910
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
911
|
+
return {
|
|
912
|
+
target: configDir,
|
|
913
|
+
removed: { plugin: pluginRemoved, agents },
|
|
914
|
+
marketplace,
|
|
915
|
+
registration,
|
|
916
|
+
legacy,
|
|
917
|
+
config: { updated: legacy.configUpdated },
|
|
918
|
+
};
|
|
919
|
+
} catch (error) {
|
|
920
|
+
if (registrationAttempted && registrationSnapshot) {
|
|
921
|
+
try {
|
|
922
|
+
restorePluginRegistration(registrationSnapshot, ctx);
|
|
923
|
+
} catch (rollbackError) {
|
|
924
|
+
throw new AggregateError(
|
|
925
|
+
[error, rollbackError],
|
|
926
|
+
'Codex uninstall and registration rollback both failed',
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
599
931
|
}
|
|
600
|
-
const marketplace = removeMarketplaceEntry(marketplaceFile);
|
|
601
|
-
const legacy = cleanupLegacyConfig(configDir);
|
|
602
|
-
const manifestDir = path.join(configDir, RUNTIME_MANIFEST_DIR);
|
|
603
|
-
if (fs.existsSync(manifestDir)) removeTree(manifestDir);
|
|
604
|
-
|
|
605
|
-
return {
|
|
606
|
-
target: configDir,
|
|
607
|
-
removed: { plugin: pluginRemoved, agents },
|
|
608
|
-
marketplace,
|
|
609
|
-
registration,
|
|
610
|
-
legacy,
|
|
611
|
-
config: { updated: legacy.configUpdated },
|
|
612
|
-
};
|
|
613
932
|
}
|
|
614
933
|
|
|
615
934
|
module.exports = {
|
|
@@ -633,6 +952,10 @@ module.exports = {
|
|
|
633
952
|
upsertMarketplace,
|
|
634
953
|
removeMarketplaceEntry,
|
|
635
954
|
runPluginCli,
|
|
955
|
+
runCodexCli,
|
|
956
|
+
snapshotPluginRegistration,
|
|
957
|
+
restorePluginRegistration,
|
|
958
|
+
inspectCodexHost,
|
|
636
959
|
pluginCacheRoot,
|
|
637
960
|
listKnownHookCacheVersions,
|
|
638
961
|
listCachedHookAdapters,
|