claude-dev-env 7.0.0 → 7.2.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 (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -0,0 +1,373 @@
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}: an unavailable Poteto source stops automatic activation without writing a record`, t => {
287
+ const f = fixture(t, host);
288
+ writeFileSync(f.poteto, '---\nname: Something Else\n---\n');
289
+ const started = f.fire('SessionStart', 'broken', { source: 'startup' })[0];
290
+ assert.match(started.systemMessage, /did not activate Poteto Mode/);
291
+ assert.equal(existsSync(f.pathFor('broken')), false);
292
+ });
293
+ }
294
+
295
+ test('cursor: session start injects Poteto Mode and compaction reloads it on the next tool result', t => {
296
+ const f = fixture(t, 'cursor');
297
+ const started = f.fire('sessionStart', 'cursor-a', { session_id: 'composer-1', composer_mode: 'agent' })[0];
298
+ assert.ok(started.additional_context.includes(potetoText));
299
+ assert.equal(f.read('cursor-a').requirements[0].automatic, true);
300
+ assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
301
+ const compacting = f.fire('preCompact', 'cursor-a', { trigger: 'auto' })[0];
302
+ assert.ok(compacting.user_message.includes('Poteto Mode'));
303
+ assert.equal(f.read('cursor-a').reload_pending, true);
304
+ const reloaded = f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' })[0];
305
+ assert.ok(reloaded.additional_context.includes(potetoText));
306
+ assert.ok(reloaded.additional_context.includes(f.pathFor('cursor-a')));
307
+ assert.equal(f.read('cursor-a').reload_pending, false);
308
+ assert.deepEqual(f.fire('postToolUse', 'cursor-a', { tool_name: 'Shell' }), [{}]);
309
+ });
310
+
311
+ test('cursor: a prompt records evidence, never blocks submission, and reloads named skills after a tool result', t => {
312
+ const f = fixture(t, 'cursor');
313
+ f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' });
314
+ const submitted = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: 'Finish the sample task.', generation_id: 'gen-1' })[0];
315
+ assert.equal(submitted.continue, true);
316
+ assert.equal(f.read('cursor-b').pending.length, 1);
317
+ const invoked = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/poteto-mode for this entire session', generation_id: 'gen-2' })[0];
318
+ assert.equal(invoked.continue, true);
319
+ assert.equal(f.read('cursor-b').reload_pending, true);
320
+ assert.ok(f.fire('postToolUse', 'cursor-b', { tool_name: 'Read' })[0].additional_context.includes(potetoText));
321
+ const off = f.fire('beforeSubmitPrompt', 'cursor-b', { prompt: '/session-continuity off', generation_id: 'gen-3' })[0];
322
+ assert.equal(off.continue, true);
323
+ assert.equal(f.read('cursor-b').status, 'inactive');
324
+ assert.deepEqual(f.fire('sessionStart', 'cursor-b', { session_id: 'composer-2' }), [{}]);
325
+ assert.deepEqual(f.fire('preCompact', 'cursor-b', { trigger: 'manual' }), [{}]);
326
+ });
327
+
328
+ test('missing host identity and corrupt records produce visible recovery failures', t => {
329
+ const f = fixture(t, 'codex');
330
+ const missing = f.fire('UserPromptSubmit', '', { prompt: '$poteto-mode' })[0];
331
+ assert.equal(missing.decision, 'block');
332
+ f.submit('corrupt', '$poteto-mode');
333
+ writeFileSync(f.pathFor('corrupt'), '{bad json');
334
+ const failure = f.fire('SessionStart', 'corrupt', { source: 'resume' })[0];
335
+ assert.match(failure.systemMessage, /Session continuity failed/);
336
+ assert.match(failure.hookSpecificOutput.additionalContext, /Stop dependent work/);
337
+ });
338
+
339
+ for (const host of ['claude', 'codex']) {
340
+ test(`${host}: two simultaneous configured callbacks keep distinct session records`, async t => {
341
+ const f = fixture(t, host);
342
+ const configuration = continuityHookConfiguration(host, f.script);
343
+ const command = configuration.UserPromptSubmit[0].hooks[0].command;
344
+ const fire = (session, scope) => new Promise((resolve, reject) => {
345
+ const env = { ...process.env };
346
+ delete env.CDE_CONTINUITY_ROOT;
347
+ delete env.CDE_POTETO_SOURCE;
348
+ const child = spawn(command, { shell: true, env });
349
+ let stdout = '';
350
+ let stderr = '';
351
+ child.stdout.on('data', chunk => { stdout += chunk; });
352
+ child.stderr.on('data', chunk => { stderr += chunk; });
353
+ child.on('error', reject);
354
+ child.on('close', code => code ? reject(new Error(stderr)) : resolve(JSON.parse(stdout)));
355
+ child.stdin.end(JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: session, cwd: f.root, prompt: `/poteto-mode for this ${scope}` }));
356
+ });
357
+ await Promise.all([fire('parallel-a', 'task'), fire('parallel-b', 'entire session')]);
358
+ assert.equal(f.read('parallel-a').requirements[0].scope, 'task');
359
+ assert.equal(f.read('parallel-b').requirements[0].scope, 'session');
360
+ assert.notEqual(f.pathFor('parallel-a'), f.pathFor('parallel-b'));
361
+ });
362
+ }
363
+
364
+ test('a new explicit invocation after task completion starts a distinct task without restoring ended rules', t => {
365
+ const f = fixture(t, 'claude');
366
+ f.submit('completed', '/poteto-mode');
367
+ const before = f.read('completed');
368
+ f.cli('checkpoint', 'completed', { expected_revision: before.revision, task_complete: true, checkpoint: { completed: ['verified'], remaining: [] } });
369
+ const output = f.submit('completed', '/poteto-mode');
370
+ assert.notEqual(f.read('completed').task.id, before.task.id);
371
+ assert.equal(f.read('completed').task.completed, false);
372
+ assert.ok(output.hookSpecificOutput.additionalContext.includes(potetoText));
373
+ });
@@ -74,10 +74,3 @@ def test_success_emits_one_json_record(
74
74
  assert exit_code == 0
75
75
  assert sync_record["status"] == "not_submodule"
76
76
  assert captured_error == ""
77
-
78
-
79
- def test_native_post_commit_source_and_test_are_removed() -> None:
80
- hook_directory = Path(__file__).resolve().parents[4] / "hooks" / "git-hooks"
81
-
82
- assert not (hook_directory / "post_commit.py").exists()
83
- assert not (hook_directory / "test_post_commit.py").exists()
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: windows-scheduled-task
3
+ description: >-
4
+ Register a repeating headless Windows scheduled task from PowerShell, with endless repetition, an S4U or Interactive principal, absolute action paths, and a documented teardown. Triggers: register a scheduled task, repeating task, headless task, S4U, Task Scheduler, RepetitionDuration, task will not register, scheduled task access denied, Unregister-ScheduledTask.
5
+ ---
6
+
7
+ # Windows scheduled task
8
+
9
+ ## Contents
10
+
11
+ - Principle
12
+ - Gotchas
13
+ - When this applies
14
+ - Process
15
+ - Endless repetition
16
+ - Headless principal
17
+ - Absolute paths in the action
18
+ - Settings
19
+ - Placement outside managed paths
20
+ - Status
21
+ - Removal and teardown
22
+ - Self-modification warning
23
+ - Verification
24
+ - Files
25
+
26
+ ## Principle
27
+
28
+ A registered task carries four decisions. How it repeats, which principal runs it, which absolute
29
+ executable it starts, and where its target files sit. Settle all four at registration time, then
30
+ prove the task by the artifact it changes.
31
+
32
+ ## Gotchas
33
+
34
+ - `New-ScheduledTaskTrigger -RepetitionDuration ([TimeSpan]::MaxValue)` fails the task XML schema with `The task XML contains a value which is incorrectly formatted or out of range. (10,42):Duration:P99999999DT23H59M59S`.
35
+ - `-LogonType S4U` requires an elevated shell. An unelevated `Register-ScheduledTask` ends with `Access is denied.`
36
+ - Task Scheduler inherits none of the shell PATH. A bare executable name in the action fails at run time.
37
+ - A helper under an installer-managed path deletes itself during an installer run.
38
+ - `LastTaskResult: 0` from a run that took the idle branch says nothing about the branch that acts.
39
+
40
+ ## When this applies
41
+
42
+ Use this skill for a repeating Windows task registered from PowerShell through the ScheduledTasks
43
+ module. Register the task in an elevated shell when the task must run while the user is signed out.
44
+
45
+ ## Process
46
+
47
+ 1. Resolve every executable in the action with `Get-Command <name>` and read `.Source`.
48
+ 2. Read `files[]` in `~/.claude/.claude-dev-env-manifest.json` and pick a directory that list omits.
49
+ 3. Build the action with the absolute executable path and `-WindowStyle Hidden` inside the argument string.
50
+ 4. Build a `-Once` trigger with a repetition interval and no duration, then clear the duration on the trigger object.
51
+ 5. Build the settings set.
52
+ 6. Register with an S4U principal, catch the registration failure, and register with an Interactive principal.
53
+ 7. Report which principal the registered task holds.
54
+ 8. Drive the branch that does the work and read the artifact it changes.
55
+ 9. Document the removal command beside the task.
56
+
57
+ ## Endless repetition
58
+
59
+ ```powershell
60
+ $Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30)
61
+ $Trigger.Repetition.Duration = ''
62
+ $Trigger.Repetition.StopAtDurationEnd = $false
63
+ ```
64
+
65
+ An empty duration string on the trigger object registers a repetition that keeps firing. Build the
66
+ trigger without a duration argument and set the two properties afterward.
67
+
68
+ ## Headless principal
69
+
70
+ ```powershell
71
+ $Principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U
72
+ try {
73
+ Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Principal $Principal -Force -ErrorAction Stop
74
+ $PrincipalKind = 'S4U'
75
+ } catch {
76
+ $Principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
77
+ Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Principal $Principal -Force
78
+ $PrincipalKind = 'Interactive'
79
+ }
80
+ ```
81
+
82
+ S4U runs the task with no window whether or not the user is signed in, and its registration needs an
83
+ elevated shell. The Interactive principal with `-WindowStyle Hidden` in the action runs windowless
84
+ while the user is signed in. Report `$PrincipalKind` so the operator knows which one the task holds.
85
+
86
+ ## Absolute paths in the action
87
+
88
+ ```powershell
89
+ $PwshPath = (Get-Command pwsh).Source
90
+ $Action = New-ScheduledTaskAction -Execute $PwshPath -Argument "-NoProfile -WindowStyle Hidden -File ""$ScriptPath"""
91
+ ```
92
+
93
+ Resolve each executable at registration time and store the absolute path in the action. Give the
94
+ script an absolute path too.
95
+
96
+ ## Settings
97
+
98
+ ```powershell
99
+ $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 1)
100
+ ```
101
+
102
+ These settings keep the task firing on battery power, catch up a missed run, drop an overlapping
103
+ start, and cap a hung run.
104
+
105
+ ## Placement outside managed paths
106
+
107
+ The claude-dev-env installer records every managed path in `~/.claude/.claude-dev-env-manifest.json`
108
+ under `files[]`, and `--update` removes those paths before it reinstalls from the package. A helper
109
+ that a task starts from a managed directory disappears mid-run. Read `files[]`, then place the
110
+ helper and its state files in a directory the manifest omits, such as a path under `<user home>`
111
+ outside `~/.claude`.
112
+
113
+ ## Status
114
+
115
+ ```powershell
116
+ $Info = Get-ScheduledTaskInfo -TaskName $TaskName
117
+ $Info.LastRunTime
118
+ $Info.LastTaskResult
119
+ $Info.NextRunTime
120
+ ```
121
+
122
+ ## Removal and teardown
123
+
124
+ ```powershell
125
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
126
+ ```
127
+
128
+ Put this command in the same document that describes the task.
129
+
130
+ ## Self-modification warning
131
+
132
+ A task that installs updates of the agent rules, hooks, and skills rewrites the running session
133
+ instructions on a timer. A major upgrade can land mid-session while the agent holds the older text.
134
+ Give the operator the teardown command, name the interval, and keep the interval wide enough for a
135
+ session to finish between runs.
136
+
137
+ ## Verification
138
+
139
+ A run that takes the idle branch reports `LastTaskResult: 0`, and that result covers the idle branch
140
+ alone. Prove the acting branch: create the condition the task acts on, wait one interval, then read
141
+ the artifact the task changes and its timestamp.
142
+
143
+ ## Files
144
+
145
+ - `SKILL.md`. Registration rules, worked examples, status, and teardown.
146
+
147
+ `packages/claude-dev-env/scripts/Install-SweepEmptyDirs.ps1` is the working model in this repository
148
+ for the install, `-Remove`, and `-Status` parameter sets and symmetric teardown.
149
+
150
+ ```text
151
+ windows-scheduled-task/
152
+ └── SKILL.md
153
+ ```
package/bin/cde.mjs CHANGED
@@ -6,17 +6,19 @@ import path from "node:path";
6
6
 
7
7
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
8
  const lintScriptPath = path.join(packageRoot, "scripts", "cde_lint.py");
9
+ const verificationScriptPath = path.join(packageRoot, "scripts", "local_verification", "cli.py");
9
10
  const posixSignalStatusOffset = 128;
10
11
  const lintCommandName = "lint";
12
+ const verifyCommandName = "verify";
11
13
  const pythonOptionName = "--python";
12
14
  const pythonEnvironmentName = "CDE_PYTHON";
13
15
 
14
16
 
15
17
  export function createHelpText() {
16
18
  return [
17
- "Usage: cde lint [options]",
19
+ "Usage: cde <lint|verify> [options]",
18
20
  "",
19
- "Run the policy linter. Source modes are mutually exclusive:",
21
+ "Run the policy linter with cde lint. Source modes are mutually exclusive:",
20
22
  " --files PATH [PATH ...] Current worktree files",
21
23
  " --staged Staged index content",
22
24
  " --base REVISION Changes from a base revision",
@@ -29,6 +31,12 @@ export function createHelpText() {
29
31
  " --python <command> Python interpreter override",
30
32
  " Exit 0 clean, 1 diagnostics, 2 invalid input or start failure, 3 failed rule",
31
33
  "",
34
+ "Run the required-check manifest with cde verify:",
35
+ " --manifest PATH JSON required-check manifest",
36
+ " --repo PATH Candidate repository root",
37
+ " --base SHA Trusted base revision",
38
+ " --output PATH JSON verification report",
39
+ "",
32
40
  "Python may also be selected with CDE_PYTHON.",
33
41
  ].join("\n");
34
42
  }
@@ -59,6 +67,14 @@ export function buildLintCommand(interpreter, forwardedArguments) {
59
67
  }
60
68
 
61
69
 
70
+ export function buildVerifyCommand(interpreter, forwardedArguments) {
71
+ return {
72
+ executable: interpreter,
73
+ arguments: [verificationScriptPath, ...forwardedArguments],
74
+ };
75
+ }
76
+
77
+
62
78
  function takePythonOverride(allArguments) {
63
79
  const remainingArguments = [];
64
80
  let interpreterOverride;
@@ -110,7 +126,7 @@ function statusCodeForSignal(signalName) {
110
126
  }
111
127
 
112
128
 
113
- export function runLintCommand(command, dependencies = {}) {
129
+ function runChildCommand(command, failureMessage, dependencies = {}) {
114
130
  const runChild = dependencies.runChild ?? spawn;
115
131
  const processEmitter = dependencies.processEmitter ?? process;
116
132
  return new Promise((resolve) => {
@@ -122,14 +138,14 @@ export function runLintCommand(command, dependencies = {}) {
122
138
  windowsHide: true,
123
139
  });
124
140
  } catch {
125
- processEmitter.stderr.write("Unable to start the policy linter.\n");
141
+ processEmitter.stderr.write(`${failureMessage}\n`);
126
142
  resolve(2);
127
143
  return;
128
144
  }
129
145
  const detachSignals = attachSignalForwarding(childProcess, processEmitter);
130
146
  childProcess.on("error", () => {
131
147
  detachSignals();
132
- processEmitter.stderr.write("Unable to start the policy linter.\n");
148
+ processEmitter.stderr.write(`${failureMessage}\n`);
133
149
  resolve(2);
134
150
  });
135
151
  childProcess.on("close", (exitCode, signalName) => {
@@ -144,12 +160,22 @@ export function runLintCommand(command, dependencies = {}) {
144
160
  }
145
161
 
146
162
 
163
+ export function runLintCommand(command, dependencies = {}) {
164
+ return runChildCommand(command, "Unable to start the policy linter.", dependencies);
165
+ }
166
+
167
+
168
+ export function runVerifyCommand(command, dependencies = {}) {
169
+ return runChildCommand(command, "Unable to start verification.", dependencies);
170
+ }
171
+
172
+
147
173
  export async function main(argumentsList = process.argv.slice(2), dependencies = {}) {
148
174
  if (argumentsList.length === 0 || argumentsList.includes("--help") || argumentsList.includes("-h")) {
149
175
  process.stdout.write(`${createHelpText()}\n`);
150
176
  return 0;
151
177
  }
152
- if (argumentsList[0] !== lintCommandName) {
178
+ if (![lintCommandName, verifyCommandName].includes(argumentsList[0])) {
153
179
  process.stderr.write(`${createHelpText()}\n`);
154
180
  return 2;
155
181
  }
@@ -164,8 +190,10 @@ export async function main(argumentsList = process.argv.slice(2), dependencies =
164
190
  process.stderr.write("No usable Python interpreter found. Use --python or CDE_PYTHON.\n");
165
191
  return 2;
166
192
  }
167
- const command = buildLintCommand(interpreter, remainingArguments);
168
- const runCommand = dependencies.runCommand ?? runLintCommand;
193
+ const buildCommand = argumentsList[0] === lintCommandName ? buildLintCommand : buildVerifyCommand;
194
+ const command = buildCommand(interpreter, remainingArguments);
195
+ const runCommand = dependencies.runCommand
196
+ ?? (argumentsList[0] === lintCommandName ? runLintCommand : runVerifyCommand);
169
197
  return await runCommand(command, dependencies);
170
198
  }
171
199