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.
Files changed (42) hide show
  1. package/CHANGELOG.md +77 -6
  2. package/README.md +380 -261
  3. package/adapters/_shared/codex-assets.cjs +24 -16
  4. package/adapters/_shared/file-ops.cjs +40 -3
  5. package/adapters/_shared/provenance.cjs +90 -6
  6. package/adapters/_shared/runtime-assets.cjs +0 -11
  7. package/adapters/claude.js +3 -1
  8. package/adapters/kimi.js +4 -30
  9. package/adapters/opencode.js +50 -14
  10. package/docs/AGENT-CONTEXT.md +11 -14
  11. package/docs/ARCHITECTURE.md +23 -15
  12. package/docs/DECISIONS.md +75 -0
  13. package/docs/LEGACY-CLI-CROSSWALK.md +3 -3
  14. package/docs/PLUGIN-ISOLATION-CONTRACT.md +54 -0
  15. package/docs/ROADMAP.md +13 -13
  16. package/docs/RUNTIME-COMPAT-MATRIX.md +24 -21
  17. package/docs/STATE-DB-ACCESS-POLICY.md +2 -2
  18. package/docs/WORKFLOW-LIFECYCLE.md +12 -10
  19. package/hooks/active_task_context.py +1 -1
  20. package/hooks/context_spine.py +1 -1
  21. package/hooks/health_check.py +19 -0
  22. package/hooks/pre_stop_check.py +1 -1
  23. package/hooks/workflow_checkpoint.py +1 -1
  24. package/hooks/workflow_context.py +1 -1
  25. package/hooks/workflow_resume.py +1 -1
  26. package/mcp-server/lib/workflow-state.cjs +67 -54
  27. package/mcp-server/server.cjs +19 -7
  28. package/orchestrator/dispatch-rules.cjs +1 -1
  29. package/package.json +4 -5
  30. package/skills/ultra-change/SKILL.md +4 -3
  31. package/skills/ultra-init/SKILL.md +2 -1
  32. package/skills/ultra-research/SKILL.md +7 -4
  33. package/spec/cli-protocol.md +4 -4
  34. package/spec/mcp-tools.yaml +6 -6
  35. package/spec/schemas/context-file.v4.5.schema.json +1 -1
  36. package/spec/schemas/mcp-tools.schema.json +3 -3
  37. package/spec/schemas/state-db.sql +3 -3
  38. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  39. package/adapters/_shared/handbook.cjs +0 -156
  40. package/bin/handbook.js +0 -54
  41. package/docs/PLAN.zh-CN.md +0 -1696
  42. 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 hookCommand(feature, ...args) {
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
- 'python3 "$PLUGIN_ROOT/hooks/adapters/codex.py"',
229
- JSON.stringify(feature),
230
- ...args.map((arg) => JSON.stringify(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(path.join(targetRoot, 'hooks.json'), JSON.stringify(buildHooksManifest(), null, 2) + '\n');
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 = `${file}.tmp-${crypto.randomUUID()}`;
26
- fs.writeFileSync(tmp, content);
27
- fs.renameSync(tmp, file);
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 = spawnSync('git', ['rev-parse', 'HEAD'], {
96
- cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
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: sourceCommit(repoRoot),
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: { repository, commit: commit || null },
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,
@@ -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': skillPolicy(name).userInvocable,
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?.skill === BOOTSTRAP_SKILL;
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,
@@ -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, body } = parseFm(buf.toString('utf8'));
103
- if (!fm) return Buffer.from(openCodeTextTransform(buf.toString('utf8'), commandName), 'utf8');
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
- return Buffer.from(serializeFm(nativeFm, openCodeTextTransform(body, commandName)), 'utf8');
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 transformed = withManagedMarker(openCodeCommandTransform(fs.readFileSync(source), file, name));
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 = skillsForRuntime('opencode');
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 skillsForRuntime('opencode')) {
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 ultra = path.join(current, ".ultra");
320
- if (fs.existsSync(ultra) && fs.statSync(ultra).isDirectory()) return current;
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
- if (!value?.breadcrumb || typeof value.text !== "string" || !value.text) {
339
- throw new Error("canonical breadcrumb response is incomplete");
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 (error) {
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 = skillsForRuntime('opencode').map((name) => ({
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);
@@ -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 the minimal
16
- Kimi session bootstrap required to establish the shared runtime boundary;
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. User handbook integration
194
+ ## 10. Plugin and user-instruction isolation
195
195
 
196
- General engineering doctrine remains user-owned in `CLAUDE.md` or `AGENTS.md`.
197
- Ultra Builder Pro contributes one managed section only. Preview or apply it with:
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
- ```bash
200
- ubp-handbook preview --runtime codex
201
- ubp-handbook apply --runtime codex
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