claude-dev-env 7.7.1 → 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.
Files changed (103) 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/prompts/category-e-dead-code.md +3 -3
  15. package/bin/install-constants.mjs +0 -5
  16. package/bin/install-pstack-plugin.mjs +192 -0
  17. package/bin/install-pstack-plugin.test.mjs +181 -0
  18. package/bin/install.agents-home.test.mjs +0 -4
  19. package/bin/install.codex-rules.test.mjs +6 -0
  20. package/bin/install.cursor-rules.test.mjs +3 -253
  21. package/bin/install.mjs +55 -160
  22. package/bin/install.test.mjs +110 -149
  23. package/bin/prune-retired-pstack-store.mjs +83 -0
  24. package/bin/prune-retired-pstack-store.test.mjs +105 -0
  25. package/bin/prune-session-continuity.mjs +57 -0
  26. package/bin/prune-session-continuity.test.mjs +70 -0
  27. package/bin/resolve-install-root.mjs +2 -0
  28. package/bin/resolve-install-root.test.mjs +69 -0
  29. package/docs/CODE_RULES.md +1 -1
  30. package/docs/agent-spawn-protocol.md +1 -1
  31. package/docs/codex-compatibility.md +4 -16
  32. package/docs/nas-ssh-invocation.md +1 -1
  33. package/docs/references/prose-style-enforcement.md +2 -2
  34. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
  35. package/hooks/blocking/code_rules_constants_config.py +1 -108
  36. package/hooks/blocking/code_rules_enforcer.py +9 -5
  37. package/hooks/blocking/code_rules_test_layout.py +6 -7
  38. package/hooks/blocking/test_code_rules_constants_config.py +13 -0
  39. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  40. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
  41. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
  42. package/hooks/blocking/test_context_control_policy_files.py +0 -11
  43. package/hooks/hooks_constants/blocking_check_limits.py +1 -1
  44. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
  45. package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
  46. package/hooks/session/test_untracked_repo_detector.py +1 -1
  47. package/hooks/validators/code_quality_checks.py +15 -2
  48. package/hooks/validators/test_code_quality_checks.py +22 -0
  49. package/package.json +1 -2
  50. package/rules/ci-owns-the-gate.md +58 -0
  51. package/rules/code-standards.md +1 -2
  52. package/rules/docstring-prose-matches-implementation.md +2 -2
  53. package/rules/failure-blast-radius.md +1 -1
  54. package/rules/git-workflow.md +23 -11
  55. package/rules/orphan-css-class.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/retired-hook-prose.md +1 -1
  58. package/rules/verify-before-asking.md +1 -1
  59. package/rules/windows-filesystem-safe.md +1 -1
  60. package/rules/workflow-substitution-slots.md +1 -1
  61. package/scripts/Sync-RepoMain.ps1 +71 -6
  62. package/scripts/policy_lint/adapter_detectors.py +116 -10
  63. package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
  64. package/scripts/policy_lint/config/constants.py +3 -0
  65. package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
  66. package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
  67. package/.agents/skills/session-continuity/AGENTS.md +0 -1
  68. package/.agents/skills/session-continuity/CLAUDE.md +0 -1
  69. package/.agents/skills/session-continuity/README.md +0 -223
  70. package/.agents/skills/session-continuity/SKILL.md +0 -125
  71. package/.agents/skills/session-continuity/continuity.mjs +0 -445
  72. package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
  73. package/bin/install-session-continuity.mjs +0 -131
  74. package/bin/install-session-continuity.test.mjs +0 -106
  75. package/bin/install.pstack-plugin.test.mjs +0 -115
  76. package/bin/pstack-codex-discovery.test.mjs +0 -79
  77. package/bin/pstack-model-preferences.claude.json +0 -23
  78. package/bin/pstack-model-preferences.codex.json +0 -24
  79. package/bin/pstack.mjs +0 -392
  80. package/bin/pstack.test.mjs +0 -347
  81. package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
  82. package/bin/select-pstack-models.test.mjs +0 -776
  83. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
  84. package/rules/agent-spawn-protocol.md +0 -9
  85. package/rules/ask-user-question-required.md +0 -31
  86. package/rules/claims-as-quotes.md +0 -55
  87. package/rules/confirm-implementation-forks.md +0 -11
  88. package/rules/file-global-constants.md +0 -81
  89. package/rules/hedging-claims.md +0 -7
  90. package/rules/measurement-denominators.md +0 -39
  91. package/rules/prompt-workflow-context-controls.md +0 -19
  92. package/rules/pstack-host-mapping.md +0 -48
  93. package/rules/pstack-models.md +0 -142
  94. package/rules/vault-context.md +0 -5
  95. package/scripts/pstack-adapters/common.md +0 -17
  96. package/scripts/pstack-adapters/create-skill.md +0 -14
  97. package/scripts/pstack-adapters/host-claude.md +0 -7
  98. package/scripts/pstack-adapters/host-codex.md +0 -11
  99. package/scripts/pstack-adapters/host-cursor.md +0 -7
  100. package/scripts/pstack.lock.json +0 -11
  101. package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
  102. package/scripts/select_pstack_models.mjs +0 -469
  103. /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
