ultra-builder-pro-cli 0.5.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 (206) hide show
  1. package/.ultra-template/delivery-report.json +9 -0
  2. package/.ultra-template/docs/research/README.md +53 -0
  3. package/.ultra-template/specs/architecture.md +250 -0
  4. package/.ultra-template/specs/discovery.md +248 -0
  5. package/.ultra-template/specs/product.md +223 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
  7. package/.ultra-template/tasks/tasks.json +33 -0
  8. package/.ultra-template/test-report.json +8 -0
  9. package/CHANGELOG.md +194 -0
  10. package/LICENSE +21 -0
  11. package/README.md +187 -0
  12. package/adapters/_shared/codex-assets.cjs +1128 -0
  13. package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
  14. package/adapters/_shared/file-ops.cjs +115 -0
  15. package/adapters/_shared/frontmatter.cjs +52 -0
  16. package/adapters/_shared/handbook.cjs +144 -0
  17. package/adapters/_shared/md-to-toml.cjs +54 -0
  18. package/adapters/_shared/path-rewrite.cjs +79 -0
  19. package/adapters/_shared/rtk-detect.cjs +86 -0
  20. package/adapters/_shared/runtime-assets.cjs +70 -0
  21. package/adapters/_shared/settings-merge.cjs +76 -0
  22. package/adapters/_shared/tests/file-ops.test.cjs +54 -0
  23. package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
  24. package/adapters/_shared/tests/handbook.test.cjs +86 -0
  25. package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
  26. package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
  27. package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
  28. package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
  29. package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
  30. package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
  31. package/adapters/_shared/tests/validate.test.cjs +30 -0
  32. package/adapters/_shared/validate.cjs +24 -0
  33. package/adapters/claude.js +182 -0
  34. package/adapters/codex.js +385 -0
  35. package/adapters/gemini.js +204 -0
  36. package/adapters/opencode.js +506 -0
  37. package/adapters/tests/claude.test.cjs +117 -0
  38. package/adapters/tests/codex-hook.test.cjs +90 -0
  39. package/adapters/tests/codex.test.cjs +356 -0
  40. package/adapters/tests/gemini.test.cjs +112 -0
  41. package/adapters/tests/opencode.test.cjs +246 -0
  42. package/agents/code-reviewer.md +102 -0
  43. package/agents/debugger.md +127 -0
  44. package/agents/review-code.md +125 -0
  45. package/agents/review-comments.md +98 -0
  46. package/agents/review-coordinator.md +146 -0
  47. package/agents/review-design.md +133 -0
  48. package/agents/review-errors.md +87 -0
  49. package/agents/review-tests.md +93 -0
  50. package/agents/tdd-runner.md +92 -0
  51. package/bin/handbook.js +54 -0
  52. package/bin/install.js +254 -0
  53. package/bin/orchestrator.js +214 -0
  54. package/commands/learn.md +41 -0
  55. package/commands/ultra-deliver.md +45 -0
  56. package/commands/ultra-dev.md +50 -0
  57. package/commands/ultra-init.md +51 -0
  58. package/commands/ultra-plan.md +49 -0
  59. package/commands/ultra-research.md +59 -0
  60. package/commands/ultra-status.md +46 -0
  61. package/commands/ultra-test.md +48 -0
  62. package/commands/ultra-think.md +45 -0
  63. package/docs/AGENT-CONTEXT.md +143 -0
  64. package/docs/ARCHITECTURE.md +197 -0
  65. package/docs/COMMIT-HASH-BACKFILL.md +101 -0
  66. package/docs/LEGACY-HERMES.md +1175 -0
  67. package/docs/PLAN.zh-CN.md +1683 -0
  68. package/docs/ROADMAP.md +102 -0
  69. package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
  70. package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
  71. package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
  72. package/hooks/README.md +24 -0
  73. package/hooks/active_task_context.py +44 -0
  74. package/hooks/adapters/codex.py +302 -0
  75. package/hooks/health_check.py +61 -0
  76. package/hooks/pre_stop_check.py +56 -0
  77. package/hooks/subagent_tracker.py +116 -0
  78. package/hooks/workflow_checkpoint.py +56 -0
  79. package/hooks/workflow_context.py +63 -0
  80. package/hooks/workflow_resume.py +50 -0
  81. package/mcp-server/lib/init-project.cjs +162 -0
  82. package/mcp-server/lib/llm-client.cjs +157 -0
  83. package/mcp-server/lib/llm-client.test.cjs +156 -0
  84. package/mcp-server/lib/plan-store.cjs +96 -0
  85. package/mcp-server/lib/plan-store.test.cjs +132 -0
  86. package/mcp-server/lib/prd-parser.cjs +110 -0
  87. package/mcp-server/lib/prd-parser.test.cjs +171 -0
  88. package/mcp-server/lib/pricing.cjs +53 -0
  89. package/mcp-server/lib/projector.cjs +158 -0
  90. package/mcp-server/lib/state-db.cjs +113 -0
  91. package/mcp-server/lib/state-db.test.cjs +116 -0
  92. package/mcp-server/lib/state-ops.cjs +725 -0
  93. package/mcp-server/lib/task-expander.cjs +123 -0
  94. package/mcp-server/lib/task-expander.test.cjs +253 -0
  95. package/mcp-server/lib/telemetry.cjs +72 -0
  96. package/mcp-server/lib/topo.cjs +131 -0
  97. package/mcp-server/lib/topo.test.cjs +158 -0
  98. package/mcp-server/server.cjs +482 -0
  99. package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
  100. package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
  101. package/mcp-server/tests/concurrency-update.test.cjs +96 -0
  102. package/mcp-server/tests/concurrency.test.cjs +112 -0
  103. package/mcp-server/tests/db-ops.test.cjs +117 -0
  104. package/mcp-server/tests/init-project.test.cjs +141 -0
  105. package/mcp-server/tests/migrate.test.cjs +144 -0
  106. package/mcp-server/tests/projector.test.cjs +130 -0
  107. package/mcp-server/tests/server-contract.test.cjs +750 -0
  108. package/mcp-server/tests/staleness.test.cjs +190 -0
  109. package/mcp-server/tests/state-ops.test.cjs +286 -0
  110. package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
  111. package/mcp-server/tests/telemetry.test.cjs +205 -0
  112. package/orchestrator/auto-merge.cjs +109 -0
  113. package/orchestrator/code-graph-watcher.cjs +121 -0
  114. package/orchestrator/daemon.cjs +112 -0
  115. package/orchestrator/dispatch-rules.cjs +93 -0
  116. package/orchestrator/parallel-orchestrator.cjs +195 -0
  117. package/orchestrator/planner/expand-prompt.cjs +62 -0
  118. package/orchestrator/planner/expand-prompt.test.cjs +52 -0
  119. package/orchestrator/planner/plan-builder.cjs +116 -0
  120. package/orchestrator/planner/plan-builder.test.cjs +165 -0
  121. package/orchestrator/planner/prd-prompt.cjs +44 -0
  122. package/orchestrator/planner/prd-prompt.test.cjs +30 -0
  123. package/orchestrator/recovery.cjs +70 -0
  124. package/orchestrator/session-runner.cjs +224 -0
  125. package/orchestrator/skill-miner.cjs +133 -0
  126. package/orchestrator/tests/auto-merge.test.cjs +216 -0
  127. package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
  128. package/orchestrator/tests/daemon.test.cjs +260 -0
  129. package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
  130. package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
  131. package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
  132. package/orchestrator/tests/recovery.test.cjs +190 -0
  133. package/orchestrator/tests/session-runner.test.cjs +314 -0
  134. package/orchestrator/tests/skill-miner.test.cjs +147 -0
  135. package/orchestrator/tests/worktree-manager.test.cjs +148 -0
  136. package/orchestrator/worktree-manager.cjs +131 -0
  137. package/output-styles/coding-vibes.md +67 -0
  138. package/output-styles/structural-thinking.md +61 -0
  139. package/package.json +109 -0
  140. package/settings.json +8 -0
  141. package/skills/cc-collab/SKILL.md +41 -0
  142. package/skills/code-review-expert/SKILL.md +118 -0
  143. package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
  144. package/skills/code-review-expert/references/integration-checklist.md +46 -0
  145. package/skills/code-review-expert/references/removal-plan.md +52 -0
  146. package/skills/code-review-expert/references/security-checklist.md +100 -0
  147. package/skills/code-review-expert/references/solid-checklist.md +65 -0
  148. package/skills/codex-collab/SKILL.md +52 -0
  149. package/skills/codex-collab/evals/evals.json +40 -0
  150. package/skills/gemini-collab/SKILL.md +51 -0
  151. package/skills/gemini-collab/evals/evals.json +42 -0
  152. package/skills/integration-rules/SKILL.md +71 -0
  153. package/skills/learn/SKILL.md +117 -0
  154. package/skills/security-rules/SKILL.md +60 -0
  155. package/skills/testing-rules/SKILL.md +56 -0
  156. package/skills/ultra-deliver/SKILL.md +171 -0
  157. package/skills/ultra-dev/SKILL.md +324 -0
  158. package/skills/ultra-init/SKILL.md +148 -0
  159. package/skills/ultra-plan/SKILL.md +314 -0
  160. package/skills/ultra-research/SKILL.md +156 -0
  161. package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
  162. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
  163. package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
  164. package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
  165. package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
  166. package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
  167. package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
  168. package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
  169. package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
  170. package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
  171. package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
  172. package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
  173. package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
  174. package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
  175. package/skills/ultra-research/steps/step-40-deployment.md +123 -0
  176. package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
  177. package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
  178. package/skills/ultra-review/SKILL.md +437 -0
  179. package/skills/ultra-review/references/unified-schema.md +127 -0
  180. package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
  181. package/skills/ultra-review/scripts/review_wait.py +120 -0
  182. package/skills/ultra-status/SKILL.md +162 -0
  183. package/skills/ultra-test/SKILL.md +195 -0
  184. package/skills/ultra-think/SKILL.md +109 -0
  185. package/skills/ultra-verify/SKILL.md +111 -0
  186. package/skills/ultra-verify/evals/evals.json +51 -0
  187. package/skills/ultra-verify/references/confidence-system.md +38 -0
  188. package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
  189. package/skills/ultra-verify/scripts/verify_wait.py +131 -0
  190. package/spec/cli-protocol.md +118 -0
  191. package/spec/mcp-tools.yaml +658 -0
  192. package/spec/schemas/command-manifest.schema.json +47 -0
  193. package/spec/schemas/context-file.v4.5.schema.json +54 -0
  194. package/spec/schemas/mcp-tools.schema.json +136 -0
  195. package/spec/schemas/skill-manifest.schema.json +94 -0
  196. package/spec/schemas/state-db.sql +154 -0
  197. package/spec/schemas/tasks.v4.5.schema.json +66 -0
  198. package/ultra-tools/cli.cjs +121 -0
  199. package/ultra-tools/commands/db.cjs +175 -0
  200. package/ultra-tools/commands/legacy-memory.cjs +159 -0
  201. package/ultra-tools/commands/migrate.cjs +354 -0
  202. package/ultra-tools/commands/session.cjs +180 -0
  203. package/ultra-tools/commands/status.cjs +158 -0
  204. package/ultra-tools/commands/task.cjs +95 -0
  205. package/ultra-tools/legacy-memory.test.cjs +80 -0
  206. package/ultra-tools/status-command.test.cjs +132 -0
