ultra-builder-pro-cli 0.25.1 → 0.26.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 (243) hide show
  1. package/.ultra-template/changes/abandoned/.gitkeep +0 -0
  2. package/.ultra-template/contexts/TEMPLATE.md +94 -0
  3. package/.ultra-template/decisions/.gitkeep +1 -0
  4. package/.ultra-template/evidence/.gitkeep +1 -0
  5. package/.ultra-template/north-star.md +46 -0
  6. package/.ultra-template/research/.gitkeep +1 -0
  7. package/.ultra-template/specs/discovery.md +2 -2
  8. package/.ultra-template/tasks.json +3 -0
  9. package/.ultra-template/test-report.json +27 -0
  10. package/CHANGELOG.md +107 -1
  11. package/README.md +217 -579
  12. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  13. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  14. package/adapters/_shared/file-ops.cjs +76 -2
  15. package/adapters/_shared/host-profile.cjs +97 -0
  16. package/adapters/_shared/plugin-core.cjs +204 -0
  17. package/adapters/_shared/runtime-assets.cjs +31 -96
  18. package/adapters/claude.js +92 -291
  19. package/adapters/codex.js +309 -842
  20. package/adapters/grok.js +143 -1469
  21. package/adapters/kimi.js +124 -1053
  22. package/adapters/opencode.js +230 -742
  23. package/bin/delegate-worker.cjs +328 -0
  24. package/bin/delegate.cjs +463 -0
  25. package/bin/install.js +38 -11
  26. package/docs/ARCHITECTURE.md +125 -216
  27. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  28. package/docs/DECISIONS.md +77 -87
  29. package/docs/PHILOSOPHY.md +208 -0
  30. package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
  32. package/docs/SKILL-AUTHORING.md +94 -73
  33. package/docs/WORKFLOW-LIFECYCLE.md +97 -189
  34. package/hooks/README.md +19 -37
  35. package/hooks/_common.py +113 -0
  36. package/hooks/adapters/codex.py +9 -16
  37. package/hooks/adapters/grok.py +5 -5
  38. package/hooks/adapters/kimi.py +8 -14
  39. package/hooks/block_dangerous_commands.py +201 -0
  40. package/hooks/compact_context.py +65 -0
  41. package/hooks/mid_workflow_recall.py +25 -0
  42. package/hooks/post_edit_guard.py +105 -0
  43. package/hooks/session_context.py +30 -0
  44. package/package.json +17 -52
  45. package/skills/ultra-change/SKILL.md +115 -50
  46. package/skills/ultra-change/references/change-contract.md +68 -3
  47. package/skills/ultra-delegate/SKILL.md +89 -0
  48. package/skills/ultra-delegate/references/delegation-contract.md +59 -0
  49. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  50. package/skills/ultra-deliver/SKILL.md +98 -42
  51. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  52. package/skills/ultra-dev/SKILL.md +115 -62
  53. package/skills/ultra-dev/references/debugging.md +22 -0
  54. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  55. package/skills/ultra-grilling/SKILL.md +118 -0
  56. package/skills/ultra-grilling/references/reframing.md +72 -0
  57. package/skills/ultra-init/SKILL.md +102 -37
  58. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  59. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  60. package/skills/ultra-plan/SKILL.md +101 -68
  61. package/skills/ultra-research/SKILL.md +68 -48
  62. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  63. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  64. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  65. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  66. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  67. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  68. package/skills/ultra-research/references/10-user-personas.md +3 -2
  69. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  70. package/skills/ultra-research/references/20-user-stories.md +4 -3
  71. package/skills/ultra-research/references/21-features-scope.md +3 -2
  72. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  73. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  74. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  75. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  76. package/skills/ultra-research/references/40-deployment.md +3 -3
  77. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  78. package/skills/ultra-research/references/99-synthesis.md +7 -7
  79. package/skills/ultra-review/SKILL.md +84 -32
  80. package/skills/ultra-review/references/code.md +17 -0
  81. package/skills/ultra-review/references/comments.md +12 -0
  82. package/skills/ultra-review/references/design.md +12 -0
  83. package/skills/ultra-review/references/errors.md +13 -0
  84. package/skills/ultra-review/references/spec.md +13 -0
  85. package/skills/ultra-review/references/tests.md +12 -0
  86. package/skills/ultra-review/references/worker-packet.md +40 -0
  87. package/skills/ultra-review/scripts/review_wait.py +26 -15
  88. package/skills/ultra-status/SKILL.md +61 -36
  89. package/skills/ultra-tdd/SKILL.md +81 -0
  90. package/skills/ultra-tdd/references/templates/README.md +20 -0
  91. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  92. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  93. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  94. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  95. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  96. package/skills/ultra-tdd/references/test-execution.md +18 -0
  97. package/skills/ultra-test/SKILL.md +75 -30
  98. package/skills/ultra-test/references/export-syntax.md +18 -0
  99. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  100. package/skills/ultra-think/SKILL.md +49 -27
  101. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  102. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  103. package/.ultra-template/docs/research/README.md +0 -55
  104. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  105. package/.ultra-template/reports/templates/test-report.json +0 -18
  106. package/.ultra-template/tasks/tasks.json +0 -15
  107. package/.ultra-template/templates/task-context.md +0 -19
  108. package/adapters/_shared/codex-assets.cjs +0 -725
  109. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  110. package/adapters/_shared/interaction-contract.cjs +0 -129
  111. package/adapters/_shared/path-rewrite.cjs +0 -79
  112. package/adapters/_shared/runtime-native.cjs +0 -132
  113. package/adapters/_shared/settings-merge.cjs +0 -76
  114. package/agents/code-reviewer.md +0 -42
  115. package/agents/debugger.md +0 -40
  116. package/agents/review-code.md +0 -49
  117. package/agents/review-comments.md +0 -38
  118. package/agents/review-coordinator.md +0 -56
  119. package/agents/review-design.md +0 -40
  120. package/agents/review-errors.md +0 -43
  121. package/agents/review-spec.md +0 -39
  122. package/agents/review-tests.md +0 -40
  123. package/agents/tdd-runner.md +0 -38
  124. package/bin/orchestrator.js +0 -383
  125. package/commands/ultra-change.md +0 -10
  126. package/commands/ultra-deliver.md +0 -10
  127. package/commands/ultra-dev.md +0 -10
  128. package/commands/ultra-doctor.md +0 -10
  129. package/commands/ultra-init.md +0 -10
  130. package/commands/ultra-plan.md +0 -10
  131. package/commands/ultra-research.md +0 -10
  132. package/commands/ultra-review.md +0 -10
  133. package/commands/ultra-status.md +0 -10
  134. package/commands/ultra-test.md +0 -10
  135. package/commands/ultra-think.md +0 -10
  136. package/docs/AGENT-CONTEXT.md +0 -191
  137. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  138. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  139. package/docs/ROADMAP.md +0 -101
  140. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  141. package/hooks/active_task_context.py +0 -152
  142. package/hooks/context_envelope.py +0 -100
  143. package/hooks/health_check.py +0 -196
  144. package/hooks/pre_stop_check.py +0 -66
  145. package/hooks/runtime_paths.py +0 -396
  146. package/hooks/subagent_tracker.py +0 -88
  147. package/hooks/workflow_checkpoint.py +0 -76
  148. package/hooks/workflow_context.py +0 -48
  149. package/hooks/workflow_resume.py +0 -84
  150. package/mcp-server/hook-context.cjs +0 -38
  151. package/mcp-server/hook-event.cjs +0 -34
  152. package/mcp-server/lib/archive-journal.cjs +0 -1119
  153. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  154. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  155. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  156. package/mcp-server/lib/canonical-json.cjs +0 -19
  157. package/mcp-server/lib/change-authority.cjs +0 -26
  158. package/mcp-server/lib/change-packet.cjs +0 -790
  159. package/mcp-server/lib/change-workflow.cjs +0 -65
  160. package/mcp-server/lib/context-envelope.cjs +0 -581
  161. package/mcp-server/lib/context-paths.cjs +0 -142
  162. package/mcp-server/lib/decision-records.cjs +0 -423
  163. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  164. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  165. package/mcp-server/lib/doctor.cjs +0 -627
  166. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  167. package/mcp-server/lib/init-project.cjs +0 -1021
  168. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  169. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  170. package/mcp-server/lib/managed-file-write.cjs +0 -172
  171. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  172. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  173. package/mcp-server/lib/plan-store.cjs +0 -794
  174. package/mcp-server/lib/prd-parser.cjs +0 -83
  175. package/mcp-server/lib/pricing.cjs +0 -44
  176. package/mcp-server/lib/project-context.cjs +0 -178
  177. package/mcp-server/lib/projector.cjs +0 -386
  178. package/mcp-server/lib/provider-refs.cjs +0 -42
  179. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  180. package/mcp-server/lib/runtime-state.cjs +0 -222
  181. package/mcp-server/lib/safe-project-file.cjs +0 -385
  182. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  183. package/mcp-server/lib/state-authority.cjs +0 -95
  184. package/mcp-server/lib/state-db.cjs +0 -1580
  185. package/mcp-server/lib/state-ops.cjs +0 -1428
  186. package/mcp-server/lib/task-contract.cjs +0 -404
  187. package/mcp-server/lib/task-expander.cjs +0 -105
  188. package/mcp-server/lib/task-ledger.cjs +0 -1964
  189. package/mcp-server/lib/telemetry.cjs +0 -85
  190. package/mcp-server/lib/topo.cjs +0 -131
  191. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  192. package/mcp-server/lib/worker-packet.cjs +0 -358
  193. package/mcp-server/server.cjs +0 -437
  194. package/orchestrator/auto-merge.cjs +0 -128
  195. package/orchestrator/daemon.cjs +0 -264
  196. package/orchestrator/dispatch-rules.cjs +0 -78
  197. package/orchestrator/parallel-orchestrator.cjs +0 -903
  198. package/orchestrator/planner/plan-builder.cjs +0 -125
  199. package/orchestrator/recovery.cjs +0 -188
  200. package/orchestrator/session-close-journal-worker.cjs +0 -5
  201. package/orchestrator/session-close-journal.cjs +0 -1014
  202. package/orchestrator/session-runner.cjs +0 -1153
  203. package/orchestrator/worktree-manager.cjs +0 -282
  204. package/settings.json +0 -7
  205. package/skills/cc-collab/SKILL.md +0 -42
  206. package/skills/code-review-expert/SKILL.md +0 -43
  207. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  208. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  209. package/skills/code-review-expert/references/removal-plan.md +0 -24
  210. package/skills/codex-collab/SKILL.md +0 -49
  211. package/skills/codex-collab/evals/evals.json +0 -35
  212. package/skills/integration-rules/SKILL.md +0 -32
  213. package/skills/security-rules/SKILL.md +0 -38
  214. package/skills/testing-rules/SKILL.md +0 -32
  215. package/skills/ultra-doctor/SKILL.md +0 -41
  216. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  217. package/skills/ultra-verify/SKILL.md +0 -46
  218. package/skills/ultra-verify/evals/evals.json +0 -46
  219. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  220. package/skills/ultra-verify/references/evidence-status.md +0 -21
  221. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  222. package/spec/cli-protocol.md +0 -102
  223. package/spec/mcp-tools.yaml +0 -312
  224. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  225. package/spec/schemas/change-delta.v1.schema.json +0 -153
  226. package/spec/schemas/command-manifest.schema.json +0 -29
  227. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  228. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  229. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  230. package/spec/schemas/mcp-tools.schema.json +0 -136
  231. package/spec/schemas/skill-manifest.schema.json +0 -24
  232. package/spec/schemas/state-db.sql +0 -641
  233. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  234. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  235. package/spec/schemas/test-report.v1.schema.json +0 -225
  236. package/ultra-tools/cli.cjs +0 -129
  237. package/ultra-tools/commands/db.cjs +0 -226
  238. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  239. package/ultra-tools/commands/migrate.cjs +0 -1386
  240. package/ultra-tools/commands/session.cjs +0 -213
  241. package/ultra-tools/commands/status.cjs +0 -559
  242. package/ultra-tools/commands/system.cjs +0 -498
  243. package/ultra-tools/commands/task.cjs +0 -105
