ultra-builder-pro-cli 0.23.0 → 0.24.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.
Files changed (95) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/.ultra-template/tasks/tasks.json +4 -2
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +55 -39
  5. package/adapters/_shared/codex-assets.cjs +121 -65
  6. package/adapters/_shared/interaction-contract.cjs +7 -1
  7. package/adapters/_shared/provenance.cjs +7 -2
  8. package/adapters/_shared/runtime-assets.cjs +3 -2
  9. package/adapters/_shared/runtime-native.cjs +132 -0
  10. package/adapters/claude.js +60 -30
  11. package/adapters/codex.js +178 -81
  12. package/adapters/grok.js +372 -0
  13. package/adapters/kimi.js +7 -3
  14. package/adapters/opencode.js +210 -32
  15. package/agents/code-reviewer.md +4 -2
  16. package/agents/debugger.md +4 -1
  17. package/agents/review-code.md +7 -3
  18. package/agents/review-comments.md +7 -4
  19. package/agents/review-coordinator.md +11 -7
  20. package/agents/review-design.md +8 -4
  21. package/agents/review-errors.md +8 -4
  22. package/agents/review-spec.md +8 -5
  23. package/agents/review-tests.md +7 -4
  24. package/agents/tdd-runner.md +5 -2
  25. package/bin/install.js +9 -4
  26. package/bin/orchestrator.js +4 -13
  27. package/docs/AGENT-CONTEXT.md +143 -229
  28. package/docs/ARCHITECTURE.md +205 -494
  29. package/docs/ARTIFACT-AUTHORITY.md +35 -28
  30. package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
  31. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  32. package/docs/ROADMAP.md +85 -110
  33. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  34. package/docs/SKILL-AUTHORING.md +3 -3
  35. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  36. package/docs/WORKFLOW-LIFECYCLE.md +189 -448
  37. package/hooks/README.md +11 -8
  38. package/hooks/active_task_context.py +16 -16
  39. package/hooks/adapters/grok.py +134 -0
  40. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  41. package/hooks/health_check.py +29 -34
  42. package/hooks/pre_stop_check.py +22 -12
  43. package/hooks/subagent_tracker.py +1 -1
  44. package/hooks/workflow_checkpoint.py +14 -13
  45. package/hooks/workflow_context.py +11 -12
  46. package/hooks/workflow_resume.py +14 -13
  47. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  48. package/mcp-server/lib/baseline-workflow.cjs +108 -82
  49. package/mcp-server/lib/canonical-json.cjs +19 -0
  50. package/mcp-server/lib/change-authority.cjs +26 -0
  51. package/mcp-server/lib/change-workflow.cjs +630 -45
  52. package/mcp-server/lib/context-envelope.cjs +508 -0
  53. package/mcp-server/lib/decision-records.cjs +337 -0
  54. package/mcp-server/lib/doctor.cjs +91 -33
  55. package/mcp-server/lib/init-project.cjs +78 -39
  56. package/mcp-server/lib/managed-file-write.cjs +116 -0
  57. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  58. package/mcp-server/lib/project-context.cjs +178 -0
  59. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  60. package/mcp-server/lib/state-db.cjs +155 -24
  61. package/mcp-server/lib/state-ops.cjs +21 -3
  62. package/mcp-server/lib/task-ledger.cjs +522 -13
  63. package/mcp-server/lib/ultra-facade.cjs +757 -442
  64. package/mcp-server/lib/worker-packet.cjs +358 -0
  65. package/mcp-server/server.cjs +200 -1103
  66. package/orchestrator/daemon.cjs +27 -9
  67. package/orchestrator/parallel-orchestrator.cjs +111 -76
  68. package/orchestrator/session-runner.cjs +135 -34
  69. package/package.json +10 -3
  70. package/skills/ultra-change/SKILL.md +10 -8
  71. package/skills/ultra-deliver/SKILL.md +17 -19
  72. package/skills/ultra-dev/SKILL.md +20 -11
  73. package/skills/ultra-doctor/SKILL.md +4 -4
  74. package/skills/ultra-init/SKILL.md +6 -5
  75. package/skills/ultra-init/references/brownfield-adoption.md +20 -14
  76. package/skills/ultra-plan/SKILL.md +13 -9
  77. package/skills/ultra-research/SKILL.md +18 -9
  78. package/skills/ultra-review/SKILL.md +14 -9
  79. package/skills/ultra-review/references/unified-schema.md +2 -0
  80. package/skills/ultra-review/scripts/review_wait.py +8 -0
  81. package/skills/ultra-status/SKILL.md +2 -2
  82. package/skills/ultra-test/SKILL.md +10 -6
  83. package/skills/ultra-think/SKILL.md +8 -7
  84. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  85. package/spec/cli-protocol.md +80 -199
  86. package/spec/mcp-tools.yaml +43 -2806
  87. package/spec/schemas/state-db.sql +107 -4
  88. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  89. package/ultra-tools/commands/status.cjs +369 -243
  90. package/mcp-server/lib/context-spine.cjs +0 -1208
  91. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  92. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  93. package/mcp-server/lib/spec-learning.cjs +0 -284
  94. package/mcp-server/lib/workflow-state.cjs +0 -3129
  95. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -14,7 +14,11 @@ const {
14
14
  removeTree,
15
15
  writeAtomic,
16
16
  } = require('./_shared/file-ops.cjs');
