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,133 +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
-
10
- const provenance = require('../provenance.cjs');
11
-
12
- function fixture() {
13
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-'));
14
- fs.mkdirSync(path.join(root, 'runtime'), { recursive: true });
15
- fs.mkdirSync(path.join(root, 'hooks'), { recursive: true });
16
- fs.writeFileSync(path.join(root, 'runtime', 'launch.cjs'), 'module.exports = true;\n');
17
- fs.writeFileSync(path.join(root, 'hooks', 'hooks.json'), '{}\n');
18
- const file = path.join(root, 'provenance.json');
19
- provenance.writeProvenance({
20
- file,
21
- adapter: 'claude',
22
- packageInfo: { name: 'ultra-builder-pro-cli', version: '1.2.3' },
23
- repository: 'https://example.test/ultra-builder-pro',
24
- sourceCommit: '0123456789abcdef0123456789abcdef01234567',
25
- roots: { plugin: root },
26
- assets: [
27
- { root: 'plugin', path: 'hooks/hooks.json' },
28
- { root: 'plugin', path: 'runtime/launch.cjs' },
29
- ],
30
- contracts: {
31
- hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
32
- mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
33
- },
34
- });
35
- return { root, file };
36
- }
37
-
38
- test('writeProvenance + inspectProvenance produce a normalized healthy report', () => {
39
- const fx = fixture();
40
- try {
41
- const manifest = JSON.parse(fs.readFileSync(fx.file, 'utf8'));
42
- assert.equal(manifest.schema_version, 1);
43
- assert.equal(manifest.adapter, 'claude');
44
- assert.equal(manifest.package.version, '1.2.3');
45
- assert.equal(manifest.source.repository, 'https://example.test/ultra-builder-pro');
46
- assert.match(manifest.content.digest, /^[0-9a-f]{64}$/);
47
- assert.deepEqual(manifest.assets.map((asset) => asset.path), [
48
- 'hooks/hooks.json', 'runtime/launch.cjs',
49
- ]);
50
-
51
- const report = provenance.inspectProvenance({
52
- file: fx.file,
53
- expectedAdapter: 'claude',
54
- expectedPackageVersion: '1.2.3',
55
- });
56
- assert.equal(report.status, 'healthy');
57
- assert.deepEqual(report.issues, []);
58
- assert.equal(report.checks.assets.checked, 2);
59
- assert.equal(report.checks.contracts.checked, 2);
60
- } finally {
61
- fs.rmSync(fx.root, { recursive: true, force: true });
62
- }
63
- });
64
-
65
- test('inspectProvenance detects content drift and missing contract targets', () => {
66
- const fx = fixture();
67
- try {
68
- fs.writeFileSync(path.join(fx.root, 'runtime', 'launch.cjs'), 'tampered\n');
69
- fs.unlinkSync(path.join(fx.root, 'hooks', 'hooks.json'));
70
- const report = provenance.inspectProvenance({
71
- file: fx.file,
72
- expectedAdapter: 'claude',
73
- expectedPackageVersion: '1.2.3',
74
- });
75
- assert.equal(report.status, 'degraded');
76
- assert.ok(report.issues.some((issue) => issue.code === 'ASSET_HASH_MISMATCH'));
77
- assert.ok(report.issues.some(
78
- (issue) => issue.code === 'CONTRACT_TARGET_MISSING' && issue.contract === 'hooks_manifest',
79
- ));
80
- } finally {
81
- fs.rmSync(fx.root, { recursive: true, force: true });
82
- }
83
- });
84
-
85
- test('inspectProvenance fails closed for missing, corrupt, or mismatched manifests', () => {
86
- const fx = fixture();
87
- try {
88
- const mismatch = provenance.inspectProvenance({
89
- file: fx.file,
90
- expectedAdapter: 'codex',
91
- expectedPackageVersion: '9.9.9',
92
- });
93
- assert.equal(mismatch.status, 'degraded');
94
- assert.ok(mismatch.issues.some((issue) => issue.code === 'ADAPTER_MISMATCH'));
95
- assert.ok(mismatch.issues.some((issue) => issue.code === 'PACKAGE_VERSION_MISMATCH'));
96
-
97
- fs.writeFileSync(fx.file, 'not-json');
98
- const corrupt = provenance.inspectProvenance({ file: fx.file });
99
- assert.equal(corrupt.status, 'degraded');
100
- assert.equal(corrupt.issues[0].code, 'PROVENANCE_INVALID');
101
-
102
- fs.unlinkSync(fx.file);
103
- const missing = provenance.inspectProvenance({ file: fx.file });
104
- assert.equal(missing.status, 'missing');
105
- assert.equal(missing.issues[0].code, 'PROVENANCE_MISSING');
106
- } finally {
107
- fs.rmSync(fx.root, { recursive: true, force: true });
108
- }
109
- });
110
-
111
- test('packageSource never attributes an enclosing consumer repository commit to the package', () => {
112
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-provenance-source-'));
113
- try {
114
- execFileSync('git', ['init', '-q'], { cwd: root });
115
- execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: root });
116
- execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: root });
117
- fs.writeFileSync(path.join(root, 'README.md'), '# consumer\n');
118
- execFileSync('git', ['add', 'README.md'], { cwd: root });
119
- execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: root });
120
- const packageRoot = path.join(root, 'node_modules', 'ultra-builder-pro-cli');
121
- fs.mkdirSync(packageRoot, { recursive: true });
122
- fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({
123
- name: 'ultra-builder-pro-cli',
124
- version: '1.2.3',
125
- repository: { url: 'https://example.test/ultra-builder-pro' },
126
- }));
127
-
128
- const source = provenance.packageSource(packageRoot);
129
- assert.equal(source.sourceCommit, null);
130
- } finally {
131
- fs.rmSync(root, { recursive: true, force: true });
132
- }
133
- });
@@ -1,75 +0,0 @@
1
- 'use strict';
2
-
3
- // Scope-precedence contract across every supported adapter.resolveTarget.
4
- //
5
- // Each adapter honors the same ordering:
6
- // ctx.configDir > ctx.scope='global' → env var or homeDir/.<runtime>
7
- // > ctx.scope='local' → cwd/.<runtime>
8
-
9
- const { test } = require('node:test');
10
- const assert = require('node:assert/strict');
11
- const path = require('node:path');
12
-
13
- const claude = require('../../../adapters/claude.js');
14
- const opencode = require('../../../adapters/opencode.js');
15
- const codex = require('../../../adapters/codex.js');
16
-
17
- // Stable fake values so assertions don't depend on real $HOME / $CWD.
18
- const FAKE_HOME = '/fake-home';
19
- const FAKE_CWD = '/fake-cwd';
20
-
21
- function withEnv(overrides, fn) {
22
- const keys = Object.keys(overrides);
23
- const backup = {};
24
- for (const k of keys) { backup[k] = process.env[k]; }
25
- for (const k of keys) {
26
- if (overrides[k] === undefined) delete process.env[k];
27
- else process.env[k] = overrides[k];
28
- }
29
- try { return fn(); }
30
- finally {
31
- for (const k of keys) {
32
- if (backup[k] === undefined) delete process.env[k];
33
- else process.env[k] = backup[k];
34
- }
35
- }
36
- }
37
-
38
- const CASES = [
39
- { name: 'claude', resolve: claude.resolveTarget, localLeaf: '.claude', globalHome: '.claude', envKey: 'CLAUDE_CONFIG_DIR' },
40
- { name: 'opencode', resolve: opencode.resolveTarget, localLeaf: '.opencode', globalHome: path.join('.config', 'opencode'), envKey: 'OPENCODE_CONFIG_DIR' },
41
- { name: 'codex', resolve: codex.resolveTarget, localLeaf: '.codex', globalHome: '.codex', envKey: 'CODEX_HOME' },
42
- ];
43
-
44
- for (const tc of CASES) {
45
- test(`${tc.name}.resolveTarget — configDir wins over scope/env/home`, () => {
46
- withEnv({ [tc.envKey]: '/env-path', XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
47
- const out = tc.resolve({ configDir: '/explicit/path', scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
48
- assert.equal(out, '/explicit/path');
49
- });
50
- });
51
-
52
- test(`${tc.name}.resolveTarget — global scope picks env var`, () => {
53
- withEnv({ [tc.envKey]: '/env-global', XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
54
- const out = tc.resolve({ scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
55
- assert.equal(out, '/env-global');
56
- });
57
- });
58
-
59
- test(`${tc.name}.resolveTarget — global scope falls back to home dir when env absent`, () => {
60
- withEnv({ [tc.envKey]: undefined, XDG_CONFIG_HOME: undefined, OPENCODE_CONFIG: undefined }, () => {
61
- const out = tc.resolve({ scope: 'global', homeDir: FAKE_HOME, cwd: FAKE_CWD });
62
- assert.equal(out, path.join(FAKE_HOME, tc.globalHome));
63
- });
64
- });
65
-
66
- test(`${tc.name}.resolveTarget — local scope uses cwd/<leaf>`, () => {
67
- const out = tc.resolve({ scope: 'local', homeDir: FAKE_HOME, cwd: FAKE_CWD });
68
- assert.equal(out, path.join(FAKE_CWD, tc.localLeaf));
69
- });
70
-
71
- test(`${tc.name}.resolveTarget — missing scope defaults to local (cwd)`, () => {
72
- const out = tc.resolve({ homeDir: FAKE_HOME, cwd: FAKE_CWD });
73
- assert.equal(out, path.join(FAKE_CWD, tc.localLeaf));
74
- });
75
- }
@@ -1,149 +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 path = require('node:path');
7
-
8
- const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
9
- const {
10
- CORE_PUBLIC_SKILLS,
11
- INTERNAL_AGENT_SKILLS,
12
- SUPPORTED_RUNTIMES,
13
- COLLAB_SKILLS_BY_RUNTIME,
14
- MCP_DEPENDENT_SKILLS,
15
- RETIRED_SKILLS,
16
- WORKFLOW_HOOK_FILES,
17
- skillPolicy,
18
- skillsForRuntime,
19
- } = require('../runtime-assets.cjs');
20
-
21
- const CORE = [
22
- 'learn',
23
- 'ultra-init',
24
- 'ultra-research',
25
- 'ultra-plan',
26
- 'ultra-dev',
27
- 'ultra-test',
28
- 'ultra-review',
29
- 'ultra-deliver',
30
- 'ultra-status',
31
- 'ultra-think',
32
- 'ultra-change',
33
- 'ultra-doctor',
34
- ];
35
-
36
- const INTERNAL = [
37
- 'code-review-expert',
38
- 'security-rules',
39
- 'integration-rules',
40
- 'testing-rules',
41
- ];
42
-
43
- const RETIRED = [
44
- 'agent-browser',
45
- 'find-skills',
46
- 'recall',
47
- 'use-railway',
48
- 'vercel-composition-patterns',
49
- 'vercel-react-best-practices',
50
- 'vercel-react-native-skills',
51
- ];
52
-
53
- test('runtime asset manifest exposes only Ultra-owned core and internal skills', () => {
54
- assert.deepEqual(CORE_PUBLIC_SKILLS, CORE);
55
- assert.deepEqual(INTERNAL_AGENT_SKILLS, INTERNAL);
56
- assert.deepEqual(RETIRED_SKILLS, RETIRED);
57
-
58
- assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.claude, [
59
- 'codex-collab', 'ultra-verify',
60
- ]);
61
- assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.codex, [
62
- 'cc-collab', 'ultra-verify',
63
- ]);
64
- assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.opencode, [
65
- 'cc-collab', 'codex-collab', 'ultra-verify',
66
- ]);
67
- assert.deepEqual(COLLAB_SKILLS_BY_RUNTIME.kimi, [
68
- 'cc-collab', 'codex-collab', 'ultra-verify',
69
- ]);
70
- assert.deepEqual(SUPPORTED_RUNTIMES, ['claude', 'opencode', 'codex', 'kimi']);
71
-
72
- for (const runtime of SUPPORTED_RUNTIMES) {
73
- const names = skillsForRuntime(runtime);
74
- for (const retired of RETIRED) assert.ok(!names.includes(retired));
75
- assert.ok(!names.some((name) => /impeccable/i.test(name)));
76
- }
77
- });
78
-
79
- test('host invocation and MCP metadata live outside source SKILL frontmatter', () => {
80
- const all = new Set(SUPPORTED_RUNTIMES.flatMap((runtime) => skillsForRuntime(runtime)));
81
- for (const name of all) {
82
- const policy = skillPolicy(name);
83
- assert.equal(policy.allowImplicitInvocation, false, name);
84
- assert.equal(policy.userInvocable, !INTERNAL_AGENT_SKILLS.includes(name), name);
85
- assert.equal(policy.requiresUltraMcp, MCP_DEPENDENT_SKILLS.includes(name), name);
86
- }
87
- assert.throws(() => skillPolicy('not-packaged'), /unknown packaged Ultra skill/);
88
- assert.ok(MCP_DEPENDENT_SKILLS.includes('ultra-research'));
89
- });
90
-
91
- test('packaged collaboration prompts use current CLI contracts and one source prompt', () => {
92
- const skills = ['codex-collab', 'ultra-verify'];
93
- for (const name of skills) {
94
- const text = fs.readFileSync(path.join(REPO_ROOT, 'skills', name, 'SKILL.md'), 'utf8');
95
- assert.doesNotMatch(text, /codex exec[^\n]*\s-a(?:\s|$)|^\s*-a\s+never/m, name);
96
- assert.doesNotMatch(text, /The (?:OpenCode|Kimi Code) owns/, name);
97
- }
98
-
99
- const adapterSource = fs.readFileSync(
100
- path.join(REPO_ROOT, 'adapters', '_shared', 'codex-assets.cjs'),
101
- 'utf8',
102
- );
103
- assert.doesNotMatch(adapterSource, /const (?:CC_COLLAB_BODY|ULTRA_VERIFY_BODY)\s*=/);
104
- });
105
-
106
- test('workflow hook allowlist contains no memory, prompt capture, or generic policy hook', () => {
107
- assert.deepEqual(WORKFLOW_HOOK_FILES, [
108
- 'active_task_context.py',
109
- 'context_spine.py',
110
- 'health_check.py',
111
- 'pre_stop_check.py',
112
- 'subagent_tracker.py',
113
- 'workflow_checkpoint.py',
114
- 'workflow_context.py',
115
- 'workflow_resume.py',
116
- ]);
117
- assert.doesNotMatch(WORKFLOW_HOOK_FILES.join('\n'), /memory|recall|journal|observation|prompt|dangerous|post_edit/);
118
- });
119
-
120
- test('bundled agents never instruct workers to own persistent memory', () => {
121
- const agentsDir = path.join(REPO_ROOT, 'agents');
122
- const agentText = fs.readdirSync(agentsDir)
123
- .filter((name) => name.endsWith('.md'))
124
- .map((name) => fs.readFileSync(path.join(agentsDir, name), 'utf8'))
125
- .join('\n');
126
-
127
- assert.doesNotMatch(agentText, /^## Memory$/m);
128
- assert.doesNotMatch(agentText, /Update your project memory|Consult memory before starting work/);
129
- assert.doesNotMatch(agentText, /\.ultra\/memory|\/recall(?:\s|`|$)/m);
130
- });
131
-
132
- test('npm publish list uses the same explicit skill boundary', () => {
133
- const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
134
- const publishedSkills = pkg.files
135
- .filter((entry) => entry.startsWith('skills/'))
136
- .map((entry) => entry.slice('skills/'.length))
137
- .sort();
138
- const expected = [...new Set([
139
- ...CORE_PUBLIC_SKILLS,
140
- ...INTERNAL_AGENT_SKILLS,
141
- ...Object.values(COLLAB_SKILLS_BY_RUNTIME).flat(),
142
- ])].sort();
143
-
144
- assert.ok(!pkg.files.includes('skills'));
145
- assert.ok(!pkg.files.includes('CLAUDE.md'));
146
- assert.deepEqual(publishedSkills, expected);
147
- assert.equal(pkg.dependencies['@anthropic-ai/sdk'], undefined);
148
- assert.equal(pkg.dependencies.openai, undefined);
149
- });
@@ -1,85 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
-
6
- const fs = require('node:fs');
7
- const os = require('node:os');
8
- const path = require('node:path');
9
-
10
- const {
11
- SENTINEL_VERSION,
12
- readJsonSafe,
13
- withSentinelBlock,
14
- removeSentinelBlock,
15
- hasSentinelBlock,
16
- mergeArrayField,
17
- } = require('../settings-merge.cjs');
18
-
19
- test('withSentinelBlock writes under the sentinel key only; user config untouched', () => {
20
- const existing = { user_prop: 'keep me', hooks: [{ name: 'user-hook' }] };
21
- const out = withSentinelBlock(existing, 'ultra_builder_pro', {
22
- mcp: { ubp: { command: 'node' } },
23
- });
24
-
25
- assert.equal(out.user_prop, 'keep me');
26
- assert.deepEqual(out.hooks, [{ name: 'user-hook' }]);
27
- assert.equal(out.ultra_builder_pro.__sentinel, SENTINEL_VERSION);
28
- assert.equal(out.ultra_builder_pro.__generated_by, 'ultra-builder-pro-cli');
29
- assert.deepEqual(out.ultra_builder_pro.mcp.ubp, { command: 'node' });
30
- });
31
-
32
- test('removeSentinelBlock leaves unrelated keys intact; noop if absent', () => {
33
- const with_ = { a: 1, mine: { __sentinel: 1, x: 2 } };
34
- const stripped = removeSentinelBlock(with_, 'mine');
35
- assert.deepEqual(stripped, { a: 1 });
36
- assert.ok(!hasSentinelBlock(stripped, 'mine'));
37
-
38
- const noop = removeSentinelBlock({ a: 1 }, 'mine');
39
- assert.deepEqual(noop, { a: 1 });
40
- });
41
-
42
- test('mergeArrayField dedupes; readJsonSafe handles missing/empty/malformed', () => {
43
- const existing = { hooks: [{ name: 'user' }, { name: 'ubp-one' }] };
44
- const out = mergeArrayField(existing, 'hooks', [
45
- { name: 'ubp-one' }, // dup — skipped
46
- { name: 'ubp-two' },
47
- ]);
48
- assert.deepEqual(out.hooks, [
49
- { name: 'user' },
50
- { name: 'ubp-one' },
51
- { name: 'ubp-two' },
52
- ]);
53
-
54
- // readJsonSafe: missing file → empty object
55
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-merge-'));
56
- try {
57
- assert.deepEqual(readJsonSafe(path.join(tmp, 'none.json')), {});
58
-
59
- const p = path.join(tmp, 'existing.json');
60
- fs.writeFileSync(p, '{"a":1}');
61
- assert.deepEqual(readJsonSafe(p), { a: 1 });
62
-
63
- // empty file treated as empty object
64
- fs.writeFileSync(p, ' \n ');
65
- assert.deepEqual(readJsonSafe(p), {});
66
-
67
- // malformed → throws
68
- fs.writeFileSync(p, '{not json');
69
- assert.throws(() => readJsonSafe(p), /cannot parse/);
70
-
71
- // P2 #7 / D45: rescue mode backs up corrupt file and returns {} instead
72
- // of throwing — lets `install` finish even if the user's settings.json
73
- // is syntactically broken.
74
- fs.writeFileSync(p, '{still not json');
75
- const rescued = readJsonSafe(p, { rescue: true });
76
- assert.deepEqual(rescued, {});
77
- const backups = fs.readdirSync(tmp).filter((f) => f.startsWith('existing.json.bak-'));
78
- assert.ok(backups.length > 0, `expected a .bak-* file in ${tmp}, saw ${fs.readdirSync(tmp).join(',')}`);
79
- } finally {
80
- fs.rmSync(tmp, { recursive: true, force: true });
81
- }
82
-
83
- // withSentinelBlock rejects bad key
84
- assert.throws(() => withSentinelBlock({}, '', {}), /non-empty string/);
85
- });
@@ -1,30 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 4 backlog P3 #13 / D45 — configDir validator.
4
-
5
- const { test } = require('node:test');
6
- const assert = require('node:assert/strict');
7
-
8
- const { validateConfigDir } = require('../validate.cjs');
9
-
10
- test('validateConfigDir: null/undefined → ok with configDir=null', () => {
11
- assert.deepEqual(validateConfigDir(null), { ok: true, configDir: null });
12
- assert.deepEqual(validateConfigDir(undefined), { ok: true, configDir: null });
13
- });
14
-
15
- test('validateConfigDir: normal path → ok', () => {
16
- const out = validateConfigDir('/tmp/work');
17
- assert.equal(out.ok, true);
18
- assert.equal(out.configDir, '/tmp/work');
19
- });
20
-
21
- test('validateConfigDir: rejects NUL byte', () => {
22
- const out = validateConfigDir('/tmp/foo\u0000bar');
23
- assert.equal(out.ok, false);
24
- assert.match(out.error, /NUL bytes/);
25
- });
26
-
27
- test('validateConfigDir: rejects non-string', () => {
28
- assert.equal(validateConfigDir(42).ok, false);
29
- assert.equal(validateConfigDir({}).ok, false);
30
- });
@@ -1,138 +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 claude = require('../claude.js');
10
- const { parse: parseFrontmatter } = require('../_shared/frontmatter.cjs');
11
- const {
12
- INTERNAL_AGENT_SKILLS,
13
- skillsForRuntime,
14
- WORKFLOW_HOOK_FILES,
15
- } = require('../_shared/runtime-assets.cjs');
16
-
17
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
18
-
19
- function mkTarget() {
20
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-claude-'));
21
- }
22
-
23
- function skillNames(root) {
24
- return fs.readdirSync(path.join(root, 'skills'), { withFileTypes: true })
25
- .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(root, 'skills', entry.name, 'SKILL.md')))
26
- .map((entry) => entry.name)
27
- .sort();
28
- }
29
-
30
- test('install builds the Claude-native plugin from the explicit Ultra allowlist', () => {
31
- const parent = mkTarget();
32
- const target = path.join(parent, 'skills', 'ultra-builder-pro');
33
- try {
34
- const report = claude.install({ configDir: parent, repoRoot: REPO_ROOT });
35
- assert.deepEqual(skillNames(target), skillsForRuntime('claude').sort());
36
- assert.deepEqual(report.copied.hooks.sort(), WORKFLOW_HOOK_FILES.slice().sort());
37
- assert.ok(fs.existsSync(path.join(target, '.claude-plugin', 'plugin.json')));
38
- assert.ok(fs.existsSync(path.join(target, 'hooks', 'hooks.json')));
39
- assert.ok(fs.existsSync(path.join(target, '.mcp.json')));
40
- assert.ok(fs.existsSync(path.join(target, 'agents', 'code-reviewer.md')));
41
-
42
- const hooks = JSON.parse(fs.readFileSync(path.join(target, 'hooks', 'hooks.json'), 'utf8'));
43
- const serialized = JSON.stringify(hooks);
44
- for (const name of WORKFLOW_HOOK_FILES.filter((value) => value !== 'context_spine.py')) {
45
- assert.match(serialized, new RegExp(name.replace('.', '\\.')));
46
- }
47
- assert.ok(fs.existsSync(path.join(target, 'hooks', 'context_spine.py')));
48
- assert.doesNotMatch(serialized, /memory|recall|journal|observation_capture|user_prompt_capture|block_dangerous|post_edit_guard/);
49
- assert.match(serialized, /\$\{CLAUDE_PLUGIN_ROOT\}/);
50
- } finally {
51
- fs.rmSync(parent, { recursive: true, force: true });
52
- }
53
- });
54
-
55
- test('Claude plugin collaboration and learn workflows are safe native plugin assets', () => {
56
- const parent = mkTarget();
57
- const target = path.join(parent, 'skills', 'ultra-builder-pro');
58
- try {
59
- claude.install({ configDir: parent, repoRoot: REPO_ROOT });
60
- const read = (name) => fs.readFileSync(path.join(target, 'skills', name, 'SKILL.md'), 'utf8');
61
- const learn = read('learn');
62
- const codexCollab = read('codex-collab');
63
- const verify = read('ultra-verify');
64
- const review = read('ultra-review');
65
- const plan = read('ultra-plan');
66
- const status = read('ultra-status');
67
-
68
- assert.match(learn, /`~\/.claude\/skills`/);
69
- assert.doesNotMatch(learn, /_unverified|learned-[^\s/]*-unverified/i);
70
- assert.match(codexCollab, /-s read-only/);
71
- assert.match(codexCollab, /--ephemeral/);
72
- assert.match(codexCollab, /--ignore-user-config/);
73
- assert.match(verify, /Keep the current host responsible/);
74
- assert.match(verify, /installed collaboration companion/);
75
- assert.doesNotMatch(verify, /codex exec|claude --safe-mode/);
76
- assert.match(verify, /scripts\/verify_wait\.py/);
77
- assert.match(review, /Claude Code Task workers/);
78
- assert.match(review, /scripts\/review_wait\.py/);
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.match(status, /Never fall\s+back to generated task JSON/i);
82
- assert.doesNotMatch(plan, /ultra-tools task create/);
83
-
84
- for (const name of skillsForRuntime('claude')) {
85
- const { fm } = parseFrontmatter(read(name));
86
- assert.equal(fm['user-invocable'], !INTERNAL_AGENT_SKILLS.includes(name), name);
87
- }
88
-
89
- for (const [name, contents] of Object.entries({ codexCollab, verify })) {
90
- assert.doesNotMatch(contents, /--yolo|--full-auto|\/codex:/, name);
91
- }
92
- } finally {
93
- fs.rmSync(parent, { recursive: true, force: true });
94
- }
95
- });
96
-
97
- test('global target is a Claude skills-directory plugin', () => {
98
- const homeDir = mkTarget();
99
- try {
100
- const ctx = { scope: 'global', homeDir };
101
- assert.equal(claude.resolveTarget(ctx), path.join(homeDir, '.claude'));
102
- assert.equal(claude.resolvePluginRoot(ctx), path.join(homeDir, '.claude', 'skills', 'ultra-builder-pro'));
103
- } finally {
104
- fs.rmSync(homeDir, { recursive: true, force: true });
105
- }
106
- });
107
-
108
- test('reinstall is deterministic and uninstall removes only the managed plugin root', () => {
109
- const parent = mkTarget();
110
- const target = path.join(parent, 'skills', 'ultra-builder-pro');
111
- const sibling = path.join(parent, 'user-plugin');
112
- try {
113
- fs.mkdirSync(sibling);
114
- fs.writeFileSync(path.join(sibling, 'keep.txt'), 'keep');
115
- claude.install({ configDir: parent, repoRoot: REPO_ROOT });
116
- claude.install({ configDir: parent, repoRoot: REPO_ROOT });
117
- claude.uninstall({ configDir: parent });
118
- assert.ok(!fs.existsSync(target));
119
- assert.equal(fs.readFileSync(path.join(sibling, 'keep.txt'), 'utf8'), 'keep');
120
- } finally {
121
- fs.rmSync(parent, { recursive: true, force: true });
122
- }
123
- });
124
-
125
- test('install refuses to replace an unmanaged directory', () => {
126
- const parent = mkTarget();
127
- const target = path.join(parent, 'skills', 'ultra-builder-pro');
128
- try {
129
- fs.mkdirSync(target, { recursive: true });
130
- fs.writeFileSync(path.join(target, 'user.txt'), 'keep');
131
- assert.throws(
132
- () => claude.install({ configDir: parent, repoRoot: REPO_ROOT }),
133
- /unmanaged Claude plugin/,
134
- );
135
- } finally {
136
- fs.rmSync(parent, { recursive: true, force: true });
137
- }
138
- });