claude-dev-env 7.7.0 → 8.0.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/.agents/agents/clean-coder.md +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/e-code-review/reference/fix.md +4 -2
- package/.agents/skills/e-code-review/reference/loop.md +4 -2
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/bin/subagent-model-policy.test.mjs +56 -19
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/docs/subagent-model-routing.md +21 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/routing/subagent_model_routing.test.mjs +65 -28
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/ci-owns-the-gate.md +58 -0
- package/rules/code-standards.md +1 -2
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +1 -1
- package/rules/git-workflow.md +23 -11
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/retired-hook-prose.md +1 -1
- package/rules/subagent-model-policy.json +1 -1
- package/rules/verify-before-asking.md +1 -1
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -727
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
package/bin/pstack.test.mjs
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join, resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { installPstack, prepareRelease, validateLock, verifyInstallation, verifyRelease } from './pstack.mjs';
|
|
9
|
-
|
|
10
|
-
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
-
const baseLock = JSON.parse(readFileSync(join(packageRoot, 'scripts', 'pstack.lock.json'), 'utf8'));
|
|
12
|
-
|
|
13
|
-
function put(path, content) {
|
|
14
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
-
writeFileSync(path, content);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function fixture(t) {
|
|
19
|
-
const temporary = mkdtempSync(join(tmpdir(), 'cde-pstack-'));
|
|
20
|
-
t.after(() => rmSync(temporary, { recursive: true, force: true }));
|
|
21
|
-
const checkout = join(temporary, 'checkout');
|
|
22
|
-
for (const [component, names] of Object.entries(baseLock.requiredSkills)) {
|
|
23
|
-
for (const name of names) put(join(checkout, component, 'skills', name, 'SKILL.md'), `---\nname: ${name}\ndescription: Test ${name}.\n---\n\nRun ${name}.\n`);
|
|
24
|
-
put(join(checkout, component, 'LICENSE'), `${component} license\n`);
|
|
25
|
-
put(join(checkout, component, '.cursor-plugin', 'plugin.json'), JSON.stringify({ name: component, skills: './skills' }));
|
|
26
|
-
}
|
|
27
|
-
put(join(checkout, 'pstack', 'skills', 'poteto-mode', 'playbooks', 'feature.md'), 'Build a small task, delegate, and verify it.\n');
|
|
28
|
-
put(join(checkout, 'pstack', 'agents', 'poteto-agent.md'), 'Read poteto-mode and report complete findings.\n');
|
|
29
|
-
put(join(checkout, 'pstack', 'scripts', 'proof.sh'), '#!/bin/sh\nprintf proof\n');
|
|
30
|
-
chmodSync(join(checkout, 'pstack', 'scripts', 'proof.sh'), 0o755);
|
|
31
|
-
put(join(checkout, 'pstack', 'assets', 'example.png'), Buffer.from([137, 80, 78, 71, 0, 255]));
|
|
32
|
-
const project = join(temporary, 'project with spaces');
|
|
33
|
-
const options = { project, lock: structuredClone(baseLock), packageRoot };
|
|
34
|
-
const dependencies = { fetchSource: (_lock, destination) => cpSync(checkout, destination, { recursive: true }), now: 10000000 };
|
|
35
|
-
return { temporary, checkout, options, dependencies, store: join(project, '.claude', 'pstack') };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
for (const host of ['claude', 'codex', 'cursor']) {
|
|
39
|
-
test(`clean ${host} filesystem install publishes all entry points and supporting files`, t => {
|
|
40
|
-
const f = fixture(t);
|
|
41
|
-
const result = installPstack({ ...f.options, host }, f.dependencies);
|
|
42
|
-
assert.equal(result.status, 'installed');
|
|
43
|
-
assert.equal(result.skillCount, 5);
|
|
44
|
-
assert.equal(result.commit, '27e2a62ff94f9af4b5e68435e41cdceacadb840c');
|
|
45
|
-
assert.equal(result.verification, 'filesystem-only');
|
|
46
|
-
const release = join(f.store, 'releases', result.release);
|
|
47
|
-
const releaseManifest = JSON.parse(readFileSync(join(release, 'release.json'), 'utf8'));
|
|
48
|
-
assert.equal(releaseManifest.skills.find(skill => skill.component === 'pstack').name, 'pstack:poteto-mode');
|
|
49
|
-
assert.equal(
|
|
50
|
-
releaseManifest.policyPath,
|
|
51
|
-
join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json'),
|
|
52
|
-
);
|
|
53
|
-
assert.equal(
|
|
54
|
-
readFileSync(join(release, 'compat', 'subagent_model_policy.mjs'), 'utf8'),
|
|
55
|
-
readFileSync(join(packageRoot, 'scripts', 'subagent_model_policy.mjs'), 'utf8'),
|
|
56
|
-
);
|
|
57
|
-
assert.equal(
|
|
58
|
-
readFileSync(join(release, 'compat', 'subagent-model-policy.json'), 'utf8'),
|
|
59
|
-
readFileSync(join(packageRoot, 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
60
|
-
);
|
|
61
|
-
assert.equal(
|
|
62
|
-
readFileSync(join(release, 'compat', 'resolve_advisor_model_route.mjs'), 'utf8'),
|
|
63
|
-
readFileSync(join(packageRoot, 'scripts', 'resolve_advisor_model_route.mjs'), 'utf8'),
|
|
64
|
-
);
|
|
65
|
-
assert.equal(
|
|
66
|
-
readFileSync(join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
67
|
-
readFileSync(join(packageRoot, 'rules', 'subagent-model-policy.json'), 'utf8'),
|
|
68
|
-
);
|
|
69
|
-
const expected = ['cde-create-skill', 'cursor-team-kit-control-cli', 'cursor-team-kit-control-ui', 'cursor-team-kit-deslop', 'pstack'];
|
|
70
|
-
for (const home of ['.claude', '.agents']) {
|
|
71
|
-
const skillsHome = join(f.options.project, home, 'skills');
|
|
72
|
-
assert.deepEqual(readdirSync(skillsHome).sort(), expected);
|
|
73
|
-
const pstackRoot = join(skillsHome, 'pstack');
|
|
74
|
-
assert.equal(lstatSync(pstackRoot).isSymbolicLink(), true);
|
|
75
|
-
const entry = join(pstackRoot, 'poteto-mode');
|
|
76
|
-
assert.equal(realpathSync(entry), join(release, 'runtime', 'pstack', 'skills', 'poteto-mode'));
|
|
77
|
-
const text = readFileSync(join(entry, 'SKILL.md'), 'utf8');
|
|
78
|
-
assert.match(text, /name: poteto-mode/);
|
|
79
|
-
assert.ok(text.includes(JSON.stringify(join(release, 'compat', 'common.md'))));
|
|
80
|
-
assert.equal(readFileSync(join(entry, 'playbooks', 'feature.md'), 'utf8'), 'Build a small task, delegate, and verify it.\n');
|
|
81
|
-
const manifest = JSON.parse(readFileSync(join(pstackRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
82
|
-
assert.equal(manifest.name, 'pstack');
|
|
83
|
-
assert.deepEqual(manifest.skills, ['./poteto-mode']);
|
|
84
|
-
}
|
|
85
|
-
assert.equal(existsSync(join(f.options.project, '.cursor')), false);
|
|
86
|
-
for (const component of baseLock.components) {
|
|
87
|
-
assert.equal(existsSync(join(release, 'runtime', component, '.cursor-plugin', 'plugin.json')), false);
|
|
88
|
-
assert.equal(readFileSync(join(release, 'upstream', component, '.cursor-plugin', 'plugin.json'), 'utf8'),
|
|
89
|
-
readFileSync(join(f.checkout, component, '.cursor-plugin', 'plugin.json'), 'utf8'));
|
|
90
|
-
}
|
|
91
|
-
assert.equal(readFileSync(join(release, 'upstream', 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'utf8'), readFileSync(join(f.checkout, 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'utf8'));
|
|
92
|
-
assert.deepEqual(readFileSync(join(release, 'upstream', 'pstack', 'assets', 'example.png')), Buffer.from([137, 80, 78, 71, 0, 255]));
|
|
93
|
-
assert.equal(readFileSync(join(release, 'runtime', 'pstack', 'agents', 'poteto-agent.md'), 'utf8'), 'Read poteto-mode and report complete findings.\n');
|
|
94
|
-
assert.equal(readFileSync(join(release, 'upstream', 'cursor-team-kit', 'LICENSE'), 'utf8'), 'cursor-team-kit license\n');
|
|
95
|
-
if (process.platform !== 'win32') assert.equal(statSync(join(release, 'runtime', 'pstack', 'scripts', 'proof.sh')).mode & 0o777, 0o755);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
test('unchanged install avoids another upstream fetch', t => {
|
|
100
|
-
const f = fixture(t);
|
|
101
|
-
const first = installPstack(f.options, f.dependencies);
|
|
102
|
-
const second = installPstack(f.options, { fetchSource: () => assert.fail('unexpected fetch') });
|
|
103
|
-
assert.equal(second.status, 'unchanged');
|
|
104
|
-
assert.equal(second.release, first.release);
|
|
105
|
-
assert.deepEqual(second.links, first.links);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('policy edits survive a pstack update', t => {
|
|
109
|
-
const f = fixture(t);
|
|
110
|
-
installPstack(f.options, f.dependencies);
|
|
111
|
-
const policyPath = join(f.options.project, '.agents', 'rules', 'subagent-model-policy.json');
|
|
112
|
-
const editedPolicy = '{"schemaVersion":1,"edited":true}\n';
|
|
113
|
-
writeFileSync(policyPath, editedPolicy);
|
|
114
|
-
installPstack({ ...f.options, lock: { ...baseLock, commit: 'b'.repeat(40) } }, f.dependencies);
|
|
115
|
-
assert.equal(readFileSync(policyPath, 'utf8'), editedPolicy);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('a release cannot redirect the installed policy path', t => {
|
|
119
|
-
const f = fixture(t);
|
|
120
|
-
const first = installPstack(f.options, f.dependencies);
|
|
121
|
-
const releaseRoot = join(f.store, 'releases', first.release);
|
|
122
|
-
const releaseManifestPath = join(releaseRoot, 'release.json');
|
|
123
|
-
const releaseManifest = JSON.parse(readFileSync(releaseManifestPath, 'utf8'));
|
|
124
|
-
const outsidePolicyPath = join(f.temporary, 'outside-policy.json');
|
|
125
|
-
releaseManifest.policyPath = outsidePolicyPath;
|
|
126
|
-
writeFileSync(releaseManifestPath, JSON.stringify(releaseManifest));
|
|
127
|
-
|
|
128
|
-
assert.throws(
|
|
129
|
-
() => installPstack({ ...f.options, strict: true }, f.dependencies),
|
|
130
|
-
/Invalid release policy path/,
|
|
131
|
-
);
|
|
132
|
-
assert.equal(existsSync(outsidePolicyPath), false);
|
|
133
|
-
assert.equal(verifyInstallation(f.options).release, first.release);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('changed, added, and removed skills converge while old releases remain readable', t => {
|
|
137
|
-
const f = fixture(t);
|
|
138
|
-
put(join(f.checkout, 'pstack', 'skills', 'old', 'SKILL.md'), '---\nname: old\ndescription: Old skill.\n---\nOld body.\n');
|
|
139
|
-
const before = installPstack(f.options, f.dependencies);
|
|
140
|
-
const oldRelease = join(f.store, 'releases', before.release);
|
|
141
|
-
const firstBody = readFileSync(join(oldRelease, 'runtime', 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'utf8');
|
|
142
|
-
rmSync(join(f.checkout, 'pstack', 'skills', 'old'), { recursive: true });
|
|
143
|
-
put(join(f.checkout, 'pstack', 'skills', 'new', 'SKILL.md'), '---\nname: new\ndescription: New skill.\n---\nNew body.\n');
|
|
144
|
-
put(join(f.checkout, 'pstack', 'skills', 'poteto-mode', 'playbooks', 'feature.md'), 'Changed playbook.\n');
|
|
145
|
-
const after = installPstack({ ...f.options, lock: { ...baseLock, commit: 'b'.repeat(40) } }, f.dependencies);
|
|
146
|
-
assert.equal(after.skillCount, 6);
|
|
147
|
-
for (const home of ['.claude', '.agents']) {
|
|
148
|
-
const pstackRoot = join(f.options.project, home, 'skills', 'pstack');
|
|
149
|
-
assert.equal(existsSync(join(pstackRoot, 'old')), false);
|
|
150
|
-
assert.equal(readFileSync(join(pstackRoot, 'new', 'SKILL.md'), 'utf8').endsWith('New body.\n'), true);
|
|
151
|
-
assert.equal(readFileSync(join(pstackRoot, 'poteto-mode', 'playbooks', 'feature.md'), 'utf8'), 'Changed playbook.\n');
|
|
152
|
-
const manifest = JSON.parse(readFileSync(join(pstackRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
153
|
-
assert.deepEqual(manifest.skills, ['./new', './poteto-mode']);
|
|
154
|
-
}
|
|
155
|
-
assert.equal(readFileSync(join(oldRelease, 'runtime', 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'utf8'), firstBody);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('missing required dependency retains the previous complete installation', t => {
|
|
159
|
-
const f = fixture(t);
|
|
160
|
-
const first = installPstack(f.options, f.dependencies);
|
|
161
|
-
const priorBytes = readFileSync(join(f.store, 'installed.json'), 'utf8');
|
|
162
|
-
rmSync(join(f.checkout, 'cursor-team-kit', 'skills', 'control-ui'), { recursive: true });
|
|
163
|
-
const result = installPstack({ ...f.options, lock: { ...baseLock, commit: 'c'.repeat(40) } }, f.dependencies);
|
|
164
|
-
assert.equal(result.status, 'retained');
|
|
165
|
-
assert.match(result.warning, /Missing dependency: cursor-team-kit:control-ui/);
|
|
166
|
-
assert.equal(result.release, first.release);
|
|
167
|
-
assert.equal(readFileSync(join(f.store, 'installed.json'), 'utf8'), priorBytes);
|
|
168
|
-
assert.equal(verifyInstallation(f.options).skillCount, 5);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('a failed first install returns failure and publishes no skills', t => {
|
|
172
|
-
const f = fixture(t);
|
|
173
|
-
assert.throws(() => installPstack(f.options, { fetchSource: () => { throw new Error('network unavailable'); } }), /network unavailable/);
|
|
174
|
-
assert.equal(existsSync(join(f.options.project, '.claude', 'skills')), false);
|
|
175
|
-
assert.equal(existsSync(join(f.store, '.install-lock')), false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('unmanaged pstack root collision preserves user files', t => {
|
|
179
|
-
const f = fixture(t);
|
|
180
|
-
const userFile = join(f.options.project, '.claude', 'skills', 'pstack', 'poteto-mode', 'SKILL.md');
|
|
181
|
-
put(userFile, 'Personal workflow\n');
|
|
182
|
-
assert.throws(() => installPstack(f.options, f.dependencies), /Keep existing unmanaged path/);
|
|
183
|
-
assert.equal(readFileSync(userFile, 'utf8'), 'Personal workflow\n');
|
|
184
|
-
assert.equal(existsSync(join(f.store, 'installed.json')), false);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test('existing shared skills-directory pointer is preserved', t => {
|
|
188
|
-
const f = fixture(t);
|
|
189
|
-
const agents = join(f.options.project, '.agents', 'skills');
|
|
190
|
-
mkdirSync(agents, { recursive: true });
|
|
191
|
-
mkdirSync(join(f.options.project, '.claude'), { recursive: true });
|
|
192
|
-
symlinkSync(agents, join(f.options.project, '.claude', 'skills'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
193
|
-
const before = readlinkSync(join(f.options.project, '.claude', 'skills'));
|
|
194
|
-
const result = installPstack(f.options, f.dependencies);
|
|
195
|
-
assert.equal(Object.keys(result.links).length, 5);
|
|
196
|
-
assert.equal(readlinkSync(join(f.options.project, '.claude', 'skills')), before);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('offline reuse checks installed bytes and rejects corruption', t => {
|
|
200
|
-
const f = fixture(t);
|
|
201
|
-
const result = installPstack(f.options, f.dependencies);
|
|
202
|
-
assert.equal(installPstack({ ...f.options, offline: true }).release, result.release);
|
|
203
|
-
put(join(f.store, 'releases', result.release, 'runtime', 'pstack', 'skills', 'poteto-mode', 'SKILL.md'), 'corrupted');
|
|
204
|
-
assert.throws(() => installPstack({ ...f.options, offline: true }), /Changed installed file/);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('unknown namespaced upstream dependency requires adapter review', t => {
|
|
208
|
-
const f = fixture(t);
|
|
209
|
-
put(join(f.checkout, 'pstack', 'skills', 'poteto-mode', 'reference.md'), 'Invoke cursor-team-kit:missing-skill.\n');
|
|
210
|
-
assert.throws(() => installPstack(f.options, f.dependencies), /Unresolved skill dependency/);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('source symlinks are rejected before publication', t => {
|
|
214
|
-
const f = fixture(t);
|
|
215
|
-
const outside = join(f.temporary, 'outside-source');
|
|
216
|
-
mkdirSync(outside);
|
|
217
|
-
put(join(outside, 'personal.txt'), 'Retain this file.');
|
|
218
|
-
const dependencies = { ...f.dependencies, fetchSource: (lock, destination) => {
|
|
219
|
-
f.dependencies.fetchSource(lock, destination);
|
|
220
|
-
const link = join(destination, 'pstack', 'escape');
|
|
221
|
-
symlinkSync(outside, link, process.platform === 'win32' ? 'junction' : 'dir');
|
|
222
|
-
assert.equal(lstatSync(link).isSymbolicLink(), true);
|
|
223
|
-
} };
|
|
224
|
-
assert.throws(() => installPstack(f.options, dependencies), /symlinks require review/);
|
|
225
|
-
assert.equal(readFileSync(join(outside, 'personal.txt'), 'utf8'), 'Retain this file.');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test('update checks use the shared record and respect the interval', t => {
|
|
229
|
-
const f = fixture(t);
|
|
230
|
-
const shared = { ...baseLock, commit: 'd'.repeat(40) };
|
|
231
|
-
let calls = 0;
|
|
232
|
-
const dependencies = { ...f.dependencies, readCentralLock: () => { calls++; return shared; } };
|
|
233
|
-
const first = installPstack({ ...f.options, refresh: true }, dependencies);
|
|
234
|
-
assert.equal(first.commit, 'd'.repeat(40));
|
|
235
|
-
const second = installPstack({ ...f.options, refresh: true }, { ...dependencies, now: dependencies.now + 100 });
|
|
236
|
-
assert.equal(second.commit, 'd'.repeat(40));
|
|
237
|
-
assert.equal(calls, 1);
|
|
238
|
-
installPstack({ ...f.options, refresh: true }, { ...dependencies, now: dependencies.now + 3600001 });
|
|
239
|
-
assert.equal(calls, 2);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('failed shared-record refresh retains and reports the previous revision', t => {
|
|
243
|
-
const f = fixture(t);
|
|
244
|
-
const first = installPstack(f.options, f.dependencies);
|
|
245
|
-
const result = installPstack({ ...f.options, refresh: true }, { ...f.dependencies, readCentralLock: () => { throw new Error('record unavailable'); } });
|
|
246
|
-
assert.equal(result.release, first.release);
|
|
247
|
-
assert.equal(result.warning, 'record unavailable');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('an active launcher lease defers updating shared instructions', t => {
|
|
251
|
-
const f = fixture(t);
|
|
252
|
-
const first = installPstack(f.options, f.dependencies);
|
|
253
|
-
put(join(f.store, 'sessions', String(process.pid)), first.release);
|
|
254
|
-
const result = installPstack({ ...f.options, lock: { ...baseLock, commit: 'e'.repeat(40) } }, { fetchSource: () => assert.fail('updated during an active session') });
|
|
255
|
-
assert.equal(result.status, 'retained');
|
|
256
|
-
assert.equal(result.release, first.release);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test('unsupported lock input fails before source fetch', t => {
|
|
260
|
-
const f = fixture(t);
|
|
261
|
-
for (const change of [{ commit: 'main' }, { adapterVersion: 2 }, { repository: 'https://example.test/repo' }, { components: ['../outside'] }, { requiredSkills: {} }]) {
|
|
262
|
-
assert.throws(() => installPstack({ ...f.options, lock: { ...baseLock, ...change } }, { fetchSource: () => assert.fail('unexpected fetch') }));
|
|
263
|
-
}
|
|
264
|
-
assert.equal(validateLock(baseLock), baseLock);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test('CLI exposes deterministic verification and rejects unknown flags', t => {
|
|
268
|
-
const f = fixture(t);
|
|
269
|
-
installPstack(f.options, f.dependencies);
|
|
270
|
-
const command = join(packageRoot, 'bin', 'pstack.mjs');
|
|
271
|
-
const verified = spawnSync(process.execPath, [command, 'verify', '--project', f.options.project], { encoding: 'utf8' });
|
|
272
|
-
assert.equal(verified.status, 0, verified.stderr);
|
|
273
|
-
assert.equal(JSON.parse(verified.stdout).skillCount, 5);
|
|
274
|
-
const invalid = spawnSync(process.execPath, [command, 'install', '--host', 'unknown'], { encoding: 'utf8' });
|
|
275
|
-
assert.equal(invalid.status, 1);
|
|
276
|
-
assert.match(invalid.stderr, /Host must be/);
|
|
277
|
-
const help = spawnSync(process.execPath, [command, '--help'], { encoding: 'utf8' });
|
|
278
|
-
assert.equal(help.status, 0);
|
|
279
|
-
assert.match(help.stdout, /cde-pstack/);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('resumed Claude hook retains the cached release and emits native hook JSON', t => {
|
|
283
|
-
const f = fixture(t);
|
|
284
|
-
const first = installPstack(f.options, f.dependencies);
|
|
285
|
-
const result = spawnSync(process.execPath, [join(packageRoot, 'bin', 'pstack.mjs'), 'hook', '--project', f.options.project], {
|
|
286
|
-
encoding: 'utf8', input: JSON.stringify({ source: 'resume' }),
|
|
287
|
-
});
|
|
288
|
-
assert.equal(result.status, 0, result.stderr);
|
|
289
|
-
const output = JSON.parse(result.stdout);
|
|
290
|
-
assert.equal(output.hookSpecificOutput.hookEventName, 'SessionStart');
|
|
291
|
-
assert.ok(output.hookSpecificOutput.additionalContext.includes(first.commit));
|
|
292
|
-
assert.ok(output.hookSpecificOutput.additionalContext.includes('common.md'));
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('release validation catches missing files and escaping manifest paths', t => {
|
|
296
|
-
const f = fixture(t);
|
|
297
|
-
const result = installPstack(f.options, f.dependencies);
|
|
298
|
-
const root = join(f.store, 'releases', result.release);
|
|
299
|
-
assert.equal(verifyRelease(root).skills.length, 5);
|
|
300
|
-
const manifest = JSON.parse(readFileSync(join(root, 'release.json'), 'utf8'));
|
|
301
|
-
manifest.fileDigests['../../outside'] = 'bad';
|
|
302
|
-
put(join(root, 'release.json'), JSON.stringify(manifest));
|
|
303
|
-
assert.throws(() => verifyRelease(root), /Invalid release path/);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test('state-write failure restores the old published pointers', t => {
|
|
307
|
-
const f = fixture(t);
|
|
308
|
-
const first = installPstack(f.options, f.dependencies);
|
|
309
|
-
const priorBytes = readFileSync(join(f.store, 'installed.json'), 'utf8');
|
|
310
|
-
mkdirSync(join(f.store, 'installed.next.json'));
|
|
311
|
-
const result = installPstack({ ...f.options, lock: { ...baseLock, commit: 'f'.repeat(40) } }, f.dependencies);
|
|
312
|
-
assert.equal(result.status, 'retained');
|
|
313
|
-
assert.equal(result.release, first.release);
|
|
314
|
-
assert.equal(readFileSync(join(f.store, 'installed.json'), 'utf8'), priorBytes);
|
|
315
|
-
assert.equal(verifyInstallation(f.options).commit, first.commit);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test('npm-style linked CLI runs its entry point', t => {
|
|
319
|
-
const f = fixture(t);
|
|
320
|
-
if (process.platform === 'win32') return;
|
|
321
|
-
const link = join(f.temporary, 'cde-pstack');
|
|
322
|
-
symlinkSync(join(packageRoot, 'bin', 'pstack.mjs'), link);
|
|
323
|
-
const result = spawnSync(process.execPath, [link, '--help'], { encoding: 'utf8' });
|
|
324
|
-
assert.equal(result.status, 0);
|
|
325
|
-
assert.match(result.stdout, /Usage: cde-pstack/);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test('session reservation is recorded before install releases its lock', t => {
|
|
329
|
-
const f = fixture(t);
|
|
330
|
-
const result = installPstack({ ...f.options, reserveSession: true }, f.dependencies);
|
|
331
|
-
assert.equal(readFileSync(join(f.store, 'sessions', String(process.pid)), 'utf8'), result.release);
|
|
332
|
-
assert.equal(existsSync(join(f.store, '.install-lock')), false);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
test('a changed installer creates a fresh generation for the same upstream pin', async t => {
|
|
337
|
-
const f = fixture(t);
|
|
338
|
-
const first = installPstack(f.options, f.dependencies);
|
|
339
|
-
const copied = join(f.temporary, 'updated-installer', 'bin', 'pstack.mjs');
|
|
340
|
-
put(copied, readFileSync(join(packageRoot, 'bin', 'pstack.mjs'), 'utf8') + '\n');
|
|
341
|
-
const updated = await import(pathToFileURL(copied).href);
|
|
342
|
-
const second = updated.installPstack(f.options, f.dependencies);
|
|
343
|
-
assert.equal(second.commit, first.commit);
|
|
344
|
-
assert.notEqual(second.adapterDigest, first.adapterDigest);
|
|
345
|
-
assert.notEqual(second.release, first.release);
|
|
346
|
-
assert.equal(verifyInstallation(f.options).release, second.release);
|
|
347
|
-
});
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import { strict as assert } from 'node:assert';
|
|
3
|
-
import {
|
|
4
|
-
mkdtempSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
symlinkSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from 'node:fs';
|
|
11
|
-
import { tmpdir } from 'node:os';
|
|
12
|
-
import { dirname, join } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import {
|
|
16
|
-
PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH,
|
|
17
|
-
findPstackSkillDirectoryNames,
|
|
18
|
-
refreshPstackPluginManifest,
|
|
19
|
-
} from '../scripts/refresh_pstack_plugin_skills.mjs';
|
|
20
|
-
|
|
21
|
-
function buildPluginRoot(homeDirectory, skillNames, manifest) {
|
|
22
|
-
const pluginRoot = join(homeDirectory, 'pstack');
|
|
23
|
-
for (const eachSkillName of skillNames) {
|
|
24
|
-
const skillDirectory = join(pluginRoot, eachSkillName);
|
|
25
|
-
mkdirSync(skillDirectory, { recursive: true });
|
|
26
|
-
writeFileSync(join(skillDirectory, 'SKILL.md'), '---\n');
|
|
27
|
-
}
|
|
28
|
-
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
29
|
-
mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
|
|
30
|
-
if (manifest !== null) {
|
|
31
|
-
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
32
|
-
}
|
|
33
|
-
return pluginRoot;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function withTemporaryHome(runAssertions) {
|
|
37
|
-
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-manifest-'));
|
|
38
|
-
try {
|
|
39
|
-
runAssertions(homeDirectory);
|
|
40
|
-
} finally {
|
|
41
|
-
rmSync(homeDirectory, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
test('finds only sub-folders that hold a skill entry file', () => {
|
|
46
|
-
withTemporaryHome((homeDirectory) => {
|
|
47
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how', 'why'], { skills: [] });
|
|
48
|
-
mkdirSync(join(pluginRoot, 'docs'), { recursive: true });
|
|
49
|
-
writeFileSync(join(pluginRoot, '.claude-plugin', 'SKILL.md'), '---\n');
|
|
50
|
-
|
|
51
|
-
assert.deepEqual(findPstackSkillDirectoryNames(pluginRoot), ['how', 'why']);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('writes a manifest when the plugin folder has none', () => {
|
|
56
|
-
withTemporaryHome((homeDirectory) => {
|
|
57
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
58
|
-
|
|
59
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
60
|
-
|
|
61
|
-
assert.equal(outcome.didWrite, true);
|
|
62
|
-
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
63
|
-
const written = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
64
|
-
assert.equal(written.name, 'pstack');
|
|
65
|
-
assert.deepEqual(written.skills, ['./how']);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('adds a skill folder the manifest does not list', () => {
|
|
70
|
-
withTemporaryHome((homeDirectory) => {
|
|
71
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how', 'why'], {
|
|
72
|
-
name: 'pstack',
|
|
73
|
-
skills: ['./how'],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
77
|
-
|
|
78
|
-
assert.equal(outcome.didWrite, true);
|
|
79
|
-
assert.deepEqual(outcome.previousSkillPaths, ['./how']);
|
|
80
|
-
assert.deepEqual(outcome.currentSkillPaths, ['./how', './why']);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('leaves the manifest byte for byte when nothing changed', () => {
|
|
85
|
-
withTemporaryHome((homeDirectory) => {
|
|
86
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
87
|
-
name: 'pstack',
|
|
88
|
-
skills: ['./how'],
|
|
89
|
-
});
|
|
90
|
-
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
91
|
-
writeFileSync(manifestPath, JSON.stringify({ name: 'pstack', skills: ['./how'] }));
|
|
92
|
-
const before = readFileSync(manifestPath, 'utf8');
|
|
93
|
-
|
|
94
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
95
|
-
|
|
96
|
-
assert.equal(outcome.didWrite, false);
|
|
97
|
-
assert.equal(readFileSync(manifestPath, 'utf8'), before);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('reads a string skills value as a one-entry list', () => {
|
|
102
|
-
withTemporaryHome((homeDirectory) => {
|
|
103
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
104
|
-
name: 'pstack',
|
|
105
|
-
skills: './',
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
109
|
-
|
|
110
|
-
assert.deepEqual(outcome.previousSkillPaths, ['./']);
|
|
111
|
-
assert.deepEqual(outcome.currentSkillPaths, ['./how']);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('keeps manifest fields the refresher does not own', () => {
|
|
116
|
-
withTemporaryHome((homeDirectory) => {
|
|
117
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
118
|
-
name: 'pstack',
|
|
119
|
-
version: '9.9.9',
|
|
120
|
-
description: 'a description the user edited',
|
|
121
|
-
skills: [],
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
refreshPstackPluginManifest(pluginRoot);
|
|
125
|
-
|
|
126
|
-
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
127
|
-
const written = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
128
|
-
assert.equal(written.version, '9.9.9');
|
|
129
|
-
assert.equal(written.description, 'a description the user edited');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('refuses a folder that is itself one skill', () => {
|
|
134
|
-
withTemporaryHome((homeDirectory) => {
|
|
135
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
136
|
-
writeFileSync(join(pluginRoot, 'SKILL.md'), '---\n');
|
|
137
|
-
|
|
138
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
139
|
-
|
|
140
|
-
assert.equal(outcome.didWrite, false);
|
|
141
|
-
assert.equal(outcome.reason, 'plugin root is itself a skill');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('running the file directly reports what it did', () => {
|
|
146
|
-
withTemporaryHome((homeDirectory) => {
|
|
147
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
148
|
-
const modulePath = join(
|
|
149
|
-
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
150
|
-
'scripts',
|
|
151
|
-
'refresh_pstack_plugin_skills.mjs',
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const firstRun = execFileSync('node', [modulePath, pluginRoot], { encoding: 'utf8' });
|
|
155
|
-
const secondRun = execFileSync('node', [modulePath, pluginRoot], { encoding: 'utf8' });
|
|
156
|
-
|
|
157
|
-
assert.match(firstRun, /Updated\. 1 skills listed\./);
|
|
158
|
-
assert.match(secondRun, /No change\. 1 skills listed\./);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('running the file through a directory link still reports what it did', () => {
|
|
163
|
-
withTemporaryHome((homeDirectory) => {
|
|
164
|
-
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
165
|
-
const scriptsDirectory = join(
|
|
166
|
-
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
167
|
-
'scripts',
|
|
168
|
-
);
|
|
169
|
-
const linkedScriptsDirectory = join(homeDirectory, 'linked-scripts');
|
|
170
|
-
symlinkSync(scriptsDirectory, linkedScriptsDirectory, 'junction');
|
|
171
|
-
const linkedModulePath = join(linkedScriptsDirectory, 'refresh_pstack_plugin_skills.mjs');
|
|
172
|
-
|
|
173
|
-
const output = execFileSync('node', [linkedModulePath, pluginRoot], { encoding: 'utf8' });
|
|
174
|
-
|
|
175
|
-
assert.match(output, /Updated\. 1 skills listed\./);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('with no folder argument it follows CLAUDE_CONFIG_DIR', () => {
|
|
180
|
-
withTemporaryHome((homeDirectory) => {
|
|
181
|
-
const claudeConfigDirectory = join(homeDirectory, 'profile', '.claude');
|
|
182
|
-
const pluginRoot = join(claudeConfigDirectory, 'skills', 'pstack');
|
|
183
|
-
mkdirSync(join(pluginRoot, 'how'), { recursive: true });
|
|
184
|
-
writeFileSync(join(pluginRoot, 'how', 'SKILL.md'), '---\n');
|
|
185
|
-
const modulePath = join(
|
|
186
|
-
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
187
|
-
'scripts',
|
|
188
|
-
'refresh_pstack_plugin_skills.mjs',
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const output = execFileSync('node', [modulePath], {
|
|
192
|
-
encoding: 'utf8',
|
|
193
|
-
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeConfigDirectory },
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
assert.match(output, /Updated\. 1 skills listed\./);
|
|
197
|
-
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
198
|
-
assert.deepEqual(JSON.parse(readFileSync(manifestPath, 'utf8')).skills, ['./how']);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test('refuses a folder that holds no skills', () => {
|
|
203
|
-
withTemporaryHome((homeDirectory) => {
|
|
204
|
-
const pluginRoot = buildPluginRoot(homeDirectory, [], null);
|
|
205
|
-
|
|
206
|
-
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
207
|
-
|
|
208
|
-
assert.equal(outcome.didWrite, false);
|
|
209
|
-
assert.equal(outcome.reason, 'plugin root holds no skills');
|
|
210
|
-
});
|
|
211
|
-
});
|