ultra-builder-pro-cli 0.14.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 (154) 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 +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,357 +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
- const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
15
- const changes = require('../../mcp-server/lib/change-workflow.cjs');
16
- const { createTask } = require('../../mcp-server/lib/state-ops.cjs');
17
-
18
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
19
-
20
- function mkTarget() {
21
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-opencode-'));
22
- }
23
-
24
- test('install builds a native OpenCode plugin and copies only allowlisted skills', () => {
25
- const target = mkTarget();
26
- try {
27
- const r = opencode.install({ configDir: target, repoRoot: REPO_ROOT });
28
- assert.ok(r.copied.commands.includes('ultra-init.md'));
29
- assert.ok(r.copied.skills.some((p) => p.includes('ultra-init/SKILL.md')));
30
- assert.ok(r.copied.plugins.includes('ultra-builder-pro.js'));
31
- assert.ok(fs.existsSync(path.join(target, 'plugins', 'ultra-builder-pro.js')));
32
- assert.deepEqual(
33
- fs.readdirSync(path.join(target, 'skills'), { withFileTypes: true })
34
- .filter((entry) => entry.isDirectory())
35
- .map((entry) => entry.name)
36
- .sort(),
37
- skillsForRuntime('opencode').sort(),
38
- );
39
-
40
- // OpenCode commands expose only fields accepted by the native command contract.
41
- const dst = fs.readFileSync(path.join(target, 'commands', 'ultra-init.md'), 'utf8');
42
- const { fm: dstFm } = parseFm(dst);
43
- assert.deepEqual(Object.keys(dstFm), ['description']);
44
-
45
- // forge an upper-case key to prove the transform works
46
- const hack = path.join(target, 'commands', 'upper.md');
47
- fs.writeFileSync(hack, '---\nDescription: mixed\n---\nbody\n');
48
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
49
- // reinstall re-runs the transform; the hack was outside commands/ in repo so
50
- // install doesn't touch it; we verify transform via a direct call:
51
- const { lowercaseKeys } = require('../_shared/frontmatter.cjs');
52
- assert.deepEqual(lowercaseKeys({ Description: 'x', Tags: ['A'] }), { description: 'x', tags: ['A'] });
53
- } finally {
54
- fs.rmSync(target, { recursive: true, force: true });
55
- }
56
- });
57
-
58
- test('install performs content-level OpenCode adaptation for commands, skills, references, and agents', () => {
59
- const target = mkTarget();
60
- try {
61
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
62
-
63
- const plan = fs.readFileSync(path.join(target, 'skills', 'ultra-plan', 'SKILL.md'), 'utf8');
64
- const learn = fs.readFileSync(path.join(target, 'skills', 'learn', 'SKILL.md'), 'utf8');
65
- const review = fs.readFileSync(path.join(target, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
66
- const codexCollab = fs.readFileSync(path.join(target, 'skills', 'codex-collab', 'SKILL.md'), 'utf8');
67
- const verify = fs.readFileSync(path.join(target, 'skills', 'ultra-verify', 'SKILL.md'), 'utf8');
68
-
69
- assert.deepEqual(Object.keys(parseFm(plan).fm), ['name', 'description']);
70
- assert.match(learn, /`~\/.config\/opencode\/skills`/);
71
- assert.doesNotMatch(learn, /_unverified|learned-[^\s/]*-unverified/i);
72
- assert.match(review, /OpenCode `task` tool/);
73
- assert.match(review, /scripts\/review_wait\.py/);
74
- assert.match(codexCollab, /OpenCode remains primary/);
75
- assert.match(verify, /Keep OpenCode responsible/);
76
- assert.match(verify, /host-analysis\.md/);
77
- assert.match(verify, /installed collaboration companion/);
78
- assert.doesNotMatch(verify, /codex exec|claude --safe-mode/);
79
- assert.doesNotMatch(plan, /LEGACY_STATE_MIGRATION_REQUIRED|v4\.4|v4\.5/);
80
- assert.match(plan, /never read or\s+write .*tasks\.json/i);
81
- assert.doesNotMatch(plan, /ultra-tools task create/);
82
-
83
- const markdown = [];
84
- for (const root of ['commands', 'skills', 'agents']) {
85
- const pending = [path.join(target, root)];
86
- while (pending.length) {
87
- const current = pending.pop();
88
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
89
- const file = path.join(current, entry.name);
90
- if (entry.isDirectory()) pending.push(file);
91
- else if (entry.isFile() && entry.name.endsWith('.md')) markdown.push(file);
92
- }
93
- }
94
- }
95
- const incompatible = /~\/.claude|CLAUDE\.md|AskUserQuestion|TaskCreate|TaskUpdate|TaskList|run_in_background:\s*true|--yolo|--full-auto|\/codex:|ask\.question|review\.run/;
96
- for (const file of markdown) {
97
- const text = fs.readFileSync(file, 'utf8');
98
- assert.doesNotMatch(text, incompatible, file);
99
- assert.doesNotMatch(text, /[\u3400-\u9fff]|ultra-review-findings-v1|Context7|Exa MCP|confidence\s*>=?\s*\d+/iu, file);
100
- }
101
- } finally {
102
- fs.rmSync(target, { recursive: true, force: true });
103
- }
104
- });
105
-
106
- test('install translates Claude agent metadata to native OpenCode agent contracts', () => {
107
- const target = mkTarget();
108
- try {
109
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
110
-
111
- const reviewTests = parseFm(
112
- fs.readFileSync(path.join(target, 'agents', 'review-tests.md'), 'utf8'),
113
- ).fm;
114
- assert.equal(reviewTests.mode, 'subagent');
115
- assert.equal(reviewTests.steps, 18);
116
- assert.equal(reviewTests.name, undefined);
117
- assert.equal(reviewTests.model, undefined);
118
- assert.equal(reviewTests.maxturns, undefined);
119
- assert.equal(reviewTests.tools, undefined);
120
- assert.equal(reviewTests.skills, undefined);
121
- assert.deepEqual(reviewTests.permission, {
122
- read: 'allow',
123
- edit: 'allow',
124
- glob: 'allow',
125
- grep: 'allow',
126
- list: 'allow',
127
- bash: 'allow',
128
- task: 'deny',
129
- external_directory: 'deny',
130
- todowrite: 'deny',
131
- question: 'deny',
132
- webfetch: 'deny',
133
- websearch: 'deny',
134
- codesearch: 'deny',
135
- lsp: 'deny',
136
- doom_loop: 'deny',
137
- skill: {
138
- '*': 'deny',
139
- 'testing-rules': 'allow',
140
- },
141
- });
142
-
143
- const tddRunner = parseFm(
144
- fs.readFileSync(path.join(target, 'agents', 'tdd-runner.md'), 'utf8'),
145
- ).fm;
146
- assert.equal(tddRunner.permission.edit, 'deny');
147
- assert.deepEqual(tddRunner.permission.skill, {
148
- '*': 'deny',
149
- 'testing-rules': 'allow',
150
- });
151
-
152
- const debuggerAgent = parseFm(
153
- fs.readFileSync(path.join(target, 'agents', 'debugger.md'), 'utf8'),
154
- ).fm;
155
- assert.equal(debuggerAgent.steps, 40);
156
- assert.equal(debuggerAgent.permission.skill, 'deny');
157
- } finally {
158
- fs.rmSync(target, { recursive: true, force: true });
159
- }
160
- });
161
-
162
- test('install writes a schema-safe opencode.json and keeps ownership outside host config', () => {
163
- const target = mkTarget();
164
- try {
165
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
166
- const config = JSON.parse(fs.readFileSync(path.join(target, 'opencode.json'), 'utf8'));
167
- assert.ok(config.mcp);
168
- assert.ok(config.mcp[opencode.MCP_SERVER_NAME]);
169
- assert.equal(config.mcp[opencode.MCP_SERVER_NAME].type, 'local');
170
- assert.equal(config.mcp[opencode.MCP_SERVER_NAME].enabled, true);
171
- assert.ok(Array.isArray(config.mcp[opencode.MCP_SERVER_NAME].command));
172
- assert.equal('_ubp_manifest' in config, false);
173
- assert.ok(fs.existsSync(path.join(target, opencode.BUNDLE_DIR, '.ubp-managed')));
174
-
175
- const plugin = fs.readFileSync(path.join(target, 'plugins', 'ultra-builder-pro.js'), 'utf8');
176
- assert.match(plugin, /experimental\.chat\.system\.transform/);
177
- assert.match(plugin, /experimental\.session\.compacting/);
178
- assert.match(plugin, /tool\.execute\.before/);
179
- assert.match(plugin, /\.ultra[\\/]tasks[\\/]tasks\.json/);
180
- assert.match(plugin, /throw new Error/);
181
- assert.match(plugin, /session\.compacted/);
182
- assert.match(plugin, /breadcrumb\.cjs/);
183
- assert.doesNotMatch(plugin, /workflow-state\.json|context-manifest\.json/);
184
- assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
185
- assert.match(plugin, /canonical breadcrumb/);
186
- assert.doesNotMatch(plugin, /providerMetadata|External providers/);
187
- } finally {
188
- fs.rmSync(target, { recursive: true, force: true });
189
- }
190
- });
191
-
192
- test('OpenCode routes a missing authority to init and always protects task projections', async () => {
193
- const target = mkTarget();
194
- const project = mkTarget();
195
- try {
196
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
197
- fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
198
- const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?baseline`);
199
- const plugin = await module.UltraBuilderProPlugin({ directory: project });
200
- const output = { system: [] };
201
- await plugin['experimental.chat.system.transform']({}, output);
202
- const text = output.system.join('\n');
203
- assert.match(text, /STATE_DB_MISSING/);
204
- assert.match(text, /ultra-init/);
205
- await assert.rejects(
206
- plugin['tool.execute.before'](
207
- { tool: 'apply_patch' },
208
- { args: { patch: '*** Begin Patch\n*** Update File: .ultra/tasks/tasks.json\n*** End Patch' } },
209
- ),
210
- /state\.db is authoritative/,
211
- );
212
- } finally {
213
- fs.rmSync(target, { recursive: true, force: true });
214
- fs.rmSync(project, { recursive: true, force: true });
215
- }
216
- });
217
-
218
- test('OpenCode injects the DB breadcrumb and ignores conflicting workflow projections', async () => {
219
- const target = mkTarget();
220
- const project = mkTarget();
221
- let state;
222
- try {
223
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
224
- execFileSync('git', ['init'], { cwd: project, stdio: 'ignore' });
225
- fs.writeFileSync(path.join(project, 'app.txt'), 'initial\n');
226
- execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
227
- execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'initial'], {
228
- cwd: project, stdio: 'ignore',
229
- });
230
- state = initStateDb(path.join(project, '.ultra', 'state.db'));
231
- state.db.prepare(
232
- `INSERT INTO baselines
233
- (id, project_name, mode, status, approved_by, approval_note, converged_at)
234
- VALUES ('baseline-db', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
235
- ).run(new Date().toISOString());
236
- const { change } = changes.createChange(state.db, {
237
- id: 'db-authority-change', title: 'DB authority', kind: 'quick',
238
- intent: 'Use one authoritative injected state.',
239
- docs_impact: { status: 'none', rationale: 'test fixture' },
240
- }, { rootDir: project });
241
- const task = createTask(state.db, {
242
- id: 'db-authority-task', title: 'Use DB authority', type: 'bugfix', priority: 'P0',
243
- change_id: change.id,
244
- });
245
- changes.compileContext(state.db, {
246
- id: change.id,
247
- task_id: task.id,
248
- role: 'implement',
249
- gate: 'implementation',
250
- execution_contract: {
251
- slice_kind: 'tracer_bullet', public_seam: 'OpenCode injection',
252
- verification_command: 'node --test adapters/tests/opencode.test.cjs',
253
- },
254
- next_action: 'Verify the DB-only OpenCode injection.',
255
- }, { rootDir: project });
256
-
257
- fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
258
- command: 'ultra-dev', task_id: 'projection-task', status: 'active', step: 'wrong',
259
- }));
260
- const projectionRoot = path.join(project, '.ultra', 'changes', 'active', 'projection-change');
261
- fs.mkdirSync(projectionRoot, { recursive: true });
262
- fs.writeFileSync(path.join(projectionRoot, 'context-manifest.json'), JSON.stringify({
263
- schema_version: '2.0',
264
- baseline: { id: 'projection-baseline', mode: 'brownfield', status: 'ready' },
265
- change: { id: 'projection-change', kind: 'quick', status: 'active' },
266
- selected_task: { id: 'projection-task', status: 'in_progress' },
267
- readiness: { status: 'ready', blockers: [], warnings: [] },
268
- }));
269
-
270
- const module = await import(`${pathToFileURL(path.join(target, 'plugins', 'ultra-builder-pro.js')).href}?db-authority`);
271
- const plugin = await module.UltraBuilderProPlugin({ directory: project });
272
- const output = { system: [] };
273
- await plugin['experimental.chat.system.transform']({}, output);
274
- const text = output.system.join('\n');
275
- assert.match(text, /Change: db-authority-change/);
276
- assert.match(text, /Task: db-authority-task/);
277
- assert.match(text, /Role: implement/);
278
- assert.doesNotMatch(text, /projection-change|projection-task|projection-baseline/);
279
- } finally {
280
- if (state) closeStateDb(state.db);
281
- fs.rmSync(target, { recursive: true, force: true });
282
- fs.rmSync(project, { recursive: true, force: true });
283
- }
284
- });
285
-
286
- test('install preserves user mcp entries; uninstall removes only owned Ultra assets', () => {
287
- const target = mkTarget();
288
- const configFile = path.join(target, 'opencode.json');
289
- try {
290
- fs.writeFileSync(configFile, JSON.stringify({
291
- theme: 'dark',
292
- mcp: { my_server: { command: 'node', args: ['./mine.js'] } },
293
- }, null, 2));
294
-
295
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
296
- const merged = JSON.parse(fs.readFileSync(configFile, 'utf8'));
297
- assert.equal(merged.theme, 'dark');
298
- assert.ok(merged.mcp.my_server);
299
- assert.ok(merged.mcp[opencode.MCP_SERVER_NAME]);
300
-
301
- opencode.uninstall({ configDir: target });
302
- const after = JSON.parse(fs.readFileSync(configFile, 'utf8'));
303
- assert.equal(after.theme, 'dark');
304
- assert.ok(after.mcp.my_server);
305
- assert.ok(!after.mcp[opencode.MCP_SERVER_NAME]);
306
- assert.ok(!('_ubp_manifest' in after));
307
- assert.ok(!fs.existsSync(path.join(target, 'commands')));
308
- assert.ok(!fs.existsSync(path.join(target, 'plugins', 'ultra-builder-pro.js')));
309
- } finally {
310
- fs.rmSync(target, { recursive: true, force: true });
311
- }
312
- });
313
-
314
- test('shared OpenCode asset roots keep unrelated user commands, skills, agents, and runtime files', () => {
315
- const target = mkTarget();
316
- const userFiles = [
317
- ['commands', 'user-command.md'],
318
- ['skills', 'user-skill', 'SKILL.md'],
319
- ['agents', 'user-agent.md'],
320
- ['runtime', 'user-runtime.txt'],
321
- ];
322
- try {
323
- for (const parts of userFiles) {
324
- const file = path.join(target, ...parts);
325
- fs.mkdirSync(path.dirname(file), { recursive: true });
326
- fs.writeFileSync(file, 'user-owned');
327
- }
328
-
329
- opencode.install({ configDir: target, repoRoot: REPO_ROOT });
330
- opencode.uninstall({ configDir: target });
331
-
332
- for (const parts of userFiles) {
333
- assert.equal(fs.readFileSync(path.join(target, ...parts), 'utf8'), 'user-owned');
334
- }
335
- assert.equal(fs.existsSync(path.join(target, 'commands', '.ubp-managed')), false);
336
- assert.equal(fs.existsSync(path.join(target, 'skills', '.ubp-managed')), false);
337
- assert.equal(fs.existsSync(path.join(target, 'agents', '.ubp-managed')), false);
338
- } finally {
339
- fs.rmSync(target, { recursive: true, force: true });
340
- }
341
- });
342
-
343
- test('install refuses to overwrite an unmanaged OpenCode asset with the same name', () => {
344
- const target = mkTarget();
345
- const conflict = path.join(target, 'skills', 'ultra-init', 'SKILL.md');
346
- try {
347
- fs.mkdirSync(path.dirname(conflict), { recursive: true });
348
- fs.writeFileSync(conflict, 'user-owned');
349
- assert.throws(
350
- () => opencode.install({ configDir: target, repoRoot: REPO_ROOT }),
351
- /unmanaged OpenCode skill/,
352
- );
353
- assert.equal(fs.readFileSync(conflict, 'utf8'), 'user-owned');
354
- } finally {
355
- fs.rmSync(target, { recursive: true, force: true });
356
- }
357
- });
@@ -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
- });