ultra-builder-pro-cli 0.17.1 → 0.19.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.
- package/CHANGELOG.md +77 -6
- package/README.md +380 -261
- package/adapters/_shared/codex-assets.cjs +24 -16
- package/adapters/_shared/file-ops.cjs +40 -3
- package/adapters/_shared/provenance.cjs +90 -6
- package/adapters/_shared/runtime-assets.cjs +0 -11
- package/adapters/claude.js +3 -1
- package/adapters/kimi.js +4 -30
- package/adapters/opencode.js +50 -14
- package/docs/AGENT-CONTEXT.md +11 -14
- package/docs/ARCHITECTURE.md +23 -15
- package/docs/DECISIONS.md +75 -0
- package/docs/LEGACY-CLI-CROSSWALK.md +3 -3
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +54 -0
- package/docs/ROADMAP.md +13 -13
- package/docs/RUNTIME-COMPAT-MATRIX.md +24 -21
- package/docs/STATE-DB-ACCESS-POLICY.md +2 -2
- package/docs/WORKFLOW-LIFECYCLE.md +12 -10
- package/hooks/active_task_context.py +1 -1
- package/hooks/context_spine.py +1 -1
- package/hooks/health_check.py +19 -0
- package/hooks/pre_stop_check.py +1 -1
- package/hooks/workflow_checkpoint.py +1 -1
- package/hooks/workflow_context.py +1 -1
- package/hooks/workflow_resume.py +1 -1
- package/mcp-server/lib/workflow-state.cjs +67 -54
- package/mcp-server/server.cjs +19 -7
- package/orchestrator/dispatch-rules.cjs +1 -1
- package/package.json +4 -5
- package/skills/ultra-change/SKILL.md +4 -3
- package/skills/ultra-init/SKILL.md +2 -1
- package/skills/ultra-research/SKILL.md +7 -4
- package/spec/cli-protocol.md +4 -4
- package/spec/mcp-tools.yaml +6 -6
- package/spec/schemas/context-file.v4.5.schema.json +1 -1
- package/spec/schemas/mcp-tools.schema.json +3 -3
- package/spec/schemas/state-db.sql +3 -3
- package/spec/schemas/tasks.v4.5.schema.json +1 -1
- package/adapters/_shared/handbook.cjs +0 -156
- package/bin/handbook.js +0 -54
- package/docs/PLAN.zh-CN.md +0 -1696
- package/docs/USER-HANDBOOK-CONTRACT.md +0 -81
|
@@ -223,47 +223,52 @@ function buildCommandMap() {
|
|
|
223
223
|
return Object.fromEntries(COMMAND_NAMES.map((name) => [`/${name}`, `$ultra-builder-pro:${name}`]));
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
function
|
|
226
|
+
function shellQuote(value) {
|
|
227
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function hookCommand(pluginRoot, feature, ...args) {
|
|
231
|
+
const adapter = path.join(path.resolve(pluginRoot), 'hooks', 'adapters', 'codex.py');
|
|
227
232
|
return [
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
...args.map((arg) =>
|
|
233
|
+
`python3 ${shellQuote(adapter)}`,
|
|
234
|
+
shellQuote(feature),
|
|
235
|
+
...args.map((arg) => shellQuote(arg)),
|
|
231
236
|
].join(' ');
|
|
232
237
|
}
|
|
233
238
|
|
|
234
|
-
function commandHook(feature, timeout, statusMessage, ...args) {
|
|
239
|
+
function commandHook(pluginRoot, feature, timeout, statusMessage, ...args) {
|
|
235
240
|
return {
|
|
236
241
|
type: 'command',
|
|
237
|
-
command: hookCommand(feature, ...args),
|
|
242
|
+
command: hookCommand(pluginRoot, feature, ...args),
|
|
238
243
|
timeout,
|
|
239
244
|
statusMessage,
|
|
240
245
|
};
|
|
241
246
|
}
|
|
242
247
|
|
|
243
|
-
function buildHooksManifest() {
|
|
248
|
+
function buildHooksManifest(pluginRoot) {
|
|
244
249
|
return {
|
|
245
250
|
hooks: {
|
|
246
251
|
SessionStart: [
|
|
247
|
-
{ hooks: [commandHook('health_check.py', 5, 'Checking Ultra runtime')] },
|
|
248
|
-
{ hooks: [commandHook('workflow_context.py', 10, 'Loading active Ultra workflow')] },
|
|
252
|
+
{ hooks: [commandHook(pluginRoot, 'health_check.py', 5, 'Checking Ultra runtime')] },
|
|
253
|
+
{ hooks: [commandHook(pluginRoot, 'workflow_context.py', 10, 'Loading active Ultra workflow')] },
|
|
249
254
|
],
|
|
250
255
|
PreToolUse: [
|
|
251
|
-
{ matcher: 'Edit|Write|apply_patch', hooks: [commandHook('active_task_context.py', 3, 'Checking active Ultra task')] },
|
|
256
|
+
{ matcher: 'Edit|Write|apply_patch', hooks: [commandHook(pluginRoot, 'active_task_context.py', 3, 'Checking active Ultra task')] },
|
|
252
257
|
],
|
|
253
258
|
PreCompact: [
|
|
254
|
-
{ matcher: 'manual|auto', hooks: [commandHook('workflow_checkpoint.py', 10, 'Saving Ultra workflow checkpoint')] },
|
|
259
|
+
{ matcher: 'manual|auto', hooks: [commandHook(pluginRoot, 'workflow_checkpoint.py', 10, 'Saving Ultra workflow checkpoint')] },
|
|
255
260
|
],
|
|
256
261
|
PostCompact: [
|
|
257
|
-
{ hooks: [commandHook('workflow_resume.py', 10, 'Restoring Ultra workflow checkpoint')] },
|
|
262
|
+
{ hooks: [commandHook(pluginRoot, 'workflow_resume.py', 10, 'Restoring Ultra workflow checkpoint')] },
|
|
258
263
|
],
|
|
259
264
|
Stop: [
|
|
260
|
-
{ hooks: [commandHook('pre_stop_check.py', 5, 'Reporting Ultra workflow position')] },
|
|
265
|
+
{ hooks: [commandHook(pluginRoot, 'pre_stop_check.py', 5, 'Reporting Ultra workflow position')] },
|
|
261
266
|
],
|
|
262
267
|
SubagentStart: [
|
|
263
|
-
{ hooks: [commandHook('subagent_tracker.py', 5, 'Tracking Ultra subagent', 'start')] },
|
|
268
|
+
{ hooks: [commandHook(pluginRoot, 'subagent_tracker.py', 5, 'Tracking Ultra subagent', 'start')] },
|
|
264
269
|
],
|
|
265
270
|
SubagentStop: [
|
|
266
|
-
{ hooks: [commandHook('subagent_tracker.py', 5, 'Tracking Ultra subagent', 'stop')] },
|
|
271
|
+
{ hooks: [commandHook(pluginRoot, 'subagent_tracker.py', 5, 'Tracking Ultra subagent', 'stop')] },
|
|
267
272
|
],
|
|
268
273
|
},
|
|
269
274
|
};
|
|
@@ -281,7 +286,10 @@ function copyHooks(repoRoot, pluginRoot) {
|
|
|
281
286
|
path.join(sourceRoot, 'adapters', 'codex.py'),
|
|
282
287
|
path.join(targetRoot, 'adapters', 'codex.py'),
|
|
283
288
|
);
|
|
284
|
-
writeAtomic(
|
|
289
|
+
writeAtomic(
|
|
290
|
+
path.join(targetRoot, 'hooks.json'),
|
|
291
|
+
JSON.stringify(buildHooksManifest(pluginRoot), null, 2) + '\n',
|
|
292
|
+
);
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
function buildMcpRuntime(repoRoot, pluginRoot, { runtime = 'codex' } = {}) {
|
|
@@ -18,13 +18,50 @@ function ensureDir(dir) {
|
|
|
18
18
|
fs.mkdirSync(dir, { recursive: true });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function resolveAtomicTarget(file) {
|
|
22
|
+
try {
|
|
23
|
+
const stat = fs.lstatSync(file);
|
|
24
|
+
if (!stat.isSymbolicLink()) return file;
|
|
25
|
+
try {
|
|
26
|
+
return fs.realpathSync(file);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === 'ENOENT') {
|
|
29
|
+
throw new Error(`refusing to replace dangling symlink: ${file}`);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code === 'ENOENT') return file;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
function writeAtomic(file, content) {
|
|
22
40
|
ensureDir(path.dirname(file));
|
|
41
|
+
// Rename over the resolved target rather than over a user-managed symlink.
|
|
42
|
+
// This keeps dotfile-manager links intact while retaining atomic replacement.
|
|
43
|
+
const target = resolveAtomicTarget(file);
|
|
44
|
+
ensureDir(path.dirname(target));
|
|
45
|
+
let mode = null;
|
|
46
|
+
try {
|
|
47
|
+
mode = fs.statSync(target).mode & 0o7777;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code !== 'ENOENT') throw error;
|
|
50
|
+
}
|
|
23
51
|
// randomUUID defeats predictable tmp-name attacks and PID+ms collisions
|
|
24
52
|
// when two writers hit the same path inside one millisecond.
|
|
25
|
-
const tmp = `${
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
const tmp = `${target}.tmp-${crypto.randomUUID()}`;
|
|
54
|
+
try {
|
|
55
|
+
fs.writeFileSync(tmp, content, mode === null ? undefined : { mode });
|
|
56
|
+
if (mode !== null) fs.chmodSync(tmp, mode);
|
|
57
|
+
fs.renameSync(tmp, target);
|
|
58
|
+
} finally {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(tmp);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== 'ENOENT') throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
28
65
|
}
|
|
29
66
|
|
|
30
67
|
function listRelative(root, { exclude = DEFAULT_EXCLUDES } = {}) {
|
|
@@ -90,15 +90,76 @@ function digestEntries(entries) {
|
|
|
90
90
|
return hash.digest('hex');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function runGit(repoRoot, args, encoding = 'utf8') {
|
|
94
|
+
const result = spawnSync('git', args, {
|
|
95
|
+
cwd: repoRoot,
|
|
96
|
+
encoding,
|
|
97
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
98
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
99
|
+
});
|
|
100
|
+
return result.status === 0 ? result.stdout : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
function sourceCommit(repoRoot) {
|
|
94
104
|
if (!fs.existsSync(path.join(repoRoot, '.git'))) return null;
|
|
95
|
-
const result =
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
const value = result.status === 0 ? result.stdout.trim() : '';
|
|
105
|
+
const result = runGit(repoRoot, ['rev-parse', 'HEAD']);
|
|
106
|
+
const value = typeof result === 'string' ? result.trim() : '';
|
|
99
107
|
return /^[0-9a-f]{40,64}$/i.test(value) ? value : null;
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
function sourceWorktreeState(repoRoot, commit) {
|
|
111
|
+
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
112
|
+
return { sourceDirty: null, sourceWorktreeDigest: null };
|
|
113
|
+
}
|
|
114
|
+
const status = runGit(
|
|
115
|
+
repoRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.'], null,
|
|
116
|
+
);
|
|
117
|
+
if (!Buffer.isBuffer(status)) {
|
|
118
|
+
return { sourceDirty: null, sourceWorktreeDigest: null };
|
|
119
|
+
}
|
|
120
|
+
const trackedDiff = commit
|
|
121
|
+
? runGit(repoRoot, ['diff', '--binary', 'HEAD', '--', '.'], null)
|
|
122
|
+
: runGit(repoRoot, ['diff', '--binary', '--cached', '--', '.'], null);
|
|
123
|
+
const unstagedDiff = commit
|
|
124
|
+
? Buffer.alloc(0)
|
|
125
|
+
: runGit(repoRoot, ['diff', '--binary', '--', '.'], null);
|
|
126
|
+
const untrackedOutput = runGit(
|
|
127
|
+
repoRoot, ['ls-files', '--others', '--exclude-standard', '-z', '--', '.'],
|
|
128
|
+
);
|
|
129
|
+
if (!Buffer.isBuffer(trackedDiff) || !Buffer.isBuffer(unstagedDiff)
|
|
130
|
+
|| typeof untrackedOutput !== 'string') {
|
|
131
|
+
return { sourceDirty: null, sourceWorktreeDigest: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hash = crypto.createHash(DIGEST_ALGORITHM);
|
|
135
|
+
hash.update('ultra-source-worktree-v1\0');
|
|
136
|
+
hash.update(commit || 'unborn');
|
|
137
|
+
hash.update('\0');
|
|
138
|
+
hash.update(status);
|
|
139
|
+
hash.update('\0');
|
|
140
|
+
hash.update(trackedDiff);
|
|
141
|
+
hash.update('\0');
|
|
142
|
+
hash.update(unstagedDiff);
|
|
143
|
+
hash.update('\0');
|
|
144
|
+
const untrackedPaths = untrackedOutput.split('\0').filter(Boolean).sort();
|
|
145
|
+
for (const relative of untrackedPaths) {
|
|
146
|
+
const target = path.resolve(repoRoot, relative);
|
|
147
|
+
if (target !== repoRoot && !target.startsWith(`${path.resolve(repoRoot)}${path.sep}`)) {
|
|
148
|
+
return { sourceDirty: null, sourceWorktreeDigest: null };
|
|
149
|
+
}
|
|
150
|
+
const stat = fs.lstatSync(target);
|
|
151
|
+
hash.update(relative);
|
|
152
|
+
hash.update('\0');
|
|
153
|
+
if (stat.isSymbolicLink()) hash.update(fs.readlinkSync(target));
|
|
154
|
+
else if (stat.isFile()) hash.update(fs.readFileSync(target));
|
|
155
|
+
hash.update('\0');
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
sourceDirty: status.length > 0,
|
|
159
|
+
sourceWorktreeDigest: hash.digest('hex'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
102
163
|
function repositoryUrl(pkg) {
|
|
103
164
|
if (typeof pkg.repository === 'string') return pkg.repository;
|
|
104
165
|
if (pkg.repository && typeof pkg.repository.url === 'string') return pkg.repository.url;
|
|
@@ -107,10 +168,12 @@ function repositoryUrl(pkg) {
|
|
|
107
168
|
|
|
108
169
|
function packageSource(repoRoot) {
|
|
109
170
|
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
171
|
+
const commit = sourceCommit(repoRoot);
|
|
110
172
|
return {
|
|
111
173
|
packageInfo: { name: pkg.name, version: pkg.version },
|
|
112
174
|
repository: repositoryUrl(pkg),
|
|
113
|
-
sourceCommit:
|
|
175
|
+
sourceCommit: commit,
|
|
176
|
+
...sourceWorktreeState(repoRoot, commit),
|
|
114
177
|
};
|
|
115
178
|
}
|
|
116
179
|
|
|
@@ -123,11 +186,18 @@ function assetRefsForTree(rootName, root, { exclude = [] } = {}) {
|
|
|
123
186
|
|
|
124
187
|
function writeProvenance({
|
|
125
188
|
file, adapter, packageInfo, repository, sourceCommit: commit = null,
|
|
189
|
+
sourceDirty: dirty = null, sourceWorktreeDigest: worktreeDigest = null,
|
|
126
190
|
roots: inputRoots, assets: inputAssets, contracts: inputContracts = {},
|
|
127
191
|
}) {
|
|
128
192
|
if (!file || !adapter || !packageInfo?.name || !packageInfo?.version || !repository) {
|
|
129
193
|
throw new Error('provenance file, adapter, package, version, and repository are required');
|
|
130
194
|
}
|
|
195
|
+
if (dirty !== null && typeof dirty !== 'boolean') {
|
|
196
|
+
throw new Error('provenance source dirty state must be a boolean or null');
|
|
197
|
+
}
|
|
198
|
+
if (worktreeDigest !== null && !/^[0-9a-f]{64}$/i.test(worktreeDigest)) {
|
|
199
|
+
throw new Error('provenance source worktree digest must be a SHA-256 digest or null');
|
|
200
|
+
}
|
|
131
201
|
const roots = normalizeRoots(inputRoots);
|
|
132
202
|
const refs = normalizeAssetRefs(inputAssets, roots);
|
|
133
203
|
const assets = refs.map((ref) => {
|
|
@@ -145,7 +215,12 @@ function writeProvenance({
|
|
|
145
215
|
schema_version: SCHEMA_VERSION,
|
|
146
216
|
adapter,
|
|
147
217
|
package: { name: packageInfo.name, version: packageInfo.version },
|
|
148
|
-
source: {
|
|
218
|
+
source: {
|
|
219
|
+
repository,
|
|
220
|
+
commit: commit || null,
|
|
221
|
+
dirty,
|
|
222
|
+
worktree_digest: worktreeDigest,
|
|
223
|
+
},
|
|
149
224
|
installed_at: new Date().toISOString(),
|
|
150
225
|
roots,
|
|
151
226
|
content: { algorithm: DIGEST_ALGORITHM, digest: digestEntries(assets) },
|
|
@@ -188,6 +263,15 @@ function inspectProvenance({ file, expectedAdapter = null, expectedPackageVersio
|
|
|
188
263
|
if (!manifest.package?.name || !manifest.package?.version || !manifest.source?.repository) {
|
|
189
264
|
throw new Error('package and source metadata required');
|
|
190
265
|
}
|
|
266
|
+
if (manifest.source.dirty !== undefined && manifest.source.dirty !== null
|
|
267
|
+
&& typeof manifest.source.dirty !== 'boolean') {
|
|
268
|
+
throw new Error('source dirty state must be a boolean or null');
|
|
269
|
+
}
|
|
270
|
+
if (manifest.source.worktree_digest !== undefined
|
|
271
|
+
&& manifest.source.worktree_digest !== null
|
|
272
|
+
&& !/^[0-9a-f]{64}$/i.test(manifest.source.worktree_digest)) {
|
|
273
|
+
throw new Error('source worktree digest must be a SHA-256 digest or null');
|
|
274
|
+
}
|
|
191
275
|
} catch (error) {
|
|
192
276
|
return invalidReport(
|
|
193
277
|
manifestPath, 'degraded', issue('PROVENANCE_INVALID', { message: error.message }), expectedAdapter,
|
|
@@ -51,16 +51,6 @@ const MCP_DEPENDENT_SKILLS = Object.freeze([
|
|
|
51
51
|
'ultra-doctor',
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
-
const RETIRED_SKILLS = Object.freeze([
|
|
55
|
-
'agent-browser',
|
|
56
|
-
'find-skills',
|
|
57
|
-
'recall',
|
|
58
|
-
'use-railway',
|
|
59
|
-
'vercel-composition-patterns',
|
|
60
|
-
'vercel-react-best-practices',
|
|
61
|
-
'vercel-react-native-skills',
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
54
|
const WORKFLOW_HOOK_FILES = Object.freeze([
|
|
65
55
|
'active_task_context.py',
|
|
66
56
|
'context_spine.py',
|
|
@@ -98,7 +88,6 @@ module.exports = {
|
|
|
98
88
|
SUPPORTED_RUNTIMES,
|
|
99
89
|
COLLAB_SKILLS_BY_RUNTIME,
|
|
100
90
|
MCP_DEPENDENT_SKILLS,
|
|
101
|
-
RETIRED_SKILLS,
|
|
102
91
|
WORKFLOW_HOOK_FILES,
|
|
103
92
|
isSupportedRuntime,
|
|
104
93
|
skillPolicy,
|
package/adapters/claude.js
CHANGED
|
@@ -68,10 +68,12 @@ function copySkills(repoRoot, target, names) {
|
|
|
68
68
|
'Claude Code Task workers using the installed review agent definitions',
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
|
+
const policy = skillPolicy(name);
|
|
71
72
|
return Buffer.from(serializeFrontmatter({
|
|
72
73
|
name,
|
|
73
74
|
description: fm.description,
|
|
74
|
-
'user-invocable':
|
|
75
|
+
'user-invocable': policy.userInvocable,
|
|
76
|
+
...(policy.userInvocable ? { 'disable-model-invocation': true } : {}),
|
|
75
77
|
}, adaptedBody));
|
|
76
78
|
},
|
|
77
79
|
});
|
package/adapters/kimi.js
CHANGED
|
@@ -20,6 +20,7 @@ const provenance = require('./_shared/provenance.cjs');
|
|
|
20
20
|
const {
|
|
21
21
|
CORE_PUBLIC_SKILLS,
|
|
22
22
|
WORKFLOW_HOOK_FILES,
|
|
23
|
+
skillPolicy,
|
|
23
24
|
skillsForRuntime,
|
|
24
25
|
} = require('./_shared/runtime-assets.cjs');
|
|
25
26
|
|
|
@@ -28,7 +29,6 @@ const MCP_SERVER_NAME = PLUGIN_ID;
|
|
|
28
29
|
const PROVENANCE_FILE = 'provenance.json';
|
|
29
30
|
const REGISTRY_RELATIVE = path.join('plugins', 'installed.json');
|
|
30
31
|
const MANAGED_RELATIVE = path.join('plugins', 'managed', PLUGIN_ID);
|
|
31
|
-
const BOOTSTRAP_SKILL = 'using-ultra-builder-pro';
|
|
32
32
|
|
|
33
33
|
function resolveTarget(ctx = {}) {
|
|
34
34
|
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
@@ -127,11 +127,13 @@ function transformSkill(buf, relPath, skillName) {
|
|
|
127
127
|
if (!relPath.endsWith('.md')) return buf;
|
|
128
128
|
const { fm, body } = parseFm(source);
|
|
129
129
|
if (!fm) return Buffer.from(kimiTextTransform(source, skillName), 'utf8');
|
|
130
|
+
const policy = skillPolicy(skillName);
|
|
130
131
|
const nativeFm = {
|
|
131
132
|
name: String(fm.name || skillName),
|
|
132
133
|
description: kimiTextTransform(fm.description || `${skillName} workflow`, skillName)
|
|
133
134
|
.replace(/\s+/g, ' ')
|
|
134
135
|
.trim(),
|
|
136
|
+
...(policy.userInvocable ? { disableModelInvocation: true } : {}),
|
|
135
137
|
};
|
|
136
138
|
return Buffer.from(serializeFm(nativeFm, kimiTextTransform(body, skillName)), 'utf8');
|
|
137
139
|
}
|
|
@@ -162,29 +164,6 @@ function copySkills(repoRoot, pluginRoot) {
|
|
|
162
164
|
});
|
|
163
165
|
installed.push(name);
|
|
164
166
|
}
|
|
165
|
-
const bootstrapRoot = path.join(pluginRoot, 'skills', BOOTSTRAP_SKILL);
|
|
166
|
-
ensureDir(bootstrapRoot);
|
|
167
|
-
writeAtomic(path.join(bootstrapRoot, 'SKILL.md'), `---
|
|
168
|
-
name: ${BOOTSTRAP_SKILL}
|
|
169
|
-
description: Bootstrap the Kimi-native Ultra Builder Pro runtime contract at session start.
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
# Ultra Builder Pro for Kimi Code
|
|
173
|
-
|
|
174
|
-
Kimi Code is the primary host and owns all edits, evidence, and final verification. Use the
|
|
175
|
-
namespaced \`/ultra-builder-pro:ultra-*\` commands or invoke the registered Ultra skills directly.
|
|
176
|
-
|
|
177
|
-
- \`.ultra/state.db\` is the only durable Ultra authority. JSON and Markdown under \`.ultra/\` are
|
|
178
|
-
projections or workflow artifacts and must never replace MCP state writes.
|
|
179
|
-
- Memory and code graph data belong to separately installed external providers. Ultra stores only
|
|
180
|
-
provider metadata references and never captures prompts, transcripts, or cross-session memory.
|
|
181
|
-
- Use Kimi \`TodoList\` for session coordination and Kimi \`Agent\` / \`AgentSwarm\` for bounded
|
|
182
|
-
workers. Bundled files under \`$KIMI_PLUGIN_ROOT/agents/\` are worker prompt templates.
|
|
183
|
-
- Before compaction, Ultra saves its workflow checkpoint. After compaction or recovery, inspect
|
|
184
|
-
\`.ultra/runtime/checkpoint.json\` and call the Ultra status/doctor MCP tools before continuing.
|
|
185
|
-
- If no Ultra workflow is active, the lifecycle hooks remain advisory except projection protection.
|
|
186
|
-
`);
|
|
187
|
-
installed.push(BOOTSTRAP_SKILL);
|
|
188
167
|
return installed;
|
|
189
168
|
}
|
|
190
169
|
|
|
@@ -280,7 +259,6 @@ function writeManifest(repoRoot, pluginRoot) {
|
|
|
280
259
|
homepage: pkg.homepage,
|
|
281
260
|
license: pkg.license || 'MIT',
|
|
282
261
|
skills: ['./skills'],
|
|
283
|
-
sessionStart: { skill: BOOTSTRAP_SKILL },
|
|
284
262
|
mcpServers: { [MCP_SERVER_NAME]: mcpManifest() },
|
|
285
263
|
hooks: buildHooksManifest(),
|
|
286
264
|
commands: ['./commands'],
|
|
@@ -351,9 +329,6 @@ function writePluginProvenance(repoRoot, target) {
|
|
|
351
329
|
mcp_launcher: { root: 'plugin', path: path.join('runtime', 'launch.cjs') },
|
|
352
330
|
hook_event_helper: { root: 'plugin', path: path.join('runtime', 'hook-event.cjs') },
|
|
353
331
|
hook_adapter: { root: 'plugin', path: path.join('hooks', 'adapters', 'kimi.py') },
|
|
354
|
-
session_bootstrap: {
|
|
355
|
-
root: 'plugin', path: path.join('skills', BOOTSTRAP_SKILL, 'SKILL.md'),
|
|
356
|
-
},
|
|
357
332
|
},
|
|
358
333
|
});
|
|
359
334
|
return { file, manifest };
|
|
@@ -444,7 +419,7 @@ function doctor(ctx = {}) {
|
|
|
444
419
|
&& manifest.skills.includes('./skills')
|
|
445
420
|
&& Array.isArray(manifest.commands)
|
|
446
421
|
&& manifest.commands.includes('./commands')
|
|
447
|
-
&& manifest.sessionStart
|
|
422
|
+
&& manifest.sessionStart === undefined;
|
|
448
423
|
const mcp = manifest.mcpServers?.[MCP_SERVER_NAME];
|
|
449
424
|
const expected = mcpManifest();
|
|
450
425
|
mcpOk = !!mcp
|
|
@@ -508,7 +483,6 @@ module.exports = {
|
|
|
508
483
|
name: 'kimi',
|
|
509
484
|
PLUGIN_ID,
|
|
510
485
|
MCP_SERVER_NAME,
|
|
511
|
-
BOOTSTRAP_SKILL,
|
|
512
486
|
buildHooksManifest,
|
|
513
487
|
kimiTextTransform,
|
|
514
488
|
resolveTarget,
|
package/adapters/opencode.js
CHANGED
|
@@ -38,6 +38,11 @@ const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
|
38
38
|
const PROVENANCE_FILE = 'provenance.json';
|
|
39
39
|
const COMMAND_NAMES = CORE_PUBLIC_SKILLS;
|
|
40
40
|
|
|
41
|
+
function discoverableSkillNames() {
|
|
42
|
+
return skillsForRuntime('opencode')
|
|
43
|
+
.filter((name) => !CORE_PUBLIC_SKILLS.includes(name));
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
function resolveTarget(ctx = {}) {
|
|
42
47
|
if (ctx.configDir) return ctx.configDir;
|
|
43
48
|
if (ctx.scope === 'global') {
|
|
@@ -97,16 +102,26 @@ function openCodeTextTransform(input, assetName = '') {
|
|
|
97
102
|
return text;
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
function openCodeCommandTransform(buf, relPath, commandName) {
|
|
105
|
+
function openCodeCommandTransform(buf, relPath, commandName, workflowFile) {
|
|
101
106
|
if (!relPath.endsWith('.md')) return buf;
|
|
102
|
-
const { fm
|
|
103
|
-
if (!fm)
|
|
107
|
+
const { fm } = parseFm(buf.toString('utf8'));
|
|
108
|
+
if (!fm) throw new Error(`missing frontmatter in OpenCode command: ${commandName}`);
|
|
104
109
|
const nativeFm = {
|
|
105
110
|
description: openCodeTextTransform(fm.description || `${commandName} workflow`, commandName)
|
|
106
111
|
.replace(/\s+/g, ' ')
|
|
107
112
|
.trim(),
|
|
108
113
|
};
|
|
109
|
-
|
|
114
|
+
const body = [
|
|
115
|
+
`# Run ${commandName}`,
|
|
116
|
+
'',
|
|
117
|
+
`Read and follow \`${workflowFile}\` as the only workflow definition.`,
|
|
118
|
+
'This explicit command is the activation boundary; do not substitute a model-discovered',
|
|
119
|
+
'public skill or start another public Ultra workflow automatically.',
|
|
120
|
+
'',
|
|
121
|
+
'Arguments: $ARGUMENTS',
|
|
122
|
+
'',
|
|
123
|
+
].join('\n');
|
|
124
|
+
return Buffer.from(serializeFm(nativeFm, body), 'utf8');
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
function openCodeSkillAssetTransform(buf, relPath, skillName) {
|
|
@@ -229,7 +244,10 @@ function copyCommands(repoRoot, target) {
|
|
|
229
244
|
const file = `${name}.md`;
|
|
230
245
|
const source = path.join(repoRoot, 'commands', file);
|
|
231
246
|
if (!fs.existsSync(source)) throw new Error(`missing allowlisted OpenCode command: ${file}`);
|
|
232
|
-
const
|
|
247
|
+
const workflowFile = path.join(target, BUNDLE_DIR, 'workflows', name, 'SKILL.md');
|
|
248
|
+
const transformed = withManagedMarker(
|
|
249
|
+
openCodeCommandTransform(fs.readFileSync(source), file, name, workflowFile),
|
|
250
|
+
);
|
|
233
251
|
writeAtomic(path.join(output, file), transformed);
|
|
234
252
|
copied.push(file);
|
|
235
253
|
}
|
|
@@ -238,7 +256,7 @@ function copyCommands(repoRoot, target) {
|
|
|
238
256
|
|
|
239
257
|
function copySkills(repoRoot, target) {
|
|
240
258
|
const copied = [];
|
|
241
|
-
const names =
|
|
259
|
+
const names = discoverableSkillNames();
|
|
242
260
|
const skillsRoot = path.join(target, 'skills');
|
|
243
261
|
ensureDir(skillsRoot);
|
|
244
262
|
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
@@ -262,6 +280,22 @@ function copySkills(repoRoot, target) {
|
|
|
262
280
|
return copied;
|
|
263
281
|
}
|
|
264
282
|
|
|
283
|
+
function copyPrivateWorkflows(repoRoot, bundleRoot) {
|
|
284
|
+
const copied = [];
|
|
285
|
+
const workflowsRoot = path.join(bundleRoot, 'workflows');
|
|
286
|
+
for (const name of CORE_PUBLIC_SKILLS) {
|
|
287
|
+
const source = path.join(repoRoot, 'skills', name);
|
|
288
|
+
if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
|
|
289
|
+
throw new Error(`missing allowlisted OpenCode workflow: ${name}`);
|
|
290
|
+
}
|
|
291
|
+
const files = copyTree(source, path.join(workflowsRoot, name), {
|
|
292
|
+
transform: (buf, relPath) => openCodeSkillAssetTransform(buf, relPath, name),
|
|
293
|
+
});
|
|
294
|
+
copied.push(...files.map((rel) => path.join('workflows', name, rel)));
|
|
295
|
+
}
|
|
296
|
+
return copied;
|
|
297
|
+
}
|
|
298
|
+
|
|
265
299
|
function copyAgents(repoRoot, target) {
|
|
266
300
|
const sourceRoot = path.join(repoRoot, 'agents');
|
|
267
301
|
const output = path.join(target, 'agents');
|
|
@@ -279,7 +313,7 @@ function preflightAssets(repoRoot, target) {
|
|
|
279
313
|
for (const name of COMMAND_NAMES) {
|
|
280
314
|
assertManagedTextTarget(path.join(target, 'commands', `${name}.md`), 'command');
|
|
281
315
|
}
|
|
282
|
-
for (const name of
|
|
316
|
+
for (const name of discoverableSkillNames()) {
|
|
283
317
|
const destination = path.join(target, 'skills', name);
|
|
284
318
|
if (fs.existsSync(destination) && !isManaged(destination)) {
|
|
285
319
|
throw new Error(`refusing to replace unmanaged OpenCode skill: ${destination}`);
|
|
@@ -316,8 +350,8 @@ const BREADCRUMB_CLI = path.resolve(
|
|
|
316
350
|
function findUltraRoot(directory) {
|
|
317
351
|
let current = path.resolve(directory);
|
|
318
352
|
while (true) {
|
|
319
|
-
const
|
|
320
|
-
if (fs.existsSync(
|
|
353
|
+
const stateDb = path.join(current, ".ultra", "state.db");
|
|
354
|
+
if (fs.existsSync(stateDb) && fs.statSync(stateDb).isFile()) return current;
|
|
321
355
|
const parent = path.dirname(current);
|
|
322
356
|
if (parent === current) return null;
|
|
323
357
|
current = parent;
|
|
@@ -335,12 +369,13 @@ function readUltraContext(directory) {
|
|
|
335
369
|
timeout: 5000,
|
|
336
370
|
});
|
|
337
371
|
const value = JSON.parse(raw);
|
|
338
|
-
|
|
339
|
-
|
|
372
|
+
// A canonical breadcrumb is injectable only while DB authority proves an active workflow.
|
|
373
|
+
if (!value?.breadcrumb || !value.breadcrumb.workflow
|
|
374
|
+
|| typeof value.text !== "string" || !value.text) {
|
|
375
|
+
return { root, breadcrumb: null, text: null };
|
|
340
376
|
}
|
|
341
377
|
return { root, breadcrumb: value.breadcrumb, text: value.text };
|
|
342
|
-
} catch
|
|
343
|
-
process.stderr.write(\`[ultra-builder-pro] cannot read canonical breadcrumb: \${error.message}\\n\`);
|
|
378
|
+
} catch {
|
|
344
379
|
return { root, breadcrumb: null, text: null };
|
|
345
380
|
}
|
|
346
381
|
}
|
|
@@ -415,6 +450,7 @@ function install(ctx = {}) {
|
|
|
415
450
|
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
416
451
|
if (fs.existsSync(bundleRoot)) removeTree(bundleRoot);
|
|
417
452
|
ensureDir(bundleRoot);
|
|
453
|
+
report.copied.workflows = copyPrivateWorkflows(repoRoot, bundleRoot);
|
|
418
454
|
const runtime = buildMcpRuntime(repoRoot, bundleRoot, { runtime: 'opencode' });
|
|
419
455
|
markManaged(bundleRoot, { adapter: 'opencode', asset: 'runtime-bundle' });
|
|
420
456
|
const configFile = path.join(target, 'opencode.json');
|
|
@@ -433,7 +469,7 @@ function install(ctx = {}) {
|
|
|
433
469
|
const bundleAssets = provenance.assetRefsForTree('config', bundleRoot, {
|
|
434
470
|
exclude: [PROVENANCE_FILE],
|
|
435
471
|
}).map((asset) => ({ ...asset, path: path.join(BUNDLE_DIR, asset.path) }));
|
|
436
|
-
const skillMarkers =
|
|
472
|
+
const skillMarkers = discoverableSkillNames().map((name) => ({
|
|
437
473
|
root: 'config', path: path.join('skills', name, '.ubp-managed'),
|
|
438
474
|
}));
|
|
439
475
|
const provenanceFile = path.join(bundleRoot, PROVENANCE_FILE);
|
package/docs/AGENT-CONTEXT.md
CHANGED
|
@@ -12,8 +12,8 @@ Ultra Builder Pro owns only:
|
|
|
12
12
|
`ultra-think`, plus the daily `ultra-change` and diagnostic `ultra-doctor`;
|
|
13
13
|
- four internal agent-only rule skills: `code-review-expert`, `security-rules`,
|
|
14
14
|
`integration-rules`, and `testing-rules`;
|
|
15
|
-
- the host-specific collaboration companions, `ultra-verify`, and
|
|
16
|
-
|
|
15
|
+
- the host-specific collaboration companions, `ultra-verify`, and host-native
|
|
16
|
+
explicit command or skill entry points;
|
|
17
17
|
- bounded review/debug agents, workflow-only hooks, MCP task state, and the
|
|
18
18
|
portable CLI/orchestrator.
|
|
19
19
|
|
|
@@ -191,20 +191,17 @@ Archive before prune; the confirmation token is intentionally required.
|
|
|
191
191
|
└── delivery/<workflow-id>.json
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
## 10.
|
|
194
|
+
## 10. Plugin and user-instruction isolation
|
|
195
195
|
|
|
196
|
-
General engineering doctrine
|
|
197
|
-
|
|
196
|
+
General engineering doctrine and long-term personal preferences live in each
|
|
197
|
+
host's durable `CLAUDE.md` or `AGENTS.md`. Ultra never writes those files. Its
|
|
198
|
+
workflow doctrine remains in plugin-owned skills and is loaded only through an
|
|
199
|
+
explicit public invocation.
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Supported runtime names are `claude`, `codex`, `opencode`, and `kimi`. Apply creates a
|
|
205
|
-
timestamped backup, replaces only the marked block, and can migrate the old
|
|
206
|
-
Codex `## Ultra Builder Pro Runtime Contract` section without touching the next
|
|
207
|
-
user section. Plugin adapters themselves do not silently overwrite handbooks.
|
|
201
|
+
Installation and uninstall own only adapter-declared commands, skills, workers,
|
|
202
|
+
hooks, runtime assets, provenance, and host registration. Project authority is
|
|
203
|
+
created only by an explicit `ultra-init`, and uninstall never deletes `.ultra/`.
|
|
204
|
+
See [`PLUGIN-ISOLATION-CONTRACT.md`](./PLUGIN-ISOLATION-CONTRACT.md).
|
|
208
205
|
|
|
209
206
|
## 11. Verification
|
|
210
207
|
|