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.
- package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- 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
|
-
|
|
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(
|
|
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(
|
|
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]
|
|
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
|
|
168
|
-
const
|
|
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
|
|