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.
Files changed (40) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/adapters/claude.js +104 -0
  3. package/adapters/codex.js +363 -40
  4. package/adapters/grok.js +1254 -58
  5. package/adapters/kimi.js +682 -13
  6. package/adapters/opencode.js +132 -11
  7. package/docs/AGENT-CONTEXT.md +14 -0
  8. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  9. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  10. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  11. package/mcp-server/lib/artifact-registry.cjs +181 -51
  12. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  13. package/mcp-server/lib/change-workflow.cjs +21 -16
  14. package/mcp-server/lib/context-envelope.cjs +124 -51
  15. package/mcp-server/lib/decision-records.cjs +164 -78
  16. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  17. package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
  18. package/mcp-server/lib/managed-file-write.cjs +56 -0
  19. package/mcp-server/lib/prd-parser.cjs +2 -2
  20. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  21. package/mcp-server/lib/state-db.cjs +186 -8
  22. package/mcp-server/lib/state-ops.cjs +214 -31
  23. package/mcp-server/lib/task-contract.cjs +404 -0
  24. package/mcp-server/lib/task-expander.cjs +2 -2
  25. package/mcp-server/lib/task-ledger.cjs +175 -62
  26. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  27. package/mcp-server/lib/worker-packet.cjs +2 -2
  28. package/mcp-server/server.cjs +23 -4
  29. package/orchestrator/parallel-orchestrator.cjs +114 -27
  30. package/orchestrator/session-runner.cjs +25 -136
  31. package/package.json +1 -1
  32. package/skills/ultra-change/SKILL.md +6 -1
  33. package/skills/ultra-dev/SKILL.md +9 -4
  34. package/skills/ultra-plan/SKILL.md +13 -4
  35. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  36. package/spec/mcp-tools.yaml +114 -9
  37. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  38. package/spec/schemas/state-db.sql +10 -6
  39. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  40. 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
@@ -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
- const result = spawnSync(codexBin, args, { encoding: 'utf8' });
187
- if (result.error) throw result.error;
188
- if (result.status !== 0) {
189
- const detail = (result.stderr || result.stdout || '').trim();
190
- throw new Error(`codex plugin ${action} failed${detail ? `: ${detail}` : ''}`);
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 { selector, stdout: result.stdout.trim() };
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 listKnownHookCacheVersions(configDir, marketplaceName, manifest = null) {
323
+ function listHookCacheVersionsOnDisk(configDir, marketplaceName) {
215
324
  const root = pluginCacheRoot(configDir, marketplaceName);
216
- const versions = [];
217
- if (fs.existsSync(root)) {
218
- versions.push(...fs.readdirSync(root, { withFileTypes: true })
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, runtimeManifest.marketplace?.name || 'personal');
542
- const currentCacheRequired = shouldRunPluginCli(ctx);
543
- for (const version of runtimeManifest.hook_cache_versions || []) {
544
- if (version === runtimeManifest.plugin.version && !currentCacheRequired) continue;
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
- const registration = shouldRunPluginCli(ctx)
591
- ? runPluginCli('remove', marketplaceBefore.name, ctx)
592
- : null;
593
- const agents = removeManagedAgents(configDir, manifest);
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
- let pluginRemoved = false;
596
- if (fs.existsSync(pluginRoot) && fs.existsSync(path.join(pluginRoot, '.ubp-managed'))) {
597
- removeTree(pluginRoot);
598
- pluginRemoved = true;
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,