package/bin/install.mjs CHANGED
@@ -35,24 +35,20 @@ import {
35
35
  CODEX_RULES_PACKAGE_DIRECTORY_NAME,
36
36
  CURSOR_SYNC_SCRIPT_FILE_NAME,
37
37
  CURSOR_RULES_DIRECTORY_NAME,
38
- PSTACK_MODEL_RULE_FILE_NAME,
39
38
  SUBAGENT_MODEL_POLICY_FILE_NAME,
40
- PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES,
41
39
  WINDOWS_PYTHON_LAUNCHER_COMMAND,
42
40
  PYTHON_PROBE_TIMEOUT_MILLISECONDS,
43
41
  CODEX_NATIVE_ROUTING_MATCHER,
44
42
  } from './install-constants.mjs';
45
43
  import {
46
- PSTACK_PLUGIN_DIRECTORY_NAME,
47
- PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH,
48
- refreshPstackPluginManifest,
49
- } from '../scripts/refresh_pstack_plugin_skills.mjs';
50
- import { installPstack } from './pstack.mjs';
44
+ installPstackPlugin,
45
+ shouldInstallPstackPlugin,
46
+ } from './install-pstack-plugin.mjs';
51
47
  import {
52
- configureContinuityHosts,
53
48
  continuityHostConfigurationPaths,
54
49
  removeContinuityHooks,
55
- } from './install-session-continuity.mjs';
50
+ } from './prune-session-continuity.mjs';
51
+ import { removeRetiredPstackInstallation } from './prune-retired-pstack-store.mjs';
56
52
  import {
57
53
  resolveInstallRoot,
58
54
  parseExplicitTargetFromArgv,
@@ -432,90 +428,22 @@ export function runCursorRuleSync(pythonCommand, scriptPath, claudeRoot, cursorR
432
428
  );
433
429
  }
434
430
 
435
- const PSTACK_RELEASE_OPT_OUT_FLAG = '--no-pstack';
436
- const PSTACK_RELEASE_OPT_OUT_VARIABLE = 'CDE_INSTALL_PSTACK';
437
- const PSTACK_RELEASE_OPT_OUT_VALUE = '0';
438
-
439
- /**
440
- * Decide whether a full install also installs the pstack release.
441
- *
442
- * The step reaches the network for the pinned upstream commit. `--no-pstack`
443
- * on the command line, or `CDE_INSTALL_PSTACK=0` in the environment, turns it
444
- * off for an air-gapped or offline run.
445
- *
446
- * @param {string[]} [argumentList] The command-line arguments after the script.
447
- * @param {Record<string, string|undefined>} [environment] The process environment.
448
- * @returns {boolean} True when this run installs the pstack release.
449
- */
450
- export function shouldInstallPstackRelease(
451
- argumentList = process.argv.slice(2),
452
- environment = process.env,
453
- ) {
454
- if (argumentList.includes(PSTACK_RELEASE_OPT_OUT_FLAG)) return false;
455
- return environment[PSTACK_RELEASE_OPT_OUT_VARIABLE] !== PSTACK_RELEASE_OPT_OUT_VALUE;
456
- }
457
-
458
- /**
459
- * Install the pstack release into a managed root without failing the run.
460
- *
461
- * The pstack store keeps its own state and its own skill pointers, so this
462
- * step reports its outcome rather than adding paths to the install manifest.
463
- * A network failure returns a warning so the rules, hooks, and skills this
464
- * run already wrote still reach their durable places.
465
- *
466
- * @param {object} [options] Options for `installPstack`, including `root`.
467
- * @param {object} [dependencies] Seams `installPstack` accepts, such as `fetchSource`.
468
- * @returns {{status: string, release: string|null, warning: string|null}} The outcome.
469
- */
470
- export function installPstackRelease(options = {}, dependencies = {}) {
471
- try {
472
- const installation = installPstack(options, dependencies);
473
- return {
474
- status: installation.status,
475
- release: installation.release,
476
- warning: installation.warning ?? null,
477
- };
478
- } catch (installError) {
479
- return { status: 'failed', release: null, warning: installError.message };
480
- }
481
- }
482
-
483
431
  /**
484
- * Refresh the pstack plugin manifest Claude Code reads, when this home has one.
432
+ * Install the pstack plugin from its marketplace into the hosts this run serves.
485
433
  *
486
- * Claude Code loads a skills-directory folder as a plugin when it holds
487
- * `.claude-plugin/plugin.json`, and namespaces its skills as `pstack:how`.
488
- * Without the manifest, a folder of skill folders loads nothing.
434
+ * The plugin carries its own skills, agents, and SessionStart hook, so the step
435
+ * reports its outcome rather than adding paths to the install manifest. A host
436
+ * without its command-line tool is skipped and a failing command is reported,
437
+ * so the rules, hooks, and skills this run already wrote still reach their
438
+ * durable places.
489
439
  *
490
- * @param {string} [skillsRoot] The skills root to read. Defaults to the root
491
- * this run installs into.
492
- * @returns {string|null} The manifest path when this run wrote it, else null.
440
+ * @returns {{status: string, hosts: object[], warning: string|null}} The outcome.
493
441
  */
