ultra-builder-pro-cli 0.23.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.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +21 -18
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -229
- package/docs/ARCHITECTURE.md +203 -494
- package/docs/ARTIFACT-AUTHORITY.md +26 -22
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +174 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +596 -44
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +630 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +9 -8
- package/skills/ultra-deliver/SKILL.md +3 -3
- package/skills/ultra-dev/SKILL.md +18 -11
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +12 -7
- package/skills/ultra-review/SKILL.md +9 -6
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +4 -2
- package/skills/ultra-think/SKILL.md +7 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +37 -2804
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
|
@@ -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
|
+
};
|
package/adapters/claude.js
CHANGED
|
@@ -14,7 +14,11 @@ const {
|
|
|
14
14
|
removeTree,
|
|
15
15
|
writeAtomic,
|
|
16
16
|
} = require('./_shared/file-ops.cjs');
|
|
17
|
-
const {
|
|
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
|
|
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,
|
|
156
|
-
report.copied.skills = copySkills(repoRoot,
|
|
157
|
-
report.copied.agents = copyTree(path.join(repoRoot, 'agents'), path.join(
|
|
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(
|
|
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(
|
|
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(
|
|
158
|
+
writeAtomic(path.join(staging, 'hooks', 'hooks.json'), JSON.stringify(buildHooksManifest(), null, 2) + '\n');
|
|
166
159
|
|
|
167
|
-
|
|
168
|
-
|
|
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:
|
|
172
|
-
args:
|
|
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(
|
|
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
|
|
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(
|
|
180
|
+
markManaged(staging, { adapter: 'claude', plugin: PLUGIN_NAME });
|
|
187
181
|
const source = provenance.packageSource(repoRoot);
|
|
188
|
-
const provenanceFile = path.join(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
491
|
+
} finally {
|
|
492
|
+
if (fs.existsSync(agentStagingConfig)) removeTree(agentStagingConfig);
|
|
344
493
|
}
|
|
345
|
-
|
|
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
|
-
|
|
456
|
-
return report;
|
|
553
|
+
return applyNativeDoctor(report, path.join(pluginRoot, 'runtime'));
|
|
457
554
|
}
|
|
458
555
|
|
|
459
556
|
function removeManagedAgents(configDir, manifest) {
|