@@ -0,0 +1,463 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { execFileSync, spawn } = require('node:child_process');
7
+
8
+ const { hostProfile } = require('../adapters/_shared/host-profile.cjs');
9
+ const { writeAtomic } = require('../adapters/_shared/file-ops.cjs');
10
+
11
+ const PERMISSION_SCHEMA = 'ultra-delegation-permission-v1';
12
+ const RECEIPT_SCHEMA = 'ultra-delegation-receipt-v1';
13
+ const RESULT_SCHEMA = 'ultra-delegation-result-v1';
14
+ const PERMISSION_FIELDS = new Set(['$schema', 'writable_roots', 'external_effects']);
15
+ const PROTECTED_ROOT = '.ultra';
16
+ const RESULT_JSON_SCHEMA = Object.freeze({
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ required: [
20
+ '$schema', 'status', 'summary', 'changed_files', 'checks', 'evidence',
21
+ 'questions', 'residual_risks',
22
+ ],
23
+ properties: {
24
+ $schema: { const: RESULT_SCHEMA },
25
+ status: { enum: ['finished', 'blocked', 'failed'] },
26
+ summary: { type: 'string', minLength: 1 },
27
+ changed_files: {
28
+ type: 'array', uniqueItems: true, items: { type: 'string', minLength: 1 },
29
+ },
30
+ checks: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ required: ['command', 'status'],
36
+ properties: {
37
+ command: { type: 'string', minLength: 1 },
38
+ status: { enum: ['passed', 'failed', 'not_run'] },
39
+ output_ref: { type: 'string', minLength: 1 },
40
+ },
41
+ },
42
+ },
43
+ evidence: { type: 'array', items: { type: 'string', minLength: 1 } },
44
+ questions: { type: 'array', items: { type: 'string', minLength: 1 } },
45
+ residual_risks: { type: 'array', items: { type: 'string', minLength: 1 } },
46
+ },
47
+ });
48
+
49
+ function inside(root, candidate) {
50
+ const relative = path.relative(root, candidate);
51
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`)
52
+ && !path.isAbsolute(relative);
53
+ }
54
+
55
+ function atOrInside(root, candidate) {
56
+ return candidate === root || inside(root, candidate);
57
+ }
58
+
59
+ function projectPath(projectRoot, value, label, { directory = false } = {}) {
60
+ const lexical = path.resolve(projectRoot, value);
61
+ const resolved = fs.existsSync(lexical) ? fs.realpathSync(lexical) : lexical;
62
+ if (!inside(projectRoot, resolved)) throw new Error(`${label} must be inside the current project`);
63
+ if (directory && (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory())) {
64
+ throw new Error(`${label} must be an existing directory`);
65
+ }
66
+ return resolved;
67
+ }
68
+
69
+ function sha256(file) {
70
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
71
+ }
72
+
73
+ function git(cwd, args) {
74
+ return execFileSync('git', args, {
75
+ cwd,
76
+ encoding: 'utf8',
77
+ maxBuffer: 16 * 1024 * 1024,
78
+ stdio: ['ignore', 'pipe', 'pipe'],
79
+ }).trim();
80
+ }
81
+
82
+ function observedChangedFiles(cwd, baseHead) {
83
+ const run = (args) => execFileSync('git', args, {
84
+ cwd,
85
+ encoding: 'buffer',
86
+ maxBuffer: 16 * 1024 * 1024,
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ }).toString('utf8').split('\0').filter(Boolean);
89
+ return [...new Set([
90
+ ...run(['diff', '--name-only', '-z', baseHead, '--']),
91
+ ...run(['ls-files', '--others', '--exclude-standard', '-z']),
92
+ ].map((file) => file.split(path.sep).join('/')))].sort();
93
+ }
94
+
95
+ function validateWorktree(worktree) {
96
+ let top;
97
+ try {
98
+ top = fs.realpathSync(git(worktree, ['rev-parse', '--show-toplevel']));
99
+ } catch {
100
+ throw new Error('worktree must be a real Git worktree');
101
+ }
102
+ if (top !== worktree) throw new Error('worktree must be the root of a real Git worktree');
103
+ const registered = git(worktree, ['worktree', 'list', '--porcelain'])
104
+ .split('\n')
105
+ .filter((line) => line.startsWith('worktree '))
106
+ .map((line) => line.slice('worktree '.length))
107
+ .some((entry) => {
108
+ try { return fs.realpathSync(entry) === worktree; } catch { return false; }
109
+ });
110
+ if (!registered) throw new Error('worktree is not registered as a Git worktree');
111
+ const dirty = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']);
112
+ if (dirty) throw new Error('delegated Git worktree must start clean');
113
+ return git(worktree, ['rev-parse', 'HEAD']);
114
+ }
115
+
116
+ function readPermission(file, worktree) {
117
+ let payload;
118
+ try {
119
+ payload = JSON.parse(fs.readFileSync(file, 'utf8'));
120
+ } catch {
121
+ throw new Error('permission must be valid JSON');
122
+ }
123
+ if (!payload || Array.isArray(payload) || typeof payload !== 'object') {
124
+ throw new Error('permission must be a JSON object');
125
+ }
126
+ for (const key of Object.keys(payload)) {
127
+ if (!PERMISSION_FIELDS.has(key)) throw new Error(`unknown permission field: ${key}`);
128
+ }
129
+ if (payload.$schema !== PERMISSION_SCHEMA) {
130
+ throw new Error(`permission $schema must be ${PERMISSION_SCHEMA}`);
131
+ }
132
+ if (!Array.isArray(payload.writable_roots)) {
133
+ throw new Error('permission writable_roots must be an array');
134
+ }
135
+ const writableRoots = [];
136
+ const writableRootPaths = [];
137
+ for (const value of payload.writable_roots) {
138
+ if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) {
139
+ throw new Error('permission writable_roots entries must be relative non-empty paths');
140
+ }
141
+ const resolved = path.resolve(worktree, value);
142
+ if (!atOrInside(worktree, resolved)) {
143
+ throw new Error('permission writable_roots must stay inside the delegated worktree');
144
+ }
145
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
146
+ throw new Error(`permission writable_roots entry must be an existing directory: ${value}`);
147
+ }
148
+ const real = fs.realpathSync(resolved);
149
+ if (!atOrInside(worktree, real)) {
150
+ throw new Error('permission writable_roots must not escape through symlinks');
151
+ }
152
+ const normalized = path.relative(worktree, real).split(path.sep).join('/') || '.';
153
+ // `.ultra` is the project's memory and has exactly one writer: the primary host.
154
+ // Rejected here so the delegation never starts, and again against the actual diff
155
+ // in the worker, which also covers the `.` grant.
156
+ if (normalized === PROTECTED_ROOT || normalized.startsWith(`${PROTECTED_ROOT}/`)) {
157
+ throw new Error(`permission writable_roots must not include ${PROTECTED_ROOT}; the primary host is its only writer`);
158
+ }
159
+ if (!writableRoots.includes(normalized)) {
160
+ writableRoots.push(normalized);
161
+ writableRootPaths.push(real);
162
+ }
163
+ }
164
+ if (!Array.isArray(payload.external_effects)) {
165
+ throw new Error('permission external_effects must be an array');
166
+ }
167
+ if (payload.external_effects.length !== 0) {
168
+ throw new Error('permission external_effects must be empty; the primary host performs separately authorized effects');
169
+ }
170
+ return { writableRoots, writableRootPaths };
171
+ }
172
+
173
+ function delegatePrompt({ instruction, permission, worktree, readOnly }) {
174
+ return [
175
+ 'Execute one bounded Ultra Builder Pro delegation in the isolated Git worktree.',
176
+ `Read the immutable instruction file: ${instruction}`,
177
+ `Read the immutable permission file: ${permission}`,
178
+ `Use this worktree as the only filesystem boundary: ${worktree}`,
179
+ readOnly
180
+ ? 'This is read-only delegation. Do not create, modify, or delete any project file.'
181
+ : 'Modify only paths covered by permission.writable_roots.',
182
+ 'Do not perform external effects, edit the primary checkout, or change Ultra task authority.',
183
+ `Return exactly one JSON object as the final response using $schema ${RESULT_SCHEMA}.`,
184
+ 'The exact fields are status, summary, changed_files, checks, evidence, questions, and residual_risks.',
185
+ 'status is finished, blocked, or failed. blocked requires at least one question.',
186
+ 'changed_files must exactly list every repository-relative path changed in the worktree.',
187
+ 'checks items use command, status (passed, failed, or not_run), and optional output_ref.',
188
+ 'Do not write the receipt yourself. The launcher validates structured output, process exit, permissions, and the actual Git diff before publication.',
189
+ ].join('\n');
190
+ }
191
+
192
+ function selectedBinary(runtime, fallback) {
193
+ return process.env[`UBP_DELEGATE_${runtime.toUpperCase()}_BIN`] || fallback;
194
+ }
195
+
196
+ function parsePairs(argv, allowed) {
197
+ const values = {};
198
+ for (let index = 0; index < argv.length; index += 2) {
199
+ const key = argv[index];
200
+ const value = argv[index + 1];
201
+ if (!allowed.has(key) || value === undefined || value === '') {
202
+ throw new Error(`invalid delegate argument: ${key || '<missing>'}`);
203
+ }
204
+ if (values[key]) throw new Error(`duplicate delegate argument: ${key}`);
205
+ values[key] = value;
206
+ }
207
+ return values;
208
+ }
209
+
210
+ function parseRun(argv) {
211
+ const values = parsePairs(argv, new Set([
212
+ '--to', '--instruction', '--permission', '--worktree', '--timeout',
213
+ ]));
214
+ for (const key of ['--to', '--instruction', '--permission', '--worktree']) {
215
+ if (!values[key]) throw new Error(`delegate run requires ${key}`);
216
+ }
217
+ const timeout = values['--timeout'] === undefined ? 1800 : Number(values['--timeout']);
218
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 86400) {
219
+ throw new Error('delegate --timeout must be greater than 0 and at most 86400 seconds');
220
+ }
221
+ return {
222
+ to: values['--to'], instruction: values['--instruction'],
223
+ permission: values['--permission'], worktree: values['--worktree'],
224
+ timeoutMs: Math.ceil(timeout * 1000),
225
+ };
226
+ }
227
+
228
+ function readJson(file) {
229
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
230
+ }
231
+
232
+ function processAlive(pid) {
233
+ if (!Number.isInteger(pid) || pid <= 0) return false;
234
+ try { process.kill(pid, 0); return true; } catch { return false; }
235
+ }
236
+
237
+ function run(argv, { projectRoot = process.cwd() } = {}) {
238
+ const args = parseRun(argv);
239
+ projectRoot = fs.realpathSync(projectRoot);
240
+ const profile = hostProfile(args.to);
241
+ const instruction = projectPath(projectRoot, args.instruction, 'instruction');
242
+ const permission = projectPath(projectRoot, args.permission, 'permission');
243
+ const worktree = projectPath(projectRoot, args.worktree, 'worktree', { directory: true });
244
+ if (!fs.statSync(instruction).isFile()) throw new Error('instruction must be a file');
245
+ if (!fs.statSync(permission).isFile()) throw new Error('permission must be a file');
246
+ if (path.dirname(instruction) !== path.dirname(permission)) {
247
+ throw new Error('instruction and permission must share one delegation directory');
248
+ }
249
+ const baseHead = validateWorktree(worktree);
250
+ const permissionContract = readPermission(permission, worktree);
251
+ const delegationRoot = path.dirname(instruction);
252
+ const delegationId = path.basename(delegationRoot);
253
+ const result = path.join(delegationRoot, 'result.json');
254
+ const receiptFile = path.join(delegationRoot, 'receipt.json');
255
+ const stdout = path.join(delegationRoot, 'stdout.log');
256
+ const stderr = path.join(delegationRoot, 'stderr.log');
257
+ const spec = path.join(delegationRoot, 'worker-spec.json');
258
+ const outputSchema = path.join(delegationRoot, 'result-schema.json');
259
+ const hostOutput = path.join(delegationRoot, 'host-result.json');
260
+ const lock = path.join(delegationRoot, 'run.lock');
261
+ const cancel = path.join(delegationRoot, 'cancel.request');
262
+ if (fs.existsSync(result)) throw new Error('delegation result.json already exists; use a new delegation id');
263
+ if (fs.existsSync(lock)) throw new Error('delegation is already running');
264
+ if (fs.existsSync(receiptFile) || fs.existsSync(spec) || fs.existsSync(outputSchema) || fs.existsSync(hostOutput)) {
265
+ throw new Error('delegation id already has run artifacts; cancel or use a new delegation id');
266
+ }
267
+ let lockFd;
268
+ try {
269
+ lockFd = fs.openSync(lock, 'wx', 0o600);
270
+ } catch (error) {
271
+ if (error.code === 'EEXIST') throw new Error('delegation is already running');
272
+ throw error;
273
+ }
274
+ const startedAt = new Date().toISOString();
275
+ const instructionDigest = sha256(instruction);
276
+ const permissionDigest = sha256(permission);
277
+ try {
278
+ fs.writeFileSync(lockFd, `${JSON.stringify({ pid: process.pid, started_at: startedAt })}\n`);
279
+ fs.closeSync(lockFd);
280
+ lockFd = undefined;
281
+ writeAtomic(outputSchema, `${JSON.stringify(RESULT_JSON_SCHEMA, null, 2)}\n`);
282
+ fs.chmodSync(outputSchema, 0o600);
283
+ const outputSchemaDigest = sha256(outputSchema);
284
+ const profileOptions = {
285
+ readOnly: permissionContract.writableRoots.length === 0,
286
+ writableRoots: permissionContract.writableRoots,
287
+ schemaFile: outputSchema,
288
+ schemaJson: JSON.stringify(RESULT_JSON_SCHEMA),
289
+ hostOutput,
290
+ };
291
+ const prompt = delegatePrompt({
292
+ instruction, permission, worktree, readOnly: profileOptions.readOnly,
293
+ });
294
+ const workerSpec = {
295
+ $schema: 'ultra-delegation-worker-spec-v1',
296
+ delegation_id: delegationId,
297
+ host: args.to,
298
+ command: selectedBinary(args.to, profile.binary),
299
+ args: profile.delegateArgv(prompt, worktree, profileOptions),
300
+ cwd: worktree,
301
+ instruction,
302
+ permission,
303
+ instruction_digest: instructionDigest,
304
+ permission_digest: permissionDigest,
305
+ output_schema: outputSchema,
306
+ output_schema_digest: outputSchemaDigest,
307
+ host_output: hostOutput,
308
+ read_only: profileOptions.readOnly,
309
+ writable_roots: permissionContract.writableRoots,
310
+ writable_root_paths: permissionContract.writableRootPaths,
311
+ base_head: baseHead,
312
+ started_at: startedAt,
313
+ timeout_ms: args.timeoutMs,
314
+ result,
315
+ stdout,
316
+ stderr,
317
+ lock,
318
+ cancel,
319
+ env: {
320
+ ...(profile.delegateEnv ? profile.delegateEnv(profileOptions) : {}),
321
+ UBP_DELEGATE_INSTRUCTION_FILE: instruction,
322
+ UBP_DELEGATE_PERMISSION_FILE: permission,
323
+ UBP_DELEGATE_WORKTREE: worktree,
324
+ },
325
+ };
326
+ writeAtomic(spec, `${JSON.stringify(workerSpec, null, 2)}\n`);
327
+ fs.chmodSync(spec, 0o600);
328
+ const specDigest = sha256(spec);
329
+ const worker = spawn(process.execPath, [
330
+ path.join(__dirname, 'delegate-worker.cjs'), spec, specDigest,
331
+ ], {
332
+ cwd: projectRoot,
333
+ detached: true,
334
+ stdio: 'ignore',
335
+ env: process.env,
336
+ });
337
+ if (!worker.pid) throw new Error('failed to start delegation worker');
338
+ worker.unref();
339
+ const receipt = {
340
+ $schema: RECEIPT_SCHEMA,
341
+ status: 'started',
342
+ delegation_id: delegationId,
343
+ host: args.to,
344
+ worker_pid: worker.pid,
345
+ instruction_digest: instructionDigest,
346
+ permission_digest: permissionDigest,
347
+ output_schema_digest: outputSchemaDigest,
348
+ spec_digest: specDigest,
349
+ base_head: baseHead,
350
+ started_at: startedAt,
351
+ timeout_ms: args.timeoutMs,
352
+ result_file: result,
353
+ read_only: profileOptions.readOnly,
354
+ stdout_file: stdout,
355
+ stderr_file: stderr,
356
+ cancel_command: `ubp delegate cancel --delegation ${delegationRoot}`,
357
+ };
358
+ writeAtomic(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
359
+ writeAtomic(lock, `${JSON.stringify({ pid: worker.pid, started_at: startedAt })}\n`);
360
+ fs.chmodSync(lock, 0o600);
361
+ if (fs.existsSync(result)) {
362
+ try { fs.unlinkSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
363
+ }
364
+ return receipt;
365
+ } catch (error) {
366
+ if (lockFd !== undefined) fs.closeSync(lockFd);
367
+ try { fs.unlinkSync(lock); } catch (unlinkError) { if (unlinkError.code !== 'ENOENT') throw unlinkError; }
368
+ for (const file of [spec, outputSchema, hostOutput]) {
369
+ try { fs.unlinkSync(file); } catch (unlinkError) { if (unlinkError.code !== 'ENOENT') throw unlinkError; }
370
+ }
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ function delegationPath(projectRoot, value) {
376
+ return projectPath(projectRoot, value, 'delegation', { directory: true });
377
+ }
378
+
379
+ function status(argv, { projectRoot = process.cwd() } = {}) {
380
+ const values = parsePairs(argv, new Set(['--delegation']));
381
+ if (!values['--delegation']) throw new Error('delegate status requires --delegation');
382
+ projectRoot = fs.realpathSync(projectRoot);
383
+ const root = delegationPath(projectRoot, values['--delegation']);
384
+ const result = path.join(root, 'result.json');
385
+ if (fs.existsSync(result)) return readJson(result);
386
+ const receiptFile = path.join(root, 'receipt.json');
387
+ const receipt = fs.existsSync(receiptFile) ? readJson(receiptFile) : null;
388
+ const lockFile = path.join(root, 'run.lock');
389
+ const lock = fs.existsSync(lockFile) ? readJson(lockFile) : null;
390
+ const cancelling = fs.existsSync(path.join(root, 'cancel.request'));
391
+ return {
392
+ $schema: 'ultra-delegation-status-v1',
393
+ status: cancelling ? 'cancelling' : lock && processAlive(lock.pid) ? 'running' : receipt ? 'stalled' : 'not_started',
394
+ delegation_id: path.basename(root),
395
+ worker_pid: lock?.pid || receipt?.worker_pid || null,
396
+ result_file: result,
397
+ repair: receipt ? `ubp delegate cancel --delegation ${root}` : null,
398
+ };
399
+ }
400
+
401
+ function failureFromSpec(spec, failureType, summary) {
402
+ let finalHead = null;
403
+ let changedFiles = [];
404
+ try { finalHead = git(spec.cwd, ['rev-parse', 'HEAD']); } catch {}
405
+ try { changedFiles = observedChangedFiles(spec.cwd, spec.base_head); } catch {}
406
+ return {
407
+ $schema: RESULT_SCHEMA,
408
+ status: 'failed',
409
+ summary,
410
+ changed_files: changedFiles,
411
+ checks: [],
412
+ evidence: [],
413
+ questions: [],
414
+ residual_risks: [],
415
+ failure_type: failureType,
416
+ delegation_id: spec.delegation_id,
417
+ host: spec.host,
418
+ instruction_digest: spec.instruction_digest,
419
+ permission_digest: spec.permission_digest,
420
+ output_schema_digest: spec.output_schema_digest,
421
+ base_head: spec.base_head,
422
+ final_head: finalHead,
423
+ started_at: spec.started_at,
424
+ finished_at: new Date().toISOString(),
425
+ exit_code: null,
426
+ };
427
+ }
428
+
429
+ function cancel(argv, { projectRoot = process.cwd() } = {}) {
430
+ const values = parsePairs(argv, new Set(['--delegation']));
431
+ if (!values['--delegation']) throw new Error('delegate cancel requires --delegation');
432
+ projectRoot = fs.realpathSync(projectRoot);
433
+ const root = delegationPath(projectRoot, values['--delegation']);
434
+ const result = path.join(root, 'result.json');
435
+ if (fs.existsSync(result)) return { status: 'already_terminal', result: readJson(result) };
436
+ const specFile = path.join(root, 'worker-spec.json');
437
+ if (!fs.existsSync(specFile)) throw new Error('delegation has no worker spec to cancel');
438
+ const spec = readJson(specFile);
439
+ const request = path.join(root, 'cancel.request');
440
+ writeAtomic(request, `${JSON.stringify({ requested_at: new Date().toISOString() })}\n`);
441
+ const receiptFile = path.join(root, 'receipt.json');
442
+ const receipt = fs.existsSync(receiptFile) ? readJson(receiptFile) : null;
443
+ if (!processAlive(receipt?.worker_pid)) {
444
+ writeAtomic(result, `${JSON.stringify(failureFromSpec(spec, 'cancelled', 'Delegation cancelled after its worker stopped.'), null, 2)}\n`);
445
+ try { fs.unlinkSync(path.join(root, 'run.lock')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
446
+ }
447
+ return {
448
+ $schema: 'ultra-delegation-cancel-v1',
449
+ status: 'cancel_requested',
450
+ delegation_id: path.basename(root),
451
+ result_file: result,
452
+ };
453
+ }
454
+
455
+ function handle(argv, options) {
456
+ if (argv[0] !== 'delegate') throw new Error('delegate command is required');
457
+ if (argv[1] === 'run') return run(argv.slice(2), options);
458
+ if (argv[1] === 'status') return status(argv.slice(2), options);
459
+ if (argv[1] === 'cancel') return cancel(argv.slice(2), options);
460
+ throw new Error('usage: ubp delegate <run|status|cancel> ...');
461
+ }
462
+
463
+ module.exports = { handle, run, status, cancel };
package/bin/install.js CHANGED
@@ -3,10 +3,9 @@
3
3
  /**
4
4
  * ultra-builder-pro-cli — multi-runtime installer.
5
5
  *
6
- * Distributes Ultra Builder Pro assets — commands, agents, skills, hooks,
7
- * MCP server — to Claude Code, OpenCode, Codex CLI, Kimi Code, and Grok Build via
8
- * runtime-specific adapters under adapters/. Install is idempotent and
9
- * uses atomic writes; uninstall reverses via sentinel/manifest blocks.
6
+ * Distributes fourteen Skills and five hooks to Claude Code, OpenCode, Codex CLI,
7
+ * Kimi Code, and Grok Build through runtime-specific adapters under adapters/.
8
+ * Install is idempotent and atomic; uninstall removes only managed assets.
10
9
  *
11
10
  * Usage:
12
11
  * npx ultra-builder-pro-cli [options]
@@ -14,7 +13,7 @@
14
13
  * --claude / --opencode / --codex / --kimi / --grok select runtime(s)
15
14
  * --all install to all supported runtimes
16
15
  * -g, --global install to runtime's global config dir
17
- * -l, --local install into current working directory
16
+ * -l, --local project scope where the host supports it
18
17
  * -u, --uninstall remove installed assets
19
18
  * -d, --doctor inspect installation provenance without mutation
20
19
  * --json emit machine-readable doctor output
@@ -77,13 +76,13 @@ function printHelp() {
77
76
 
78
77
  ${paint('yellow', 'Scope:')}
79
78
  ${paint('cyan', '-g, --global')} install to runtime's global config directory
80
- ${paint('cyan', '-l, --local')} install to current working directory
79
+ ${paint('cyan', '-l, --local')} project scope (Claude, OpenCode, and Codex)
81
80
 
82
81
  ${paint('yellow', 'Other:')}
83
82
  ${paint('cyan', '-u, --uninstall')} remove installed assets
84
83
  ${paint('cyan', '-d, --doctor')} verify installed version, provenance, managed assets, and live entry points
85
84
  ${paint('cyan', '--json')} emit machine-readable doctor output (doctor only)
86
- ${paint('cyan', '-c, --config-dir')} override runtime's config directory (string path)
85
+ ${paint('cyan', '-c, --config-dir')} override one runtime's config directory; multi-runtime installs use <path>/<runtime>
87
86
  ${paint('cyan', '-h, --help')} show this help
88
87
  ${paint('cyan', '-v, --version')} show CLI version
89
88
 
@@ -91,8 +90,8 @@ function printHelp() {
91
90
  ${paint('dim', '# Install to Claude Code globally')}
92
91
  npx ultra-builder-pro-cli --claude --global
93
92
 
94
- ${paint('dim', '# Install to all supported runtimes locally in this repo')}
95
- npx ultra-builder-pro-cli --all --local
93
+ ${paint('dim', '# Install all five native plugins in their user scopes')}
94
+ npx ultra-builder-pro-cli --all --global
96
95
 
97
96
  ${paint('dim', '# Uninstall from OpenCode')}
98
97
  npx ultra-builder-pro-cli --opencode --global --uninstall
@@ -192,6 +191,11 @@ function resolveScope(flags) {
192
191
  }
193
192
 
194
193
  async function main() {
194
+ if (process.argv[2] === 'delegate') {
195
+ const { handle } = require('./delegate.cjs');
196
+ process.stdout.write(`${JSON.stringify(handle(process.argv.slice(2)))}\n`);
197
+ return;
198
+ }
195
199
  const { runtimes, flags } = parseArgs(process.argv);
196
200
 
197
201
  if (flags.version) {
@@ -215,6 +219,10 @@ async function main() {
215
219
  const scope = resolveScope(flags);
216
220
  const repoRoot = path.resolve(__dirname, '..');
217
221
  const configDir = flags.configDir ? expandTilde(flags.configDir) : null;
222
+ const userScopedOnly = runtimes.filter((runtime) => ['kimi', 'grok'].includes(runtime));
223
+ if (scope === 'local' && !configDir && userScopedOnly.length) {
224
+ bail(`${userScopedOnly.join(', ')} plugins are user-scoped by their hosts; use --global or an isolated --config-dir`);
225
+ }
218
226
 
219
227
  if (!flags.json) printBanner();
220
228
 
@@ -223,16 +231,34 @@ async function main() {
223
231
  console.log(` ${paint('bold', 'Mode:')} ${mode}`);
224
232
  console.log(` ${paint('bold', 'Scope:')} ${scope}`);
225
233
  console.log(` ${paint('bold', 'Runtimes:')} ${runtimes.join(', ')}`);
226
- if (configDir) console.log(` ${paint('bold', 'ConfigDir:')} ${configDir}`);
234
+ if (configDir) {
235
+ const suffix = runtimes.length > 1 ? '/<runtime>' : '';
236
+ console.log(` ${paint('bold', 'ConfigDir:')} ${configDir}${suffix}`);
237
+ }
227
238
  console.log();
228
239
  }
229
240
 
230
- const ctx = { repoRoot, scope, configDir, homeDir: os.homedir() };
241
+ // A config-dir override is also the isolation root for host-owned sidecars
242
+ // that live outside the primary config directory (for example Codex's
243
+ // plugin source and personal marketplace). Without this, a sandbox install
244
+ // can still mutate the caller's real HOME.
245
+ const contextFor = (runtime) => {
246
+ const runtimeConfigDir = configDir && runtimes.length > 1
247
+ ? path.join(configDir, runtime)
248
+ : configDir;
249
+ return {
250
+ repoRoot,
251
+ scope,
252
+ configDir: runtimeConfigDir,
253
+ homeDir: runtimeConfigDir || os.homedir(),
254
+ };
255
+ };
231
256
 
232
257
  if (flags.doctor) {
233
258
  const reports = [];
234
259
  for (const runtime of runtimes) {
235
260
  const adapter = loadAdapter(runtime);
261
+ const ctx = contextFor(runtime);
236
262
  try {
237
263
  if (typeof adapter.doctor !== 'function') throw new Error('adapter does not expose doctor()');
238
264
  reports.push(await adapter.doctor(ctx));
@@ -270,6 +296,7 @@ async function main() {
270
296
  let failed = 0;
271
297
  for (const runtime of runtimes) {
272
298
  const adapter = loadAdapter(runtime);
299
+ const ctx = contextFor(runtime);
273
300
  try {
274
301
  console.log(` ${paint('cyan', '▸')} ${runtime} — starting ${mode}...`);
275
302
  await adapter[mode](ctx);