ultra-builder-pro-cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,259 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
-
9
- const kimi = require('../kimi.js');
10
- const {
11
- CORE_PUBLIC_SKILLS,
12
- WORKFLOW_HOOK_FILES,
13
- skillsForRuntime,
14
- } = require('../_shared/runtime-assets.cjs');
15
-
16
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
17
- const PLUGIN_ID = 'ultra-builder-pro';
18
-
19
- function mkTarget() {
20
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-'));
21
- }
22
-
23
- function layout(home) {
24
- return {
25
- pluginRoot: path.join(home, 'plugins', 'managed', PLUGIN_ID),
26
- registryFile: path.join(home, 'plugins', 'installed.json'),
27
- };
28
- }
29
-
30
- function readJson(file) {
31
- return JSON.parse(fs.readFileSync(file, 'utf8'));
32
- }
33
-
34
- test('install builds and registers one Kimi-native plugin without changing config.toml', () => {
35
- const home = mkTarget();
36
- const { pluginRoot, registryFile } = layout(home);
37
- const configFile = path.join(home, 'config.toml');
38
- const userConfig = '[[hooks]]\nevent = "Stop"\ncommand = "/user/hook.sh"\n';
39
- try {
40
- fs.writeFileSync(configFile, userConfig);
41
- const report = kimi.install({ configDir: home, repoRoot: REPO_ROOT });
42
-
43
- assert.equal(report.target, pluginRoot);
44
- assert.equal(fs.readFileSync(configFile, 'utf8'), userConfig);
45
- assert.ok(fs.existsSync(path.join(pluginRoot, '.ubp-managed')));
46
- assert.ok(fs.existsSync(path.join(pluginRoot, 'provenance.json')));
47
-
48
- const registry = readJson(registryFile);
49
- assert.equal(registry.version, 1);
50
- assert.equal(registry.plugins.length, 1);
51
- assert.deepEqual(registry.plugins[0], {
52
- id: PLUGIN_ID,
53
- root: pluginRoot,
54
- source: 'local-path',
55
- enabled: true,
56
- installedAt: registry.plugins[0].installedAt,
57
- updatedAt: registry.plugins[0].updatedAt,
58
- originalSource: REPO_ROOT,
59
- });
60
- } finally {
61
- fs.rmSync(home, { recursive: true, force: true });
62
- }
63
- });
64
-
65
- test('Kimi manifest exposes native skills, commands, hooks, session bootstrap, and MCP', () => {
66
- const home = mkTarget();
67
- const { pluginRoot } = layout(home);
68
- try {
69
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
70
- const manifest = readJson(path.join(pluginRoot, 'kimi.plugin.json'));
71
-
72
- assert.equal(manifest.name, PLUGIN_ID);
73
- assert.deepEqual(manifest.skills, ['./skills']);
74
- assert.deepEqual(manifest.commands, ['./commands']);
75
- assert.deepEqual(manifest.sessionStart, { skill: 'using-ultra-builder-pro' });
76
- assert.equal(manifest.agents, undefined, 'Kimi 0.26/0.27 has no custom-agent manifest field');
77
- assert.deepEqual(manifest.mcpServers[PLUGIN_ID], {
78
- transport: 'stdio',
79
- command: process.platform === 'win32' ? 'node.exe' : 'env',
80
- args: process.platform === 'win32'
81
- ? ['./runtime/launch.cjs']
82
- : ['node', './runtime/launch.cjs'],
83
- enabled: true,
84
- });
85
-
86
- const hookText = JSON.stringify(manifest.hooks);
87
- assert.match(hookText, /hooks\/adapters\/kimi\.py/);
88
- for (const hook of WORKFLOW_HOOK_FILES.filter((value) => value !== 'context_spine.py')) {
89
- assert.match(hookText, new RegExp(hook.replace('.', '\\.')));
90
- }
91
- assert.ok(fs.existsSync(path.join(pluginRoot, 'hooks', 'context_spine.py')));
92
- assert.doesNotMatch(hookText, /memory|recall|journal|prompt[_ -]?capture|block_dangerous|post_edit_guard/i);
93
-
94
- const commands = fs.readdirSync(path.join(pluginRoot, 'commands')).sort();
95
- assert.deepEqual(commands, CORE_PUBLIC_SKILLS.map((name) => `${name}.md`).sort());
96
- const commandText = fs.readFileSync(path.join(pluginRoot, 'commands', 'ultra-init.md'), 'utf8');
97
- assert.match(commandText, /Use the registered `ultra-init` skill/);
98
- assert.match(commandText, /\$ARGUMENTS/);
99
- } finally {
100
- fs.rmSync(home, { recursive: true, force: true });
101
- }
102
- });
103
-
104
- test('Kimi assets are allowlisted and adapted to native tools and paths', () => {
105
- const home = mkTarget();
106
- const { pluginRoot } = layout(home);
107
- try {
108
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
109
-
110
- const skills = fs.readdirSync(path.join(pluginRoot, 'skills'), { withFileTypes: true })
111
- .filter((entry) => entry.isDirectory()
112
- && fs.existsSync(path.join(pluginRoot, 'skills', entry.name, 'SKILL.md')))
113
- .map((entry) => entry.name)
114
- .sort();
115
- assert.deepEqual(skills, [...skillsForRuntime('kimi'), 'using-ultra-builder-pro'].sort());
116
-
117
- const learn = fs.readFileSync(path.join(pluginRoot, 'skills', 'learn', 'SKILL.md'), 'utf8');
118
- const review = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
119
- const init = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-init', 'SKILL.md'), 'utf8');
120
- const bootstrap = fs.readFileSync(
121
- path.join(pluginRoot, 'skills', 'using-ultra-builder-pro', 'SKILL.md'),
122
- 'utf8',
123
- );
124
- assert.match(learn, /`~\/.kimi-code\/skills`/);
125
- assert.doesNotMatch(learn, /_unverified|learned-[^\s/]*-unverified/i);
126
- assert.match(review, /Kimi `AgentSwarm`/);
127
- assert.match(review, /\$KIMI_PLUGIN_ROOT\/agents\//);
128
- assert.match(review, /scripts\/review_wait\.py/);
129
- assert.match(review, /spec_fidelity/);
130
- assert.match(review, /engineering_standards/);
131
- assert.doesNotMatch(review, /background mode|run_in_background/);
132
- assert.match(init, /task\.init_project/);
133
- assert.doesNotMatch(init, /Claude Code|OpenCode|Codex/);
134
- assert.match(bootstrap, /\.ultra\/state\.db/);
135
- assert.match(bootstrap, /external providers/);
136
- assert.match(fs.readFileSync(path.join(pluginRoot, 'skills', 'codex-collab', 'SKILL.md'), 'utf8'), /--ephemeral/);
137
-
138
- const allPromptAssets = [];
139
- for (const root of ['commands', 'skills', 'agents']) {
140
- const pending = [path.join(pluginRoot, root)];
141
- while (pending.length) {
142
- const current = pending.pop();
143
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
144
- const file = path.join(current, entry.name);
145
- if (entry.isDirectory()) pending.push(file);
146
- else if (entry.isFile() && /\.(?:md|json)$/.test(entry.name)) allPromptAssets.push(file);
147
- }
148
- }
149
- }
150
- const foreign = /~\/.claude|~\/.codex|~\/.config\/opencode|CLAUDE\.md|TaskCreate|TaskUpdate|TaskList|AskUserQuestion|run_in_background:\s*true|\$CLAUDE_PLUGIN_ROOT|Kimi `Shell`/;
151
- for (const file of allPromptAssets) {
152
- const text = fs.readFileSync(file, 'utf8');
153
- assert.doesNotMatch(text, foreign, file);
154
- assert.doesNotMatch(text, /(^|[\s`("'])\/ultra-(?!builder-pro:)[a-z]/m, file);
155
- assert.doesNotMatch(text, /[\u3400-\u9fff]|ultra-review-findings-v1|Context7|Exa MCP|confidence\s*>=?\s*\d+/iu, file);
156
- }
157
- } finally {
158
- fs.rmSync(home, { recursive: true, force: true });
159
- }
160
- });
161
-
162
- test('reinstall preserves unrelated records and existing Kimi capability choices', () => {
163
- const home = mkTarget();
164
- const { registryFile } = layout(home);
165
- try {
166
- fs.mkdirSync(path.dirname(registryFile), { recursive: true });
167
- fs.writeFileSync(registryFile, JSON.stringify({
168
- version: 1,
169
- plugins: [{
170
- id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
171
- installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
172
- }],
173
- }, null, 2));
174
-
175
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
176
- const first = readJson(registryFile);
177
- const ultra = first.plugins.find((entry) => entry.id === PLUGIN_ID);
178
- ultra.enabled = false;
179
- ultra.capabilities = { mcpServers: { [PLUGIN_ID]: { enabled: false } } };
180
- fs.writeFileSync(registryFile, JSON.stringify(first, null, 2));
181
-
182
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
183
- const second = readJson(registryFile);
184
- assert.deepEqual(second.plugins.map((entry) => entry.id).sort(), ['mine', PLUGIN_ID]);
185
- const refreshed = second.plugins.find((entry) => entry.id === PLUGIN_ID);
186
- assert.equal(refreshed.installedAt, ultra.installedAt);
187
- assert.equal(refreshed.enabled, false);
188
- assert.deepEqual(refreshed.capabilities, ultra.capabilities);
189
- } finally {
190
- fs.rmSync(home, { recursive: true, force: true });
191
- }
192
- });
193
-
194
- test('uninstall removes only the owned Kimi plugin record and managed root', () => {
195
- const home = mkTarget();
196
- const { pluginRoot, registryFile } = layout(home);
197
- try {
198
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
199
- const registry = readJson(registryFile);
200
- registry.plugins.unshift({
201
- id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
202
- installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
203
- });
204
- fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
205
-
206
- kimi.uninstall({ configDir: home });
207
- assert.equal(fs.existsSync(pluginRoot), false);
208
- assert.deepEqual(readJson(registryFile).plugins.map((entry) => entry.id), ['mine']);
209
- } finally {
210
- fs.rmSync(home, { recursive: true, force: true });
211
- }
212
- });
213
-
214
- test('install and uninstall refuse to replace or remove an unmanaged Kimi root', () => {
215
- const home = mkTarget();
216
- const { pluginRoot, registryFile } = layout(home);
217
- try {
218
- fs.mkdirSync(pluginRoot, { recursive: true });
219
- fs.writeFileSync(path.join(pluginRoot, 'user.txt'), 'keep');
220
- assert.throws(
221
- () => kimi.install({ configDir: home, repoRoot: REPO_ROOT }),
222
- /unmanaged Kimi plugin/,
223
- );
224
-
225
- fs.mkdirSync(path.dirname(registryFile), { recursive: true });
226
- fs.writeFileSync(registryFile, JSON.stringify({
227
- version: 1,
228
- plugins: [{
229
- id: PLUGIN_ID, root: pluginRoot, source: 'local-path', enabled: true,
230
- installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/foreign',
231
- }],
232
- }));
233
- assert.throws(() => kimi.uninstall({ configDir: home }), /unmanaged Kimi plugin/);
234
- assert.equal(fs.readFileSync(path.join(pluginRoot, 'user.txt'), 'utf8'), 'keep');
235
- } finally {
236
- fs.rmSync(home, { recursive: true, force: true });
237
- }
238
- });
239
-
240
- test('doctor validates registration and provenance without mutating the Kimi home', () => {
241
- const home = mkTarget();
242
- const { registryFile } = layout(home);
243
- try {
244
- kimi.install({ configDir: home, repoRoot: REPO_ROOT });
245
- const before = fs.readFileSync(registryFile);
246
- const healthy = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
247
- assert.equal(healthy.status, 'healthy', JSON.stringify(healthy, null, 2));
248
- assert.deepEqual(fs.readFileSync(registryFile), before);
249
-
250
- const registry = readJson(registryFile);
251
- registry.plugins[0].enabled = false;
252
- fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
253
- const degraded = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
254
- assert.equal(degraded.status, 'degraded');
255
- assert.ok(degraded.issues.some((issue) => issue.code === 'PLUGIN_REGISTRATION_INVALID'));
256
- } finally {
257
- fs.rmSync(home, { recursive: true, force: true });
258
- }
259
- });
@@ -1,371 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { execFileSync } = require('node:child_process');
9
- const { pathToFileURL } = require('node:url');
10
-
11
- const opencode = require('../opencode.js');
12
- const { parse: parseFm } = require('../_shared/frontmatter.cjs');
13
- const { skillsForRuntime } = require('../_shared/runtime-assets.cjs');
14
-
15
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
16
-
17
- function mkTarget() {
18
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-opencode-'));
19
- }
20
-
21
- test('install builds a native OpenCode plugin and copies only allowlisted skills', () => {
22
- const target = mkTarget();
23
- try {
24
- const r = opencode.install({ configDir: target, repoRoot: REPO_ROOT });
25
- assert.ok(r.copied.commands.includes('ultra-init.md'));
26
- assert.ok(r.copied.skills.some((p) => p.includes('ultra-init/SKILL.md')));
27
- assert.ok(r.copied.plugins.includes('ultra-builder-pro.js'));
28
- assert.ok(fs.existsSync(path.join(target, 'plugins', 'ultra-builder-pro.js')));
29
- assert.deepEqual(
30
- fs.readdirSync(path.join(target, 'skills'), { withFileTypes: true })
31
- .filter((entry) => entry.isDirectory())
32
- .map((entry) => entry.name)
33
- .sort(),
34
- skillsForRuntime('opencode').sort(),
35
- );
36
-
37
- // OpenCode commands expose only fields accepted by the native command contract.
38
- const dst = fs.readFileSync(path.join(target, 'commands', 'ultra-init.md'), 'utf8');
39
- const { fm: dstFm } = parseFm(dst);
40
- assert.deepEqual(Object.keys(dstFm), ['description']);
41
-
42
- // forge an upper-case key to prove the transform works
43
- const hack = path.join(target, 'commands', 'upper.md');
44
- fs.writeFileSync(hack, '---\nDescription: mixed\n---\nbody\n');
45
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
46
- // reinstall re-runs the transform; the hack was outside commands/ in repo so
47
- // install doesn't touch it; we verify transform via a direct call:
48
- const { lowercaseKeys } = require('../_shared/frontmatter.cjs');
49
- assert.deepEqual(lowercaseKeys({ Description: 'x', Tags: ['A'] }), { description: 'x', tags: ['A'] });
50
- } finally {
51
- fs.rmSync(target, { recursive: true, force: true });
52
- }
53
- });
54
-
55
- test('install performs content-level OpenCode adaptation for commands, skills, references, and agents', () => {
56
- const target = mkTarget();
57
- try {
58
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
59
-
60
- const plan = fs.readFileSync(path.join(target, 'skills', 'ultra-plan', 'SKILL.md'), 'utf8');
61
- const learn = fs.readFileSync(path.join(target, 'skills', 'learn', 'SKILL.md'), 'utf8');
62
- const review = fs.readFileSync(path.join(target, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
63
- const codexCollab = fs.readFileSync(path.join(target, 'skills', 'codex-collab', 'SKILL.md'), 'utf8');
64
- const verify = fs.readFileSync(path.join(target, 'skills', 'ultra-verify', 'SKILL.md'), 'utf8');
65
-
66
- assert.deepEqual(Object.keys(parseFm(plan).fm), ['name', 'description']);
67
- assert.match(learn, /`~\/.config\/opencode\/skills`/);
68
- assert.doesNotMatch(learn, /_unverified|learned-[^\s/]*-unverified/i);
69
- assert.match(review, /OpenCode `task` tool/);
70
- assert.match(review, /scripts\/review_wait\.py/);
71
- assert.match(codexCollab, /OpenCode remains primary/);
72
- assert.match(verify, /OpenCode writes the first analysis/);
73
- assert.match(verify, /opencode-analysis\.md/);
74
- assert.match(verify, /codex exec -s read-only/);
75
- assert.match(verify, /--ignore-user-config/);
76
- assert.doesNotMatch(plan, /LEGACY_STATE_MIGRATION_REQUIRED|v4\.4|v4\.5/);
77
- assert.match(plan, /never read or\s+write .*tasks\.json/i);
78
- assert.doesNotMatch(plan, /ultra-tools task create/);
79
-
80
- const markdown = [];
81
- for (const root of ['commands', 'skills', 'agents']) {
82
- const pending = [path.join(target, root)];
83
- while (pending.length) {
84
- const current = pending.pop();
85
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
86
- const file = path.join(current, entry.name);
87
- if (entry.isDirectory()) pending.push(file);
88
- else if (entry.isFile() && entry.name.endsWith('.md')) markdown.push(file);
89
- }
90
- }
91
- }
92
- const incompatible = /~\/.claude|CLAUDE\.md|AskUserQuestion|TaskCreate|TaskUpdate|TaskList|run_in_background:\s*true|--yolo|--full-auto|\/codex:|ask\.question|review\.run/;
93
- for (const file of markdown) {
94
- const text = fs.readFileSync(file, 'utf8');
95
- assert.doesNotMatch(text, incompatible, file);
96
- assert.doesNotMatch(text, /[\u3400-\u9fff]|ultra-review-findings-v1|Context7|Exa MCP|confidence\s*>=?\s*\d+/iu, file);
97
- }
98
- } finally {
99
- fs.rmSync(target, { recursive: true, force: true });
100
- }
101
- });
102
-
103
- test('install translates Claude agent metadata to native OpenCode agent contracts', () => {
104
- const target = mkTarget();
105
- try {
106
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
107
-
108
- const reviewTests = parseFm(
109
- fs.readFileSync(path.join(target, 'agents', 'review-tests.md'), 'utf8'),
110
- ).fm;
111
- assert.equal(reviewTests.mode, 'subagent');
112
- assert.equal(reviewTests.steps, 18);
113
- assert.equal(reviewTests.name, undefined);
114
- assert.equal(reviewTests.model, undefined);
115
- assert.equal(reviewTests.maxturns, undefined);
116
- assert.equal(reviewTests.tools, undefined);
117
- assert.equal(reviewTests.skills, undefined);
118
- assert.deepEqual(reviewTests.permission, {
119
- read: 'allow',
120
- edit: 'allow',
121
- glob: 'allow',
122
- grep: 'allow',
123
- list: 'allow',
124
- bash: 'allow',
125
- task: 'deny',
126
- external_directory: 'deny',
127
- todowrite: 'deny',
128
- question: 'deny',
129
- webfetch: 'deny',
130
- websearch: 'deny',
131
- codesearch: 'deny',
132
- lsp: 'deny',
133
- doom_loop: 'deny',
134
- skill: {
135
- '*': 'deny',
136
- 'testing-rules': 'allow',
137
- },
138
- });
139
-
140
- const tddRunner = parseFm(
141
- fs.readFileSync(path.join(target, 'agents', 'tdd-runner.md'), 'utf8'),
142
- ).fm;
143
- assert.equal(tddRunner.permission.edit, 'deny');
144
- assert.deepEqual(tddRunner.permission.skill, {
145
- '*': 'deny',
146
- 'testing-rules': 'allow',
147
- });
148
-
149
- const debuggerAgent = parseFm(
150
- fs.readFileSync(path.join(target, 'agents', 'debugger.md'), 'utf8'),
151
- ).fm;
152
- assert.equal(debuggerAgent.steps, 40);
153
- assert.equal(debuggerAgent.permission.skill, 'deny');
154
- } finally {
155
- fs.rmSync(target, { recursive: true, force: true });
156
- }
157
- });
158
-
159
- test('install writes a schema-safe opencode.json and keeps ownership outside host config', () => {
160
- const target = mkTarget();
161
- try {
162
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
163
- const config = JSON.parse(fs.readFileSync(path.join(target, 'opencode.json'), 'utf8'));
164
- assert.ok(config.mcp);
165
- assert.ok(config.mcp[opencode.MCP_SERVER_NAME]);
166
- assert.equal(config.mcp[opencode.MCP_SERVER_NAME].type, 'local');
167
- assert.equal(config.mcp[opencode.MCP_SERVER_NAME].enabled, true);
168
- assert.ok(Array.isArray(config.mcp[opencode.MCP_SERVER_NAME].command));
169
- assert.equal('_ubp_manifest' in config, false);
170
- assert.ok(fs.existsSync(path.join(target, opencode.BUNDLE_DIR, '.ubp-managed')));
171
-
172
- const plugin = fs.readFileSync(path.join(target, 'plugins', 'ultra-builder-pro.js'), 'utf8');
173
- assert.match(plugin, /experimental\.chat\.system\.transform/);
174
- assert.match(plugin, /experimental\.session\.compacting/);
175
- assert.match(plugin, /tool\.execute\.before/);
176
- assert.match(plugin, /\.ultra[\\/]tasks[\\/]tasks\.json/);
177
- assert.match(plugin, /throw new Error/);
178
- assert.match(plugin, /session\.compacted/);
179
- assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
180
- assert.match(plugin, /Ultra context spine/);
181
- assert.doesNotMatch(plugin, /providerMetadata|External providers/);
182
- } finally {
183
- fs.rmSync(target, { recursive: true, force: true });
184
- }
185
- });
186
-
187
- test('OpenCode context covers active changes and projection writes are blocked for Ultra projects', async () => {
188
- const target = mkTarget();
189
- const project = mkTarget();
190
- try {
191
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
192
- const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
193
- const module = await import(pathToFileURL(pluginFile).href);
194
- const plugin = await module.UltraBuilderProPlugin({ directory: project });
195
-
196
- fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
197
- fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
198
- execFileSync('git', ['init'], { cwd: project, stdio: 'ignore' });
199
- fs.writeFileSync(path.join(project, 'app.txt'), 'initial\n');
200
- execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
201
- execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'initial'], {
202
- cwd: project, stdio: 'ignore',
203
- });
204
- const compiledHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: project, encoding: 'utf8' }).trim();
205
- const changeRoot = path.join(project, '.ultra', 'changes', 'active', 'daily-fix');
206
- fs.mkdirSync(changeRoot, { recursive: true });
207
- fs.writeFileSync(path.join(changeRoot, 'context-manifest.json'), JSON.stringify({
208
- schema_version: '2.0',
209
- baseline: { id: 'project-baseline', mode: 'brownfield', status: 'ready', repository_revision: compiledHead },
210
- change: { id: 'daily-fix', title: 'Daily fix', kind: 'quick', status: 'active', intent: 'Fix drift' },
211
- role: 'implement',
212
- gate: 'implementation',
213
- next_action: 'Run the exact regression test.',
214
- readiness: {
215
- status: 'ready', blockers: [], warnings: ['CONTEXT_FILE_BUDGET_EXCEEDED'],
216
- },
217
- git: { head: compiledHead },
218
- selected_task: { id: 'daily-task', status: 'in_progress' },
219
- context: { token_estimate: 1200, budget: { max_tokens: 8000, max_files: 8 } },
220
- providers: { memory: { provider: 'cloud-mem', status: 'available' } },
221
- }));
222
-
223
- const output = { system: [] };
224
- await plugin['experimental.chat.system.transform']({}, output);
225
- assert.match(output.system.join('\n'), /Ultra context spine/);
226
- assert.match(output.system.join('\n'), /daily-fix/);
227
- assert.match(output.system.join('\n'), /daily-task/);
228
- assert.match(output.system.join('\n'), /Role: implement/);
229
- assert.match(output.system.join('\n'), /Gate: implementation/);
230
- assert.match(output.system.join('\n'), /Readiness: ready/);
231
- assert.match(output.system.join('\n'), /Warnings: CONTEXT_FILE_BUDGET_EXCEEDED/);
232
- assert.match(output.system.join('\n'), /Run the exact regression test/);
233
- assert.doesNotMatch(output.system.join('\n'), /Fix drift|cloud-mem/);
234
-
235
- const manifestFile = path.join(changeRoot, 'context-manifest.json');
236
- const legacyManifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
237
- legacyManifest.schema_version = '1.0';
238
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
239
- const legacyOutput = { system: [] };
240
- await plugin['experimental.chat.system.transform']({}, legacyOutput);
241
- assert.match(legacyOutput.system.join('\n'), /CONTEXT_SNAPSHOT_UPGRADE_REQUIRED/);
242
- legacyManifest.schema_version = '2.0';
243
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
244
-
245
- delete legacyManifest.baseline;
246
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
247
- const missingBaselineOutput = { system: [] };
248
- await plugin['experimental.chat.system.transform']({}, missingBaselineOutput);
249
- assert.match(missingBaselineOutput.system.join('\n'), /CONTEXT_SNAPSHOT_UPGRADE_REQUIRED/);
250
- legacyManifest.baseline = {
251
- id: 'project-baseline', mode: 'brownfield', status: 'ready', repository_revision: compiledHead,
252
- };
253
- fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
254
-
255
- fs.writeFileSync(path.join(project, 'app.txt'), 'changed\n');
256
- execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
257
- execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'change'], {
258
- cwd: project, stdio: 'ignore',
259
- });
260
- const staleOutput = { system: [] };
261
- await plugin['experimental.chat.system.transform']({}, staleOutput);
262
- assert.match(staleOutput.system.join('\n'), /Readiness: blocked/);
263
- assert.match(staleOutput.system.join('\n'), /CONTEXT_HEAD_STALE/);
264
- assert.match(staleOutput.system.join('\n'), /recompile change\.context/);
265
-
266
- await assert.rejects(
267
- plugin['tool.execute.before'](
268
- { tool: 'apply_patch' },
269
- { args: { patch: '*** Begin Patch\n*** Update File: .ultra/tasks/tasks.json\n*** End Patch' } },
270
- ),
271
- /state\.db is authoritative/,
272
- );
273
- } finally {
274
- fs.rmSync(target, { recursive: true, force: true });
275
- fs.rmSync(project, { recursive: true, force: true });
276
- }
277
- });
278
-
279
- test('OpenCode does not assume a project baseline is ready when no active change manifest exists', async () => {
280
- const target = mkTarget();
281
- const project = mkTarget();
282
- try {
283
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
284
- fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
285
- fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
286
- const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?baseline`);
287
- const plugin = await module.UltraBuilderProPlugin({ directory: project });
288
- const output = { system: [] };
289
- await plugin['experimental.chat.system.transform']({}, output);
290
- const text = output.system.join('\n');
291
- assert.match(text, /ultra-status/);
292
- assert.match(text, /ultra-init/);
293
- assert.doesNotMatch(text, /Start daily work with the ultra-change workflow/);
294
- } finally {
295
- fs.rmSync(target, { recursive: true, force: true });
296
- fs.rmSync(project, { recursive: true, force: true });
297
- }
298
- });
299
-
300
- test('install preserves user mcp entries; uninstall removes only owned Ultra assets', () => {
301
- const target = mkTarget();
302
- const configFile = path.join(target, 'opencode.json');
303
- try {
304
- fs.writeFileSync(configFile, JSON.stringify({
305
- theme: 'dark',
306
- mcp: { my_server: { command: 'node', args: ['./mine.js'] } },
307
- }, null, 2));
308
-
309
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
310
- const merged = JSON.parse(fs.readFileSync(configFile, 'utf8'));
311
- assert.equal(merged.theme, 'dark');
312
- assert.ok(merged.mcp.my_server);
313
- assert.ok(merged.mcp[opencode.MCP_SERVER_NAME]);
314
-
315
- opencode.uninstall({ configDir: target });
316
- const after = JSON.parse(fs.readFileSync(configFile, 'utf8'));
317
- assert.equal(after.theme, 'dark');
318
- assert.ok(after.mcp.my_server);
319
- assert.ok(!after.mcp[opencode.MCP_SERVER_NAME]);
320
- assert.ok(!('_ubp_manifest' in after));
321
- assert.ok(!fs.existsSync(path.join(target, 'commands')));
322
- assert.ok(!fs.existsSync(path.join(target, 'plugins', 'ultra-builder-pro.js')));
323
- } finally {
324
- fs.rmSync(target, { recursive: true, force: true });
325
- }
326
- });
327
-
328
- test('shared OpenCode asset roots keep unrelated user commands, skills, agents, and runtime files', () => {
329
- const target = mkTarget();
330
- const userFiles = [
331
- ['commands', 'user-command.md'],
332
- ['skills', 'user-skill', 'SKILL.md'],
333
- ['agents', 'user-agent.md'],
334
- ['runtime', 'user-runtime.txt'],
335
- ];
336
- try {
337
- for (const parts of userFiles) {
338
- const file = path.join(target, ...parts);
339
- fs.mkdirSync(path.dirname(file), { recursive: true });
340
- fs.writeFileSync(file, 'user-owned');
341
- }
342
-
343
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
344
- opencode.uninstall({ configDir: target });
345
-
346
- for (const parts of userFiles) {
347
- assert.equal(fs.readFileSync(path.join(target, ...parts), 'utf8'), 'user-owned');
348
- }
349
- assert.equal(fs.existsSync(path.join(target, 'commands', '.ubp-managed')), false);
350
- assert.equal(fs.existsSync(path.join(target, 'skills', '.ubp-managed')), false);
351
- assert.equal(fs.existsSync(path.join(target, 'agents', '.ubp-managed')), false);
352
- } finally {
353
- fs.rmSync(target, { recursive: true, force: true });
354
- }
355
- });
356
-
357
- test('install refuses to overwrite an unmanaged OpenCode asset with the same name', () => {
358
- const target = mkTarget();
359
- const conflict = path.join(target, 'skills', 'ultra-init', 'SKILL.md');
360
- try {
361
- fs.mkdirSync(path.dirname(conflict), { recursive: true });
362
- fs.writeFileSync(conflict, 'user-owned');
363
- assert.throws(
364
- () => opencode.install({ configDir: target, repoRoot: REPO_ROOT }),
365
- /unmanaged OpenCode skill/,
366
- );
367
- assert.equal(fs.readFileSync(conflict, 'utf8'), 'user-owned');
368
- } finally {
369
- fs.rmSync(target, { recursive: true, force: true });
370
- }
371
- });
@@ -1,35 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
-
9
- const journal = require('./archive-journal.cjs');
10
-
11
- test('archive journal restores the exact active directory when finalization cannot run', () => {
12
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-archive-journal-'));
13
- const source = path.join(rootDir, '.ultra', 'changes', 'active', 'change-a');
14
- fs.mkdirSync(source, { recursive: true });
15
- fs.writeFileSync(path.join(source, 'intent.md'), '# Change A\n');
16
- try {
17
- const prepared = journal.prepareArchiveMove({
18
- rootDir,
19
- change: { id: 'change-a', title: 'Change A', artifact_root: '.ultra/changes/active/change-a' },
20
- summary: 'Archive Change A.', baselineUpdates: [],
21
- noBaselineChangeReason: 'No baseline content changed.',
22
- now: new Date('2026-07-18T00:00:00.000Z'),
23
- });
24
- assert.equal(fs.existsSync(prepared.source), false);
25
- assert.equal(journal.listArchiveIntents(rootDir).length, 1);
26
-
27
- journal.rollbackArchiveIntent(rootDir, prepared.intent);
28
- assert.equal(fs.existsSync(source), true);
29
- assert.equal(fs.existsSync(path.join(source, 'intent.md')), true);
30
- assert.equal(fs.existsSync(path.join(source, 'archive-summary.md')), false);
31
- assert.equal(journal.listArchiveIntents(rootDir).length, 0);
32
- } finally {
33
- fs.rmSync(rootDir, { recursive: true, force: true });
34
- }
35
- });