17
- const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
17
+ const {
18
+ applyNativeDoctor,
19
+ buildMcpRuntime,
20
+ mcpCommand,
21
+ } = require('./_shared/codex-assets.cjs');
18
22
  const { parse: parseFrontmatter, serialize: serializeFrontmatter } = require('./_shared/frontmatter.cjs');
19
23
  const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
20
24
  const provenance = require('./_shared/provenance.cjs');
@@ -139,83 +143,109 @@ function buildHooksManifest() {
139
143
  };
140
144
  }
141
145
 
142
- function install(ctx = {}) {
143
- const target = resolvePluginRoot(ctx);
144
- const repoRoot = resolveRepoRoot(ctx);
145
- if (fs.existsSync(target)) {
146
- const entries = fs.readdirSync(target);
147
- if (entries.length > 0 && !isManaged(target)) {
148
- throw new Error(`refusing to replace unmanaged Claude plugin: ${target}`);
149
- }
150
- if (entries.length > 0) removeTree(target);
151
- }
152
- ensureDir(target);
153
-
146
+ function buildStaging(repoRoot, staging, target) {
154
147
  const report = { target, copied: {}, config: { updated: false } };
155
- report.copied.commands = copyCommands(repoRoot, target);
156
- report.copied.skills = copySkills(repoRoot, target, skillsForRuntime('claude'));
157
- report.copied.agents = copyTree(path.join(repoRoot, 'agents'), path.join(target, 'agents'));
148
+ report.copied.commands = copyCommands(repoRoot, staging);
149
+ report.copied.skills = copySkills(repoRoot, staging, skillsForRuntime('claude'));
150
+ report.copied.agents = copyTree(path.join(repoRoot, 'agents'), path.join(staging, 'agents'));
158
151
 
159
- ensureDir(path.join(target, 'hooks'));
152
+ ensureDir(path.join(staging, 'hooks'));
160
153
  report.copied.hooks = [];
161
154
  for (const name of WORKFLOW_HOOK_FILES) {
162
- fs.copyFileSync(path.join(repoRoot, 'hooks', name), path.join(target, 'hooks', name));
155
+ fs.copyFileSync(path.join(repoRoot, 'hooks', name), path.join(staging, 'hooks', name));
163
156
  report.copied.hooks.push(name);
164
157
  }
165
- writeAtomic(path.join(target, 'hooks', 'hooks.json'), JSON.stringify(buildHooksManifest(), null, 2) + '\n');
158
+ writeAtomic(path.join(staging, 'hooks', 'hooks.json'), JSON.stringify(buildHooksManifest(), null, 2) + '\n');
166
159
 
167
- const runtime = buildMcpRuntime(repoRoot, target, { runtime: 'claude' });
168
- writeAtomic(path.join(target, '.mcp.json'), JSON.stringify({
160
+ buildMcpRuntime(repoRoot, staging, { runtime: 'claude' });
161
+ const command = mcpCommand(path.join(target, 'runtime', 'launch.cjs'));
162
+ writeAtomic(path.join(staging, '.mcp.json'), JSON.stringify({
169
163
  mcpServers: {
170
164
  [MCP_SERVER_NAME]: {
171
- command: process.execPath,
172
- args: [runtime.launcher],
165
+ command: command.command,
166
+ args: command.args,
173
167
  },
174
168
  },
175
169
  }, null, 2) + '\n');
176
170
 
177
171
  const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
178
- writeAtomic(path.join(target, '.claude-plugin', 'plugin.json'), JSON.stringify({
172
+ writeAtomic(path.join(staging, '.claude-plugin', 'plugin.json'), JSON.stringify({
179
173
  name: PLUGIN_NAME,
180
174
  version: pkg.version,
181
- description: 'Claude Code native Ultra Builder Pro workflows, agents, hooks, and MCP task state.',
175
+ description: 'Claude Code native Ultra Builder Pro workflows with a persistent safety kernel.',
182
176
  author: { name: typeof pkg.author === 'string' ? pkg.author : 'Ultra Builder Pro contributors' },
183
177
  repository: pkg.homepage,
184
178
  license: pkg.license || 'MIT',
185
179
  }, null, 2) + '\n');
186
- markManaged(target, { adapter: 'claude', plugin: PLUGIN_NAME });
180
+ markManaged(staging, { adapter: 'claude', plugin: PLUGIN_NAME });
187
181
  const source = provenance.packageSource(repoRoot);
188
- const provenanceFile = path.join(target, PROVENANCE_FILE);
182
+ const provenanceFile = path.join(staging, PROVENANCE_FILE);
189
183
  report.provenance = provenance.writeProvenance({
190
184
  file: provenanceFile,
191
185
  adapter: 'claude',
192
186
  ...source,
193
187
  roots: { plugin: target },
194
- assets: provenance.assetRefsForTree('plugin', target, {
188
+ assetRoots: { plugin: staging },
189
+ assets: provenance.assetRefsForTree('plugin', staging, {
195
190
  exclude: ['.ubp-managed', PROVENANCE_FILE],
196
191
  }),
197
192
  contracts: {
198
193
  plugin_manifest: { root: 'plugin', path: '.claude-plugin/plugin.json' },
199
194
  mcp_registration: { root: 'plugin', path: '.mcp.json' },
200
195
  mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
196
+ native_runtime: { root: 'plugin', path: 'runtime/native-runtime.json' },
197
+ context_envelope_helper: { root: 'plugin', path: 'runtime/hook-context.cjs' },
201
198
  hook_event_helper: { root: 'plugin', path: 'runtime/hook-event.cjs' },
202
199
  hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
203
200
  checkpoint_hook: { root: 'plugin', path: 'hooks/workflow_checkpoint.py' },
204
201
  resume_hook: { root: 'plugin', path: 'hooks/workflow_resume.py' },
205
202
  },
206
203
  });
207
- report.provenance.file = provenanceFile;
204
+ report.provenance.file = path.join(target, PROVENANCE_FILE);
208
205
  return report;
209
206
  }
210
207
 
208
+ function install(ctx = {}) {
209
+ const target = resolvePluginRoot(ctx);
210
+ const repoRoot = resolveRepoRoot(ctx);
211
+ if (fs.existsSync(target)) {
212
+ const entries = fs.readdirSync(target);
213
+ if (entries.length > 0 && !isManaged(target)) {
214
+ throw new Error(`refusing to replace unmanaged Claude plugin: ${target}`);
215
+ }
216
+ }
217
+ ensureDir(path.dirname(target));
218
+ const staging = fs.mkdtempSync(path.join(path.dirname(target), `.${PLUGIN_NAME}-staging-`));
219
+ const backup = `${staging}-previous`;
220
+ let movedPrevious = false;
221
+ let published = false;
222
+ try {
223
+ const report = buildStaging(repoRoot, staging, target);
224
+ if (fs.existsSync(target)) {
225
+ fs.renameSync(target, backup);
226
+ movedPrevious = true;
227
+ }
228
+ fs.renameSync(staging, target);
229
+ published = true;
230
+ if (movedPrevious) removeTree(backup);
231
+ return report;
232
+ } catch (error) {
233
+ if (published && fs.existsSync(target)) removeTree(target);
234
+ else if (fs.existsSync(staging)) removeTree(staging);
235
+ if (movedPrevious && fs.existsSync(backup)) fs.renameSync(backup, target);
236
+ throw error;
237
+ }
238
+ }
239
+
211
240
  function doctor(ctx = {}) {
212
241
  const repoRoot = resolveRepoRoot(ctx);
213
242
  const source = provenance.packageSource(repoRoot);
214
- return provenance.inspectProvenance({
243
+ const report = provenance.inspectProvenance({
215
244
  file: path.join(resolvePluginRoot(ctx), PROVENANCE_FILE),
216
245
  expectedAdapter: 'claude',
217
246
  expectedPackageVersion: source.packageInfo.version,
218
247
  });
248
+ return applyNativeDoctor(report, path.join(resolvePluginRoot(ctx), 'runtime'));
219
249
  }
220
250
 
221
251
  function uninstall(ctx = {}) {
package/adapters/codex.js CHANGED
@@ -30,6 +30,7 @@ const {
30
30
  const {
31
31
  PLUGIN_NAME,
32
32
  MANAGED_MARKER,
33
+ applyNativeDoctor,
33
34
  buildPlugin,
34
35
  installAgents,
35
36
  } = require('./_shared/codex-assets.cjs');
@@ -145,13 +146,18 @@ function marketplaceEntry() {
145
146
  }
146
147
 
147
148
  function upsertMarketplace(file) {
149
+ const payload = mergedMarketplace(file);
150
+ writeAtomic(file, JSON.stringify(payload, null, 2) + '\n');
151
+ return { file, name: payload.name, entry: marketplaceEntry() };
152
+ }
153
+
154
+ function mergedMarketplace(file) {
148
155
  const payload = loadMarketplace(file);
149
156
  const entry = marketplaceEntry();
150
157
  const index = payload.plugins.findIndex((plugin) => plugin && plugin.name === PLUGIN_NAME);
151
158
  if (index === -1) payload.plugins.push(entry);
152
159
  else payload.plugins[index] = entry;
153
- writeAtomic(file, JSON.stringify(payload, null, 2) + '\n');
154
- return { file, name: payload.name, entry };
160
+ return payload;
155
161
  }
156
162
 
157
163
  function removeMarketplaceEntry(file) {
@@ -300,6 +306,54 @@ function writeRuntimeManifest(configDir, data) {
300
306
  return file;
301
307
  }
302
308
 
309
+ function captureFiles(files) {
310
+ return [...new Set(files)].map((file) => {
311
+ try {
312
+ const stat = fs.statSync(file);
313
+ return { file, exists: true, contents: fs.readFileSync(file), mode: stat.mode & 0o7777 };
314
+ } catch (error) {
315
+ if (error.code === 'ENOENT') return { file, exists: false };
316
+ throw error;
317
+ }
318
+ });
319
+ }
320
+
321
+ function restoreFiles(snapshots) {
322
+ for (const snapshot of snapshots) {
323
+ if (!snapshot.exists) {
324
+ try {
325
+ fs.unlinkSync(snapshot.file);
326
+ } catch (error) {
327
+ if (error.code !== 'ENOENT') throw error;
328
+ }
329
+ continue;
330
+ }
331
+ writeAtomic(snapshot.file, snapshot.contents);
332
+ fs.chmodSync(snapshot.file, snapshot.mode);
333
+ }
334
+ }
335
+
336
+ function publishStagedAgents(staged, configDir) {
337
+ const targetRoot = path.join(configDir, 'agents');
338
+ ensureDir(targetRoot);
339
+ for (const file of staged.installed) {
340
+ const target = path.join(targetRoot, file);
341
+ if (fs.existsSync(target)) {
342
+ const existing = fs.readFileSync(target, 'utf8');
343
+ if (!existing.startsWith(`# ${MANAGED_MARKER}`)) {
344
+ throw new Error(`refusing to overwrite unmanaged Codex agent: ${target}`);
345
+ }
346
+ }
347
+ }
348
+ for (const file of staged.installed) {
349
+ writeAtomic(
350
+ path.join(targetRoot, file),
351
+ fs.readFileSync(path.join(staged.root, file)),
352
+ );
353
+ }
354
+ return { root: targetRoot, installed: staged.installed };
355
+ }
356
+
303
357
  function install(ctx = {}) {
304
358
  const configDir = resolveTarget(ctx);
305
359
  const repoRoot = resolveRepoRoot(ctx);
@@ -308,93 +362,137 @@ function install(ctx = {}) {
308
362
  ensureDir(configDir);
309
363
 
310
364
  const previousManifest = readRuntimeManifest(configDir);
311
- const legacy = cleanupLegacyConfig(configDir);
312
- const plugin = buildPlugin({ repoRoot, pluginRoot });
313
- const agents = installAgents({ repoRoot, configDir });
314
- const marketplace = upsertMarketplace(marketplaceFile);
315
- const previousVersions = listKnownHookCacheVersions(
316
- configDir,
317
- marketplace.name,
318
- previousManifest,
319
- );
320
-
365
+ if (fs.existsSync(pluginRoot) && !isManaged(pluginRoot)) {
366
+ throw new Error(`refusing to replace unmanaged plugin directory: ${pluginRoot}`);
367
+ }
368
+ ensureDir(path.dirname(pluginRoot));
369
+ const stagingRoot = fs.mkdtempSync(path.join(path.dirname(pluginRoot), `.${PLUGIN_NAME}-staging-`));
370
+ const backupRoot = `${stagingRoot}-previous`;
371
+ const agentStagingConfig = fs.mkdtempSync(path.join(configDir, `.${PLUGIN_NAME}-agents-`));
372
+ let plugin = null;
373
+ let stagedAgents = null;
374
+ let agents = null;
375
+ let marketplace = null;
376
+ let previousVersions = [];
377
+ let previousAdapters = [];
378
+ let snapshots = [];
379
+ let movedPrevious = false;
380
+ let published = false;
321
381
  let registration = null;
322
382
  let hookCompatibility = null;
323
- if (shouldRunPluginCli(ctx)) {
324
- const previousAdapters = hookAdaptersForVersions(
383
+ let legacy = { configUpdated: false, removed: [] };
384
+ try {
385
+ plugin = buildPlugin({ repoRoot, pluginRoot: stagingRoot, publishedRoot: pluginRoot });
386
+ stagedAgents = installAgents({ repoRoot, configDir: agentStagingConfig });
387
+ const marketplacePayload = mergedMarketplace(marketplaceFile);
388
+ marketplace = {
389
+ file: marketplaceFile,
390
+ name: marketplacePayload.name,
391
+ entry: marketplaceEntry(),
392
+ };
393
+ previousVersions = listKnownHookCacheVersions(
394
+ configDir,
395
+ marketplace.name,
396
+ previousManifest,
397
+ );
398
+ previousAdapters = hookAdaptersForVersions(
325
399
  configDir,
326
400
  marketplace.name,
327
401
  previousVersions,
328
402
  );
329
- try {
403
+ snapshots = captureFiles([
404
+ marketplaceFile,
405
+ runtimeManifestFile(configDir),
406
+ path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE),
407
+ ...stagedAgents.installed.map((file) => path.join(configDir, 'agents', file)),
408
+ ...previousAdapters,
409
+ ]);
410
+
411
+ if (fs.existsSync(pluginRoot)) {
412
+ fs.renameSync(pluginRoot, backupRoot);
413
+ movedPrevious = true;
414
+ }
415
+ fs.renameSync(stagingRoot, pluginRoot);
416
+ published = true;
417
+ agents = publishStagedAgents(stagedAgents, configDir);
418
+ writeAtomic(marketplaceFile, JSON.stringify(marketplacePayload, null, 2) + '\n');
419
+
420
+ if (shouldRunPluginCli(ctx)) {
330
421
  registration = runPluginCli('add', marketplace.name, ctx);
331
- } catch (registrationError) {
332
- try {
333
- hookCompatibility = restoreCachedHookAdapters(
334
- previousAdapters,
335
- currentHookAdapter(configDir, marketplace.name, plugin),
336
- );
337
- } catch (recoveryError) {
338
- throw new AggregateError(
339
- [registrationError, recoveryError],
340
- `codex plugin add and hook cache recovery both failed`,
341
- );
422
+ hookCompatibility = restoreCachedHookAdapters(
423
+ previousAdapters,
424
+ currentHookAdapter(configDir, marketplace.name, plugin),
425
+ );
426
+ }
427
+ const hookCacheVersions = normalizeHookCacheVersions([
428
+ ...previousVersions,
429
+ plugin.version,
430
+ ]);
431
+ const manifestFile = writeRuntimeManifest(configDir, {
432
+ plugin: { root: plugin.root, version: plugin.version },
433
+ marketplace: { file: marketplace.file, name: marketplace.name },
434
+ agents: agents.installed,
435
+ hookCacheVersions,
436
+ });
437
+ const source = provenance.packageSource(repoRoot);
438
+ const provenanceFile = path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE);
439
+ const provenanceManifest = provenance.writeProvenance({
440
+ file: provenanceFile,
441
+ adapter: 'codex',
442
+ ...source,
443
+ roots: { plugin: pluginRoot, config: configDir },
444
+ assets: [
445
+ ...provenance.assetRefsForTree('plugin', pluginRoot, {
446
+ exclude: ['.ubp-managed'],
447
+ }),
448
+ ...agents.installed.map((file) => ({ root: 'config', path: path.join('agents', file) })),
449
+ { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
450
+ ],
451
+ contracts: {
452
+ plugin_manifest: { root: 'plugin', path: '.codex-plugin/plugin.json' },
453
+ mcp_registration: { root: 'plugin', path: '.mcp.json' },
454
+ mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
455
+ native_runtime: { root: 'plugin', path: 'runtime/native-runtime.json' },
456
+ context_envelope_helper: { root: 'plugin', path: 'runtime/hook-context.cjs' },
457
+ hook_event_helper: { root: 'plugin', path: 'runtime/hook-event.cjs' },
458
+ hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
459
+ hook_adapter: { root: 'plugin', path: HOOK_ADAPTER_RELATIVE },
460
+ runtime_manifest: { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
461
+ },
462
+ });
463
+ provenanceManifest.file = provenanceFile;
464
+ legacy = cleanupLegacyConfig(configDir);
465
+ if (movedPrevious) removeTree(backupRoot);
466
+ removeTree(agentStagingConfig);
467
+ return {
468
+ target: configDir,
469
+ plugin,
470
+ agents,
471
+ marketplace,
472
+ registration,
473
+ hookCompatibility,
474
+ legacy,
475
+ manifestFile,
476
+ provenance: provenanceManifest,
477
+ config: { updated: legacy.configUpdated },
478
+ };
479
+ } catch (error) {
480
+ if (published && fs.existsSync(pluginRoot)) removeTree(pluginRoot);
481
+ else if (fs.existsSync(stagingRoot)) removeTree(stagingRoot);
482
+ if (movedPrevious && fs.existsSync(backupRoot)) fs.renameSync(backupRoot, pluginRoot);
483
+ try {
484
+ restoreFiles(snapshots);
485
+ const currentCache = marketplace && plugin
486
+ ? path.join(pluginCacheRoot(configDir, marketplace.name), plugin.version)
487
+ : null;
488
+ if (currentCache && !previousVersions.includes(plugin.version) && fs.existsSync(currentCache)) {
489
+ removeTree(currentCache);
342
490
  }
343
- throw registrationError;
491
+ } finally {
492
+ if (fs.existsSync(agentStagingConfig)) removeTree(agentStagingConfig);
344
493
  }
345
- hookCompatibility = restoreCachedHookAdapters(
346
- previousAdapters,
347
- currentHookAdapter(configDir, marketplace.name, plugin),
348
- );
494
+ throw error;
349
495
  }
350
- const hookCacheVersions = normalizeHookCacheVersions([
351
- ...previousVersions,
352
- plugin.version,
353
- ]);
354
- const manifestFile = writeRuntimeManifest(configDir, {
355
- plugin: { root: plugin.root, version: plugin.version },
356
- marketplace: { file: marketplace.file, name: marketplace.name },
357
- agents: agents.installed,
358
- hookCacheVersions,
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
- hook_event_helper: { root: 'plugin', path: 'runtime/hook-event.cjs' },
379
- hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
380
- hook_adapter: { root: 'plugin', path: HOOK_ADAPTER_RELATIVE },
381
- runtime_manifest: { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
382
- },
383
- });
384
- provenanceManifest.file = provenanceFile;
385
-
386
- return {
387
- target: configDir,
388
- plugin,
389
- agents,
390
- marketplace,
391
- registration,
392
- hookCompatibility,
393
- legacy,
394
- manifestFile,
395
- provenance: provenanceManifest,
396
- config: { updated: legacy.configUpdated },
397
- };
398
496
  }
399
497
 
400
498
  function doctor(ctx = {}) {
@@ -452,8 +550,7 @@ function doctor(ctx = {}) {
452
550
  }
453
551
  }
454
552
  report.checks.hook_targets = { status: hookTargetsOk ? 'pass' : 'fail' };
455
- if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
456
- return report;
553
+ return applyNativeDoctor(report, path.join(pluginRoot, 'runtime'));
457
554
  }
458
555
 
459
556
  function removeManagedAgents(configDir, manifest) {