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.
- package/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +44 -1
- package/README.md +34 -19
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +15 -8
- package/docs/ARCHITECTURE.md +33 -12
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +8 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +6 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +161 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +384 -63
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- 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
|
-
});
|