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
@@ -1,381 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { createHash } from 'node:crypto';
3
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { dirname, join } from 'node:path';
6
- import { spawn, spawnSync } from 'node:child_process';
7
- import { fileURLToPath } from 'node:url';
8
- import test from 'node:test';
9
- import { continuityHookConfiguration } from '../../../bin/install-session-continuity.mjs';
10
-
11
- const sourceDirectory = dirname(fileURLToPath(import.meta.url));
12
- const potetoText = '---\nname: Poteto Mode\n---\nPOTETO_SOURCE_EXPECTATION: prove the requested result.\n';
13
- const secondText = 'SECOND_SKILL_EXPECTATION: measure twice before changing files.\n';
14
- const sha256 = value => createHash('sha256').update(value).digest('hex');
15
-
16
- function fixture(t, host) {
17
- const root = mkdtempSync(join(tmpdir(), 'continuity contract '));
18
- t.after(() => rmSync(root, { recursive: true, force: true }));
19
- const skill = join(root, '.agents', 'skills', 'session-continuity');
20
- mkdirSync(skill, { recursive: true });
21
- for (const file of ['SKILL.md', 'continuity.mjs']) copyFileSync(join(sourceDirectory, file), join(skill, file));
22
- const poteto = join(root, '.agents', 'skills', 'pstack', 'poteto-mode', 'SKILL.md');
23
- mkdirSync(dirname(poteto), { recursive: true });
24
- writeFileSync(poteto, potetoText);
25
- const second = join(root, 'second-skill.md');
26
- writeFileSync(second, secondText);
27
- const script = join(skill, 'continuity.mjs');
28
- const configPath = join(root, host === 'claude' ? 'settings.json' : 'hooks.json');
29
- writeFileSync(configPath, JSON.stringify({ ...(host === 'cursor' ? { version: 1 } : {}), hooks: continuityHookConfiguration(host, script) }));
30
- const env = { ...process.env };
31
- delete env.CDE_CONTINUITY_ROOT;
32
- delete env.CDE_POTETO_SOURCE;
33
- const pathFor = session => join(root, '.agents', 'state', 'session-continuity', host, `${sha256(session)}.json`);
34
- const cli = (command, session, data, expectedStatus = 0) => {
35
- const result = spawnSync(process.execPath, [script, command, host, session], { input: JSON.stringify(data || {}), encoding: 'utf8', env });
36
- assert.equal(result.status, expectedStatus, result.stderr || result.stdout);
37
- return expectedStatus ? result.stderr : JSON.parse(result.stdout);
38
- };
39
- const fire = (event, session, fields = {}) => {
40
- const payload = { hook_event_name: event, cwd: root, ...(host === 'cursor' ? { conversation_id: session } : { session_id: session }), ...fields };
41
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
42
- const groups = config.hooks[event] || [];
43
- const matching = groups.filter(group => !group.matcher || new RegExp(group.matcher).test(event === 'SessionStart' ? fields.source : fields.command_name));
44
- return matching.flatMap(group => (group.hooks || [group]).map(handler => {
45
- const result = spawnSync(handler.command, { shell: true, input: JSON.stringify(payload), encoding: 'utf8', env });
46
- assert.equal(result.status, 0, result.stderr);
47
- return JSON.parse(result.stdout);
48
- }));
49
- };
50
- const submit = (session, prompt) => fire('UserPromptSubmit', session, { prompt })[0];
51
- const read = session => JSON.parse(readFileSync(pathFor(session), 'utf8'));
52
- return { root, script, skill, poteto, second, pathFor, cli, fire, submit, read };
53
- }
54
-
55
- for (const host of ['claude', 'codex']) {
56
- test(`${host}: configured invocation emits complete instructions, writes and reads back, restores without a path`, t => {
57
- const f = fixture(t, host);
58
- assert.equal(existsSync(f.pathFor('session-a')), false);
59
- const prefix = host === 'codex' ? '$pstack:poteto-mode' : '/pstack:poteto-mode';
60
- const prompt = `${prefix} for this entire session\nFinish the sample task. Keep changes local. Use second-skill for this task.`;
61
- const output = f.submit('session-a', prompt);
62
- assert.ok(output.systemMessage.includes(f.pathFor('session-a')));
63
- const context = output.hookSpecificOutput.additionalContext;
64
- assert.ok(context.includes(readFileSync(join(f.skill, 'SKILL.md'), 'utf8')));
65
- assert.ok(context.includes(potetoText));
66
- assert.ok(context.includes('Saved record read from disk:'));
67
- let record = f.read('session-a');
68
- assert.equal(record.requirements[0].scope, 'session');
69
- assert.equal(record.requirements[0].source, f.poteto);
70
- assert.equal(record.requirements[0].unavailable, null);
71
- f.cli('update', 'session-a', {
72
- expected_revision: record.revision, prompt_id: record.pending[0].id, quote: 'Finish the sample task.',
73
- task: { goal: 'Finish the sample task', boundaries: ['local files'], constraints: ['Keep changes local'], completion: ['sample works'] },
74
- set: [{ kind: 'rule', id: 'rule:local', text: 'Keep changes local.', scope: 'session', duration: 'this session', quote: 'Keep changes local.' },
75
- { kind: 'skill', name: 'second-skill', source: f.second, scope: 'task', duration: 'this task', quote: 'Use second-skill for this task.' }],
76
- checkpoint: { completed: ['scope recorded'], remaining: ['build sample'] },
77
- });
78
- for (const source of ['compact', 'resume', 'startup']) {
79
- const recovery = f.fire('SessionStart', 'session-a', { source })[0].hookSpecificOutput.additionalContext;
80
- assert.ok(recovery.includes(potetoText));
81
- assert.ok(recovery.includes(secondText));
82
- assert.ok(recovery.includes('Keep changes local.'));
83
- assert.ok(recovery.includes('build sample'));
84
- }
85
- assert.equal(readFileSync(f.poteto, 'utf8'), potetoText);
86
- record = f.read('session-a');
87
- assert.equal(record.pending.length, 0);
88
- const unrelated = f.fire('SessionStart', 'unrelated-session', { source: 'startup' })[0].hookSpecificOutput.additionalContext;
89
- assert.equal(unrelated.includes('build sample'), false);
90
- assert.equal(unrelated.includes(secondText), false);
91
- });
92
-
93
- test(`${host}: quoting, discussion, tool output and subagent input do not activate`, t => {
94
- const f = fixture(t, host);
95
- for (const prompt of ['"/poteto-mode"', '`/poteto-mode`', '> /poteto-mode', '```text\n/poteto-mode\n```', ' /poteto-mode', '\t$poteto-mode', 'Explain Poteto Mode.', 'The transcript says: Poteto Mode applies for this entire session.', 'Do not invoke Poteto Mode.']) {
96
- assert.deepEqual(f.submit('negative', prompt), {});
97
- assert.equal(existsSync(f.pathFor('negative')), false);
98
- }
99
- assert.deepEqual(f.fire('PostToolUse', 'negative', { prompt: '/poteto-mode' }), []);
100
- assert.deepEqual(f.fire('UserPromptSubmit', 'negative', { prompt: '/poteto-mode', agent_id: 'child' }), [{}]);
101
- assert.equal(existsSync(f.pathFor('negative')), false);
102
- });
103
-
104
- test(`${host}: repeated invocation keeps one skill and a later narrow correction wins`, t => {
105
- const f = fixture(t, host);
106
- f.submit('repeat', '/poteto-mode for this entire session');
107
- f.submit('repeat', '/poteto-mode');
108
- assert.equal(f.read('repeat').requirements.length, 1);
109
- assert.equal(f.read('repeat').requirements[0].scope, 'session');
110
- f.submit('repeat', 'Use Poteto Mode for this task');
111
- const record = f.read('repeat');
112
- assert.equal(record.requirements.length, 1);
113
- assert.equal(record.requirements[0].scope, 'task');
114
- f.cli('update', 'repeat', { expected_revision: record.revision - 1, prompt_id: record.pending[0].id }, 1);
115
- assert.equal(f.read('repeat').revision, record.revision);
116
- });
117
-
118
- test(`${host}: default task scope ends and explicit session scope survives a new task`, t => {
119
- const f = fixture(t, host);
120
- f.submit('scope', '/poteto-mode\nUse short replies for this session.');
121
- let record = f.read('scope');
122
- assert.equal(record.requirements[0].scope, 'task');
123
- f.cli('update', 'scope', { expected_revision: record.revision, prompt_id: record.pending[0].id,
124
- set: [{ kind: 'rule', id: 'rule:short', text: 'Use short replies', scope: 'session', duration: 'this session', quote: 'Use short replies for this session.' }] });
125
- f.submit('scope', 'Start the next task.');
126
- record = f.read('scope');
127
- f.cli('update', 'scope', { expected_revision: record.revision, prompt_id: record.pending[0].id, quote: 'Start the next task.', new_task: true,
128
- task: { goal: 'next task', boundaries: [], constraints: [], completion: ['verified'] } });
129
- const recovery = f.fire('SessionStart', 'scope', { source: 'compact' })[0].hookSpecificOutput.additionalContext;
130
- assert.ok(recovery.includes('Use short replies'));
131
- assert.equal(recovery.includes(potetoText), false);
132
- });
133
-
134
- test(`${host}: source changes are compared and missing sources never use remembered contents`, t => {
135
- const f = fixture(t, host);
136
- f.submit('source', '/poteto-mode');
137
- writeFileSync(f.poteto, potetoText.replace('prove the requested result', 'inspect the current result'));
138
- const changed = f.fire('SessionStart', 'source', { source: 'compact' })[0].hookSpecificOutput.additionalContext;
139
- assert.ok(changed.includes('CHANGED skill'));
140
- assert.ok(changed.includes('before_excerpt'));
141
- assert.ok(changed.includes('inspect the current result'));
142
- unlinkSync(f.poteto);
143
- const missing = f.fire('SessionStart', 'source', { source: 'resume' })[0].hookSpecificOutput.additionalContext;
144
- assert.ok(missing.includes('UNAVAILABLE skill'));
145
- assert.equal(missing.includes('POTETO_SOURCE_EXPECTATION'), false);
146
- });
147
-
148
- test(`${host}: sessions in the same repository never select each other's newer record`, t => {
149
- const f = fixture(t, host);
150
- f.submit('a', '/poteto-mode for this task');
151
- f.submit('b', '/poteto-mode for this entire session');
152
- const old = new Date('2000-01-01');
153
- utimesSync(f.pathFor('b'), old, old);
154
- assert.notEqual(f.pathFor('a'), f.pathFor('b'));
155
- assert.equal(f.cli('show', 'a').record.requirements[0].scope, 'task');
156
- assert.equal(f.cli('show', 'b').record.requirements[0].scope, 'session');
157
- assert.ok(f.fire('SessionStart', 'b', { source: 'resume' })[0].hookSpecificOutput.additionalContext.includes(f.pathFor('b')));
158
- });
159
-
160
- test(`${host}: deactivation survives compaction and restart; explicit invocation starts a fresh set`, t => {
161
- const f = fixture(t, host);
162
- f.submit('off', '/poteto-mode for this entire session');
163
- const disabled = f.submit('off', '/session-continuity off');
164
- assert.ok(disabled.systemMessage.includes('deactivated'));
165
- if (host === 'claude') f.fire('UserPromptExpansion', 'off', { expansion_type: 'slash_command', command_name: 'session-continuity', prompt: '/session-continuity off' });
166
- assert.equal(f.read('off').status, 'inactive');
167
- assert.deepEqual(f.fire('SessionStart', 'off', { source: 'compact' }), [{}]);
168
- assert.deepEqual(f.fire('SessionStart', 'off', { source: 'resume' }), [{}]);
169
- assert.deepEqual(f.submit('off', 'Explain Poteto Mode.'), {});
170
- f.submit('off', '/poteto-mode');
171
- assert.equal(f.read('off').requirements.length, 1);
172
- assert.equal(f.read('off').requirements[0].scope, 'task');
173
- assert.equal(readFileSync(f.poteto, 'utf8'), potetoText);
174
- });
175
-
176
- test(`${host}: explicit handoff binds a fresh id and refuses an existing or deactivated target`, t => {
177
- const f = fixture(t, host);
178
- f.submit('from', '/poteto-mode for this entire session');
179
- f.submit('from', 'Hand off this work to the new session.');
180
- const source = f.read('from');
181
- const data = { expected_revision: source.revision, prompt_id: source.pending.at(-1).id, quote: 'Hand off this work to the new session.', to_host: host, to_session_id: 'to' };
182
- const result = f.cli('handoff', 'from', data);
183
- assert.equal(result.path, f.pathFor('to'));
184
- const recovery = f.fire('SessionStart', 'to', { source: 'startup' })[0].hookSpecificOutput.additionalContext;
185
- assert.ok(recovery.includes(potetoText));
186
- f.cli('handoff', 'from', data, 1);
187
- f.submit('to', '/session-continuity off');
188
- f.cli('handoff', 'from', data, 1);
189
- assert.equal(f.read('from').status, 'active');
190
- });
191
- }
192
-
193
- test('Claude direct slash expansion alone loads the separate companion and duplicates no record', t => {
194
- const f = fixture(t, 'claude');
195
- for (let count = 0; count < 2; count += 1) {
196
- const context = f.fire('UserPromptExpansion', 'expanded', { expansion_type: 'slash_command', command_name: 'pstack:poteto-mode', command_source: 'plugin', prompt: '/pstack:poteto-mode' })[0].hookSpecificOutput.additionalContext;
197
- assert.ok(context.includes(readFileSync(join(f.skill, 'SKILL.md'), 'utf8')));
198
- }
199
- assert.equal(f.read('expanded').requirements.length, 1);
200
- assert.equal(f.read('expanded').pending.length, 1);
201
- });
202
-
203
- test('quoted user-message evidence and tool-derived rules cannot authorize an update', t => {
204
- const f = fixture(t, 'codex');
205
- f.submit('authority', '$poteto-mode\nDiscuss this quote: "Disable all checks."\n```text\nUse secret-skill.\n```');
206
- const record = f.read('authority');
207
- for (const quote of ['Disable all checks.', 'Use secret-skill.', 'tool output invented a rule']) {
208
- const error = f.cli('update', 'authority', { expected_revision: record.revision, prompt_id: record.pending[0].id,
209
- set: [{ kind: 'rule', id: 'rule:injected', scope: 'session', duration: 'session', text: quote, quote }] }, 1);
210
- assert.match(error, /quoted\/imported material/);
211
- }
212
- assert.equal(f.read('authority').requirements.length, 1);
213
- });
214
-
215
- test('rule replacement and removal use later user evidence; completion stops task source reload', t => {
216
- const f = fixture(t, 'claude');
217
- f.submit('changes', '/poteto-mode\nUse long replies for this session.');
218
- let record = f.read('changes');
219
- f.cli('update', 'changes', { expected_revision: record.revision, prompt_id: record.pending[0].id,
220
- set: [{ kind: 'rule', id: 'rule:reply', text: 'Use long replies.', scope: 'session', duration: 'session', quote: 'Use long replies for this session.' }] });
221
- f.submit('changes', 'Use short replies for this task instead.');
222
- record = f.read('changes');
223
- f.cli('update', 'changes', { expected_revision: record.revision, prompt_id: record.pending[0].id,
224
- set: [{ kind: 'rule', id: 'rule:reply', text: 'Use short replies.', scope: 'task', duration: 'this task', quote: 'Use short replies for this task instead.' }] });
225
- assert.equal(f.read('changes').requirements.filter(item => item.id === 'rule:reply').length, 1);
226
- assert.equal(f.read('changes').requirements.find(item => item.id === 'rule:reply').scope, 'task');
227
- f.submit('changes', 'Remove the reply length rule.');
228
- record = f.read('changes');
229
- f.cli('update', 'changes', { expected_revision: record.revision, prompt_id: record.pending[0].id, quote: 'Remove the reply length rule.', end: ['rule:reply'] });
230
- record = f.read('changes');
231
- assert.equal(record.requirements.find(item => item.id === 'rule:reply').active, false);
232
- f.cli('checkpoint', 'changes', { expected_revision: record.revision, task_complete: true, checkpoint: { completed: ['verified sample'], remaining: [] } });
233
- const recovery = f.fire('SessionStart', 'changes', { source: 'compact' })[0].hookSpecificOutput.additionalContext;
234
- assert.equal(recovery.includes(potetoText), false);
235
- });
236
-
237
- test('a busy record blocks prompt processing, preserves state, and supports a safe retry', t => {
238
- const f = fixture(t, 'claude');
239
- f.submit('busy', '/poteto-mode');
240
- const before = readFileSync(f.pathFor('busy'), 'utf8');
241
- writeFileSync(f.pathFor('busy') + '.lock', 'test-writer');
242
- const blocked = f.submit('busy', 'Use short replies for this session.');
243
- assert.equal(blocked.decision, 'block');
244
- assert.match(blocked.reason, /Record busy/);
245
- assert.equal(readFileSync(f.pathFor('busy'), 'utf8'), before);
246
- unlinkSync(f.pathFor('busy') + '.lock');
247
- f.submit('busy', 'Use short replies for this session.');
248
- assert.equal(f.read('busy').pending.length, 2);
249
- });
250
-
251
- test('clear drops a reused host id rather than inheriting an unrelated task', t => {
252
- const f = fixture(t, 'claude');
253
- f.submit('clear', '/poteto-mode for this entire session\nKeep the sample local.');
254
- const before = f.read('clear');
255
- f.fire('SessionStart', 'clear', { source: 'clear' });
256
- const record = f.read('clear');
257
- assert.equal(record.status, 'active');
258
- assert.notEqual(record.task.id, before.task.id);
259
- assert.deepEqual(record.pending, []);
260
- assert.deepEqual(record.requirements.map(entry => entry.id), ['skill:pstack:poteto-mode']);
261
- assert.equal(record.requirements[0].automatic, true);
262
- });
263
-
264
- for (const host of ['claude', 'codex']) {
265
- test(`${host}: session start and compaction activate Poteto Mode with no explicit invocation`, t => {
266
- const f = fixture(t, host);
267
- const started = f.fire('SessionStart', 'auto', { source: 'startup' })[0];
268
- assert.ok(started.systemMessage.includes(f.pathFor('auto')));
269
- assert.ok(started.hookSpecificOutput.additionalContext.includes(potetoText));
270
- const record = f.read('auto');
271
- assert.equal(record.requirements.length, 1);
272
- assert.equal(record.requirements[0].scope, 'session');
273
- assert.equal(record.requirements[0].automatic, true);
274
- assert.equal(record.requirements[0].source, f.poteto);
275
- assert.deepEqual(record.pending, []);
276
- for (const source of ['compact', 'resume']) {
277
- const later = f.fire('SessionStart', 'auto', { source })[0].hookSpecificOutput.additionalContext;
278
- assert.ok(later.includes(potetoText));
279
- }
280
- assert.equal(f.read('auto').revision, record.revision);
281
- f.submit('auto', '/session-continuity off');
282
- assert.deepEqual(f.fire('SessionStart', 'auto', { source: 'compact' }), [{}]);
283
- assert.deepEqual(f.fire('SessionStart', 'auto', { source: 'startup' }), [{}]);
284
- });
285
-
286
- test(`${host}: session start accepts an unrelated display name from the canonical Poteto path`, t => {
287
- const f = fixture(t, host);
288
- writeFileSync(f.poteto, '---\nname: Concise Session Style\n---\nPOTETO_SOURCE_EXPECTATION: prove the requested result.\n');
289
- const started = f.fire('SessionStart', 'unrelated-display-name', { source: 'startup' })[0];
290
- assert.ok(started.hookSpecificOutput, started.systemMessage);
291
- assert.ok(started.hookSpecificOutput.additionalContext.includes('POTETO_SOURCE_EXPECTATION'));
292
- });
293
-
294
- test(`${host}: a missing Poteto source stops automatic activation without writing a record`, t => {
295
- const f = fixture(t, host);
296
- unlinkSync(f.poteto);
297
- const started = f.fire('SessionStart', 'missing-source', { source: 'startup' })[0];
298
- assert.match(started.systemMessage, /did not activate Poteto Mode/);
299
- assert.equal(existsSync(f.pathFor('missing-source')), false);
300
- });
301
- }
302
-
303
- test('cursor: session start injects Poteto Mode and compaction reloads it on the next tool result', t => {
304
- const f = fixture(t, 'cursor');
305
- const started = f.fire('sessionStart', 'cursor-a', { session_id: 'composer-1', composer_mode: 'agent' })[0];
306
- assert.ok(started.additional_context.includes(potetoText));
307
- assert.equal(f.read('cursor-a').requirements[0].automatic, true);
308
- assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
309
- const compacting = f.fire('preCompact', 'cursor-a', { trigger: 'auto' })[0];
310
- assert.ok(compacting.user_message.includes('Poteto Mode'));
311
- assert.equal(f.read('cursor-a').reload_pending, true);
312
- const reloaded = f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' })[0];
313
- assert.ok(reloaded.additional_context.includes(potetoText));
314
- assert.ok(reloaded.additional_context.includes(f.pathFor('cursor-a')));
315
- assert.equal(f.read('cursor-a').reload_pending, false);
316
- assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
317
- });
318
-
319
- test('cursor: a prompt records evidence, never blocks submission, and reloads named skills after a tool result', t => {
320
- const f = fixture(t, 'cursor');
321
- f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' });
322
- const submitted = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: 'Finish the sample task.', generation_id: 'gen-1' })[0];
323
- assert.equal(submitted.continue, true);
324
- assert.equal(f.read('cursor-b').pending.length, 1);
325
- const invoked = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/poteto-mode for this entire session', generation_id: 'gen-2' })[0];
326
- assert.equal(invoked.continue, true);
327
- assert.equal(f.read('cursor-b').reload_pending, true);
328
- assert.ok(f.fire('postToolUse', 'cursor-b', { tool_name: 'Read' })[0].additional_context.includes(potetoText));
329
- const off = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/session-continuity off', generation_id: 'gen-3' })[0];
330
- assert.equal(off.continue, true);
331
- assert.equal(f.read('cursor-b').status, 'inactive');
332
- assert.deepEqual(f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' }), [{}]);
333
- assert.deepEqual(f.fire('preCompact', 'cursor-b', { trigger: 'manual' }), [{}]);
334
- });
335
-
336
- test('missing host identity and corrupt records produce visible recovery failures', t => {
337
- const f = fixture(t, 'codex');
338
- const missing = f.fire('UserPromptSubmit', '', { prompt: '$poteto-mode' })[0];
339
- assert.equal(missing.decision, 'block');
340
- f.submit('corrupt', '$poteto-mode');
341
- writeFileSync(f.pathFor('corrupt'), '{bad json');
342
- const failure = f.fire('SessionStart', 'corrupt', { source: 'resume' })[0];
343
- assert.match(failure.systemMessage, /Session continuity failed/);
344
- assert.match(failure.hookSpecificOutput.additionalContext, /Stop dependent work/);
345
- });
346
-
347
- for (const host of ['claude', 'codex']) {
348
- test(`${host}: two simultaneous configured callbacks keep distinct session records`, async t => {
349
- const f = fixture(t, host);
350
- const configuration = continuityHookConfiguration(host, f.script);
351
- const command = configuration.UserPromptSubmit[0].hooks[0].command;
352
- const fire = (session, scope) => new Promise((resolve, reject) => {
353
- const env = { ...process.env };
354
- delete env.CDE_CONTINUITY_ROOT;
355
- delete env.CDE_POTETO_SOURCE;
356
- const child = spawn(command, { shell: true, env });
357
- let stdout = '';
358
- let stderr = '';
359
- child.stdout.on('data', chunk => { stdout += chunk; });
360
- child.stderr.on('data', chunk => { stderr += chunk; });
361
- child.on('error', reject);
362
- child.on('close', code => code ? reject(new Error(stderr)) : resolve(JSON.parse(stdout)));
363
- child.stdin.end(JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: session, cwd: f.root, prompt: `/poteto-mode for this ${scope}` }));
364
- });
365
- await Promise.all([fire('parallel-a', 'task'), fire('parallel-b', 'entire session')]);
366
- assert.equal(f.read('parallel-a').requirements[0].scope, 'task');
367
- assert.equal(f.read('parallel-b').requirements[0].scope, 'session');
368
- assert.notEqual(f.pathFor('parallel-a'), f.pathFor('parallel-b'));
369
- });
370
- }
371
-
372
- test('a new explicit invocation after task completion starts a distinct task without restoring ended rules', t => {
373
- const f = fixture(t, 'claude');
374
- f.submit('completed', '/poteto-mode');
375
- const before = f.read('completed');
376
- f.cli('checkpoint', 'completed', { expected_revision: before.revision, task_complete: true, checkpoint: { completed: ['verified'], remaining: [] } });
377
- const output = f.submit('completed', '/poteto-mode');
378
- assert.notEqual(f.read('completed').task.id, before.task.id);
379
- assert.equal(f.read('completed').task.completed, false);
380
- assert.ok(output.hookSpecificOutput.additionalContext.includes(potetoText));
381
- });
@@ -1,131 +0,0 @@
1
- import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- const supportedHosts = ['claude', 'codex', 'cursor'];
6
-
7
- export function continuityHookConfiguration(host, script) {
8
- if (!supportedHosts.includes(host)) throw new Error(`Unsupported host ${host}`);
9
- if (/[\r\n"`$%]/.test(script)) throw new Error('Hook script path contains shell expansion characters');
10
- const command = `node "${script.replace(/\\/g, '/')}" hook ${host}`;
11
- if (host === 'cursor') {
12
- return Object.fromEntries(['sessionStart', 'beforeSubmitPrompt', 'preCompact', 'postToolUse'].map(event => [event, [{ command }]]));
13
- }
14
- const events = host === 'claude' ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart'] : ['UserPromptSubmit', 'SessionStart'];
15
- return Object.fromEntries(events.map(event => [event, [{
16
- ...(event === 'SessionStart' ? { matcher: host === 'claude' ? 'startup|resume|compact|clear|fork' : 'startup|resume|compact|clear' } : {}),
17
- ...(event === 'UserPromptExpansion' ? { matcher: '^(pstack:)?poteto-mode$|^(claude-dev-env:)?session-continuity$' } : {}),
18
- hooks: [{ type: 'command', command, timeout: 10 }],
19
- }]]));
20
- }
21
-
22
- export function mergeContinuityHooks(existing, host, script) {
23
- const additions = continuityHookConfiguration(host, script);
24
- const mergedConfiguration = structuredClone(existing);
25
- if (host === 'cursor') mergedConfiguration.version = 1;
26
- mergedConfiguration.hooks ||= {};
27
- for (const [event, groups] of Object.entries(additions)) {
28
- const current = mergedConfiguration.hooks[event] || [];
29
- const command = host === 'cursor' ? groups[0].command : groups[0].hooks[0].command;
30
- for (const entry of current) {
31
- for (const hook of entry.hooks || [entry]) {
32
- if (hook.command?.includes('/session-continuity/continuity.mjs') && hook.command !== command) {
33
- throw new Error('Another continuity installation owns this host config. Select its profile or remove that installation explicitly.');
34
- }
35
- }
36
- }
37
- const kept = current.flatMap(entry => {
38
- if (host === 'cursor') return entry.command === command ? [] : [entry];
39
- const hooks = (entry.hooks || []).filter(hook => hook.command !== command);
40
- return hooks.length ? [{ ...entry, hooks }] : [];
41
- });
42
- mergedConfiguration.hooks[event] = kept.concat(groups);
43
- }
44
- return mergedConfiguration;
45
- }
46
-
47
- export const CONTINUITY_SCRIPT_MARKER = '/session-continuity/continuity.mjs';
48
-
49
- export function isContinuityHookCommand(command) {
50
- return typeof command === 'string' && command.includes(CONTINUITY_SCRIPT_MARKER);
51
- }
52
-
53
- export function continuityScriptPath(roots) {
54
- return join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
55
- }
56
-
57
- export function continuityHostConfigurationPaths(roots) {
58
- return {
59
- claude: join(roots.managedRoot, 'settings.json'),
60
- codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json'),
61
- cursor: join(roots.cursorInstallDirectory, 'hooks.json'),
62
- };
63
- }
64
-
65
- export function removeContinuityHooks(configuration) {
66
- let removedCount = 0;
67
- for (const [event, entries] of Object.entries(configuration.hooks || {})) {
68
- const kept = entries.flatMap(entry => {
69
- if (!entry.hooks) {
70
- if (!isContinuityHookCommand(entry.command)) return [entry];
71
- removedCount += 1;
72
- return [];
73
- }
74
- const hooks = entry.hooks.filter(hook => !isContinuityHookCommand(hook.command));
75
- removedCount += entry.hooks.length - hooks.length;
76
- return hooks.length ? [{ ...entry, hooks }] : [];
77
- });
78
- if (kept.length) configuration.hooks[event] = kept;
79
- else delete configuration.hooks[event];
80
- }
81
- return removedCount;
82
- }
83
-
84
- function continuityConfigurationPlan(host, path, script) {
85
- if (!existsSync(dirname(path))) throw new Error(`Host config directory is absent: ${dirname(path)}`);
86
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
87
- const merged = mergeContinuityHooks(existing ? JSON.parse(existing) : {}, host, script);
88
- return { host, path, existing, content: JSON.stringify(merged, null, 2) + '\n' };
89
- }
90
-
91
- function applyContinuityConfigurationPlan(plan) {
92
- if (plan.existing === plan.content) return { host: plan.host, path: plan.path, changed: false };
93
- if (plan.existing !== null) {
94
- const backup = `${plan.path}.before-session-continuity`;
95
- if (!existsSync(backup)) writeFileSync(backup, plan.existing, { flag: 'wx', mode: 0o600 });
96
- }
97
- const current = existsSync(plan.path) ? readFileSync(plan.path, 'utf8') : null;
98
- if (current !== plan.existing) throw new Error(`Config changed during setup: ${plan.path}. Rerun setup.`);
99
- const temporary = `${plan.path}.continuity-${process.pid}.tmp`;
100
- writeFileSync(temporary, plan.content, { flag: 'wx', mode: 0o600 });
101
- renameSync(temporary, plan.path);
102
- if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
103
- return { host: plan.host, path: plan.path, changed: true };
104
- }
105
-
106
- export function configureContinuityHosts(roots, selected) {
107
- const unsupported = selected.find(host => !supportedHosts.includes(host));
108
- if (unsupported) throw new Error(`Unsupported host ${unsupported}`);
109
- const script = continuityScriptPath(roots);
110
- if (!existsSync(script) || !existsSync(join(dirname(script), 'SKILL.md'))) {
111
- throw new Error('Run the full claude-dev-env installer from this checkout first so the companion is in the canonical agents home.');
112
- }
113
- const paths = continuityHostConfigurationPaths(roots);
114
- return selected
115
- .map(host => continuityConfigurationPlan(host, paths[host], script))
116
- .map(applyContinuityConfigurationPlan);
117
- }
118
-
119
- async function main() {
120
- const requested = process.argv.slice(2);
121
- const selected = requested.length ? requested : supportedHosts;
122
- const { resolveInstallRoot } = await import('./resolve-install-root.mjs');
123
- for (const result of configureContinuityHosts(resolveInstallRoot(), selected)) {
124
- console.log(`${result.changed ? 'Configured and read back' : 'Already configured'}: ${result.path}`);
125
- }
126
- console.log('Review and trust the new hooks in each host.');
127
- }
128
-
129
- if (process.argv[1] && resolve(realpathSync(process.argv[1])) === fileURLToPath(import.meta.url)) {
130
- main().catch(error => { console.error(error.message); process.exitCode = 1; });
131
- }
@@ -1,106 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import {
7
- configureContinuityHosts,
8
- continuityHookConfiguration,
9
- mergeContinuityHooks,
10
- removeContinuityHooks,
11
- } from './install-session-continuity.mjs';
12
-
13
- function installRootFixture() {
14
- const root = mkdtempSync(join(tmpdir(), 'continuity-roots-'));
15
- const skillsInstallDirectory = join(root, 'agents', 'skills');
16
- const companionDirectory = join(skillsInstallDirectory, 'session-continuity');
17
- mkdirSync(companionDirectory, { recursive: true });
18
- writeFileSync(join(companionDirectory, 'continuity.mjs'), 'export default null;\n');
19
- writeFileSync(join(companionDirectory, 'SKILL.md'), '# companion\n');
20
- const managedRoot = join(root, 'claude');
21
- const codexRulesInstallDirectory = join(root, 'codex', 'rules');
22
- const cursorInstallDirectory = join(root, 'cursor');
23
- for (const directory of [managedRoot, codexRulesInstallDirectory, cursorInstallDirectory]) {
24
- mkdirSync(directory, { recursive: true });
25
- }
26
- return { managedRoot, skillsInstallDirectory, codexRulesInstallDirectory, cursorInstallDirectory };
27
- }
28
-
29
- test('setup preserves other hooks and settings, is idempotent, and rejects competing profiles', () => {
30
- const existing = { permissions: { deny: ['secret'] }, hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'existing-hook' }] }] } };
31
- const script = '/home/jon/.agents/skills/session-continuity/continuity.mjs';
32
- const first = mergeContinuityHooks(existing, 'claude', script);
33
- assert.deepEqual(mergeContinuityHooks(first, 'claude', script), first);
34
- assert.deepEqual(first.permissions, existing.permissions);
35
- assert.equal(first.hooks.SessionStart[0].hooks[0].command, 'existing-hook');
36
- assert.equal(existing.hooks.SessionStart.length, 1);
37
- assert.throws(() => mergeContinuityHooks(first, 'claude', '/other/session-continuity/continuity.mjs'), /Another continuity installation/);
38
- assert.throws(() => continuityHookConfiguration('claude', '/path/with$expansion/continuity.mjs'), /expansion/);
39
- });
40
-
41
- test('Cursor setup keeps other hooks, sets the file version, and stays idempotent', () => {
42
- const existing = { hooks: { beforeShellExecution: [{ command: 'existing' }] } };
43
- const script = '/home/jon/.agents/skills/session-continuity/continuity.mjs';
44
- const first = mergeContinuityHooks(existing, 'cursor', script);
45
- assert.equal(first.version, 1);
46
- assert.equal(first.hooks.beforeShellExecution[0].command, 'existing');
47
- assert.deepEqual(mergeContinuityHooks(first, 'cursor', script), first);
48
- assert.equal(existing.hooks.beforeShellExecution.length, 1);
49
- assert.throws(() => mergeContinuityHooks(first, 'cursor', '/other/session-continuity/continuity.mjs'), /Another continuity installation/);
50
- });
51
-
52
- test('Cursor setup registers only documented context-carrying events', () => {
53
- const configuration = continuityHookConfiguration('cursor', '/home/jon/.agents/skills/session-continuity/continuity.mjs');
54
- assert.deepEqual(Object.keys(configuration), ['sessionStart', 'beforeSubmitPrompt', 'preCompact', 'postToolUse']);
55
- for (const entries of Object.values(configuration)) {
56
- assert.equal(entries.length, 1);
57
- assert.equal(entries[0].command, 'node "/home/jon/.agents/skills/session-continuity/continuity.mjs" hook cursor');
58
- }
59
- });
60
-
61
- for (const host of ['claude', 'codex']) {
62
- test(`${host}: setup registers only documented context-loading events`, () => {
63
- const configuration = continuityHookConfiguration(host, '/home/jon/.agents/skills/session-continuity/continuity.mjs');
64
- const expected = host === 'claude'
65
- ? ['UserPromptSubmit', 'UserPromptExpansion', 'SessionStart']
66
- : ['UserPromptSubmit', 'SessionStart'];
67
- assert.deepEqual(Object.keys(configuration), expected);
68
- for (const groups of Object.values(configuration)) {
69
- assert.equal(groups.length, 1);
70
- assert.equal(groups[0].hooks[0].type, 'command');
71
- assert.equal(groups[0].hooks[0].command, `node "/home/jon/.agents/skills/session-continuity/continuity.mjs" hook ${host}`);
72
- }
73
- assert.equal(new RegExp(configuration.SessionStart[0].matcher).test('compact'), true);
74
- if (host === 'claude') {
75
- assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('pstack:poteto-mode'), true);
76
- assert.equal(new RegExp(configuration.UserPromptExpansion[0].matcher).test('explain-poteto-mode'), false);
77
- }
78
- });
79
- }
80
-
81
- test('configureContinuityHosts writes each host config, reads it back, and repeats without a change', () => {
82
- const roots = installRootFixture();
83
- const settingsPath = join(roots.managedRoot, 'settings.json');
84
- writeFileSync(settingsPath, JSON.stringify({ permissions: { deny: ['secret'] } }, null, 2) + '\n');
85
- const first = configureContinuityHosts(roots, ['claude', 'codex', 'cursor']);
86
- assert.deepEqual(first.map(result => result.changed), [true, true, true]);
87
- const written = JSON.parse(readFileSync(settingsPath, 'utf8'));
88
- assert.deepEqual(written.permissions.deny, ['secret']);
89
- const command = written.hooks.SessionStart[0].hooks[0].command;
90
- assert.equal(command.endsWith('session-continuity/continuity.mjs" hook claude'), true);
91
- const second = configureContinuityHosts(roots, ['claude', 'codex', 'cursor']);
92
- assert.deepEqual(second.map(result => result.changed), [false, false, false]);
93
- });
94
-
95
- test('removeContinuityHooks takes out only the companion registrations', () => {
96
- const roots = installRootFixture();
97
- configureContinuityHosts(roots, ['claude']);
98
- const settingsPath = join(roots.managedRoot, 'settings.json');
99
- const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
100
- settings.hooks.SessionStart.unshift({ hooks: [{ type: 'command', command: 'python other_hook.py' }] });
101
- const removedCount = removeContinuityHooks(settings);
102
- assert.equal(removedCount, 3);
103
- assert.deepEqual(Object.keys(settings.hooks), ['SessionStart']);
104
- assert.equal(settings.hooks.SessionStart.length, 1);
105
- assert.equal(settings.hooks.SessionStart[0].hooks[0].command, 'python other_hook.py');
106
- });