ultra-builder-pro-cli 0.22.0 → 0.24.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 (93) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +68 -35
  4. package/adapters/_shared/codex-assets.cjs +121 -60
  5. package/adapters/_shared/interaction-contract.cjs +20 -7
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +4 -3
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +61 -31
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +8 -4
  13. package/adapters/opencode.js +211 -33
  14. package/agents/code-reviewer.md +4 -2
  15. package/agents/debugger.md +4 -1
  16. package/agents/review-code.md +7 -3
  17. package/agents/review-comments.md +7 -4
  18. package/agents/review-coordinator.md +11 -7
  19. package/agents/review-design.md +8 -4
  20. package/agents/review-errors.md +8 -4
  21. package/agents/review-spec.md +8 -5
  22. package/agents/review-tests.md +7 -4
  23. package/agents/tdd-runner.md +5 -2
  24. package/bin/install.js +9 -4
  25. package/bin/orchestrator.js +4 -13
  26. package/docs/AGENT-CONTEXT.md +141 -231
  27. package/docs/ARCHITECTURE.md +203 -498
  28. package/docs/ARTIFACT-AUTHORITY.md +38 -35
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -442
  35. package/hooks/README.md +11 -8
  36. package/hooks/active_task_context.py +16 -16
  37. package/hooks/adapters/grok.py +134 -0
  38. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  39. package/hooks/health_check.py +19 -29
  40. package/hooks/pre_stop_check.py +22 -12
  41. package/hooks/subagent_tracker.py +1 -1
  42. package/hooks/workflow_checkpoint.py +14 -13
  43. package/hooks/workflow_context.py +11 -12
  44. package/hooks/workflow_resume.py +14 -13
  45. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  46. package/mcp-server/lib/artifact-registry.cjs +25 -1
  47. package/mcp-server/lib/baseline-workflow.cjs +36 -38
  48. package/mcp-server/lib/canonical-json.cjs +19 -0
  49. package/mcp-server/lib/change-workflow.cjs +637 -49
  50. package/mcp-server/lib/context-envelope.cjs +508 -0
  51. package/mcp-server/lib/decision-records.cjs +337 -0
  52. package/mcp-server/lib/doctor.cjs +91 -33
  53. package/mcp-server/lib/init-project.cjs +78 -39
  54. package/mcp-server/lib/managed-file-write.cjs +116 -0
  55. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  56. package/mcp-server/lib/project-context.cjs +178 -0
  57. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  58. package/mcp-server/lib/state-db.cjs +155 -24
  59. package/mcp-server/lib/task-ledger.cjs +522 -13
  60. package/mcp-server/lib/ultra-facade.cjs +834 -0
  61. package/mcp-server/lib/worker-packet.cjs +358 -0
  62. package/mcp-server/server.cjs +208 -1059
  63. package/orchestrator/daemon.cjs +27 -9
  64. package/orchestrator/parallel-orchestrator.cjs +111 -76
  65. package/orchestrator/session-runner.cjs +135 -34
  66. package/package.json +10 -3
  67. package/skills/ultra-change/SKILL.md +39 -98
  68. package/skills/ultra-deliver/SKILL.md +42 -65
  69. package/skills/ultra-dev/SKILL.md +52 -99
  70. package/skills/ultra-doctor/SKILL.md +35 -57
  71. package/skills/ultra-init/SKILL.md +48 -88
  72. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  73. package/skills/ultra-plan/SKILL.md +54 -132
  74. package/skills/ultra-research/SKILL.md +58 -112
  75. package/skills/ultra-review/SKILL.md +28 -46
  76. package/skills/ultra-review/references/unified-schema.md +2 -0
  77. package/skills/ultra-review/scripts/review_wait.py +8 -0
  78. package/skills/ultra-status/SKILL.md +22 -45
  79. package/skills/ultra-test/SKILL.md +27 -61
  80. package/skills/ultra-think/SKILL.md +29 -48
  81. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  82. package/spec/cli-protocol.md +80 -187
  83. package/spec/mcp-tools.yaml +130 -2738
  84. package/spec/schemas/mcp-tools.schema.json +1 -1
  85. package/spec/schemas/state-db.sql +107 -4
  86. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  87. package/ultra-tools/commands/status.cjs +369 -243
  88. package/mcp-server/lib/context-spine.cjs +0 -1208
  89. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  90. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  91. package/mcp-server/lib/spec-learning.cjs +0 -284
  92. package/mcp-server/lib/workflow-state.cjs +0 -2965
  93. package/skills/ultra-think/references/decision-dialogue.md +0 -153