494
- export function refreshInstalledPstackPluginManifest(
495
- skillsRoot = join(CLAUDE_HOME, MANAGED_SKILLS_DIRECTORY_NAME),
496
- ) {
497
- const pluginRoot = join(skillsRoot, PSTACK_PLUGIN_DIRECTORY_NAME);
498
- const outcome = refreshPstackPluginManifest(pluginRoot);
499
- if (!outcome.didWrite) return null;
500
- return join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
501
- }
502
-
503
- function seedPstackPreferences(preferenceFileName) {
504
- const preferenceDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
505
- const preferencePath = join(preferenceDirectory, preferenceFileName);
506
- const preferenceSource = join(PACKAGE_ROOT, 'bin', preferenceFileName);
507
- mkdirSync(preferenceDirectory, { recursive: true });
508
- try {
509
- copyFileSync(
510
- preferenceSource,
511
- preferencePath,
512
- filesystemConstants.COPYFILE_EXCL,
513
- );
514
- return preferencePath;
515
- } catch (copyError) {
516
- if (copyError.code === 'EEXIST') return null;
517
- throw copyError;
518
- }
442
+ export function installPstackPluginForHosts() {
443
+ return installPstackPlugin({
444
+ claudeRoot: CLAUDE_HOME,
445
+ codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
446
+ });
519
447
  }
520
448
 
521
449
  function seedSubagentModelPolicy() {
@@ -533,58 +461,30 @@ function seedSubagentModelPolicy() {
533
461
  }
534
462
 
535
463
  /**
536
- * Registers the session-continuity companion in each host configuration this
537
- * install root resolves. Without this step the companion ships to the agents
538
- * home but no host ever calls it, so Poteto Mode only activates when someone
539
- * remembers a second setup command.
464
+ * Removes the retired session-continuity registrations from the named hosts.
540
465
  *
541
- * A competing profile's registration, or an unwritable host configuration, is
542
- * reported and leaves the rest of the install intact.
543
- *
544
- * @returns {string[]} The host configuration paths this run registered.
545
- */
546
- function registerSessionContinuityHooks() {
547
- const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
548
- const hosts = Object.keys(configurationPaths)
549
- .filter(host => existsSync(dirname(configurationPaths[host])));
550
- if (hosts.length === 0) return [];
551
- try {
552
- const results = configureContinuityHosts(INSTALL_ROOT_RESOLUTION, hosts);
553
- for (const result of results) {
554
- const state = result.changed ? 'registered' : 'already registered';
555
- console.log(` Session continuity: ${state} in ${result.path}`);
556
- }
557
- return results.map(result => result.path);
558
- } catch (registrationError) {
559
- console.warn(
560
- ` Warning: session-continuity hooks were not registered (${registrationError.message}) — `
561
- + 'run bin/install-session-continuity.mjs once the cause is resolved.',
562
- );
563
- return [];
564
- }
565
- }
566
-
567
- /**
568
- * Removes the companion's registrations from the Codex and Cursor hook files
569
- * during an uninstall. The Claude settings.json copy is pruned inside the
570
- * settings block, which owns that file's single write.
571
- *
572
- * Each host this installer registered is a host it has to clean, or the host
573
- * keeps calling a script the uninstall deleted.
466
+ * An earlier claude-dev-env registered the companion in Claude, Codex, and
467
+ * Cursor. The pstack plugin now carries the SessionStart context the companion
468
+ * supplied, so a registration left behind points every session at a deleted
469
+ * script. Every install clears all three hosts; an uninstall passes the two the
470
+ * settings block does not already own.
574
471
  *
472
+ * @param {string[]} [hosts] The hosts to clear. Defaults to all three.
575
473
  * @returns {string[]} The host configuration paths this run changed.
576
474
  */
577
- function removeSessionContinuityHooksFromOtherHosts() {
475
+ function removeRetiredSessionContinuityHooks(hosts = ['claude', 'codex', 'cursor']) {
578
476
  const configurationPaths = continuityHostConfigurationPaths(INSTALL_ROOT_RESOLUTION);
579
477
  const changedPaths = [];
580
- for (const host of ['codex', 'cursor']) {
478
+ for (const host of hosts) {
581
479
  const configurationPath = configurationPaths[host];
582
480
  if (!existsSync(configurationPath)) continue;
583
- const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
481
+ const configurationText = readFileSync(configurationPath, 'utf8');
482
+ const configuration = JSON.parse(configurationText);
584
483
  if (removeContinuityHooks(configuration) === 0) continue;
585
- writeFileSync(configurationPath, JSON.stringify(configuration, null, 2) + '\n');
484
+ const indent = hostConfigurationIndent(configurationText);
485
+ writeFileSync(configurationPath, JSON.stringify(configuration, null, indent) + '\n');
586
486
  changedPaths.push(configurationPath);
587
- console.log(` Session continuity: hook registrations removed from ${configurationPath}`);
487
+ console.log(` Session continuity: retired hook registrations removed from ${configurationPath}`);
588
488
  }
589
489
  return changedPaths;
590
490
  }
@@ -2811,20 +2711,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2811
2711
  allInstalledFiles.push(...generatedCursorPaths);
2812
2712
  summary.cursorRules = { created: generatedCursorPaths.length, updated: 0, paths: generatedCursorPaths };
2813
2713
  syncWrittenPaths(allInstalledFiles);
2814
- const pstackRuleSource = join(INSTALL_ROOT_RESOLUTION.cursorRulesInstallDirectory, PSTACK_MODEL_RULE_FILE_NAME);
2815
- const sharedRuleDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
2816
- const sharedRulePath = join(sharedRuleDirectory, PSTACK_MODEL_RULE_FILE_NAME);
2817
- mkdirSync(sharedRuleDirectory, { recursive: true });
2818
- copyFileSync(pstackRuleSource, sharedRulePath);
2819
- allInstalledFiles.push(sharedRulePath);
2820
- syncWrittenPaths(allInstalledFiles);
2821
- for (const eachPreferenceFileName of PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES) {
2822
- const seededPreferencePath = seedPstackPreferences(eachPreferenceFileName);
2823
- if (seededPreferencePath) {
2824
- allInstalledFiles.push(seededPreferencePath);
2825
- allUserOwnedPreferencePaths.add(seededPreferencePath);
2826
- }
2827
- }
2828
2714
  const seededPolicyPath = seedSubagentModelPolicy();
2829
2715
  if (seededPolicyPath) {
2830
2716
  allInstalledFiles.push(seededPolicyPath);
@@ -2860,16 +2746,28 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2860
2746
  }
2861
2747
  summary.skills = { created: skillsCreated, updated: skillsUpdated, pruned: 0, paths: skillPaths };
2862
2748
  allInstalledFiles.push(...skillPaths);
2863
- if (!selectedGroups && shouldInstallPstackRelease()) {
2864
- const pstackRelease = installPstackRelease({ root: CLAUDE_HOME });
2865
- summary.pstackRelease = pstackRelease;
2866
- console.log(pstackRelease.warning
2867
- ? ` Pstack: ${pstackRelease.status} \u2014 ${pstackRelease.warning}`
2868
- : ` Pstack: ${pstackRelease.status} (${pstackRelease.release})`);
2869
- }
2870
- const pstackManifestPath = refreshInstalledPstackPluginManifest();
2871
- if (pstackManifestPath) allInstalledFiles.push(pstackManifestPath);
2872
- summary.pstackPlugin = { manifestPath: pstackManifestPath };
2749
+ if (!selectedGroups) {
2750
+ summary.retiredSessionContinuity = {
2751
+ changedPaths: removeRetiredSessionContinuityHooks(),
2752
+ };
2753
+ const retiredPstack = removeRetiredPstackInstallation(INSTALL_ROOT_RESOLUTION);
2754
+ summary.retiredPstackStore = retiredPstack;
2755
+ for (const pointerPath of retiredPstack.removedPointerPaths) {
2756
+ console.log(` Pstack: retired pointer removed from ${pointerPath}`);
2757
+ }
2758
+ if (retiredPstack.removedStorePath) {
2759
+ console.log(` Pstack: retired release store removed from ${retiredPstack.removedStorePath}`);
2760
+ }
2761
+ }
2762
+ if (!selectedGroups && shouldInstallPstackPlugin()) {
2763
+ const pstackPlugin = installPstackPluginForHosts();
2764
+ summary.pstackPlugin = pstackPlugin;
2765
+ for (const eachHost of pstackPlugin.hosts) {
2766
+ console.log(eachHost.warning
2767
+ ? ` Pstack (${eachHost.host}): ${eachHost.status} \u2014 ${eachHost.warning}`
2768
+ : ` Pstack (${eachHost.host}): ${eachHost.status}`);
2769
+ }
2770
+ }
2873
2771
  syncWrittenPaths([...allInstalledFiles, ...publishedPointerPaths]);
2874
2772
  throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
2875
2773
  throwIfFault(FAULT_PHASES.BEFORE_DURABLE_PROMOTION);
@@ -2999,9 +2897,6 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2999
2897
  console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
3000
2898
  }
3001
2899
  const isFullInstall = !selectedGroups;
3002
- if (isFullInstall && shouldInstallAnyHooks) {
3003
- summary.sessionContinuity = { configuredPaths: registerSessionContinuityHooks() };
3004
- }
3005
2900
  const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
3006
2901
  let failedPrunePaths = [];
3007
2902
  let stalePrunedTotal = 0;
@@ -3268,7 +3163,7 @@ function executeUninstallPlan(plan, helpers = {}) {
3268
3163
  if (!codexRoutingHookExists()) {
3269
3164
  pruneManagedHooksFromHostConfiguration(CODEX_HOOKS_CONFIGURATION_PATH, 2);
3270
3165
  }
3271
- removeSessionContinuityHooksFromOtherHosts();
3166
+ removeRetiredSessionContinuityHooks(['codex', 'cursor']);
3272
3167
  throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
3273
3168
 
3274
3169
  unsetGlobalGitHooksPathIfOurs();
@@ -3394,7 +3289,7 @@ Usage:
3394
3289
  npx ${PACKAGE_NAME} --target DIR Install into DIR instead of ~/.claude (overrides CLAUDE_CONFIG_DIR)
3395
3290
  npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
3396
3291
  npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
3397
- npx ${PACKAGE_NAME} --no-pstack Full install without the pinned pstack release (also CDE_INSTALL_PSTACK=0)
3292
+ npx ${PACKAGE_NAME} --no-pstack Full install without the pstack plugin (also CDE_INSTALL_PSTACK=0)
3398
3293
  npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
3399
3294
  npx ${PACKAGE_NAME} --help Show this help
3400
3295
 
@@ -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 and vault guidance use current session tools', () => {
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 pstackUpstreamFixture(t) {
2994
- const temporaryRoot = mkdtempSync(join(tmpdir(), 'cdev-install-pstack-'));
2995
- t.after(() => rmSync(temporaryRoot, { recursive: true, force: true }));
2996
- const checkout = join(temporaryRoot, 'checkout');
2997
- for (const [component, allSkillNames] of Object.entries(PSTACK_TEST_LOCK.requiredSkills)) {
2998
- for (const skillName of allSkillNames) {
2999
- writePstackFixtureFile(
3000
- join(checkout, component, 'skills', skillName, 'SKILL.md'),
3001
- `---\nname: ${skillName}\ndescription: Test ${skillName}.\n---\n\nRun ${skillName}.\n`,
3002
- );
3003
- }
3004
- }
3005
- const managedRoot = join(temporaryRoot, 'home', '.claude');
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
- GIT_ALLOW_PROTOCOL: 'none',
2967
+ ...environmentOverrides,
3032
2968
  },
3033
2969
  });
3034
2970
  }
3035
2971
 
3036
- function withPstackTemporaryHome(t, runAssertions) {
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
- runAssertions(homeDirectory);
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('the base install publishes the pstack poteto-mode entry into the managed skills home', t => {
3043
- const fixture = pstackUpstreamFixture(t);
2994
+ test('a full install adds the pstack marketplace and plugin on both hosts', t => {
2995
+ const sandbox = pstackPluginSandbox(t);
3044
2996
 
3045
- const outcome = installPstackRelease(fixture.options, fixture.dependencies);
2997
+ const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3046
2998
 
3047
- assert.equal(outcome.status, 'installed');
3048
- assert.equal(outcome.warning, null);
3049
- const pstackRoot = join(fixture.managedRoot, 'skills', 'pstack');
3050
- const entryPath = join(pstackRoot, 'poteto-mode');
3051
- assert.equal(lstatSync(pstackRoot).isSymbolicLink(), true, 'the pstack root is a pointer into the release');
3052
- assert.equal(existsSync(join(entryPath, 'SKILL.md')), true, 'the nested entry resolves to a skill');
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 unreachable pstack upstream reports a warning and keeps the base install going', t => {
3056
- const fixture = pstackUpstreamFixture(t);
3009
+ test('an absent host command is reported and leaves the other host installed', t => {
3010
+ const sandbox = pstackPluginSandbox(t);
3057
3011
 
3058
- const outcome = installPstackRelease(fixture.options, {
3059
- fetchSource: () => { throw new Error('network unavailable'); },
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.equal(outcome.status, 'failed');
3063
- assert.equal(outcome.release, null);
3064
- assert.match(outcome.warning, /network unavailable/);
3065
- });
3066
-
3067
- test('--no-pstack and CDE_INSTALL_PSTACK=0 each turn the pstack step off', () => {
3068
- assert.equal(shouldInstallPstackRelease([], {}), true);
3069
- assert.equal(shouldInstallPstackRelease(['--no-pstack'], {}), false);
3070
- assert.equal(shouldInstallPstackRelease([], { CDE_INSTALL_PSTACK: '0' }), false);
3071
- assert.equal(shouldInstallPstackRelease([], { CDE_INSTALL_PSTACK: '1' }), true);
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('a full install runs the pstack step and reports an upstream failure without stopping', t => {
3075
- withPstackTemporaryHome(t, homeDirectory => {
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
- test('--no-pstack leaves the pstack store absent', t => {
3084
- withPstackTemporaryHome(t, homeDirectory => {
3085
- const installerOutput = runPstackInstaller(homeDirectory, ['--no-pstack']);
3033
+ const installerOutput = runPstackInstaller(
3034
+ sandbox.homeDirectory, ['--no-pstack'], sandbox.environment,
3035
+ );
3086
3036
 
3087
- assert.doesNotMatch(installerOutput, /Pstack:/);
3088
- assert.equal(existsSync(join(homeDirectory, '.claude', 'pstack')), false);
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 registers the session-continuity companion in every host, and uninstall takes it back out', t => {
3120
- const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-continuity-install-'));
3121
- t.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
3122
- const allConfigurationPaths = [
3123
- join(homeDirectory, '.claude', 'settings.json'),
3124
- join(homeDirectory, '.codex', 'hooks.json'),
3125
- join(homeDirectory, '.cursor', 'hooks.json'),
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
- runContinuityInstaller(homeDirectory, []);
3069
+ const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
3129
3070
 
3130
- for (const configurationPath of allConfigurationPaths) {
3071
+ for (const pointerPath of allPointerPaths) {
3131
3072
  assert.equal(
3132
- continuityCommandCount(configurationPath) > 0,
3133
- true,
3134
- `the install registers the companion in ${configurationPath} without a second setup command`,
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
- runContinuityInstaller(homeDirectory, ['--uninstall']);
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
- `uninstall leaves no registration in ${configurationPath} pointing at a removed script`,
3105
+ `the install leaves no registration in ${configurationPath} pointing at a removed script`,
3145
3106
  );
3146
3107
  }
3147
3108
  });