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.
Files changed (118) hide show
  1. package/.agents/agents/clean-coder.md +5 -6
  2. package/.agents/agents/issue-tracker.md +1 -1
  3. package/.agents/agents/pr-description-writer.md +1 -1
  4. package/.agents/agents/skill-writer-agent.md +1 -12
  5. package/.agents/agents/test_agent_frontmatter.py +2 -19
  6. package/.agents/skills/issue-tracker/SKILL.md +1 -1
  7. package/.agents/skills/orchestrator/SKILL.md +4 -5
  8. package/.agents/skills/pull-request/SKILL.md +8 -7
  9. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  10. package/.agents/skills/team-advisor/SKILL.md +2 -3
  11. package/_shared/advisor/advisor-protocol.md +0 -1
  12. package/_shared/advisor/reference/lifecycle.md +0 -1
  13. package/_shared/advisor/reference/warm-up.md +1 -2
  14. package/audit-rubrics/audit-categories.json +0 -4
  15. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  16. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
  17. package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
  18. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
  19. package/bin/install-constants.mjs +0 -5
  20. package/bin/install-pstack-plugin.mjs +192 -0
  21. package/bin/install-pstack-plugin.test.mjs +181 -0
  22. package/bin/install.agents-home.test.mjs +0 -4
  23. package/bin/install.codex-rules.test.mjs +6 -0
  24. package/bin/install.cursor-rules.test.mjs +3 -253
  25. package/bin/install.mjs +55 -160
  26. package/bin/install.test.mjs +110 -149
  27. package/bin/prune-retired-pstack-store.mjs +83 -0
  28. package/bin/prune-retired-pstack-store.test.mjs +105 -0
  29. package/bin/prune-session-continuity.mjs +57 -0
  30. package/bin/prune-session-continuity.test.mjs +70 -0
  31. package/bin/resolve-install-root.mjs +2 -0
  32. package/bin/resolve-install-root.test.mjs +69 -0
  33. package/docs/CODE_RULES.md +1 -1
  34. package/docs/agent-spawn-protocol.md +1 -1
  35. package/docs/codex-compatibility.md +4 -16
  36. package/docs/nas-ssh-invocation.md +1 -1
  37. package/docs/references/prose-style-enforcement.md +2 -2
  38. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
  39. package/hooks/blocking/code_rules_constants_config.py +1 -108
  40. package/hooks/blocking/code_rules_enforcer.py +9 -5
  41. package/hooks/blocking/code_rules_test_layout.py +6 -7
  42. package/hooks/blocking/test_code_rules_constants_config.py +13 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  44. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
  46. package/hooks/blocking/test_context_control_policy_files.py +0 -11
  47. package/hooks/hooks_constants/blocking_check_limits.py +1 -1
  48. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
  49. package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
  50. package/hooks/session/test_untracked_repo_detector.py +1 -1
  51. package/hooks/validators/code_quality_checks.py +15 -2
  52. package/hooks/validators/test_code_quality_checks.py +22 -0
  53. package/package.json +1 -2
  54. package/rules/anti-corollary-tests.md +3 -1
  55. package/rules/archiving-agent-config.md +70 -0
  56. package/rules/asd-ste100-language.md +1 -1
  57. package/rules/ci-owns-the-gate.md +73 -0
  58. package/rules/cleanup-temp-files.md +1 -1
  59. package/rules/code-standards.md +3 -4
  60. package/rules/docstring-prose-matches-implementation.md +3 -3
  61. package/rules/explore-thoroughly.md +2 -2
  62. package/rules/failure-blast-radius.md +10 -9
  63. package/rules/git-workflow.md +27 -12
  64. package/rules/long-horizon-autonomy.md +3 -1
  65. package/rules/nas-ssh-invocation.md +1 -1
  66. package/rules/orphan-css-class.md +1 -1
  67. package/rules/paired-test-coverage.md +1 -1
  68. package/rules/research-mode.md +3 -1
  69. package/rules/retired-hook-prose.md +10 -2
  70. package/rules/testing.md +3 -3
  71. package/rules/verify-before-asking.md +4 -1
  72. package/rules/verify-runtime-state.md +2 -2
  73. package/rules/windows-filesystem-safe.md +1 -1
  74. package/rules/workflow-substitution-slots.md +1 -1
  75. package/scripts/Sync-RepoMain.ps1 +71 -6
  76. package/scripts/policy_lint/adapter_detectors.py +116 -10
  77. package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
  78. package/scripts/policy_lint/config/constants.py +3 -0
  79. package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
  80. package/scripts/tests/test_codex_compat_materializer.py +3 -3
  81. package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
  82. package/.agents/skills/session-continuity/AGENTS.md +0 -1
  83. package/.agents/skills/session-continuity/CLAUDE.md +0 -1
  84. package/.agents/skills/session-continuity/README.md +0 -223
  85. package/.agents/skills/session-continuity/SKILL.md +0 -125
  86. package/.agents/skills/session-continuity/continuity.mjs +0 -445
  87. package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
  88. package/bin/install-session-continuity.mjs +0 -131
  89. package/bin/install-session-continuity.test.mjs +0 -106
  90. package/bin/install.pstack-plugin.test.mjs +0 -115
  91. package/bin/pstack-codex-discovery.test.mjs +0 -79
  92. package/bin/pstack-model-preferences.claude.json +0 -23
  93. package/bin/pstack-model-preferences.codex.json +0 -24
  94. package/bin/pstack.mjs +0 -392
  95. package/bin/pstack.test.mjs +0 -347
  96. package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
  97. package/bin/select-pstack-models.test.mjs +0 -776
  98. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
  99. package/rules/agent-spawn-protocol.md +0 -9
  100. package/rules/ask-user-question-required.md +0 -31
  101. package/rules/claims-as-quotes.md +0 -55
  102. package/rules/confirm-implementation-forks.md +0 -11
  103. package/rules/file-global-constants.md +0 -81
  104. package/rules/hedging-claims.md +0 -7
  105. package/rules/measurement-denominators.md +0 -39
  106. package/rules/prompt-workflow-context-controls.md +0 -19
  107. package/rules/pstack-host-mapping.md +0 -48
  108. package/rules/pstack-models.md +0 -142
  109. package/rules/vault-context.md +0 -5
  110. package/scripts/pstack-adapters/common.md +0 -17
  111. package/scripts/pstack-adapters/create-skill.md +0 -14
  112. package/scripts/pstack-adapters/host-claude.md +0 -7
  113. package/scripts/pstack-adapters/host-codex.md +0 -11
  114. package/scripts/pstack-adapters/host-cursor.md +0 -7
  115. package/scripts/pstack.lock.json +0 -11
  116. package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
  117. package/scripts/select_pstack_models.mjs +0 -469
  118. /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