@@ -0,0 +1,54 @@
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 { copyTree, writeAtomic, removeTree } = require('../file-ops.cjs');
10
+
11
+ function mk(prefix = 'ubp-shared-') {
12
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
13
+ }
14
+
15
+ test('copyTree copies recursively and skips .DS_Store', () => {
16
+ const src = mk();
17
+ const dst = mk();
18
+ try {
19
+ fs.mkdirSync(path.join(src, 'nested'));
20
+ fs.writeFileSync(path.join(src, 'a.txt'), 'A');
21
+ fs.writeFileSync(path.join(src, '.DS_Store'), 'junk');
22
+ fs.writeFileSync(path.join(src, 'nested', 'b.txt'), 'B');
23
+
24
+ const files = copyTree(src, dst);
25
+ assert.deepEqual(files.sort(), ['a.txt', path.join('nested', 'b.txt')]);
26
+ assert.equal(fs.readFileSync(path.join(dst, 'a.txt'), 'utf8'), 'A');
27
+ assert.equal(fs.readFileSync(path.join(dst, 'nested', 'b.txt'), 'utf8'), 'B');
28
+ assert.ok(!fs.existsSync(path.join(dst, '.DS_Store')));
29
+ } finally {
30
+ removeTree(src);
31
+ removeTree(dst);
32
+ }
33
+ });
34
+
35
+ test('writeAtomic never leaves a torn write — tmp file is renamed', () => {
36
+ const dir = mk();
37
+ try {
38
+ const target = path.join(dir, 'out', 'file.txt');
39
+ writeAtomic(target, 'hello');
40
+ assert.equal(fs.readFileSync(target, 'utf8'), 'hello');
41
+
42
+ const siblings = fs.readdirSync(path.dirname(target));
43
+ assert.deepEqual(siblings, ['file.txt']); // no .tmp-* left behind
44
+ } finally {
45
+ removeTree(dir);
46
+ }
47
+ });
48
+
49
+ test('removeTree refuses to wipe filesystem root without allowRoot', () => {
50
+ assert.throws(
51
+ () => removeTree('/', { allowRoot: false }),
52
+ /refusing to remove filesystem root/,
53
+ );
54
+ });
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const { parse, serialize, lowercaseKeys, patch } = require('../frontmatter.cjs');
7
+
8
+ test('parse extracts frontmatter and body; degraded inputs survive', () => {
9
+ const withFm = '---\ndescription: hi\nmodel: opus\n---\n\n# Hello\n';
10
+ const out = parse(withFm);
11
+ assert.equal(out.fm.description, 'hi');
12
+ assert.equal(out.fm.model, 'opus');
13
+ assert.ok(out.body.startsWith('# Hello'));
14
+
15
+ // no delimiter at all
16
+ const plain = parse('# No frontmatter\n');
17
+ assert.equal(plain.fm, null);
18
+ assert.equal(plain.body, '# No frontmatter\n');
19
+
20
+ // delimiter open but never closed — treat as plain text
21
+ const unclosed = parse('---\nfoo: 1\nno closing delimiter\n');
22
+ assert.equal(unclosed.fm, null);
23
+ assert.ok(unclosed.body.includes('no closing delimiter'));
24
+
25
+ // type guard
26
+ assert.throws(() => parse(42), /expects string/);
27
+ });
28
+
29
+ test('serialize → parse round-trip preserves data', () => {
30
+ const fm = { description: 'round-trip', tags: ['a', 'b'] };
31
+ const body = '# Body\n\nparagraph\n';
32
+ const text = serialize(fm, body);
33
+ const { fm: got, body: gotBody } = parse(text);
34
+ assert.deepEqual(got, fm);
35
+ assert.equal(gotBody, body);
36
+ });
37
+
38
+ test('lowercaseKeys normalizes nested frontmatter (OpenCode contract)', () => {
39
+ const input = { Name: 'X', TAGS: ['T1'], Nested: { Key: 1 } };
40
+ const out = lowercaseKeys(input);
41
+ assert.deepEqual(out, { name: 'X', tags: ['T1'], nested: { key: 1 } });
42
+
43
+ // patch() should let callers transform fm in-place
44
+ const patched = patch('---\nFoo: 1\n---\nbody', (fm) => ({ foo: fm.Foo }));
45
+ const { fm } = parse(patched);
46
+ assert.deepEqual(fm, { foo: 1 });
47
+ });
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const test = require('node:test');
8
+
9
+ const {
10
+ BEGIN_MARKER,
11
+ END_MARKER,
12
+ applyHandbook,
13
+ mergeHandbook,
14
+ renderHandbook,
15
+ resolveHandbookFile,
16
+ } = require('../handbook.cjs');
17
+
18
+ test('renders one common contract with host-native invocation syntax', () => {
19
+ const claude = renderHandbook('claude');
20
+ const codex = renderHandbook('codex');
21
+ const opencode = renderHandbook('opencode');
22
+
23
+ assert.match(claude, /\/ultra-plan/);
24
+ assert.match(codex, /\$ultra-builder-pro:ultra-plan/);
25
+ assert.match(opencode, /\/ultra-plan/);
26
+ for (const rendered of [claude, codex, opencode]) {
27
+ assert.match(rendered, /\.ultra\/state\.db/);
28
+ assert.match(rendered, /claude-mem|cloud-mem/);
29
+ assert.doesNotMatch(rendered, /\.ultra\/memory|memory\.retain|memory\.recall/);
30
+ assert.match(rendered, new RegExp(BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
31
+ assert.match(rendered, new RegExp(END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
32
+ }
33
+ });
34
+
35
+ test('merge preserves user content and is idempotent', () => {
36
+ const existing = '# User rules\n\nKeep this.\n';
37
+ const once = mergeHandbook(existing, 'claude');
38
+ const twice = mergeHandbook(once, 'claude');
39
+
40
+ assert.match(once, /^# User rules/m);
41
+ assert.match(once, /Keep this\./);
42
+ assert.equal(once, twice);
43
+ assert.equal(once.split(BEGIN_MARKER).length - 1, 1);
44
+ });
45
+
46
+ test('Codex legacy Ultra section is replaced without touching the next section', () => {
47
+ const existing = [
48
+ '# Handbook',
49
+ '',
50
+ '## Ultra Builder Pro Runtime Contract',
51
+ '',
52
+ 'Old memory contract under `.ultra/memory/`.',
53
+ '',
54
+ '## Standard Operating Workflow',
55
+ '',
56
+ 'Keep this workflow.',
57
+ '',
58
+ ].join('\n');
59
+
60
+ const merged = mergeHandbook(existing, 'codex');
61
+ assert.doesNotMatch(merged, /Old memory contract/);
62
+ assert.match(merged, /## Standard Operating Workflow\n\nKeep this workflow\./);
63
+ assert.equal(merged.split(BEGIN_MARKER).length - 1, 1);
64
+ });
65
+
66
+ test('apply creates a backup and reports a no-op on the second run', () => {
67
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-handbook-'));
68
+ const file = path.join(root, 'AGENTS.md');
69
+ fs.writeFileSync(file, '# Existing\n');
70
+
71
+ const first = applyHandbook({ runtime: 'codex', file, now: '20260715T120000Z' });
72
+ assert.equal(first.changed, true);
73
+ assert.equal(first.backup, `${file}.ubp-backup-20260715T120000Z`);
74
+ assert.equal(fs.readFileSync(first.backup, 'utf8'), '# Existing\n');
75
+
76
+ const second = applyHandbook({ runtime: 'codex', file, now: '20260715T120001Z' });
77
+ assert.equal(second.changed, false);
78
+ assert.equal(second.backup, null);
79
+ });
80
+
81
+ test('default handbook paths are host-specific', () => {
82
+ const homeDir = '/tmp/example-home';
83
+ assert.equal(resolveHandbookFile('claude', { homeDir }), path.join(homeDir, '.claude', 'CLAUDE.md'));
84
+ assert.equal(resolveHandbookFile('codex', { homeDir }), path.join(homeDir, '.codex', 'AGENTS.md'));
85
+ assert.equal(resolveHandbookFile('opencode', { homeDir }), path.join(homeDir, '.config', 'opencode', 'AGENTS.md'));
86
+ });
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const { mdCommandToToml } = require('../md-to-toml.cjs');
7
+
8
+ test('mdCommandToToml extracts description from frontmatter', () => {
9
+ const md = [
10
+ '---',
11
+ 'description: Initialize project',
12
+ 'workflow-ref: "@skills/ultra-init/SKILL.md"',
13
+ '---',
14
+ '',
15
+ '# /ultra-init',
16
+ 'body content here',
17
+ ].join('\n');
18
+
19
+ const toml = mdCommandToToml(md);
20
+ assert.match(toml, /description = "Initialize project"/);
21
+ assert.match(toml, /prompt = """/);
22
+ assert.match(toml, /body content here/);
23
+ assert.match(toml, /workflow reference: @skills\/ultra-init\/SKILL\.md/);
24
+ });
25
+
26
+ test('mdCommandToToml includes mcp_tools_required and cli_fallback when present', () => {
27
+ const md = [
28
+ '---',
29
+ 'description: plan tasks',
30
+ 'mcp_tools_required:',
31
+ ' - task.create',
32
+ ' - ask.question',
33
+ 'cli_fallback: "task create"',
34
+ '---',
35
+ 'body',
36
+ ].join('\n');
37
+
38
+ const toml = mdCommandToToml(md);
39
+ assert.match(toml, /MCP tools required: task\.create, ask\.question/);
40
+ assert.match(toml, /CLI fallback: ultra-tools task create/);
41
+ });
42
+
43
+ test('mdCommandToToml defaults description when frontmatter absent', () => {
44
+ const md = '# plain markdown, no frontmatter';
45
+ const toml = mdCommandToToml(md);
46
+ assert.match(toml, /description = "Ultra Builder Pro command"/);
47
+ assert.match(toml, /plain markdown, no frontmatter/);
48
+ });
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const path = require('node:path');
6
+
7
+ const {
8
+ parseSkillRef,
9
+ resolveSkillRef,
10
+ rewriteWorkflowRefInText,
11
+ resolveAssetTarget,
12
+ } = require('../path-rewrite.cjs');
13
+
14
+ test('parseSkillRef accepts canonical @skills/<slug>/SKILL.md only', () => {
15
+ assert.deepEqual(parseSkillRef('@skills/ultra-init/SKILL.md'), {
16
+ name: 'ultra-init',
17
+ filename: 'SKILL.md',
18
+ });
19
+ assert.equal(parseSkillRef('skills/ultra-init/SKILL.md'), null); // missing @
20
+ assert.equal(parseSkillRef('@skills/Bad-Case/SKILL.md'), null); // uppercase slug
21
+ assert.equal(parseSkillRef(null), null);
22
+ });
23
+
24
+ test('resolveSkillRef routes to runtime-specific root under homeDir', () => {
25
+ const home = '/Users/test';
26
+ const claude = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'claude', homeDir: home });
27
+ const opencode = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'opencode', homeDir: home });
28
+ const codex = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'codex', homeDir: home });
29
+ const gemini = resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'gemini', homeDir: home });
30
+
31
+ assert.equal(claude, path.join(home, '.claude/skills/ultra-init/SKILL.md'));
32
+ assert.equal(opencode, path.join(home, '.config/opencode/skills/ultra-init/SKILL.md'));
33
+ assert.equal(codex, path.join(home, '.agents/skills/ultra-init/SKILL.md'));
34
+ assert.equal(gemini, path.join(home, '.gemini/extensions/ultra-builder-pro/skills/ultra-init/SKILL.md'));
35
+
36
+ assert.throws(
37
+ () => resolveSkillRef('@skills/ultra-init/SKILL.md', { runtime: 'unknown', homeDir: home }),
38
+ /unknown runtime/,
39
+ );
40
+ });
41
+
42
+ test('rewriteWorkflowRefInText + resolveAssetTarget give predictable paths', () => {
43
+ const home = '/h';
44
+ const cwd = '/w';
45
+ const input = 'see @skills/ultra-init/SKILL.md for details';
46
+ const out = rewriteWorkflowRefInText(input, { runtime: 'claude', homeDir: home });
47
+ assert.equal(out, `see ${path.join(home, '.claude/skills/ultra-init/SKILL.md')} for details`);
48
+
49
+ // Cover every runtime × every scope so runtimeRoot/localRoot branches hit
50
+ for (const rt of ['claude', 'opencode', 'codex', 'gemini']) {
51
+ const g = resolveAssetTarget('x', { runtime: rt, scope: 'global', homeDir: home, cwd });
52
+ const l = resolveAssetTarget('x', { runtime: rt, scope: 'local', homeDir: home, cwd });
53
+ assert.ok(g.startsWith(home));
54
+ assert.ok(l.startsWith(cwd));
55
+ }
56
+ assert.throws(
57
+ () => resolveAssetTarget('x', { runtime: 'unknown', scope: 'global', homeDir: home, cwd }),
58
+ /unknown runtime/,
59
+ );
60
+ });
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ // Phase 4.6b reviewer suggestion — scope-precedence contract across all
4
+ // four adapter.resolveTarget implementations.
5
+ //
6
+ // Each adapter honors the same ordering:
7
+ // ctx.configDir > ctx.scope='global' → env var or homeDir/.<runtime>
8
+ // > ctx.scope='local' → cwd/.<runtime>
9
+ //
10
+ // Gemini also exposes resolveExtensionRoot that appends extensions/<NAME>.
11
+
12
+ const { test } = require('node:test');
13
+ const assert = require('node:assert/strict');
14
+ const path = require('node:path');
15
+
16
+ const claude = require('../../../adapters/claude.js');
17
+ const opencode = require('../../../adapters/opencode.js');
18
+ const codex = require('../../../adapters/codex.js');
19
+ const gemini = require('../../../adapters/gemini.js');
20
+
21
+ // Stable fake values so assertions don't depend on real $HOME / $CWD.
22
+ const FAKE_HOME = '/fake-home';
23
+ const FAKE_CWD = '/fake-cwd';
24
+
25
+ function withEnv(overrides, fn) {
26
+ const keys = Object.keys(overrides);
27
+ const backup = {};
28
+ for (const k of keys) { backup[k] = process.env[k]; }
29
+ for (const k of keys) {
30
+ if (overrides[k] === undefined) delete process.env[k];
31
+ else process.env[k] = overrides[k];
32
+ }
33
+ try { return fn(); }
34
+ finally {
35
+ for (const k of keys) {
36
+ if (backup[k] === undefined) delete process.env[k];
37
+ else process.env[k] = backup[k];
38
+ }
39
+ }
40
+ }
41
+
42
+ const CASES = [
43
+ { name: 'claude', resolve: claude.resolveTarget, localLeaf: '.claude', globalHome: '.claude', envKey: 'CLAUDE_CONFIG_DIR' },
44
+ { name: 'opencode', resolve: opencode.resolveTarget, localLeaf: '.opencode', globalHome: path.join('.config', 'opencode'), envKey: 'OPENCODE_CONFIG_DIR' },
45
+ { name: 'codex', resolve: codex.resolveTarget, localLeaf: '.codex', globalHome: '.codex', envKey: 'CODEX_HOME' },
46
+ { name: 'gemini', resolve: gemini.resolveTarget, localLeaf: '.gemini', globalHome: '.gemini', envKey: 'GEMINI_CONFIG_DIR' },
47
+ ];
48
+
49
+ for (const tc of CASES) {
50
+ test(`${tc.name}.resolveTarget — configDir wins over scope/env/home`, () => {
51
+ withEnv({ [tc.envKey]: '/env-path', XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
52
+ const out = tc.resolve({ configDir: '/explicit/path', scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
53
+ assert.equal(out, '/explicit/path');
54
+ });
55
+ });
56
+
57
+ test(`${tc.name}.resolveTarget — global scope picks env var`, () => {
58
+ withEnv({ [tc.envKey]: '/env-global', XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
59
+ const out = tc.resolve({ scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
60
+ assert.equal(out, '/env-global');
61
+ });
62
+ });
63
+
64
+ test(`${tc.name}.resolveTarget — global scope falls back to home dir when env absent`, () => {
65
+ withEnv({ [tc.envKey]: undefined, XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
66
+ const out = tc.resolve({ scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
67
+ assert.equal(out, path.join(FAKE_HOME, tc.globalHome));
68
+ });
69
+ });
70
+
71
+ test(`${tc.name}.resolveTarget — local scope uses cwd/<leaf>`, () => {
72
+ const out = tc.resolve({ scope: 'local', homeDir: FAKE_HOME, cwd: FAKE_CWD });
73
+ assert.equal(out, path.join(FAKE_CWD, tc.localLeaf));
74
+ });
75
+
76
+ test(`${tc.name}.resolveTarget — missing scope defaults to local (cwd)`, () => {
77
+ const out = tc.resolve({ homeDir: FAKE_HOME, cwd: FAKE_CWD });
78
+ assert.equal(out, path.join(FAKE_CWD, tc.localLeaf));
79
+ });
80
+ }
81
+
82
+ test('gemini.resolveExtensionRoot layers extensions/<NAME> on top of resolveTarget', () => {
83
+ const out = gemini.resolveExtensionRoot({ configDir: '/explicit' });
84
+ assert.equal(out, path.join('/explicit', 'extensions', 'ultra-builder-pro'));
85
+ });
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // Phase 6.1 — RTK soft dependency:
4
+ // • detectRtk(opts): locates the `rtk` binary via PATH; returns false
5
+ // (never throws) when missing or on any exec error.
6
+ // • initRtk(opts): runs `rtk init -g`; only call after detectRtk() true.
7
+
8
+ const { test } = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ const rtk = require('../rtk-detect.cjs');
15
+
16
+ function mkEmptyBinDir() {
17
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-rtk-empty-bin-'));
18
+ }
19
+
20
+ function mkFakeBinDir({ succeed = true } = {}) {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-rtk-fake-bin-'));
22
+ const script = succeed
23
+ ? '#!/usr/bin/env bash\nif [ "$1" = "--version" ]; then echo "rtk 1.2.3"; exit 0; fi\nif [ "$1" = "init" ]; then echo "init ok"; exit 0; fi\nexit 0\n'
24
+ : '#!/usr/bin/env bash\nexit 42\n';
25
+ const target = path.join(dir, 'rtk');
26
+ fs.writeFileSync(target, script);
27
+ fs.chmodSync(target, 0o755);
28
+ return dir;
29
+ }
30
+
31
+ test('detectRtk returns true when rtk --version succeeds', () => {
32
+ const binDir = mkFakeBinDir({ succeed: true });
33
+ try {
34
+ const result = rtk.detectRtk({ pathOverride: binDir });
35
+ assert.equal(result.available, true);
36
+ assert.match(result.version, /rtk/i);
37
+ } finally {
38
+ fs.rmSync(binDir, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ test('detectRtk returns false when rtk missing from PATH (no throw)', () => {
43
+ const binDir = mkEmptyBinDir();
44
+ try {
45
+ const result = rtk.detectRtk({ pathOverride: binDir });
46
+ assert.equal(result.available, false);
47
+ assert.ok(result.reason);
48
+ } finally {
49
+ fs.rmSync(binDir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ test('detectRtk returns false when rtk exits non-zero (no throw)', () => {
54
+ const binDir = mkFakeBinDir({ succeed: false });
55
+ try {
56
+ const result = rtk.detectRtk({ pathOverride: binDir });
57
+ assert.equal(result.available, false);
58
+ } finally {
59
+ fs.rmSync(binDir, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ test('initRtk runs rtk init -g and returns success', () => {
64
+ const binDir = mkFakeBinDir({ succeed: true });
65
+ try {
66
+ const result = rtk.initRtk({ pathOverride: binDir, scope: 'global' });
67
+ assert.equal(result.ok, true);
68
+ assert.match(result.stdout, /init ok/);
69
+ } finally {
70
+ fs.rmSync(binDir, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test('initRtk reports failure when binary is missing (does not throw)', () => {
75
+ const binDir = mkEmptyBinDir();
76
+ try {
77
+ const result = rtk.initRtk({ pathOverride: binDir, scope: 'global' });
78
+ assert.equal(result.ok, false);
79
+ assert.ok(result.error);
80
+ } finally {
81
+ fs.rmSync(binDir, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test('installHook: skip=true short-circuits with a skipped=true record', () => {
86
+ const binDir = mkEmptyBinDir();
87
+ try {
88
+ const result = rtk.installHook({ skip: true, pathOverride: binDir });
89
+ assert.equal(result.skipped, true);
90
+ assert.equal(result.available, undefined);
91
+ } finally {
92
+ fs.rmSync(binDir, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test('installHook: missing rtk → returns {available:false} with install hint', () => {
97
+ const binDir = mkEmptyBinDir();
98
+ try {
99
+ const result = rtk.installHook({ skip: false, pathOverride: binDir });
100
+ assert.equal(result.available, false);
101
+ assert.ok(result.hint, 'should surface an install hint');
102
+ } finally {
103
+ fs.rmSync(binDir, { recursive: true, force: true });
104
+ }
105
+ });
106
+
107
+ test('installHook: present rtk → runs init and returns {available:true, initialized:true}', () => {
108
+ const binDir = mkFakeBinDir({ succeed: true });
109
+ try {
110
+ const result = rtk.installHook({ skip: false, pathOverride: binDir, scope: 'global' });
111
+ assert.equal(result.available, true);
112
+ assert.equal(result.initialized, true);
113
+ } finally {
114
+ fs.rmSync(binDir, { recursive: true, force: true });
115
+ }
116
+ });
@@ -0,0 +1,107 @@
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 path = require('node:path');
7
+
8
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
9
+ const {
10
+ CORE_PUBLIC_SKILLS,
11
+ INTERNAL_AGENT_SKILLS,
12
+ COLLAB_SKILLS_BY_RUNTIME,
13
+ RETIRED_SKILLS,
14
+ WORKFLOW_HOOK_FILES,
15
+ skillsForRuntime,
16
+ } = require('../runtime-assets.cjs');
17
+
18
+ const CORE = [
19
+ 'learn',
20
+ 'ultra-init',
21
+ 'ultra-research',
22
+ 'ultra-plan',
23
+ 'ultra-dev',
24
+ 'ultra-test',
25
+ 'ultra-review',
26
+ 'ultra-deliver',
27
+ 'ultra-status',
28
+ 'ultra-think',
29
+ ];
30
+
31
+ const INTERNAL = [
32
+ 'code-review-expert',
33
+ 'security-rules',
34
+ 'integration-rules',
35
+ 'testing-rules',
36
+ ];
37
+
38
+ const RETIRED = [
39
+ 'agent-browser',
40
+ 'find-skills',
41
+ 'recall',
42
+ 'use-railway',
43
+ 'vercel-composition-patterns',
44
+ 'vercel-react-best-practices',
45
+ 'vercel-react-native-skills',
46
+ ];
47
+
48
+ test('runtime asset manifest exposes only Ultra-owned core and internal skills', () => {
49
+ assert.deepEqual(CORE_PUBLIC_SKILLS, CORE);
50
+ assert.deepEqual(INTERNAL_AGENT_SKILLS, INTERNAL);
51
+ assert.deepEqual(RETIRED_SKILLS, RETIRED);
52
+
53
+ assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.claude, [
54
+ 'codex-collab', 'gemini-collab', 'ultra-verify',
55
+ ]);
56
+ assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.codex, [
57
+ 'cc-collab', 'gemini-collab', 'ultra-verify',
58
+ ]);
59
+ assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.opencode, [
60
+ 'cc-collab', 'codex-collab', 'gemini-collab', 'ultra-verify',
61
+ ]);
62
+ assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.gemini, [
63
+ 'cc-collab', 'codex-collab', 'ultra-verify',
64
+ ]);
65
+
66
+ for (const runtime of ['claude', 'codex', 'opencode', 'gemini']) {
67
+ const names = skillsForRuntime(runtime);
68
+ for (const retired of RETIRED) assert.ok(!names.includes(retired));
69
+ assert.ok(!names.some((name) => /impeccable/i.test(name)));
70
+ }
71
+ });
72
+
73
+ test('code-review-expert and rule skills are internal, never implicitly exposed', () => {
74
+ const codexAssets = fs.readFileSync(path.join(REPO_ROOT, 'adapters', '_shared', 'codex-assets.cjs'), 'utf8');
75
+ assert.match(codexAssets, /const INTERNAL_SKILLS = new Set\(INTERNAL_AGENT_SKILLS\)/);
76
+ assert.match(codexAssets, /INTERNAL_SKILLS\.has\(name\)/);
77
+ });
78
+
79
+ test('workflow hook allowlist contains no memory, prompt capture, or generic policy hook', () => {
80
+ assert.deepEqual(WORKFLOW_HOOK_FILES, [
81
+ 'active_task_context.py',
82
+ 'health_check.py',
83
+ 'pre_stop_check.py',
84
+ 'subagent_tracker.py',
85
+ 'workflow_checkpoint.py',
86
+ 'workflow_context.py',
87
+ 'workflow_resume.py',
88
+ ]);
89
+ assert.doesNotMatch(WORKFLOW_HOOK_FILES.join('\n'), /memory|recall|journal|observation|prompt|dangerous|post_edit/);
90
+ });
91
+
92
+ test('npm publish list uses the same explicit skill boundary', () => {
93
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
94
+ const publishedSkills = pkg.files
95
+ .filter((entry) => entry.startsWith('skills/'))
96
+ .map((entry) => entry.slice('skills/'.length))
97
+ .sort();
98
+ const expected = [...new Set([
99
+ ...CORE_PUBLIC_SKILLS,
100
+ ...INTERNAL_AGENT_SKILLS,
101
+ ...Object.values(COLLAB_SKILLS_BY_RUNTIME).flat(),
102
+ ])].sort();
103
+
104
+ assert.ok(!pkg.files.includes('skills'));
105
+ assert.ok(!pkg.files.includes('CLAUDE.md'));
106
+ assert.deepEqual(publishedSkills, expected);
107
+ });