@@ -23,16 +23,21 @@ const SURFACES = Object.freeze({
23
23
  fallback: 'direct_user_interaction',
24
24
  availability: 'interactive_non_auto_mode',
25
25
  }),
26
+ grok: Object.freeze({
27
+ primary: 'AskUserQuestion',
28
+ fallback: 'direct_user_interaction',
29
+ availability: 'interactive_session',
30
+ }),
26
31
  });
27
32
 
28
33
  const GENERIC_QUESTION_GUIDANCE =
29
- /Use the host-native structured question surface declared by the installed interaction\s+contract(?: when it exists)?\./g;
34
+ /Use the host-native (?:structured )?question surface declared by the installed interaction\s+contract(?: when it exists)?(?:;?\s+prefer its structured form when the host exposes one)?\./g;
30
35
 
31
36
  function interactionContract(runtime) {
32
37
  const question = SURFACES[runtime];
33
38
  if (!question) throw new Error(`unsupported interaction runtime: ${runtime}`);
34
39
  return {
35
- schema_version: '1.2',
40
+ schema_version: '1.3',
36
41
  runtime,
37
42
  public_capability_graph: PUBLIC_CAPABILITY_GRAPH,
38
43
  authority: {
@@ -50,9 +55,9 @@ function interactionContract(runtime) {
50
55
  'accepted_intent_normalization',
51
56
  ],
52
57
  ultra_mcp: [
53
- 'durable_state_and_evidence_refs',
54
- 'digests_freshness_locks_and_recovery',
55
- 'allowed_and_required_transitions',
58
+ 'durable_checkpoints_and_evidence_refs',
59
+ 'digests_freshness_leases_archives_and_recovery',
60
+ 'mutable_semantic_diagnostics',
56
61
  ],
57
62
  },
58
63
  interaction: {
@@ -85,8 +90,15 @@ function interactionContract(runtime) {
85
90
  routing: {
86
91
  semantic_recommendation_owner: 'host_model',
87
92
  durable_recommendation_authority: false,
88
- hard_invariant_field: 'required_transition',
89
- valid_capabilities_field: 'allowed_transitions',
93
+ current_context_tool: 'ultra.context',
94
+ checkpoint_diagnostic_fields: ['warnings', 'blockers'],
95
+ hard_blocker_boundary: [
96
+ 'corruption',
97
+ 'unsafe_path',
98
+ 'concurrency_conflict',
99
+ 'permission',
100
+ 'irreversible_external_effect',
101
+ ],
90
102
  },
91
103
  };
92
104
  }
@@ -99,6 +111,7 @@ function interactionPrompt(runtime) {
99
111
  codex: 'Codex',
100
112
  opencode: 'OpenCode',
101
113
  kimi: 'Kimi Code',
114
+ grok: 'Grok Build',
102
115
  }[runtime];
103
116
  const primary = runtime === 'opencode' ? `\`${surface.primary}\`` : surface.primary;
104
117
  return `Use ${host} ${primary} when ${surface.availability.replaceAll('_', ' ')} permits it.`;
@@ -187,7 +187,8 @@ function assetRefsForTree(rootName, root, { exclude = [] } = {}) {
187
187
  function writeProvenance({
188
188
  file, adapter, packageInfo, repository, sourceCommit: commit = null,
189
189
  sourceDirty: dirty = null, sourceWorktreeDigest: worktreeDigest = null,
190
- roots: inputRoots, assets: inputAssets, contracts: inputContracts = {},
190
+ roots: inputRoots, assetRoots: inputAssetRoots = inputRoots,
191
+ assets: inputAssets, contracts: inputContracts = {},
191
192
  }) {
192
193
  if (!file || !adapter || !packageInfo?.name || !packageInfo?.version || !repository) {
193
194
  throw new Error('provenance file, adapter, package, version, and repository are required');
@@ -199,9 +200,13 @@ function writeProvenance({
199
200
  throw new Error('provenance source worktree digest must be a SHA-256 digest or null');
200
201
  }
201
202
  const roots = normalizeRoots(inputRoots);
203
+ const assetRoots = normalizeRoots(inputAssetRoots);
204
+ for (const root of Object.keys(roots)) {
205
+ if (!assetRoots[root]) throw new Error(`missing provenance asset root: ${root}`);
206
+ }
202
207
  const refs = normalizeAssetRefs(inputAssets, roots);
203
208
  const assets = refs.map((ref) => {
204
- const { target } = resolveRef(roots, ref);
209
+ const { target } = resolveRef(assetRoots, ref);
205
210
  const stat = fs.statSync(target);
206
211
  if (!stat.isFile()) throw new Error(`provenance asset is not a file: ${target}`);
207
212
  return { ...ref, size_bytes: stat.size, sha256: hashFile(target) };
@@ -41,7 +41,7 @@ const PUBLIC_CAPABILITY_GRAPH = Object.freeze(Object.fromEntries(
41
41
  Object.freeze({
42
42
  mode: PUBLIC_CAPABILITY_MODES[name],
43
43
  activation: 'explicit_only',
44
- next_capability_source: 'mcp_allowed_transitions',
44
+ next_capability_source: 'host_model_from_ultra_context',
45
45
  recommendation_owner: 'host_model',
46
46
  selection_owner: 'user',
47
47
  automatic_invocation: false,
@@ -56,13 +56,14 @@ const INTERNAL_AGENT_SKILLS = Object.freeze([
56
56
  'testing-rules',
57
57
  ]);
58
58
 
59
- const SUPPORTED_RUNTIMES = Object.freeze(['claude', 'opencode', 'codex', 'kimi']);
59
+ const SUPPORTED_RUNTIMES = Object.freeze(['claude', 'opencode', 'codex', 'kimi', 'grok']);
60
60
 
61
61
  const COLLAB_SKILLS_BY_RUNTIME = Object.freeze({
62
62
  claude: Object.freeze(['codex-collab', 'ultra-verify']),
63
63
  codex: Object.freeze(['cc-collab', 'ultra-verify']),
64
64
  opencode: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
65
65
  kimi: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
66
+ grok: Object.freeze(['cc-collab', 'codex-collab', 'ultra-verify']),
66
67
  });
67
68
 
68
69
  const MCP_DEPENDENT_SKILLS = Object.freeze([
@@ -80,7 +81,7 @@ const MCP_DEPENDENT_SKILLS = Object.freeze([
80
81
 
81
82
  const WORKFLOW_HOOK_FILES = Object.freeze([
82
83
  'active_task_context.py',
83
- 'context_spine.py',
84
+ 'context_envelope.py',
84
85
  'health_check.py',
85
86
  'pre_stop_check.py',
86
87
  'runtime_paths.py',
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const MANIFEST_FILE = 'native-runtime.json';
8
+
9
+ class RuntimeNativeError extends Error {
10
+ constructor(code, message, details = {}) {
11
+ super(message);
12
+ this.name = 'RuntimeNativeError';
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+
18
+ function sha256(file) {
19
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
20
+ }
21
+
22
+ function readManifest(runtimeRoot) {
23
+ const file = path.join(path.resolve(runtimeRoot), MANIFEST_FILE);
24
+ if (!fs.existsSync(file)) {
25
+ throw new RuntimeNativeError(
26
+ 'RUNTIME_NATIVE_MISSING',
27
+ `Ultra native runtime manifest is missing: ${file}`,
28
+ { path: file },
29
+ );
30
+ }
31
+ let manifest;
32
+ try {
33
+ manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
34
+ } catch (cause) {
35
+ throw new RuntimeNativeError(
36
+ 'RUNTIME_NATIVE_MISSING',
37
+ `Ultra native runtime manifest is invalid: ${file}`,
38
+ { path: file, cause: cause.message },
39
+ );
40
+ }
41
+ if (manifest?.schema_version !== 1
42
+ || typeof manifest.native_module !== 'string'
43
+ || !/^[0-9a-f]{64}$/i.test(manifest.native_sha256 || '')) {
44
+ throw new RuntimeNativeError(
45
+ 'RUNTIME_NATIVE_MISSING',
46
+ `Ultra native runtime manifest is incomplete: ${file}`,
47
+ { path: file },
48
+ );
49
+ }
50
+ return { file, manifest };
51
+ }
52
+
53
+ function inspectRuntimeNative(runtimeRoot, { load = true } = {}) {
54
+ const root = path.resolve(runtimeRoot);
55
+ const { file, manifest } = readManifest(root);
56
+ const expected = {
57
+ platform: process.platform,
58
+ arch: process.arch,
59
+ modules: process.versions.modules,
60
+ };
61
+ const actual = {
62
+ platform: manifest.platform,
63
+ arch: manifest.arch,
64
+ modules: String(manifest.modules || ''),
65
+ };
66
+ if (actual.platform !== expected.platform
67
+ || actual.arch !== expected.arch
68
+ || actual.modules !== expected.modules) {
69
+ throw new RuntimeNativeError(
70
+ 'RUNTIME_ABI_MISMATCH',
71
+ `Ultra native runtime requires ${actual.platform}/${actual.arch} ABI ${
72
+ actual.modules
73
+ }, but the active Node is ${expected.platform}/${expected.arch} ABI ${expected.modules}`,
74
+ { expected: actual, actual: expected, manifest: file },
75
+ );
76
+ }
77
+ const nativeFile = path.resolve(root, manifest.native_module);
78
+ if (nativeFile !== root && !nativeFile.startsWith(`${root}${path.sep}`)) {
79
+ throw new RuntimeNativeError(
80
+ 'RUNTIME_NATIVE_MISSING',
81
+ 'Ultra native module path escapes the runtime root',
82
+ { path: manifest.native_module },
83
+ );
84
+ }
85
+ if (!fs.existsSync(nativeFile) || !fs.statSync(nativeFile).isFile()) {
86
+ throw new RuntimeNativeError(
87
+ 'RUNTIME_NATIVE_MISSING',
88
+ `Ultra native SQLite driver is missing: ${nativeFile}`,
89
+ { path: nativeFile },
90
+ );
91
+ }
92
+ const digest = sha256(nativeFile);
93
+ if (digest !== manifest.native_sha256) {
94
+ throw new RuntimeNativeError(
95
+ 'RUNTIME_NATIVE_MISSING',
96
+ `Ultra native SQLite driver digest does not match its manifest: ${nativeFile}`,
97
+ { path: nativeFile, expected: manifest.native_sha256, actual: digest },
98
+ );
99
+ }
100
+ if (load) {
101
+ let Database;
102
+ try {
103
+ Database = require(require.resolve('better-sqlite3', {
104
+ paths: [path.join(root, 'node_modules')],
105
+ }));
106
+ const db = new Database(':memory:');
107
+ db.prepare('SELECT 1 AS ok').get();
108
+ db.close();
109
+ } catch (cause) {
110
+ throw new RuntimeNativeError(
111
+ 'RUNTIME_ABI_MISMATCH',
112
+ `Ultra native SQLite driver cannot load in the active Node runtime: ${cause.message}`,
113
+ { path: nativeFile, cause: cause.code || cause.message },
114
+ );
115
+ }
116
+ }
117
+ return {
118
+ status: 'healthy',
119
+ manifest: file,
120
+ platform: actual.platform,
121
+ arch: actual.arch,
122
+ modules: actual.modules,
123
+ native_module: nativeFile,
124
+ native_sha256: digest,
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ MANIFEST_FILE,
130
+ RuntimeNativeError,
131
+ inspectRuntimeNative,
132
+ };
@@ -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');
@@ -66,7 +70,7 @@ function copySkills(repoRoot, target, names) {
66
70
  let adaptedBody = adaptInteractionGuidance(body, 'claude');
67
71
  if (name === 'ultra-review') {
68
72
  adaptedBody = adaptedBody.replace(
69
- /the current host's native bounded-worker\s+mechanism/g,
73
+ /(?:the current host's native|the host-native) bounded-worker\s+mechanism/g,
70
74
  'Claude Code Task workers using the installed review agent definitions',
71
75
  );
72
76
  }
@@ -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 = {}) {