@@ -1,115 +0,0 @@
1
- import { test } from 'node:test';
2
- import { strict as assert } from 'node:assert';
3
- import {
4
- mkdtempSync,
5
- mkdirSync,
6
- writeFileSync,
7
- readFileSync,
8
- existsSync,
9
- rmSync,
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 { resolveInstallRoot } from './resolve-install-root.mjs';
16
- import { PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH } from '../scripts/refresh_pstack_plugin_skills.mjs';
17
-
18
- const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
19
- const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
20
- const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
21
-
22
- function runInstaller(homeDirectory, extraArguments) {
23
- return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
24
- cwd: PACKAGE_DIRECTORY,
25
- encoding: 'utf8',
26
- env: {
27
- ...process.env,
28
- CDE_INSTALL_PSTACK: '0',
29
- HOME: homeDirectory,
30
- USERPROFILE: homeDirectory,
31
- CODEX_HOME: join(homeDirectory, '.codex'),
32
- CLAUDE_CONFIG_DIR: join(homeDirectory, '.claude'),
33
- GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
34
- },
35
- });
36
- }
37
-
38
- function seedPstackSkills(agentsHome, skillNames) {
39
- const pluginRoot = join(agentsHome, 'skills', 'pstack');
40
- for (const eachSkillName of skillNames) {
41
- const skillDirectory = join(pluginRoot, eachSkillName);
42
- mkdirSync(skillDirectory, { recursive: true });
43
- writeFileSync(join(skillDirectory, 'SKILL.md'), '---\nname: ' + eachSkillName + '\n---\n');
44
- }
45
- return pluginRoot;
46
- }
47
-
48
- function withTemporaryHome(runAssertions) {
49
- const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-plugin-'));
50
- try {
51
- const resolution = resolveInstallRoot({
52
- homeDirectory,
53
- environment: {},
54
- explicitTarget: null,
55
- });
56
- runAssertions(homeDirectory, resolution);
57
- } finally {
58
- rmSync(homeDirectory, { recursive: true, force: true });
59
- }
60
- }
61
-
62
- test('a pstack folder the user installed gains a plugin manifest', () => {
63
- withTemporaryHome((homeDirectory, resolution) => {
64
- const pluginRoot = seedPstackSkills(resolution.agentsHome, ['how', 'why']);
65
-
66
- runInstaller(homeDirectory, ['--only', 'core']);
67
-
68
- const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
69
- assert.equal(existsSync(manifestPath), true);
70
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
71
- assert.equal(manifest.name, 'pstack');
72
- assert.deepEqual(manifest.skills, ['./how', './why']);
73
- });
74
- });
75
-
76
- test('a second install leaves an unchanged manifest byte for byte', () => {
77
- withTemporaryHome((homeDirectory, resolution) => {
78
- const pluginRoot = seedPstackSkills(resolution.agentsHome, ['how']);
79
-
80
- runInstaller(homeDirectory, ['--only', 'core']);
81
- const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
82
- const afterFirstInstall = readFileSync(manifestPath, 'utf8');
83
- runInstaller(homeDirectory, ['--only', 'core']);
84
-
85
- assert.equal(readFileSync(manifestPath, 'utf8'), afterFirstInstall);
86
- });
87
- });
88
-
89
- test('a later install lists a skill a pstack update added', () => {
90
- withTemporaryHome((homeDirectory, resolution) => {
91
- const pluginRoot = seedPstackSkills(resolution.agentsHome, ['how']);
92
- runInstaller(homeDirectory, ['--only', 'core']);
93
-
94
- seedPstackSkills(resolution.agentsHome, ['why']);
95
- runInstaller(homeDirectory, ['--only', 'core']);
96
-
97
- const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
98
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
99
- assert.deepEqual(manifest.skills, ['./how', './why']);
100
- });
101
- });
102
-
103
- test('an install with no pstack folder writes no manifest and does not fail', () => {
104
- withTemporaryHome((homeDirectory, resolution) => {
105
- runInstaller(homeDirectory, ['--only', 'core']);
106
-
107
- const manifestPath = join(
108
- resolution.agentsHome,
109
- 'skills',
110
- 'pstack',
111
- PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH,
112
- );
113
- assert.equal(existsSync(manifestPath), false);
114
- });
115
- });
@@ -1,79 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { spawn, spawnSync } from 'node:child_process';
3
- import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { join, resolve } from 'node:path';
6
- import { createInterface } from 'node:readline';
7
- import test from 'node:test';
8
- import { verifyInstallation } from './pstack.mjs';
9
-
10
- const executable = process.env.CDE_CODEX_EXECUTABLE;
11
- const project = process.env.CDE_PSTACK_PROOF_PROJECT;
12
-
13
- test('native Codex discovers every installed pstack skill and its real entry path', {
14
- skip: !executable || !project ? 'Set CDE_CODEX_EXECUTABLE and CDE_PSTACK_PROOF_PROJECT for native discovery' : false,
15
- timeout: 60000,
16
- }, async t => {
17
- const installation = verifyInstallation({ project });
18
- const release = join(resolve(project), '.claude', 'pstack', 'releases', installation.release);
19
- const manifest = JSON.parse(readFileSync(join(release, 'release.json'), 'utf8'));
20
- const home = mkdtempSync(join(tmpdir(), 'pstack codex home '));
21
- const codexHome = join(home, '.codex');
22
- mkdirSync(codexHome);
23
- const env = { PATH: process.env.PATH, HOME: home, CODEX_HOME: codexHome };
24
- const version = spawnSync(executable, ['--version'], { env, encoding: 'utf8', timeout: 15000 });
25
- assert.equal(version.status, 0, version.stderr || version.error?.message);
26
- const child = spawn(executable, ['app-server'], { cwd: project, env, stdio: ['pipe', 'pipe', 'pipe'] });
27
- const lines = createInterface({ input: child.stdout });
28
- let stderr = '';
29
- child.stderr.on('data', chunk => { stderr += chunk; });
30
- child.stdin.on('error', () => {});
31
- t.after(async () => {
32
- lines.close();
33
- const stopped = child.exitCode !== null || child.signalCode !== null
34
- ? Promise.resolve() : new Promise(accept => child.once('close', accept));
35
- child.kill();
36
- await stopped;
37
- rmSync(home, { recursive: true, force: true });
38
- });
39
- const transcript = [];
40
- const send = message => child.stdin.write(JSON.stringify(message) + '\n');
41
- const request = (id, method, params) => new Promise((accept, reject) => {
42
- const timer = setTimeout(() => finish(new Error(`Codex ${method} timed out: ${stderr}`)), 15000);
43
- const failed = error => finish(error);
44
- const closed = code => finish(new Error(`Codex exited ${code}: ${stderr}`));
45
- const receive = line => {
46
- let message;
47
- try { message = JSON.parse(line); } catch (error) { finish(error); return; }
48
- transcript.push(message);
49
- if (message.id === id) finish(message.error ? new Error(JSON.stringify(message.error)) : null, message.result);
50
- };
51
- const finish = (error, response) => {
52
- clearTimeout(timer);
53
- child.off('error', failed);
54
- child.off('close', closed);
55
- lines.off('line', receive);
56
- if (error) reject(error); else accept(response);
57
- };
58
- child.once('error', failed);
59
- child.once('close', closed);
60
- lines.on('line', receive);
61
- send({ id, method, params });
62
- });
63
- await request(0, 'initialize', { clientInfo: { name: 'skillcheck', version: '1.0.0' } });
64
- send({ method: 'initialized', params: {} });
65
- const discovered = await request(1, 'skills/list', { cwds: [resolve(project)], forceReload: true });
66
- const evidence = { verification: 'native-codex-skill-discovery', codexVersion: version.stdout.trim(),
67
- upstreamCommit: installation.commit, adapterDigest: installation.adapterDigest, transcript };
68
- if (process.env.CDE_PSTACK_DISCOVERY_REPORT) writeFileSync(process.env.CDE_PSTACK_DISCOVERY_REPORT, JSON.stringify(evidence, null, 2) + '\n');
69
- const workspace = discovered.data.find(entry => resolve(entry.cwd) === resolve(project));
70
- assert.ok(workspace, 'Codex returned the requested workspace');
71
- assert.deepEqual(workspace.errors, []);
72
- for (const expected of manifest.skills) {
73
- const found = workspace.skills.filter(skill => skill.name === expected.name);
74
- assert.equal(found.length, 1, `Codex discovers ${expected.name} once`);
75
- assert.equal(found[0].enabled, true, expected.name);
76
- assert.equal(realpathSync(found[0].path), realpathSync(join(release, expected.path, 'SKILL.md')));
77
- }
78
- t.diagnostic(`${version.stdout.trim()}: ${manifest.skills.length} installed skills discovered through Codex`);
79
- });
@@ -1,23 +0,0 @@
1
- {
2
- "host": "claude",
3
- "modelsByRole": {
4
- "feature, refactoring": ["opus"],
5
- "bug-fix": ["opus"],
6
- "perf-issue": ["fable"],
7
- "hillclimb": ["fable"],
8
- "judgment and prose": ["fable"],
9
- "hardest tasks": ["fable"],
10
- "how explorer": ["sonnet"],
11
- "how explainer": ["opus"],
12
- "how critics": ["sonnet", "opus", "fable"],
13
- "why investigators": ["sonnet"],
14
- "why synthesizer": ["fable"],
15
- "reflect tooling": ["opus"],
16
- "reflect judgment, divergent, synthesizer": ["fable"],
17
- "arena runners": ["sonnet", "opus", "fable"],
18
- "arena cross-judge pool": ["opus", "fable"],
19
- "swarm workers": ["sonnet"],
20
- "architect runners": ["opus", "fable"],
21
- "interrogate reviewers": ["sonnet", "opus", "fable"]
22
- }
23
- }
@@ -1,24 +0,0 @@
1
- {
2
- "host": "codex",
3
- "defaultEffort": "medium",
4
- "modelsByRole": {
5
- "feature, refactoring": ["gpt-5.6-sol"],
6
- "bug-fix": ["gpt-5.6-sol"],
7
- "perf-issue": ["gpt-6-astra"],
8
- "hillclimb": ["gpt-6-astra"],
9
- "judgment and prose": ["gpt-6-astra"],
10
- "hardest tasks": ["gpt-6-astra"],
11
- "how explorer": ["gpt-5.6-terra"],
12
- "how explainer": ["gpt-5.6-sol"],
13
- "how critics": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"],
14
- "why investigators": ["gpt-5.6-terra"],
15
- "why synthesizer": ["gpt-6-astra"],
16
- "reflect tooling": ["gpt-5.6-sol"],
17
- "reflect judgment, divergent, synthesizer": ["gpt-6-astra"],
18
- "arena runners": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"],
19
- "arena cross-judge pool": ["gpt-5.6-sol", "gpt-6-astra"],
20
- "swarm workers": ["gpt-5.6-luna"],
21
- "architect runners": ["gpt-5.6-sol", "gpt-6-astra"],
22
- "interrogate reviewers": ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-6-astra"]
23
- }
24
- }
package/bin/pstack.mjs DELETED
@@ -1,392 +0,0 @@
1
- #!/usr/bin/env node
2
- import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { createHash } from 'node:crypto';
4
- import { execFileSync, spawnSync } from 'node:child_process';
5
- import { homedir } from 'node:os';
6
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
- const bundledLockPath = join(packageRoot, 'scripts', 'pstack.lock.json');
11
- const hosts = ['claude', 'codex', 'cursor'];
12
- const digest = value => createHash('sha256').update(value).digest('hex');
13
- const readJson = path => JSON.parse(readFileSync(path, 'utf8'));
14
- const json = value => JSON.stringify(value, null, 2) + '\n';
15
- const entryExists = path => { try { lstatSync(path); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
16
-
17
- export function validateLock(lock) {
18
- if (lock?.schemaVersion !== 1 || lock?.adapterVersion !== 1
19
- || lock.repository !== 'https://github.com/cursor/plugins.git'
20
- || !/^[0-9a-f]{40}$/.test(lock.commit ?? '')
21
- || JSON.stringify(lock.components) !== JSON.stringify(['pstack', 'cursor-team-kit'])) {
22
- throw new Error('Unsupported pstack lock. Update claude-dev-env for a newer adapter.');
23
- }
24
- const required = { pstack: ['poteto-mode'], 'cursor-team-kit': ['deslop', 'control-cli', 'control-ui'] };
25
- for (const [component, names] of Object.entries(required)) {
26
- const declared = lock.requiredSkills?.[component];
27
- if (!Array.isArray(declared) || declared.some(name => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
28
- || names.some(name => !declared.includes(name))) throw new Error(`Invalid ${component} dependencies`);
29
- }
30
- return lock;
31
- }
32
-
33
- function filesUnder(root) {
34
- return readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap(entry => {
35
- const path = join(root, entry.name);
36
- if (entry.isSymbolicLink()) throw new Error(`Upstream symlinks require review: ${path}`);
37
- if (entry.isDirectory()) return filesUnder(path);
38
- if (!entry.isFile()) throw new Error(`Unsupported upstream file: ${path}`);
39
- return [path];
40
- });
41
- }
42
-
43
- function runGit(args, cwd) {
44
- return execFileSync('git', args, {
45
- cwd, encoding: 'utf8', timeout: 40000, maxBuffer: 16 * 1024 * 1024,
46
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
47
- stdio: ['ignore', 'pipe', 'pipe'],
48
- }).trim();
49
- }
50
-
51
- export function fetchUpstream(lock, destination) {
52
- mkdirSync(destination, { recursive: true });
53
- runGit(['init', '--quiet'], destination);
54
- runGit(['config', 'core.autocrlf', 'false'], destination);
55
- runGit(['config', 'core.hooksPath', join(destination, '.disabled-hooks')], destination);
56
- runGit(['remote', 'add', 'origin', lock.repository], destination);
57
- runGit(['config', 'core.sparseCheckout', 'true'], destination);
58
- writeFileSync(join(destination, '.git', 'info', 'sparse-checkout'), '/pstack/\n/cursor-team-kit/\n/LICENSE\n');
59
- runGit(['fetch', '--quiet', '--depth=1', '--filter=blob:none', 'origin', lock.commit], destination);
60
- if (runGit(['rev-parse', 'FETCH_HEAD'], destination) !== lock.commit) throw new Error('Upstream commit mismatch');
61
- runGit(['checkout', '--quiet', '--detach', 'FETCH_HEAD'], destination);
62
- }
63
-
64
- function adapterFiles(root) {
65
- const adapters = join(root, 'scripts', 'pstack-adapters');
66
- return Object.fromEntries([
67
- ...['common.md', ...hosts.map(host => `host-${host}.md`), 'create-skill.md'].map(name => [name, join(adapters, name)]),
68
- ...['pstack-host-mapping.md', 'pstack-models.md'].map(name => [name, join(root, 'rules', name)]),
69
- ['select_pstack_models.mjs', join(root, 'scripts', 'select_pstack_models.mjs')],
70
- ['subagent_model_policy.mjs', join(root, 'scripts', 'subagent_model_policy.mjs')],
71
- ['resolve_advisor_model_route.mjs', join(root, 'scripts', 'resolve_advisor_model_route.mjs')],
72
- ['subagent-model-policy.json', join(root, 'rules', 'subagent-model-policy.json')],
73
- ].map(([name, path]) => [name, readFileSync(path, 'utf8')]));
74
- }
75
-
76
- function adaptSkill(text, name, releaseRoot, skillRoot) {
77
- const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/);
78
- if (!match || !/^description:\s*\S/m.test(match[1])) throw new Error(`Missing skill metadata: ${name}`);
79
- const frontmatter = match[1].replace(/^name:.*$/m, `name: ${name}`);
80
- if (!/^name:/m.test(frontmatter)) throw new Error(`Missing skill name: ${name}`);
81
- const safeFrontmatter = frontmatter.replace(/^(?:context|agent|model):.*\r?\n?/gm, '');
82
- const fork = /^context:\s*fork\s*$/m.test(frontmatter)
83
- ? 'The upstream entry requests a fork. Delegate through the native host after reading the compatibility mapping.\n' : '';
84
- return `---\n${safeFrontmatter}\n---\n\n## Host compatibility\n\n`
85
- + `Read ${JSON.stringify(join(releaseRoot, 'compat', 'common.md'))} and the matching host file before following this workflow.\n`
86
- + `Resolve this skill's relative paths from ${JSON.stringify(skillRoot)}. Read ${JSON.stringify(join(releaseRoot, 'release.json'))} for skill and agent paths.\n`
87
- + fork + '\n' + match[2].trimStart();
88
- }
89
-
90
- function writePstackPluginManifest(stage, skills) {
91
- const manifest = {
92
- $schema: 'https://anthropic.com/claude-code/plugin.schema.json',
93
- name: 'pstack',
94
- skills: skills.filter(skill => skill.component === 'pstack').map(skill => `./${skill.slug}`),
95
- };
96
- const pluginRoot = join(stage, 'runtime', 'pstack', 'skills');
97
- mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
98
- writeFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), json(manifest));
99
- }
100
-
101
- export function prepareRelease(checkout, stage, finalRoot, lock, adapters) {
102
- const skills = [];
103
- const agents = [];
104
- mkdirSync(join(stage, 'compat'), { recursive: true });
105
- for (const [name, content] of Object.entries(adapters)) writeFileSync(join(stage, 'compat', name), content);
106
- for (const component of lock.components) {
107
- const source = join(checkout, component);
108
- filesUnder(source);
109
- cpSync(source, join(stage, 'upstream', component), { recursive: true });
110
- cpSync(source, join(stage, 'runtime', component), { recursive: true });
111
- rmSync(join(stage, 'runtime', component, '.cursor-plugin', 'plugin.json'), { force: true });
112
- const skillHome = join(source, 'skills');
113
- const names = readdirSync(skillHome).filter(name => existsSync(join(skillHome, name, 'SKILL.md'))).sort();
114
- for (const name of lock.requiredSkills[component]) {
115
- if (!names.includes(name)) throw new Error(`Missing dependency: ${component}:${name}`);
116
- }
117
- for (const slug of names) {
118
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new Error(`Invalid skill directory: ${slug}`);
119
- const name = component === 'pstack' ? `pstack:${slug}` : `${component}-${slug}`;
120
- const frontmatterName = component === 'pstack' ? slug : name;
121
- const path = join('runtime', component, 'skills', slug);
122
- const text = readFileSync(join(source, 'skills', slug, 'SKILL.md'), 'utf8');
123
- writeFileSync(join(stage, path, 'SKILL.md'), adaptSkill(text, frontmatterName, finalRoot, join(finalRoot, path)));
124
- skills.push({ name, component, slug, path });
125
- }
126
- const agentHome = join(source, 'agents');
127
- if (existsSync(agentHome)) {
128
- for (const path of filesUnder(agentHome)) agents.push({ component, path: join('runtime', component, 'agents', relative(agentHome, path)) });
129
- }
130
- }
131
- if (existsSync(join(checkout, 'LICENSE'))) cpSync(join(checkout, 'LICENSE'), join(stage, 'upstream', 'LICENSE'));
132
- const creatorPath = join('runtime', 'cde-create-skill');
133
- mkdirSync(join(stage, creatorPath), { recursive: true });
134
- writeFileSync(join(stage, creatorPath, 'SKILL.md'), adapters['create-skill.md']);
135
- skills.push({ name: 'cde-create-skill', component: 'cde', slug: 'create-skill', path: creatorPath });
136
- writePstackPluginManifest(stage, skills);
137
- const knownNames = new Set(skills.map(skill => `${skill.component}:${skill.slug}`));
138
- for (const path of filesUnder(join(stage, 'upstream')).filter(path => path.endsWith('.md') && (path.includes(sep + 'skills' + sep) || path.includes(sep + 'agents' + sep)))) {
139
- const references = readFileSync(path, 'utf8').matchAll(/\b(pstack|cursor-team-kit):([a-z][a-z0-9-]*)/g);
140
- for (const [, component, slug] of references) {
141
- if (!knownNames.has(`${component}:${slug}`)) throw new Error(`Unresolved skill dependency: ${component}:${slug}`);
142
- }
143
- }
144
- const fileDigests = Object.fromEntries(filesUnder(stage).map(path => [relative(stage, path), digest(readFileSync(path))]));
145
- const managedRoot = dirname(dirname(dirname(finalRoot)));
146
- const agentsRoot = basename(managedRoot) === '.claude' ? join(dirname(managedRoot), '.agents') : managedRoot + '.agents';
147
- const release = { preferencesDirectory: join(agentsRoot, 'rules'), policyPath: join(agentsRoot, 'rules', 'subagent-model-policy.json'), selectorPath: join(finalRoot, 'compat', 'select_pstack_models.mjs'), schemaVersion: 1, commit: lock.commit, adapterVersion: lock.adapterVersion, skills, agents, fileDigests, verification: 'filesystem-only' };
148
- writeFileSync(join(stage, 'release.json'), json(release));
149
- return release;
150
- }
151
-
152
- function assertContained(root, path) {
153
- const rel = relative(root, resolve(root, path));
154
- if (isAbsolute(rel) || rel === '..' || rel.startsWith('..' + sep) || resolve(root, path) === resolve(root)) throw new Error('Invalid release path');
155
- return resolve(root, path);
156
- }
157
-
158
- export function verifyRelease(root) {
159
- const release = readJson(join(root, 'release.json'));
160
- if (release.schemaVersion !== 1 || !Array.isArray(release.skills) || !release.skills.length || !release.fileDigests) throw new Error('Invalid release manifest');
161
- for (const [path, hash] of Object.entries(release.fileDigests)) {
162
- if (digest(readFileSync(assertContained(root, path))) !== hash) throw new Error(`Changed installed file: ${path}`);
163
- }
164
- for (const skill of release.skills) {
165
- const validPart = value => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
166
- const expectedName = skill.component === 'pstack' ? `pstack:${skill.slug}` : `${skill.component}-${skill.slug}`;
167
- if (!validPart(skill.component) || !validPart(skill.slug) || skill.name !== expectedName) throw new Error('Invalid installed skill name');
168
- const path = join(assertContained(root, skill.path), 'SKILL.md');
169
- if (!Object.hasOwn(release.fileDigests, relative(root, path)) || !existsSync(path)) throw new Error(`Missing entry: ${skill.name}`);
170
- }
171
- return release;
172
- }
173
-
174
- function layout(options) {
175
- const root = resolve(options.root ?? (options.project ? join(options.project, '.claude') : process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude')));
176
- const agents = basename(root) === '.claude' ? join(dirname(root), '.agents') : root + '.agents';
177
- return { root, agents, store: join(root, 'pstack'), skillHomes: [join(root, 'skills'), join(agents, 'skills')] };
178
- }
179
-
180
- function loadState(store) {
181
- const path = join(store, 'installed.json');
182
- return existsSync(path) ? readJson(path) : null;
183
- }
184
-
185
- export function verifyInstallation(options = {}) {
186
- const { store } = layout(options);
187
- const state = loadState(store);
188
- if (!state) throw new Error('Pstack is not installed');
189
- const release = verifyRelease(assertContained(join(store, 'releases'), state.release));
190
- for (const [path, target] of Object.entries(state.links)) {
191
- if (!lstatSync(path).isSymbolicLink() || realpathSync(path) !== realpathSync(target)) throw new Error(`Changed skill pointer: ${path}`);
192
- }
193
- return { ...state, skillCount: release.skills.length, verification: release.verification };
194
- }
195
-
196
- function publish(releaseRoot, release, homes, prior) {
197
- const uniqueHomes = [...new Set(homes.map(home => { mkdirSync(home, { recursive: true }); return realpathSync(home); }))];
198
- const pstackSkill = release.skills.find(skill => skill.component === 'pstack');
199
- const directSkills = release.skills.filter(skill => skill.component !== 'pstack');
200
- const links = Object.fromEntries(uniqueHomes.flatMap(home => [
201
- ...(pstackSkill ? [[join(home, 'pstack'), join(releaseRoot, dirname(pstackSkill.path))]] : []),
202
- ...directSkills.map(skill => [join(home, skill.name), join(releaseRoot, skill.path)]),
203
- ]));
204
- const paths = new Set([...Object.keys(prior?.links ?? {}), ...Object.keys(links)]);
205
- const before = new Map();
206
- for (const path of paths) {
207
- if (entryExists(path)) {
208
- if (!lstatSync(path).isSymbolicLink() || !prior?.links[path]
209
- || resolve(dirname(path), readlinkSync(path)) !== resolve(prior.links[path])) {
210
- throw new Error(`Keep existing unmanaged path: ${path}`);
211
- }
212
- before.set(path, readlinkSync(path));
213
- } else before.set(path, null);
214
- }
215
- const restore = () => {
216
- for (const [path, target] of before) {
217
- if (entryExists(path)) unlinkSync(path);
218
- if (target !== null) symlinkSync(target, path, process.platform === 'win32' ? 'junction' : 'dir');
219
- }
220
- };
221
- try {
222
- for (const path of paths) {
223
- if (entryExists(path)) unlinkSync(path);
224
- if (links[path]) symlinkSync(links[path], path, process.platform === 'win32' ? 'junction' : 'dir');
225
- }
226
- } catch (error) { restore(); throw error; }
227
- return { links, restore };
228
- }
229
-
230
- function seedPolicy(releaseRoot, release, agentsRoot) {
231
- const policyPath = join(agentsRoot, 'rules', 'subagent-model-policy.json');
232
- if (typeof release.policyPath !== 'string') throw new Error('Invalid release policy path');
233
- if (resolve(release.policyPath) !== resolve(policyPath)) throw new Error('Invalid release policy path');
234
- mkdirSync(dirname(policyPath), { recursive: true });
235
- if (entryExists(policyPath)) {
236
- if (!lstatSync(policyPath).isFile()) throw new Error(`Keep existing unmanaged policy path: ${policyPath}`);
237
- return () => {};
238
- }
239
- writeFileSync(policyPath, readFileSync(join(releaseRoot, 'compat', 'subagent-model-policy.json')), { flag: 'wx' });
240
- return () => { if (entryExists(policyPath)) unlinkSync(policyPath); };
241
- }
242
-
243
- function liveLeases(store) {
244
- const root = join(store, 'sessions');
245
- if (!existsSync(root)) return false;
246
- return readdirSync(root).some(name => {
247
- const pid = Number(name);
248
- if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('Invalid session lease');
249
- try { process.kill(pid, 0); return true; } catch (error) {
250
- if (error.code !== 'ESRCH') return true;
251
- unlinkSync(join(root, name));
252
- return false;
253
- }
254
- });
255
- }
256
-
257
- function readCentralLock(temporaryRoot) {
258
- const repository = join(temporaryRoot, 'record');
259
- mkdirSync(repository);
260
- runGit(['init', '--quiet'], repository);
261
- runGit(['fetch', '--quiet', '--depth=1', '--filter=blob:none', 'https://github.com/jl-cmd/claude-dev-env.git', 'main'], repository);
262
- return JSON.parse(runGit(['show', 'FETCH_HEAD:packages/claude-dev-env/scripts/pstack.lock.json'], repository));
263
- }
264
-
265
- export function installPstack(options = {}, dependencies = {}) {
266
- const { agents, store, skillHomes } = layout(options);
267
- mkdirSync(store, { recursive: true });
268
- const lockDirectory = join(store, '.install-lock');
269
- mkdirSync(lockDirectory);
270
- let temporaryRoot;
271
- const finish = installation => {
272
- if (options.reserveSession) {
273
- const sessions = join(store, 'sessions');
274
- mkdirSync(sessions, { recursive: true });
275
- writeFileSync(join(sessions, String(process.pid)), installation.release);
276
- }
277
- return installation;
278
- };
279
- try {
280
- const prior = loadState(store);
281
- if ((options.offline || liveLeases(store)) && prior) return finish({ ...verifyInstallation(options), status: 'retained' });
282
- if (options.offline) throw new Error('Offline install needs an existing verified release');
283
- temporaryRoot = mkdtempSync(join(store, '.prepare-'));
284
- const now = dependencies.now ?? Date.now();
285
- const interval = options.intervalMs ?? 3600000;
286
- if (!Number.isFinite(interval) || interval < 0) throw new Error('Invalid update interval');
287
- const bundled = options.lock ?? prior?.lock ?? readJson(bundledLockPath);
288
- let lock = bundled;
289
- let checkedAt = prior?.checkedAt ?? 0;
290
- if (options.refresh && (options.force || now - checkedAt >= interval)) {
291
- lock = (dependencies.readCentralLock ?? readCentralLock)(temporaryRoot);
292
- checkedAt = now;
293
- } else if (options.refresh && prior?.lock) lock = prior.lock;
294
- validateLock(lock);
295
- const adapters = adapterFiles(options.packageRoot ?? packageRoot);
296
- const installerSource = readFileSync(fileURLToPath(import.meta.url), 'utf8').replace(/\r\n/g, '\n');
297
- const adapterDigest = digest(json(adapters) + installerSource);
298
- const id = `${lock.commit}-${adapterDigest.slice(0, 16)}`;
299
- const releaseRoot = join(store, 'releases', id);
300
- mkdirSync(dirname(releaseRoot), { recursive: true });
301
- if (!existsSync(releaseRoot)) {
302
- const checkout = join(temporaryRoot, 'source');
303
- (dependencies.fetchSource ?? fetchUpstream)(lock, checkout);
304
- const stage = join(temporaryRoot, 'release');
305
- prepareRelease(checkout, stage, releaseRoot, lock, adapters);
306
- verifyRelease(stage);
307
- renameSync(stage, releaseRoot);
308
- }
309
- const release = verifyRelease(releaseRoot);
310
- const policyPublication = seedPolicy(releaseRoot, release, agents);
311
- let publication;
312
- try {
313
- publication = publish(releaseRoot, release, skillHomes, prior);
314
- const state = { release: id, commit: lock.commit, adapterVersion: lock.adapterVersion, adapterDigest, checkedAt, lock, links: publication.links };
315
- writeFileSync(join(store, 'installed.next.json'), json(state));
316
- renameSync(join(store, 'installed.next.json'), join(store, 'installed.json'));
317
- } catch (error) {
318
- publication?.restore();
319
- policyPublication();
320
- throw error;
321
- }
322
- return finish({ ...verifyInstallation(options), status: prior?.release === id ? 'unchanged' : 'installed' });
323
- } catch (error) {
324
- if (!options.strict) {
325
- try { return finish({ ...verifyInstallation(options), status: 'retained', warning: error.message }); } catch { }
326
- }
327
- throw error;
328
- } finally {
329
- if (temporaryRoot) rmSync(temporaryRoot, { recursive: true, force: true });
330
- rmSync(lockDirectory, { recursive: true, force: true });
331
- }
332
- }
333
-
334
- function parseArguments(args) {
335
- const options = {};
336
- const tailIndex = args.indexOf('--');
337
- const tokens = tailIndex < 0 ? args : args.slice(0, tailIndex);
338
- const forwarded = tailIndex < 0 ? [] : args.slice(tailIndex + 1);
339
- for (let index = 0; index < tokens.length; index++) {
340
- const flag = tokens[index];
341
- if (['--offline', '--refresh', '--force', '--strict'].includes(flag)) options[flag.slice(2)] = true;
342
- else if (['--root', '--project', '--host', '--lock', '--interval-ms'].includes(flag)) {
343
- const argument = tokens[++index];
344
- if (!argument || argument.startsWith('--')) throw new Error(`${flag} requires a value`);
345
- if (flag === '--lock') options.lock = readJson(resolve(argument));
346
- else if (flag === '--interval-ms') options.intervalMs = Number(argument);
347
- else options[flag.slice(2)] = argument;
348
- } else throw new Error(`Unknown option: ${flag}`);
349
- }
350
- if (options.host && !hosts.includes(options.host)) throw new Error('Host must be claude, codex, or cursor');
351
- if (options.root && options.project) throw new Error('Choose --root or --project');
352
- return { options, forwarded };
353
- }
354
-
355
- export function main(args = process.argv.slice(2)) {
356
- try {
357
- const [command = 'help', ...rest] = args;
358
- if (['help', '--help', '-h'].includes(command)) {
359
- console.log('Usage: cde-pstack <install|verify|launch|hook> [--root PATH|--project PATH] [--refresh] [--offline] [--strict] [--lock PATH] [--host claude|codex|cursor] [-- agent arguments]');
360
- return 0;
361
- }
362
- const { options, forwarded } = parseArguments(rest);
363
- if (!['install', 'verify', 'launch', 'hook'].includes(command)) throw new Error(`Unknown command: ${command}`);
364
- let hookInput;
365
- if (command === 'hook') {
366
- hookInput = JSON.parse(readFileSync(0, 'utf8') || '{}');
367
- options.project ??= process.env.CLAUDE_PROJECT_DIR || process.cwd();
368
- options.offline = hookInput.source !== 'startup';
369
- options.refresh = !options.offline && loadState(layout(options).store) !== null;
370
- }
371
- if (command === 'launch' && !options.host) throw new Error('launch requires --host');
372
- if (command === 'launch') { options.refresh = !options.offline && !options.lock; options.reserveSession = true; }
373
- const installation = command === 'verify' ? verifyInstallation(options) : installPstack(options);
374
- if (installation.warning) console.error(`Pstack update failed; keeping ${installation.commit}: ${installation.warning}`);
375
- if (command === 'hook') {
376
- const { store } = layout(options);
377
- const release = join(store, 'releases', installation.release);
378
- console.log(json({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `Pstack ${installation.commit} is installed. Use the immutable release recorded by session-continuity or the loaded skill entry for this session. Read common.md and host-claude.md in that release's compat directory. New sessions can use the installed inventory at ${join(release, 'release.json')}. ${installation.warning ?? ''}` } }));
379
- } else if (command === 'launch') {
380
- const sessions = join(layout(options).store, 'sessions');
381
- const lease = join(sessions, String(process.pid));
382
- try {
383
- const child = spawnSync(options.host, forwarded, { stdio: 'inherit', shell: false, env: { ...process.env, CDE_PSTACK_RELEASE: join(layout(options).store, 'releases', installation.release) } });
384
- if (child.error) throw child.error;
385
- return child.status ?? 1;
386
- } finally { unlinkSync(lease); }
387
- } else console.log(json(installation));
388
- return 0;
389
- } catch (error) { console.error(`Pstack: ${error.message}`); return 1; }
390
- }
391
-
392
- if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main();