claude-dev-env 2.17.0 → 2.19.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/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +89 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/orchestrator/SKILL.md +6 -4
- package/.agents/skills/orchestrator-refresh/SKILL.md +9 -5
- package/.agents/skills/pr-cleanup/SKILL.md +17 -85
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills/team-advisor/SKILL.md +5 -5
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/AGENTS.md +5 -5
- package/_shared/advisor/advisor-protocol.md +54 -31
- package/_shared/advisor/reference/advisor-block.md +5 -1
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +28 -0
- package/_shared/advisor/reference/lifecycle.md +8 -1
- package/_shared/advisor/reference/sol-rung.md +12 -7
- package/_shared/advisor/reference/spawn-walk-log.md +6 -5
- package/_shared/advisor/reference/third-party-bind.md +5 -6
- package/_shared/advisor/reference/warm-up.md +9 -2
- package/_shared/advisor/scripts/codex_sol_advisor.py +66 -11
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +9 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -4
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +11 -10
- package/_shared/advisor/scripts/model_tier_run_validator.py +50 -23
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +108 -3
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +171 -82
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +43 -0
- package/_shared/advisor/scripts/tier_model_ids.py +75 -7
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/worker-spawn.md +4 -3
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +4 -0
- package/docs/references/team-advisor-skill.md +2 -2
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +308 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +42 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +168 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/pre_commit.py +6 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/json_file_reader.py +22 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/package.json +1 -1
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -11,6 +11,7 @@ Runtime documents and scripts shared by every PR-loop skill. Changes here affect
|
|
|
11
11
|
| `post-audit-thread-contract.md` | Single source of truth for the `post_audit_thread.py` invocation string, exit-code table, and per-caller policy (bugteam exit 2 = hard blocker; autoconverge clean-audit failed post = recorded bypass) |
|
|
12
12
|
| `fix-protocol.md` | Ordered sequence a fix lens follows: read, capture SHA, TDD, apply, validate, self-audit, commit, push, reply + resolve |
|
|
13
13
|
| `gh-payloads.md` | How to build GitHub review and reply payloads via MCP tools; describes the one-review-per-loop pattern |
|
|
14
|
+
| `preflight-proposal.md` | Shared immutable proposal evidence and mutation-boundary contract for audit preflight worktrees |
|
|
14
15
|
| `state-schema.md` | Fields each PR-loop workflow tracks across iterations; documents common fields and per-skill extensions |
|
|
15
16
|
| `code-rules-gate.md` | Reference for the CODE_RULES pre-commit gate check; describes what the gate blocks and when it runs |
|
|
16
17
|
| `precatch-rubric.md` | Shared pre-catch lane checklist — deterministic sweep, doc-vs-code parity, test-assertion completeness, PR-description parity, adversarial audit — that autoconverge's lenses and pr-converge's CODE_REVIEW step read on demand |
|
|
@@ -24,4 +25,4 @@ Runtime documents and scripts shared by every PR-loop skill. Changes here affect
|
|
|
24
25
|
|
|
25
26
|
## Breaking-change rule
|
|
26
27
|
|
|
27
|
-
Any shape change in
|
|
28
|
+
Any shape change in a canonical contract listed under **Key documents** requires updating every consuming skill in the same commit.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Preflight proposal contract
|
|
2
|
+
|
|
3
|
+
Audit skills use this contract when the caller selects `preflight-proposal`. The mode prepares a local candidate and returns evidence for downstream selection.
|
|
4
|
+
|
|
5
|
+
Each skill hub defines its command name and finding classification. Commands use this shape:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
<audit-skill> preflight-proposal <pr_number> --base-sha <base_sha> --head-sha <head_sha> --worktree <isolated_worktree>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Required inputs
|
|
12
|
+
|
|
13
|
+
| Input | Contract |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `pr_number` | Resolved open pull-request number |
|
|
16
|
+
| `--base-sha` | Full immutable base commit SHA |
|
|
17
|
+
| `--head-sha` | Full immutable head commit SHA |
|
|
18
|
+
| `--worktree` | Caller-supplied isolated worktree path |
|
|
19
|
+
|
|
20
|
+
## Contract
|
|
21
|
+
|
|
22
|
+
1. Validate every required input before the first audit command.
|
|
23
|
+
2. Run `git rev-parse HEAD` in the supplied worktree and require exact equality with the head SHA.
|
|
24
|
+
3. Gather changed paths and audit scope from the exact `base_sha...head_sha` range. Use the resolved PR number for PR metadata.
|
|
25
|
+
4. Run the complete audit and findings against the immutable range and worktree.
|
|
26
|
+
5. Keep every audit edit and test inside the caller-supplied isolated worktree.
|
|
27
|
+
6. Create an immutable proposal identity. Use the existing commit SHA for committed proposals. For working-tree edits, use a deterministic diff hash over the baseline `HEAD` SHA, base SHA, head SHA, sorted normalized changed paths, status records, and file bytes.
|
|
28
|
+
7. Record every changed path, including tracked, untracked, renamed, and deleted paths.
|
|
29
|
+
8. Record exact tests and outcomes: each test command, exit code, and outcome.
|
|
30
|
+
9. Return selected-candidate-ready proposal evidence with the proposal ID, isolated worktree path, changed paths, findings, tests, and outcomes.
|
|
31
|
+
10. Keep commit, push, pull-request body, pull-request comment, pull-request review, pull-request update, merge, rebase, and Ready-state mutations disabled.
|
|
32
|
+
11. Return the proposal for downstream disposition. The downstream owner adds the proposal ID to its selected or dispositioned proposal collection. Reapplication uses exactly the selected records and their changed paths. Give each later finding a new proposal ID and evidence record.
|
|
33
|
+
|
|
34
|
+
## Evidence record
|
|
35
|
+
|
|
36
|
+
Use this record shape:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
mode: preflight-proposal
|
|
40
|
+
pr_number: <resolved PR number>
|
|
41
|
+
base_sha: <immutable base SHA>
|
|
42
|
+
head_sha: <immutable head SHA>
|
|
43
|
+
proposal_id:
|
|
44
|
+
kind: commit-sha | diff-sha256
|
|
45
|
+
value: <immutable identity>
|
|
46
|
+
isolated_worktree: <caller-supplied path>
|
|
47
|
+
changed_paths:
|
|
48
|
+
- <normalized path>
|
|
49
|
+
findings:
|
|
50
|
+
- <audit-classified finding>
|
|
51
|
+
tests:
|
|
52
|
+
- command: <exact command>
|
|
53
|
+
exit_code: <integer>
|
|
54
|
+
outcome: <exact outcome>
|
|
55
|
+
downstream_selection: required
|
|
56
|
+
```
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
from code_rules_gate_parts import gate_running
|
|
7
6
|
|
|
7
|
+
from code_rules_gate_parts import enforcer_loading, gate_running
|
|
8
|
+
from code_rules_gate_parts.tests._repo_test_helpers import (
|
|
9
|
+
init_repository,
|
|
10
|
+
write_commit_and_stage_change,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
|
|
14
|
+
def _clean_validate(
|
|
15
|
+
_content: str, _path: str, _prior: str = "", **_kwargs: object
|
|
16
|
+
) -> list[str]:
|
|
10
17
|
return []
|
|
11
18
|
|
|
12
19
|
|
|
13
|
-
def _dirty_validate(
|
|
20
|
+
def _dirty_validate(
|
|
21
|
+
_content: str, _path: str, _prior: str = "", **_kwargs: object
|
|
22
|
+
) -> list[str]:
|
|
14
23
|
return ["Line 1: bad"]
|
|
15
24
|
|
|
16
25
|
|
|
@@ -92,8 +101,42 @@ def test_print_violation_section_groups_by_relative_path(
|
|
|
92
101
|
) -> None:
|
|
93
102
|
module_path = (tmp_path / "module.py").resolve()
|
|
94
103
|
|
|
95
|
-
gate_running.print_violation_section(
|
|
104
|
+
gate_running.print_violation_section(
|
|
105
|
+
"HEADER", {module_path: ["Line 1: issue"]}, tmp_path
|
|
106
|
+
)
|
|
96
107
|
|
|
97
108
|
captured = capsys.readouterr()
|
|
98
109
|
assert "HEADER" in captured.err
|
|
99
110
|
assert "Line 1: issue" in captured.err
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_gate_caller_scans_staged_content_with_commit_stage_scope(
|
|
114
|
+
tmp_path: Path,
|
|
115
|
+
capsys: pytest.CaptureFixture[str],
|
|
116
|
+
) -> None:
|
|
117
|
+
repository_root = tmp_path / "repository"
|
|
118
|
+
repository_root.mkdir()
|
|
119
|
+
init_repository(repository_root)
|
|
120
|
+
committed_source = "def calculate_total() -> int:\n return 0\n"
|
|
121
|
+
staged_source = (
|
|
122
|
+
"def calculate_total() -> int:\n total = 9999\n return total\n"
|
|
123
|
+
)
|
|
124
|
+
file_path = write_commit_and_stage_change(
|
|
125
|
+
repository_root,
|
|
126
|
+
"service.py",
|
|
127
|
+
committed_source,
|
|
128
|
+
staged_source,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
resolved_file_path = file_path.resolve()
|
|
132
|
+
exit_code = gate_running.run_gate(
|
|
133
|
+
validate_content=enforcer_loading.load_validate_content(),
|
|
134
|
+
all_file_paths=[resolved_file_path],
|
|
135
|
+
repository_root=repository_root,
|
|
136
|
+
all_added_lines_by_path={resolved_file_path: {2, 3}},
|
|
137
|
+
should_read_staged_content=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
captured = capsys.readouterr()
|
|
141
|
+
assert exit_code == 1
|
|
142
|
+
assert "9999" in captured.err
|
|
@@ -121,13 +121,14 @@ preflight/runner classification strings (for example `grok_binary_missing`,
|
|
|
121
121
|
After a grok fallthrough the dispatcher calls `detect_host_profile()` from
|
|
122
122
|
`tier_model_ids` (advisor scripts). Detection order:
|
|
123
123
|
|
|
124
|
-
1. `ADVISOR_HOST_PROFILE` when set — must be `Claude` or `ThirdParty` (any letter case).
|
|
124
|
+
1. `ADVISOR_HOST_PROFILE` when set — must be `Claude`, `Codex`, or `ThirdParty` (any letter case).
|
|
125
125
|
2. `THIRD_PARTY` when truthy — `1`, `true`, `yes`, or `on`.
|
|
126
126
|
3. Default: `Claude`.
|
|
127
127
|
|
|
128
|
-
Profiles are the real names `Claude` and `ThirdParty` (not a separate Grok host
|
|
128
|
+
Profiles are the real names `Claude`, `Codex`, and `ThirdParty` (not a separate Grok host
|
|
129
129
|
profile). Grok is tier 1 on every host; host profile only chooses the post-grok
|
|
130
|
-
branch (Agent-tool handoff vs headless claude chain).
|
|
130
|
+
branch (Agent-tool handoff vs headless claude chain). A Codex profile takes the
|
|
131
|
+
non-Claude branch.
|
|
131
132
|
|
|
132
133
|
## Code-review host routing
|
|
133
134
|
|
|
@@ -30,6 +30,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
30
30
|
'build-goal-prompt',
|
|
31
31
|
'bugteam',
|
|
32
32
|
'caveman',
|
|
33
|
+
'cert-classification-rule',
|
|
33
34
|
'closeout',
|
|
34
35
|
'code',
|
|
35
36
|
'comments',
|
|
@@ -40,6 +41,9 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
40
41
|
'copilot-review',
|
|
41
42
|
'deep-research',
|
|
42
43
|
'descriptions',
|
|
44
|
+
'e-code-review',
|
|
45
|
+
'e-simplify',
|
|
46
|
+
'eli5',
|
|
43
47
|
'emergencies',
|
|
44
48
|
'everything-search',
|
|
45
49
|
'findbugs',
|
|
@@ -64,6 +68,8 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
64
68
|
'midjourney-prompt',
|
|
65
69
|
'midjourney-submit',
|
|
66
70
|
'monitor-open-prs',
|
|
71
|
+
'pr-name-by-capability',
|
|
72
|
+
'pr-refinement',
|
|
67
73
|
'orchestrator',
|
|
68
74
|
'orchestrator-refresh',
|
|
69
75
|
'plan-to-pr',
|
|
@@ -71,6 +77,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
71
77
|
'pmid',
|
|
72
78
|
'pmin',
|
|
73
79
|
'post-audit-findings',
|
|
80
|
+
'pr-cleanup',
|
|
74
81
|
'pr-consistency-audit',
|
|
75
82
|
'pr-converge',
|
|
76
83
|
'pr-fix-protocol',
|
|
@@ -80,6 +87,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
80
87
|
'pr-scope-resolve',
|
|
81
88
|
'pre-compact',
|
|
82
89
|
'privacy-hygiene',
|
|
90
|
+
'prototype',
|
|
83
91
|
'prompt-generator',
|
|
84
92
|
'qbug',
|
|
85
93
|
'rebase',
|
|
@@ -90,14 +98,20 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
90
98
|
'release-notes-html',
|
|
91
99
|
'remember',
|
|
92
100
|
'research-mode',
|
|
101
|
+
'review-router',
|
|
102
|
+
'review-tier',
|
|
93
103
|
'reviewer-gates',
|
|
94
104
|
'reviews',
|
|
95
105
|
'run-claude-dev-env',
|
|
96
106
|
'session-log',
|
|
97
107
|
'session-tidy',
|
|
108
|
+
'pr-shared-extraction',
|
|
98
109
|
'show',
|
|
99
110
|
'skill-builder',
|
|
100
111
|
'small-cl',
|
|
112
|
+
'pr-small-cl',
|
|
113
|
+
'source-command-logifix',
|
|
114
|
+
'source-command-sr-loop',
|
|
101
115
|
'split-pr',
|
|
102
116
|
'structure-prompt',
|
|
103
117
|
'task-build',
|
|
@@ -170,3 +170,8 @@ export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
|
|
|
170
170
|
* Windows Python launcher command the installer may bake into hook settings.
|
|
171
171
|
*/
|
|
172
172
|
export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Maximum time each Python interpreter probe may run during detection.
|
|
176
|
+
*/
|
|
177
|
+
export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 10_000;
|
|
@@ -33,6 +33,10 @@ const SHIPPED_SKILL_NAME = 'privacy-hygiene';
|
|
|
33
33
|
const ELI5_SKILL_NAME = 'eli5';
|
|
34
34
|
const SHIPPED_AGENT_FILE_NAME = 'clean-coder.md';
|
|
35
35
|
const PERSONAL_SKILL_NAME = 'my-notes';
|
|
36
|
+
const SHARED_DIRECTORY_NAME = '_shared';
|
|
37
|
+
const PR_LOOP_DIRECTORY_NAME = 'pr-loop';
|
|
38
|
+
const PREFLIGHT_PROPOSAL_FILE_NAME = 'preflight-proposal.md';
|
|
39
|
+
const PREFLIGHT_PROPOSAL_POINTER = '@~/.claude/_shared/pr-loop/preflight-proposal.md';
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* @param {string} homeDirectory
|
|
@@ -53,6 +57,24 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* @param {{ homeDirectory: string, skillsInstallDirectory: string }} installationPaths
|
|
62
|
+
*/
|
|
63
|
+
function assertProposalContractInstallation(installationPaths) {
|
|
64
|
+
const { homeDirectory, skillsInstallDirectory } = installationPaths;
|
|
65
|
+
const allContractPathSegments = [
|
|
66
|
+
SHARED_DIRECTORY_NAME, PR_LOOP_DIRECTORY_NAME, PREFLIGHT_PROPOSAL_FILE_NAME,
|
|
67
|
+
];
|
|
68
|
+
const installedPointerPath = join(skillsInstallDirectory, ...allContractPathSegments);
|
|
69
|
+
const pointerLine = readFileSync(installedPointerPath, 'utf8').trimEnd().split('\n').at(-1);
|
|
70
|
+
const allTargetSegments = pointerLine.slice('@~/'.length).split('/');
|
|
71
|
+
const installedContractPath = join(homeDirectory, ...allTargetSegments);
|
|
72
|
+
const sourceContractPath = join(PACKAGE_DIRECTORY, ...allContractPathSegments);
|
|
73
|
+
|
|
74
|
+
assert.equal(pointerLine, PREFLIGHT_PROPOSAL_POINTER);
|
|
75
|
+
assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
test('CONTENT_DIRECTORIES omits agents because that tree installs to the agents home', () => {
|
|
57
79
|
assert.equal(CONTENT_DIRECTORIES.includes(MANAGED_AGENTS_DIRECTORY_NAME), false);
|
|
58
80
|
});
|
|
@@ -106,6 +128,7 @@ test('a full install writes skills and agents under .agents and points .claude a
|
|
|
106
128
|
);
|
|
107
129
|
assert.equal(realpathSync(lookupSkillFile), realpathSync(canonicalSkillFile));
|
|
108
130
|
assert.equal(realpathSync(lookupEli5SkillFile), realpathSync(canonicalEli5SkillFile));
|
|
131
|
+
assertProposalContractInstallation({ homeDirectory, skillsInstallDirectory });
|
|
109
132
|
assert.equal(
|
|
110
133
|
readFileSync(lookupAgentFile, 'utf8'),
|
|
111
134
|
readFileSync(canonicalAgentFile, 'utf8'),
|
|
@@ -23,16 +23,32 @@ import {
|
|
|
23
23
|
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
24
24
|
DEFAULT_CODEX_DIRECTORY_NAME,
|
|
25
25
|
} from './install-constants.mjs';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
CONTENT_DIRECTORIES,
|
|
28
|
+
detectPython,
|
|
29
|
+
INSTALL_GROUPS,
|
|
30
|
+
pythonFileAndPrefixArguments,
|
|
31
|
+
} from './install.mjs';
|
|
27
32
|
|
|
28
33
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
29
34
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
30
35
|
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
36
|
+
const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
|
|
31
37
|
const SHIPPED_RULES_SOURCE_PATH = join(
|
|
32
38
|
PACKAGE_DIRECTORY,
|
|
33
39
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
34
40
|
CODEX_RULES_SHIPPED_FILE_NAME,
|
|
35
41
|
);
|
|
42
|
+
const BLOCKING_HOOKS_DIRECTORY = join(PACKAGE_DIRECTORY, 'hooks', 'blocking');
|
|
43
|
+
const PYTHON_PII_SCAN_SCRIPT = [
|
|
44
|
+
'import json',
|
|
45
|
+
'import sys',
|
|
46
|
+
'from dataclasses import asdict',
|
|
47
|
+
'from pii_scanner import scan_text_for_pii',
|
|
48
|
+
'all_findings = scan_text_for_pii(sys.stdin.read())',
|
|
49
|
+
"home_path_findings = [asdict(each_finding) for each_finding in all_findings if each_finding.category == 'home-path']",
|
|
50
|
+
'print(json.dumps(home_path_findings))',
|
|
51
|
+
].join('\n');
|
|
36
52
|
|
|
37
53
|
function runInstaller(homeDirectory, extraArguments) {
|
|
38
54
|
return execFileSync('node', [INSTALLER_PATH, ...extraArguments], {
|
|
@@ -45,9 +61,44 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
45
61
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
46
62
|
[CODEX_HOME_ENVIRONMENT_VARIABLE]: join(homeDirectory, DEFAULT_CODEX_DIRECTORY_NAME),
|
|
47
63
|
},
|
|
64
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
48
65
|
});
|
|
49
66
|
}
|
|
50
67
|
|
|
68
|
+
function scanTextWithProductionPiiScanner(scannedText) {
|
|
69
|
+
const temporaryDirectory = mkdtempSync(join(tmpdir(), 'cdev-pii-scan-'));
|
|
70
|
+
try {
|
|
71
|
+
const detectedPythonCommand = detectPython();
|
|
72
|
+
if (!detectedPythonCommand) {
|
|
73
|
+
throw new Error('No usable Python interpreter found for the PII scanner test');
|
|
74
|
+
}
|
|
75
|
+
const { file, prefixArguments } = pythonFileAndPrefixArguments(
|
|
76
|
+
detectedPythonCommand,
|
|
77
|
+
);
|
|
78
|
+
const scannerJson = execFileSync(
|
|
79
|
+
file,
|
|
80
|
+
[...prefixArguments, '-c', PYTHON_PII_SCAN_SCRIPT],
|
|
81
|
+
{
|
|
82
|
+
cwd: PACKAGE_DIRECTORY,
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
env: {
|
|
85
|
+
...process.env,
|
|
86
|
+
PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
|
|
87
|
+
CLAUDE_LOCAL_IDENTITY_PATH: join(
|
|
88
|
+
temporaryDirectory,
|
|
89
|
+
'missing-local-identity.json',
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
input: scannedText,
|
|
93
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
return JSON.parse(scannerJson);
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
test('CONTENT_DIRECTORIES omits codex-rules because that tree installs to the Codex home', () => {
|
|
52
103
|
assert.equal(CONTENT_DIRECTORIES.includes(CODEX_RULES_PACKAGE_DIRECTORY_NAME), false);
|
|
53
104
|
});
|
|
@@ -123,11 +174,15 @@ test('a full install copies shipped Codex rules and leaves a local default.rules
|
|
|
123
174
|
}
|
|
124
175
|
});
|
|
125
176
|
|
|
126
|
-
test('
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
assert.
|
|
177
|
+
test('the shipped Codex rules file has no production-scanner home-path findings', () => {
|
|
178
|
+
const allHomePathFindings = scanTextWithProductionPiiScanner(
|
|
179
|
+
readFileSync(SHIPPED_RULES_SOURCE_PATH, 'utf8'),
|
|
180
|
+
);
|
|
181
|
+
assert.deepEqual(
|
|
182
|
+
allHomePathFindings,
|
|
183
|
+
[],
|
|
184
|
+
`home-path findings: ${JSON.stringify(allHomePathFindings, null, 2)}`,
|
|
185
|
+
);
|
|
131
186
|
});
|
|
132
187
|
|
|
133
188
|
test('--only journal skips Codex rules; --only core copies them', () => {
|
package/bin/install.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
CODEX_RULES_PACKAGE_DIRECTORY_NAME,
|
|
28
28
|
CURSOR_SYNC_SCRIPT_FILE_NAME,
|
|
29
29
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
30
|
+
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
30
31
|
} from './install-constants.mjs';
|
|
31
32
|
import {
|
|
32
33
|
resolveInstallRoot,
|
|
@@ -163,7 +164,7 @@ export const CORE_INCLUDE_DIRECTORIES = [
|
|
|
163
164
|
export const CORE_SKILLS = [
|
|
164
165
|
'orchestrator', 'orchestrator-refresh', 'team-advisor',
|
|
165
166
|
'grok-spawn',
|
|
166
|
-
'small-cl',
|
|
167
|
+
'pr-small-cl',
|
|
167
168
|
'everything-search',
|
|
168
169
|
'privacy-hygiene',
|
|
169
170
|
'issue-tracker',
|
|
@@ -434,6 +435,42 @@ export function interpreterCommandFromPath(executablePath) {
|
|
|
434
435
|
return forwardSlashedPath.includes(' ') ? `"${forwardSlashedPath}"` : forwardSlashedPath;
|
|
435
436
|
}
|
|
436
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Formats a path as one shell argument for the selected platform.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} path Path to format.
|
|
442
|
+
* @param {string} platform Platform whose shell will parse the argument.
|
|
443
|
+
* @returns {string} A quoted shell argument.
|
|
444
|
+
*/
|
|
445
|
+
export function commandArgumentFromPath(path, platform = process.platform) {
|
|
446
|
+
const forwardSlashedPath = path.replace(/\\/g, '/');
|
|
447
|
+
if (platform === 'win32') {
|
|
448
|
+
if (/^[A-Za-z0-9_./:@+=,-]+$/.test(forwardSlashedPath)) return forwardSlashedPath;
|
|
449
|
+
return forwardSlashedPath.split('%').map(segment => `"${segment}"`).join('^%');
|
|
450
|
+
}
|
|
451
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(forwardSlashedPath)) return forwardSlashedPath;
|
|
452
|
+
return `'${forwardSlashedPath.replace(/'/g, "'\\''")}'`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Quotes standalone plugin-root hook paths while preserving inline commands.
|
|
457
|
+
*
|
|
458
|
+
* @param {string} commandString Source hook command.
|
|
459
|
+
* @param {string} pluginRootForward Forward-slash plugin root directory.
|
|
460
|
+
* @returns {string} Rewritten hook command.
|
|
461
|
+
*/
|
|
462
|
+
function quoteStandalonePluginRootHookPath(commandString, pluginRootForward) {
|
|
463
|
+
const standalonePathPattern = /(^|\s)(["']?)\$\{CLAUDE_PLUGIN_ROOT\}(\/hooks\/[^\s"']*?\.py)\2(?=$|[\s;&|])/g;
|
|
464
|
+
return commandString.replace(
|
|
465
|
+
standalonePathPattern,
|
|
466
|
+
(_match, argumentPrefix, _sourceQuote, relativeHookPath) => (
|
|
467
|
+
`${argumentPrefix}${commandArgumentFromPath(
|
|
468
|
+
`${pluginRootForward}${relativeHookPath}`,
|
|
469
|
+
)}`
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
437
474
|
/**
|
|
438
475
|
* Picks the interpreter command baked into every managed hook in settings.json.
|
|
439
476
|
* On win32 the first working candidate is resolved to its absolute
|
|
@@ -444,14 +481,14 @@ export function interpreterCommandFromPath(executablePath) {
|
|
|
444
481
|
*
|
|
445
482
|
* @returns {string|null} The interpreter command, or null when none is usable.
|
|
446
483
|
*/
|
|
447
|
-
function detectPython() {
|
|
484
|
+
export function detectPython() {
|
|
448
485
|
const candidates = pythonCandidatesForPlatform(process.platform);
|
|
449
486
|
for (const { command, versionFlag } of candidates) {
|
|
450
487
|
try {
|
|
451
|
-
const version = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
488
|
+
const version = execSync(`${command} ${versionFlag}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
452
489
|
if (!version.includes('Python 3.')) continue;
|
|
453
490
|
if (process.platform !== 'win32') return command;
|
|
454
|
-
const executablePath = execSync(`${command} -c "import sys; print(sys.executable)"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
491
|
+
const executablePath = execSync(`${command} -c "import sys; print(sys.executable)"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: PYTHON_PROBE_TIMEOUT_MILLISECONDS }).trim();
|
|
455
492
|
if (!executablePath || isWindowsStorePythonStub(executablePath)) continue;
|
|
456
493
|
return interpreterCommandFromPath(executablePath);
|
|
457
494
|
} catch { /* try next */ }
|
|
@@ -1266,11 +1303,13 @@ function commandTailEndsAtManagedHook(normalizedCommand, relativePath) {
|
|
|
1266
1303
|
* @returns {boolean} True when the command is the inline validators runner.
|
|
1267
1304
|
*/
|
|
1268
1305
|
export function commandIsInlineManagedValidatorRunner(normalizedCommand) {
|
|
1269
|
-
|
|
1270
|
-
return
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
);
|
|
1306
|
+
if (!normalizedCommand.includes('sys.path.insert(')) return false;
|
|
1307
|
+
if (!normalizedCommand.includes('from validators.run_all_validators import main')) return false;
|
|
1308
|
+
const hasLiteralHooksPath = normalizedCommand.includes('/.claude/hooks');
|
|
1309
|
+
const hasShippedBase64Path = normalizedCommand.includes('__claude_dev_env_managed_hooks__');
|
|
1310
|
+
if (!hasLiteralHooksPath && !hasShippedBase64Path) return false;
|
|
1311
|
+
const inlineValidatorRunnerMarker = /sys\.path\.insert\([\s\S]*from validators\.run_all_validators import main/;
|
|
1312
|
+
return inlineValidatorRunnerMarker.test(normalizedCommand);
|
|
1274
1313
|
}
|
|
1275
1314
|
|
|
1276
1315
|
/**
|
|
@@ -1348,6 +1387,7 @@ function startEventFromHookGroupList(settings, eventType) {
|
|
|
1348
1387
|
export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pythonCommand) {
|
|
1349
1388
|
const managedHookRelativePaths = managedHookScriptRelativePaths(hooksConfig);
|
|
1350
1389
|
const pluginRootForward = pluginRootDir.replace(/\\/g, '/');
|
|
1390
|
+
const encodedPluginHooksPath = Buffer.from(`${pluginRootForward}/hooks`, 'utf8').toString('base64');
|
|
1351
1391
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
1352
1392
|
let groupCount = 0;
|
|
1353
1393
|
for (const [eventType, matcherGroups] of Object.entries(hooksConfig.hooks)) {
|
|
@@ -1356,6 +1396,14 @@ export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pyt
|
|
|
1356
1396
|
for (const sourceGroup of matcherGroups) {
|
|
1357
1397
|
const rewrittenHooks = sourceGroup.hooks.map(hook => {
|
|
1358
1398
|
let command = hook.command;
|
|
1399
|
+
command = command.replace(
|
|
1400
|
+
/r(['"])\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\1/g,
|
|
1401
|
+
() => (
|
|
1402
|
+
`(__import__('base64').b64decode('${encodedPluginHooksPath}').decode(), `
|
|
1403
|
+
+ "'__claude_dev_env_managed_hooks__')[0]"
|
|
1404
|
+
),
|
|
1405
|
+
);
|
|
1406
|
+
command = quoteStandalonePluginRootHookPath(command, pluginRootForward);
|
|
1359
1407
|
command = command.replace(
|
|
1360
1408
|
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
1361
1409
|
() => pluginRootForward,
|
|
@@ -13,10 +13,16 @@ import {
|
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import { join, dirname, basename } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import {
|
|
17
|
+
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
18
|
+
PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
|
|
19
|
+
} from './install-constants.mjs';
|
|
20
|
+
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
16
21
|
|
|
17
22
|
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
18
23
|
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
19
24
|
const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
|
|
25
|
+
const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
|
|
20
26
|
const EXCLUDED_PACKAGE_COPY_DIRECTORY = 'node_modules';
|
|
21
27
|
|
|
22
28
|
const RETIRED_SKILL_DIRECTORIES = [
|
|
@@ -35,7 +41,7 @@ const STALE_SKILL_FILE_RELATIVE_SEGMENTS = ['scripts', 'retired_module.py'];
|
|
|
35
41
|
const RUNTIME_ARTIFACT_RELATIVE_SEGMENTS = ['scripts', '__pycache__', 'helper.cpython-312.pyc'];
|
|
36
42
|
const SCOPED_GROUP_SKILL_DIRECTORY = 'orchestrator';
|
|
37
43
|
const CORE_REVIEW_GUIDE_SKILL_DIRECTORIES = [
|
|
38
|
-
'small-cl',
|
|
44
|
+
'pr-small-cl',
|
|
39
45
|
];
|
|
40
46
|
const PRIOR_RUN_BACKUP_DIRECTORY_NAMES = [
|
|
41
47
|
'2020-01-01T00-00-00-000Z',
|
|
@@ -102,8 +108,8 @@ function prunedSkillBackupContains(claudeDirectory, skillsRelativePath) {
|
|
|
102
108
|
*
|
|
103
109
|
* @returns {{homeDirectory: string, claudeDirectory: string, skillsDirectory: string, manifestPath: string}}
|
|
104
110
|
*/
|
|
105
|
-
function createSandbox() {
|
|
106
|
-
const homeDirectory = mkdtempSync(join(tmpdir(),
|
|
111
|
+
function createSandbox(homeDirectoryPrefix = 'cdev-prune-home-') {
|
|
112
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), homeDirectoryPrefix));
|
|
107
113
|
const claudeDirectory = join(homeDirectory, '.claude');
|
|
108
114
|
const skillsDirectory = join(claudeDirectory, 'skills');
|
|
109
115
|
mkdirSync(skillsDirectory, { recursive: true });
|
|
@@ -111,6 +117,61 @@ function createSandbox() {
|
|
|
111
117
|
return { homeDirectory, claudeDirectory, skillsDirectory, manifestPath };
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
test('the retired-skill fallback covers every current shipped skill', () => {
|
|
121
|
+
const sourceSkillsDirectory = join(
|
|
122
|
+
PACKAGE_DIRECTORY,
|
|
123
|
+
PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
|
|
124
|
+
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
125
|
+
);
|
|
126
|
+
const allCurrentSkillNames = readdirSync(sourceSkillsDirectory, { withFileTypes: true })
|
|
127
|
+
.filter(eachEntry => eachEntry.isDirectory())
|
|
128
|
+
.filter(eachEntry => existsSync(join(sourceSkillsDirectory, eachEntry.name, 'SKILL.md')))
|
|
129
|
+
.map(eachEntry => eachEntry.name);
|
|
130
|
+
const allUncoveredSkillNames = allCurrentSkillNames.filter(
|
|
131
|
+
eachSkillName => !EVER_SHIPPED_SKILL_NAMES.has(eachSkillName),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(allUncoveredSkillNames, [], 'every current skill must enter the fallback set');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a core install runs the spaced-path Write dispatcher and captures its red denial', () => {
|
|
138
|
+
const sandbox = createSandbox('cdev prune & (home)-');
|
|
139
|
+
try {
|
|
140
|
+
runInstaller(sandbox.homeDirectory, ['--only', 'core']);
|
|
141
|
+
const settings = readSettings(sandbox.claudeDirectory);
|
|
142
|
+
const dispatcherHooks = settings.hooks.PreToolUse
|
|
143
|
+
.flatMap(eachGroup => eachGroup.hooks)
|
|
144
|
+
.filter(eachHook => /(?:^|[\\/])pre_tool_use_dispatcher\.py["']?(?:\s|$)/.test(eachHook.command));
|
|
145
|
+
assert.equal(dispatcherHooks.length, 1, 'the core install writes one PreToolUse dispatcher');
|
|
146
|
+
|
|
147
|
+
const protectedFilePath = join(sandbox.homeDirectory, 'protected file.txt');
|
|
148
|
+
writeFileSync(protectedFilePath, 'existing content\n');
|
|
149
|
+
const writePayload = JSON.stringify({
|
|
150
|
+
tool_name: 'Write',
|
|
151
|
+
tool_input: { file_path: protectedFilePath, content: 'replacement content\n' },
|
|
152
|
+
});
|
|
153
|
+
const { childEnvironment } = resolveInstallerInvocation(sandbox.homeDirectory);
|
|
154
|
+
const dispatcherRun = spawnSync(dispatcherHooks[0].command, {
|
|
155
|
+
cwd: dirname(INSTALLER_PATH),
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
env: childEnvironment,
|
|
158
|
+
input: `${writePayload}\n`,
|
|
159
|
+
shell: true,
|
|
160
|
+
timeout: 30000,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(dispatcherRun.status, 0, dispatcherRun.stderr);
|
|
164
|
+
const shownRedDecision = JSON.parse(dispatcherRun.stdout.trim());
|
|
165
|
+
assert.equal(shownRedDecision.hookSpecificOutput.permissionDecision, 'deny');
|
|
166
|
+
assert.match(
|
|
167
|
+
shownRedDecision.hookSpecificOutput.permissionDecisionReason,
|
|
168
|
+
/BLOCKED: Write on existing file/,
|
|
169
|
+
);
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
114
175
|
/**
|
|
115
176
|
* Plant a skill directory under the sandbox with a single marker file.
|
|
116
177
|
*
|
|
@@ -142,6 +203,7 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
142
203
|
cwd: dirname(installerPath),
|
|
143
204
|
encoding: 'utf8',
|
|
144
205
|
env: childEnvironment,
|
|
206
|
+
timeout: INSTALLER_PROCESS_TIMEOUT_MS,
|
|
145
207
|
});
|
|
146
208
|
}
|
|
147
209
|
|