claude-dev-env 7.7.1 → 8.1.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/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/audit-categories.json +0 -4
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
- 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/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/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/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/anti-corollary-tests.md +3 -1
- package/rules/archiving-agent-config.md +70 -0
- package/rules/asd-ste100-language.md +1 -1
- package/rules/ci-owns-the-gate.md +73 -0
- package/rules/cleanup-temp-files.md +1 -1
- package/rules/code-standards.md +3 -4
- package/rules/docstring-prose-matches-implementation.md +3 -3
- package/rules/explore-thoroughly.md +2 -2
- package/rules/failure-blast-radius.md +10 -9
- package/rules/git-workflow.md +27 -12
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/nas-ssh-invocation.md +1 -1
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/research-mode.md +3 -1
- package/rules/retired-hook-prose.md +10 -2
- package/rules/testing.md +3 -3
- package/rules/verify-before-asking.md +4 -1
- package/rules/verify-runtime-state.md +2 -2
- 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_codex_compat_materializer.py +3 -3
- 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 -776
- 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/install.test.mjs
CHANGED
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
readdirSync,
|
|
12
12
|
existsSync,
|
|
13
13
|
copyFileSync,
|
|
14
|
-
cpSync,
|
|
15
|
-
lstatSync,
|
|
16
14
|
} from 'node:fs';
|
|
17
15
|
import { tmpdir } from 'node:os';
|
|
18
16
|
import { join, dirname } from 'node:path';
|
|
@@ -20,8 +18,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
20
18
|
|
|
21
19
|
import {
|
|
22
20
|
collectPackageSourceConflicts,
|
|
23
|
-
installPstackRelease,
|
|
24
|
-
shouldInstallPstackRelease,
|
|
25
21
|
CONTENT_DIRECTORIES,
|
|
26
22
|
CORE_INCLUDE_DIRECTORIES,
|
|
27
23
|
CORE_SKILLS,
|
|
@@ -54,7 +50,6 @@ import {
|
|
|
54
50
|
hookCommandComparisonSpelling,
|
|
55
51
|
detectPython,
|
|
56
52
|
retainNewestRunBackupOnly,
|
|
57
|
-
refreshInstalledPstackPluginManifest,
|
|
58
53
|
} from './install.mjs';
|
|
59
54
|
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
60
55
|
|
|
@@ -2881,42 +2876,6 @@ test('pruneStaleInstalledFiles leaves the run backup root standing when the rena
|
|
|
2881
2876
|
}
|
|
2882
2877
|
});
|
|
2883
2878
|
|
|
2884
|
-
test('the pstack step writes a manifest for a skills root that carries pstack', () => {
|
|
2885
|
-
const skillsRoot = mkdtempSync(join(tmpdir(), 'cdev-pstack-step-'));
|
|
2886
|
-
try {
|
|
2887
|
-
const skillDirectory = join(skillsRoot, 'pstack', 'how');
|
|
2888
|
-
mkdirSync(skillDirectory, { recursive: true });
|
|
2889
|
-
writeFileSync(join(skillDirectory, 'SKILL.md'), '---\nname: how\n---\n');
|
|
2890
|
-
|
|
2891
|
-
const manifestPath = refreshInstalledPstackPluginManifest(skillsRoot);
|
|
2892
|
-
|
|
2893
|
-
assert.equal(
|
|
2894
|
-
manifestPath,
|
|
2895
|
-
join(skillsRoot, 'pstack', '.claude-plugin', 'plugin.json'),
|
|
2896
|
-
'the step reports the manifest it wrote so the run records the path',
|
|
2897
|
-
);
|
|
2898
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
2899
|
-
assert.deepEqual(manifest.skills, ['./how']);
|
|
2900
|
-
assert.equal(
|
|
2901
|
-
refreshInstalledPstackPluginManifest(skillsRoot),
|
|
2902
|
-
null,
|
|
2903
|
-
'a second run writes nothing and reports no path',
|
|
2904
|
-
);
|
|
2905
|
-
} finally {
|
|
2906
|
-
rmSync(skillsRoot, { recursive: true, force: true });
|
|
2907
|
-
}
|
|
2908
|
-
});
|
|
2909
|
-
|
|
2910
|
-
test('the pstack step reports no path for a skills root without pstack', () => {
|
|
2911
|
-
const skillsRoot = mkdtempSync(join(tmpdir(), 'cdev-pstack-absent-'));
|
|
2912
|
-
try {
|
|
2913
|
-
assert.equal(refreshInstalledPstackPluginManifest(skillsRoot), null);
|
|
2914
|
-
assert.equal(existsSync(join(skillsRoot, 'pstack')), false);
|
|
2915
|
-
} finally {
|
|
2916
|
-
rmSync(skillsRoot, { recursive: true, force: true });
|
|
2917
|
-
}
|
|
2918
|
-
});
|
|
2919
|
-
|
|
2920
2879
|
const RETIRED_SKILLS = ['pr-small-cl', 'session-log', 'session-tidy'];
|
|
2921
2880
|
|
|
2922
2881
|
test('each core skill has a shipped entry point', () => {
|
|
@@ -2934,13 +2893,9 @@ test('retired skills leave install groups and stay in the cleanup registry', ()
|
|
|
2934
2893
|
}
|
|
2935
2894
|
});
|
|
2936
2895
|
|
|
2937
|
-
test('README
|
|
2896
|
+
test('the README names current session tools', () => {
|
|
2938
2897
|
const readme = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8');
|
|
2939
|
-
const vaultRule = readFileSync(new URL('../rules/vault-context.md', import.meta.url), 'utf8');
|
|
2940
2898
|
assert.doesNotMatch(readme, /--only journal|session-log|session-tidy/);
|
|
2941
|
-
assert.doesNotMatch(vaultRule, /`session-log`|`recall`/);
|
|
2942
|
-
assert.match(vaultRule, /obsidian MCP tools/);
|
|
2943
|
-
assert.match(vaultRule, /project.*frontmatter/);
|
|
2944
2899
|
});
|
|
2945
2900
|
|
|
2946
2901
|
test('the help output states that the installer reads only flags', () => {
|
|
@@ -2980,43 +2935,24 @@ test('pruneRetiredHookEntriesFromSettings warns and returns 0 when the host conf
|
|
|
2980
2935
|
|
|
2981
2936
|
const PSTACK_TEST_PACKAGE_ROOT = dirname(fileURLToPath(new URL('./install.mjs', import.meta.url)));
|
|
2982
2937
|
const PSTACK_TEST_INSTALLER_PATH = fileURLToPath(new URL('./install.mjs', import.meta.url));
|
|
2983
|
-
const PSTACK_TEST_LOCK = JSON.parse(readFileSync(
|
|
2984
|
-
new URL('../scripts/pstack.lock.json', import.meta.url),
|
|
2985
|
-
'utf8',
|
|
2986
|
-
));
|
|
2987
|
-
|
|
2988
|
-
function writePstackFixtureFile(filePath, fileContent) {
|
|
2989
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
2990
|
-
writeFileSync(filePath, fileContent);
|
|
2991
|
-
}
|
|
2992
2938
|
|
|
2993
|
-
function
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
return
|
|
3007
|
-
managedRoot,
|
|
3008
|
-
options: {
|
|
3009
|
-
root: managedRoot,
|
|
3010
|
-
lock: structuredClone(PSTACK_TEST_LOCK),
|
|
3011
|
-
packageRoot: dirname(PSTACK_TEST_PACKAGE_ROOT),
|
|
3012
|
-
},
|
|
3013
|
-
dependencies: {
|
|
3014
|
-
fetchSource: (_lock, destination) => cpSync(checkout, destination, { recursive: true }),
|
|
3015
|
-
},
|
|
3016
|
-
};
|
|
2939
|
+
function writeRecordingHostCommand(directory, name, logPath) {
|
|
2940
|
+
mkdirSync(directory, { recursive: true });
|
|
2941
|
+
if (process.platform === 'win32') {
|
|
2942
|
+
const batchPath = join(directory, `${name}.cmd`);
|
|
2943
|
+
writeFileSync(batchPath, `@echo off\r\n>>"${logPath}" echo ${name} %*\r\n`);
|
|
2944
|
+
return batchPath;
|
|
2945
|
+
}
|
|
2946
|
+
const commandPath = join(directory, name);
|
|
2947
|
+
writeFileSync(
|
|
2948
|
+
commandPath,
|
|
2949
|
+
`#!/bin/sh\nprintf '%s %s\\n' "${name}" "$*" >> "${logPath}"\n`,
|
|
2950
|
+
{ mode: 0o755 },
|
|
2951
|
+
);
|
|
2952
|
+
return commandPath;
|
|
3017
2953
|
}
|
|
3018
2954
|
|
|
3019
|
-
function runPstackInstaller(homeDirectory, extraArguments) {
|
|
2955
|
+
function runPstackInstaller(homeDirectory, extraArguments, environmentOverrides = {}) {
|
|
3020
2956
|
return execFileSync('node', [PSTACK_TEST_INSTALLER_PATH, ...extraArguments], {
|
|
3021
2957
|
cwd: dirname(PSTACK_TEST_PACKAGE_ROOT),
|
|
3022
2958
|
encoding: 'utf8',
|
|
@@ -3028,82 +2964,80 @@ function runPstackInstaller(homeDirectory, extraArguments) {
|
|
|
3028
2964
|
CLAUDE_CONFIG_DIR: join(homeDirectory, '.claude'),
|
|
3029
2965
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3030
2966
|
CDE_INSTALL_PSTACK: '1',
|
|
3031
|
-
|
|
2967
|
+
...environmentOverrides,
|
|
3032
2968
|
},
|
|
3033
2969
|
});
|
|
3034
2970
|
}
|
|
3035
2971
|
|
|
3036
|
-
function
|
|
2972
|
+
function pstackPluginSandbox(t) {
|
|
3037
2973
|
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-install-pstack-run-'));
|
|
3038
2974
|
t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3039
|
-
|
|
2975
|
+
const commandLogPath = join(homeDirectory, 'host-commands.log');
|
|
2976
|
+
const commandDirectory = join(homeDirectory, 'host-commands');
|
|
2977
|
+
return {
|
|
2978
|
+
homeDirectory,
|
|
2979
|
+
commandLogPath,
|
|
2980
|
+
environment: {
|
|
2981
|
+
CDE_CLAUDE_EXECUTABLE: writeRecordingHostCommand(commandDirectory, 'claude', commandLogPath),
|
|
2982
|
+
CDE_CODEX_EXECUTABLE: writeRecordingHostCommand(commandDirectory, 'codex', commandLogPath),
|
|
2983
|
+
},
|
|
2984
|
+
recordedCommands() {
|
|
2985
|
+
if (!existsSync(commandLogPath)) return [];
|
|
2986
|
+
return readFileSync(commandLogPath, 'utf8')
|
|
2987
|
+
.split(/\r?\n/)
|
|
2988
|
+
.map(eachLine => eachLine.trim())
|
|
2989
|
+
.filter(Boolean);
|
|
2990
|
+
},
|
|
2991
|
+
};
|
|
3040
2992
|
}
|
|
3041
2993
|
|
|
3042
|
-
test('
|
|
3043
|
-
const
|
|
2994
|
+
test('a full install adds the pstack marketplace and plugin on both hosts', t => {
|
|
2995
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3044
2996
|
|
|
3045
|
-
const
|
|
2997
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3046
2998
|
|
|
3047
|
-
assert.
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
2999
|
+
assert.deepEqual(sandbox.recordedCommands(), [
|
|
3000
|
+
'claude plugin marketplace add michael-denyer/pstack-claude',
|
|
3001
|
+
'claude plugin install pstack@pstack-claude',
|
|
3002
|
+
'codex plugin marketplace add michael-denyer/pstack-claude',
|
|
3003
|
+
'codex plugin add pstack@pstack-claude',
|
|
3004
|
+
]);
|
|
3005
|
+
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
3006
|
+
assert.match(installerOutput, /Pstack \(codex\): installed/);
|
|
3053
3007
|
});
|
|
3054
3008
|
|
|
3055
|
-
test('an
|
|
3056
|
-
const
|
|
3009
|
+
test('an absent host command is reported and leaves the other host installed', t => {
|
|
3010
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3057
3011
|
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3012
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], {
|
|
3013
|
+
...sandbox.environment,
|
|
3014
|
+
CDE_CODEX_EXECUTABLE: join(sandbox.homeDirectory, 'host-commands', 'absent-codex-command'),
|
|
3060
3015
|
});
|
|
3061
3016
|
|
|
3062
|
-
assert.
|
|
3063
|
-
assert.
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
assert.
|
|
3069
|
-
assert.
|
|
3070
|
-
|
|
3071
|
-
|
|
3017
|
+
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
3018
|
+
assert.match(
|
|
3019
|
+
installerOutput,
|
|
3020
|
+
/Pstack \(codex\): (skipped|failed)/,
|
|
3021
|
+
'the absent host is reported either way, and the two unit tests pin which signal maps to skipped',
|
|
3022
|
+
);
|
|
3023
|
+
assert.doesNotMatch(installerOutput, /Pstack \(codex\): installed/);
|
|
3024
|
+
assert.deepEqual(
|
|
3025
|
+
sandbox.recordedCommands().filter(eachCommand => eachCommand.startsWith('codex')),
|
|
3026
|
+
[],
|
|
3027
|
+
);
|
|
3072
3028
|
});
|
|
3073
3029
|
|
|
3074
|
-
test('
|
|
3075
|
-
|
|
3076
|
-
const installerOutput = runPstackInstaller(homeDirectory, []);
|
|
3077
|
-
|
|
3078
|
-
assert.match(installerOutput, /Pstack:/, 'the run reports the pstack step');
|
|
3079
|
-
assert.equal(existsSync(join(homeDirectory, '.claude', 'pstack')), true);
|
|
3080
|
-
});
|
|
3081
|
-
});
|
|
3030
|
+
test('--no-pstack leaves both hosts untouched', t => {
|
|
3031
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3082
3032
|
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3033
|
+
const installerOutput = runPstackInstaller(
|
|
3034
|
+
sandbox.homeDirectory, ['--no-pstack'], sandbox.environment,
|
|
3035
|
+
);
|
|
3086
3036
|
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
});
|
|
3037
|
+
assert.deepEqual(sandbox.recordedCommands(), []);
|
|
3038
|
+
assert.doesNotMatch(installerOutput, /Pstack \(/);
|
|
3090
3039
|
});
|
|
3091
3040
|
|
|
3092
|
-
function runContinuityInstaller(homeDirectory, extraArguments) {
|
|
3093
|
-
return execFileSync('node', [PSTACK_TEST_INSTALLER_PATH, ...extraArguments], {
|
|
3094
|
-
cwd: dirname(PSTACK_TEST_PACKAGE_ROOT),
|
|
3095
|
-
encoding: 'utf8',
|
|
3096
|
-
env: {
|
|
3097
|
-
...process.env,
|
|
3098
|
-
CDE_INSTALL_PSTACK: '0',
|
|
3099
|
-
HOME: homeDirectory,
|
|
3100
|
-
USERPROFILE: homeDirectory,
|
|
3101
|
-
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3102
|
-
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3103
|
-
},
|
|
3104
|
-
});
|
|
3105
|
-
}
|
|
3106
|
-
|
|
3107
3041
|
function continuityCommandCount(configurationPath) {
|
|
3108
3042
|
if (!existsSync(configurationPath)) return 0;
|
|
3109
3043
|
const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
|
|
@@ -3116,32 +3050,59 @@ function continuityCommandCount(configurationPath) {
|
|
|
3116
3050
|
).length;
|
|
3117
3051
|
}
|
|
3118
3052
|
|
|
3119
|
-
test('a full install
|
|
3120
|
-
const
|
|
3121
|
-
|
|
3122
|
-
const
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3053
|
+
test('a full install removes the retired pstack pointers and release store', t => {
|
|
3054
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3055
|
+
const managedRoot = join(sandbox.homeDirectory, '.claude');
|
|
3056
|
+
const agentsSkillsRoot = join(sandbox.homeDirectory, '.agents', 'skills');
|
|
3057
|
+
const releaseSkillsRoot = join(managedRoot, 'pstack', 'releases', 'abc123', 'runtime', 'pstack', 'skills');
|
|
3058
|
+
mkdirSync(join(releaseSkillsRoot, 'poteto-mode'), { recursive: true });
|
|
3059
|
+
mkdirSync(join(managedRoot, 'skills'), { recursive: true });
|
|
3060
|
+
mkdirSync(agentsSkillsRoot, { recursive: true });
|
|
3061
|
+
const allPointerPaths = [
|
|
3062
|
+
join(managedRoot, 'skills', 'pstack'),
|
|
3063
|
+
join(agentsSkillsRoot, 'pstack'),
|
|
3126
3064
|
];
|
|
3065
|
+
for (const pointerPath of allPointerPaths) {
|
|
3066
|
+
symlinkSync(releaseSkillsRoot, pointerPath, 'dir');
|
|
3067
|
+
}
|
|
3127
3068
|
|
|
3128
|
-
|
|
3069
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3129
3070
|
|
|
3130
|
-
for (const
|
|
3071
|
+
for (const pointerPath of allPointerPaths) {
|
|
3131
3072
|
assert.equal(
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
`the install
|
|
3073
|
+
existsSync(pointerPath),
|
|
3074
|
+
false,
|
|
3075
|
+
`the install removes ${pointerPath}, which Claude Code would load as a second pstack plugin`,
|
|
3135
3076
|
);
|
|
3136
3077
|
}
|
|
3078
|
+
assert.equal(existsSync(join(managedRoot, 'pstack')), false);
|
|
3079
|
+
assert.match(installerOutput, /Pstack: retired release store removed/);
|
|
3080
|
+
});
|
|
3081
|
+
|
|
3082
|
+
test('a full install clears a retired session-continuity registration from every host', t => {
|
|
3083
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3084
|
+
const companionCommand = 'node "/stale/.agents/skills/session-continuity/continuity.mjs" hook claude';
|
|
3085
|
+
const allConfigurationPaths = [
|
|
3086
|
+
join(sandbox.homeDirectory, '.claude', 'settings.json'),
|
|
3087
|
+
join(sandbox.homeDirectory, '.codex', 'hooks.json'),
|
|
3088
|
+
join(sandbox.homeDirectory, '.cursor', 'hooks.json'),
|
|
3089
|
+
];
|
|
3090
|
+
for (const configurationPath of allConfigurationPaths) {
|
|
3091
|
+
mkdirSync(dirname(configurationPath), { recursive: true });
|
|
3092
|
+
writeFileSync(configurationPath, JSON.stringify({
|
|
3093
|
+
hooks: {
|
|
3094
|
+
SessionStart: [{ hooks: [{ type: 'command', command: companionCommand }] }],
|
|
3095
|
+
},
|
|
3096
|
+
}, null, 2) + '\n');
|
|
3097
|
+
}
|
|
3137
3098
|
|
|
3138
|
-
|
|
3099
|
+
runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3139
3100
|
|
|
3140
3101
|
for (const configurationPath of allConfigurationPaths) {
|
|
3141
3102
|
assert.equal(
|
|
3142
3103
|
continuityCommandCount(configurationPath),
|
|
3143
3104
|
0,
|
|
3144
|
-
`
|
|
3105
|
+
`the install leaves no registration in ${configurationPath} pointing at a removed script`,
|
|
3145
3106
|
);
|
|
3146
3107
|
}
|
|
3147
3108
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readlinkSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export const RETIRED_PSTACK_STORE_DIRECTORY_NAME = 'pstack';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Name the store directory the retired pstack installer kept its releases in.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} managedRoot The managed Claude root.
|
|
17
|
+
* @returns {string} The retired store root.
|
|
18
|
+
*/
|
|
19
|
+
export function retiredPstackStoreRoot(managedRoot) {
|
|
20
|
+
return join(managedRoot, RETIRED_PSTACK_STORE_DIRECTORY_NAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pointsIntoStore(entryPath, storeRoot) {
|
|
24
|
+
let entryStats;
|
|
25
|
+
try {
|
|
26
|
+
entryStats = lstatSync(entryPath);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (!entryStats.isSymbolicLink()) return false;
|
|
31
|
+
let linkTarget;
|
|
32
|
+
try {
|
|
33
|
+
linkTarget = readlinkSync(entryPath);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const resolvedTarget = isAbsolute(linkTarget)
|
|
38
|
+
? resolve(linkTarget)
|
|
39
|
+
: resolve(join(entryPath, '..', linkTarget));
|
|
40
|
+
return resolvedTarget.startsWith(resolve(storeRoot) + sep);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function removePointersUnder(skillsRoot, storeRoot) {
|
|
44
|
+
if (!skillsRoot || !existsSync(skillsRoot)) return [];
|
|
45
|
+
const removedPointerPaths = [];
|
|
46
|
+
for (const entryName of readdirSync(skillsRoot)) {
|
|
47
|
+
const entryPath = join(skillsRoot, entryName);
|
|
48
|
+
if (!pointsIntoStore(entryPath, storeRoot)) continue;
|
|
49
|
+
unlinkSync(entryPath);
|
|
50
|
+
removedPointerPaths.push(entryPath);
|
|
51
|
+
}
|
|
52
|
+
return removedPointerPaths;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove the retired pstack installation this package used to publish.
|
|
57
|
+
*
|
|
58
|
+
* The retired installer kept a release store under the managed root and
|
|
59
|
+
* published one symlink per skill into it. Claude Code loads the `pstack`
|
|
60
|
+
* pointer as a skills-directory plugin, so a pointer left behind collides with
|
|
61
|
+
* the `pstack@pstack-claude` plugin the marketplace now installs.
|
|
62
|
+
*
|
|
63
|
+
* Only a symlink whose target resolves inside the store is removed, so a
|
|
64
|
+
* directory a user keeps under a similar name stays in place. A pointer left
|
|
65
|
+
* dangling by a store someone already deleted is removed too.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} roots An `InstallRootResolution`.
|
|
68
|
+
* @returns {{removedPointerPaths: string[], removedStorePath: string|null}} What this call removed.
|
|
69
|
+
*/
|
|
70
|
+
export function removeRetiredPstackInstallation(roots) {
|
|
71
|
+
const storeRoot = retiredPstackStoreRoot(roots.managedRoot);
|
|
72
|
+
const allSkillsRoots = [...new Set([
|
|
73
|
+
roots.skillsLookupDirectory,
|
|
74
|
+
roots.skillsInstallDirectory,
|
|
75
|
+
])];
|
|
76
|
+
const removedPointerPaths = allSkillsRoots
|
|
77
|
+
.flatMap(skillsRoot => removePointersUnder(skillsRoot, storeRoot));
|
|
78
|
+
if (!existsSync(storeRoot)) {
|
|
79
|
+
return { removedPointerPaths, removedStorePath: null };
|
|
80
|
+
}
|
|
81
|
+
rmSync(storeRoot, { recursive: true, force: true });
|
|
82
|
+
return { removedPointerPaths, removedStorePath: storeRoot };
|
|
83
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
removeRetiredPstackInstallation,
|
|
16
|
+
retiredPstackStoreRoot,
|
|
17
|
+
} from './prune-retired-pstack-store.mjs';
|
|
18
|
+
|
|
19
|
+
function retiredInstallationFixture(t) {
|
|
20
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-retired-pstack-'));
|
|
21
|
+
t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
22
|
+
const managedRoot = join(homeDirectory, '.claude');
|
|
23
|
+
const agentsHome = join(homeDirectory, '.agents');
|
|
24
|
+
const releaseRoot = join(managedRoot, 'pstack', 'releases', 'abc123', 'runtime');
|
|
25
|
+
mkdirSync(join(releaseRoot, 'pstack', 'skills', 'poteto-mode'), { recursive: true });
|
|
26
|
+
mkdirSync(join(releaseRoot, 'cursor-team-kit', 'skills', 'deslop'), { recursive: true });
|
|
27
|
+
writeFileSync(join(managedRoot, 'pstack', 'installed.json'), '{}\n');
|
|
28
|
+
const managedSkillsRoot = join(managedRoot, 'skills');
|
|
29
|
+
const agentsSkillsRoot = join(agentsHome, 'skills');
|
|
30
|
+
mkdirSync(managedSkillsRoot, { recursive: true });
|
|
31
|
+
mkdirSync(agentsSkillsRoot, { recursive: true });
|
|
32
|
+
const pointers = [
|
|
33
|
+
[join(managedSkillsRoot, 'pstack'), join(releaseRoot, 'pstack', 'skills')],
|
|
34
|
+
[join(agentsSkillsRoot, 'pstack'), join(releaseRoot, 'pstack', 'skills')],
|
|
35
|
+
[
|
|
36
|
+
join(agentsSkillsRoot, 'cursor-team-kit-deslop'),
|
|
37
|
+
join(releaseRoot, 'cursor-team-kit', 'skills', 'deslop'),
|
|
38
|
+
],
|
|
39
|
+
];
|
|
40
|
+
for (const [pointerPath, targetPath] of pointers) {
|
|
41
|
+
symlinkSync(targetPath, pointerPath, 'dir');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
roots: { managedRoot, agentsHome, skillsLookupDirectory: managedSkillsRoot, skillsInstallDirectory: agentsSkillsRoot },
|
|
45
|
+
managedSkillsRoot,
|
|
46
|
+
agentsSkillsRoot,
|
|
47
|
+
pointerPaths: pointers.map(([pointerPath]) => pointerPath),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('the store root sits under the managed root', () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
retiredPstackStoreRoot(join('/managed', '.claude')),
|
|
54
|
+
join('/managed', '.claude', 'pstack'),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('every pointer into the retired store is removed along with the store', t => {
|
|
59
|
+
const fixture = retiredInstallationFixture(t);
|
|
60
|
+
|
|
61
|
+
const outcome = removeRetiredPstackInstallation(fixture.roots);
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(outcome.removedPointerPaths.sort(), fixture.pointerPaths.sort());
|
|
64
|
+
assert.equal(outcome.removedStorePath, retiredPstackStoreRoot(fixture.roots.managedRoot));
|
|
65
|
+
for (const pointerPath of fixture.pointerPaths) {
|
|
66
|
+
assert.equal(existsSync(pointerPath), false);
|
|
67
|
+
}
|
|
68
|
+
assert.equal(existsSync(retiredPstackStoreRoot(fixture.roots.managedRoot)), false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a skill that is not a pointer into the store stays in place', t => {
|
|
72
|
+
const fixture = retiredInstallationFixture(t);
|
|
73
|
+
const ownSkillDirectory = join(fixture.agentsSkillsRoot, 'pull-request');
|
|
74
|
+
mkdirSync(ownSkillDirectory, { recursive: true });
|
|
75
|
+
writeFileSync(join(ownSkillDirectory, 'SKILL.md'), '---\nname: pull-request\n---\n');
|
|
76
|
+
const unrelatedTarget = join(fixture.roots.managedRoot, 'elsewhere', 'pstack');
|
|
77
|
+
mkdirSync(unrelatedTarget, { recursive: true });
|
|
78
|
+
const unrelatedPointer = join(fixture.agentsSkillsRoot, 'pstack-of-my-own');
|
|
79
|
+
symlinkSync(unrelatedTarget, unrelatedPointer, 'dir');
|
|
80
|
+
|
|
81
|
+
removeRetiredPstackInstallation(fixture.roots);
|
|
82
|
+
|
|
83
|
+
assert.equal(existsSync(join(ownSkillDirectory, 'SKILL.md')), true);
|
|
84
|
+
assert.equal(lstatSync(unrelatedPointer).isSymbolicLink(), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a home with no retired installation reports nothing removed', t => {
|
|
88
|
+
const fixture = retiredInstallationFixture(t);
|
|
89
|
+
removeRetiredPstackInstallation(fixture.roots);
|
|
90
|
+
|
|
91
|
+
const secondOutcome = removeRetiredPstackInstallation(fixture.roots);
|
|
92
|
+
|
|
93
|
+
assert.deepEqual(secondOutcome.removedPointerPaths, []);
|
|
94
|
+
assert.equal(secondOutcome.removedStorePath, null);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('a dangling pointer into the store is removed after the store is gone', t => {
|
|
98
|
+
const fixture = retiredInstallationFixture(t);
|
|
99
|
+
rmSync(retiredPstackStoreRoot(fixture.roots.managedRoot), { recursive: true, force: true });
|
|
100
|
+
|
|
101
|
+
const outcome = removeRetiredPstackInstallation(fixture.roots);
|
|
102
|
+
|
|
103
|
+
assert.deepEqual(outcome.removedPointerPaths.sort(), fixture.pointerPaths.sort());
|
|
104
|
+
assert.equal(outcome.removedStorePath, null);
|
|
105
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export const CONTINUITY_SCRIPT_MARKER = '/session-continuity/continuity.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when a hook command calls the retired session-continuity companion.
|
|
7
|
+
*
|
|
8
|
+
* @param {unknown} command The `command` field of one host hook entry.
|
|
9
|
+
* @returns {boolean} True when this entry calls the retired companion.
|
|
10
|
+
*/
|
|
11
|
+
export function isContinuityHookCommand(command) {
|
|
12
|
+
return typeof command === 'string' && command.includes(CONTINUITY_SCRIPT_MARKER);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Locate the hook configuration file each host keeps its registrations in.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} roots An `InstallRootResolution`.
|
|
19
|
+
* @returns {Record<string, string>} One configuration path per host.
|
|
20
|
+
*/
|
|
21
|
+
export function continuityHostConfigurationPaths(roots) {
|
|
22
|
+
return {
|
|
23
|
+
claude: join(roots.managedRoot, 'settings.json'),
|
|
24
|
+
codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json'),
|
|
25
|
+
cursor: join(roots.cursorInstallDirectory, 'hooks.json'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strip every retired session-continuity registration from one host
|
|
31
|
+
* configuration, in place, and drop an event left with no entries.
|
|
32
|
+
*
|
|
33
|
+
* An earlier claude-dev-env registered this companion in each host. The pstack
|
|
34
|
+
* plugin now carries the SessionStart context the companion supplied, so a
|
|
35
|
+
* registration left behind points every session at a deleted script.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} configuration A parsed host hook configuration.
|
|
38
|
+
* @returns {number} How many registrations this call removed.
|
|
39
|
+
*/
|
|
40
|
+
export function removeContinuityHooks(configuration) {
|
|
41
|
+
let removedCount = 0;
|
|
42
|
+
for (const [event, entries] of Object.entries(configuration.hooks || {})) {
|
|
43
|
+
const kept = entries.flatMap(entry => {
|
|
44
|
+
if (!entry.hooks) {
|
|
45
|
+
if (!isContinuityHookCommand(entry.command)) return [entry];
|
|
46
|
+
removedCount += 1;
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const hooks = entry.hooks.filter(hook => !isContinuityHookCommand(hook.command));
|
|
50
|
+
removedCount += entry.hooks.length - hooks.length;
|
|
51
|
+
return hooks.length ? [{ ...entry, hooks }] : [];
|
|
52
|
+
});
|
|
53
|
+
if (kept.length) configuration.hooks[event] = kept;
|
|
54
|
+
else delete configuration.hooks[event];
|
|
55
|
+
}
|
|
56
|
+
return removedCount;
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
continuityHostConfigurationPaths,
|
|
6
|
+
isContinuityHookCommand,
|
|
7
|
+
removeContinuityHooks,
|
|
8
|
+
} from './prune-session-continuity.mjs';
|
|
9
|
+
|
|
10
|
+
const ROOTS = {
|
|
11
|
+
managedRoot: join('/managed', '.claude'),
|
|
12
|
+
codexRulesInstallDirectory: join('/managed', '.codex', 'rules'),
|
|
13
|
+
cursorInstallDirectory: join('/managed', '.cursor'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const COMPANION_COMMAND = 'node "/managed/.agents/skills/session-continuity/continuity.mjs" hook claude';
|
|
17
|
+
|
|
18
|
+
test('a companion command is recognized and another hook command is not', () => {
|
|
19
|
+
assert.equal(isContinuityHookCommand(COMPANION_COMMAND), true);
|
|
20
|
+
assert.equal(isContinuityHookCommand('node "/managed/.agents/hooks/routing/other.mjs"'), false);
|
|
21
|
+
assert.equal(isContinuityHookCommand(undefined), false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('each host names the configuration file that holds its registrations', () => {
|
|
25
|
+
assert.deepEqual(continuityHostConfigurationPaths(ROOTS), {
|
|
26
|
+
claude: join('/managed', '.claude', 'settings.json'),
|
|
27
|
+
codex: join('/managed', '.codex', 'hooks.json'),
|
|
28
|
+
cursor: join('/managed', '.cursor', 'hooks.json'),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('a grouped registration is removed and the sibling hook stays', () => {
|
|
33
|
+
const configuration = {
|
|
34
|
+
hooks: {
|
|
35
|
+
SessionStart: [{
|
|
36
|
+
matcher: 'startup',
|
|
37
|
+
hooks: [
|
|
38
|
+
{ type: 'command', command: COMPANION_COMMAND },
|
|
39
|
+
{ type: 'command', command: 'node "/managed/.agents/hooks/keep.mjs"' },
|
|
40
|
+
],
|
|
41
|
+
}],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
45
|
+
assert.deepEqual(configuration.hooks.SessionStart[0].hooks, [
|
|
46
|
+
{ type: 'command', command: 'node "/managed/.agents/hooks/keep.mjs"' },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an event left with no entries is dropped rather than left empty', () => {
|
|
51
|
+
const configuration = {
|
|
52
|
+
hooks: {
|
|
53
|
+
UserPromptExpansion: [{ hooks: [{ type: 'command', command: COMPANION_COMMAND }] }],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
57
|
+
assert.deepEqual(configuration.hooks, {});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("Cursor's flat entry shape is removed the same way", () => {
|
|
61
|
+
const configuration = { version: 1, hooks: { sessionStart: [{ command: COMPANION_COMMAND }] } };
|
|
62
|
+
assert.equal(removeContinuityHooks(configuration), 1);
|
|
63
|
+
assert.deepEqual(configuration.hooks, {});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a configuration holding no registration is left untouched', () => {
|
|
67
|
+
const configuration = { hooks: { SessionStart: [{ hooks: [{ command: 'node "/keep.mjs"' }] }] } };
|
|
68
|
+
assert.equal(removeContinuityHooks(configuration), 0);
|
|
69
|
+
assert.deepEqual(configuration.hooks.SessionStart, [{ hooks: [{ command: 'node "/keep.mjs"' }] }]);
|
|
70
|
+
});
|