claude-dev-env 7.0.0 → 7.1.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 +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -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 +78 -0
- package/bin/install-session-continuity.test.mjs +42 -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
|
@@ -50,3 +50,39 @@ def test_validate_proposed_file_keeps_ruff_as_an_explicit_native_step(
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
assert "Ruff" in _result_names(all_results)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _mixed_newline_overlong_source() -> str:
|
|
56
|
+
all_line_endings = ("\r\n", "\n")
|
|
57
|
+
all_body_lines = "".join(
|
|
58
|
+
" running_total = running_total + 1"
|
|
59
|
+
+ all_line_endings[each_line_index % len(all_line_endings)]
|
|
60
|
+
for each_line_index in range(29)
|
|
61
|
+
)
|
|
62
|
+
return (
|
|
63
|
+
"def calculate_total(running_total: int) -> int:"
|
|
64
|
+
+ all_line_endings[0]
|
|
65
|
+
+ all_body_lines
|
|
66
|
+
+ " return running_total"
|
|
67
|
+
+ all_line_endings[1]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_validate_proposed_file_preserves_mixed_newline_diagnostics(
|
|
72
|
+
tmp_path: Path,
|
|
73
|
+
) -> None:
|
|
74
|
+
all_results = run_all_validators.validate_proposed_file(
|
|
75
|
+
str(tmp_path / "probe.py"),
|
|
76
|
+
_mixed_newline_overlong_source(),
|
|
77
|
+
include_ruff=False,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
code_quality_result = next(
|
|
81
|
+
each_result
|
|
82
|
+
for each_result in all_results
|
|
83
|
+
if each_result.name == "Code Quality"
|
|
84
|
+
)
|
|
85
|
+
assert code_quality_result.passed is False
|
|
86
|
+
assert "Function 'calculate_total' is 31 lines (max 30)" in (
|
|
87
|
+
code_quality_result.output
|
|
88
|
+
)
|
|
@@ -29,6 +29,30 @@ def connect():
|
|
|
29
29
|
return password
|
|
30
30
|
'''
|
|
31
31
|
|
|
32
|
+
METADATA_SOURCE_FIXTURE = '''
|
|
33
|
+
AUTHORIZATION_HEADER = "Authorization"
|
|
34
|
+
TOKEN_KEY = "token"
|
|
35
|
+
INSTALLATION_TOKEN_ENDPOINT_TEMPLATE = "/app/installations/{installation_id}/access_tokens"
|
|
36
|
+
TOKEN_RESOURCE_NAME = "installation token"
|
|
37
|
+
'''
|
|
38
|
+
|
|
39
|
+
HEADER_SOURCE_FIXTURE = '''
|
|
40
|
+
AUTHORIZATION_HEADER = "actualsecret"
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
ERROR_SOURCE_FIXTURE = '''
|
|
44
|
+
PASSWORD_ERROR = "my secret password"
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
UNSAFE_KEY_METADATA_SOURCE_FIXTURES = (
|
|
48
|
+
'''
|
|
49
|
+
secret_key = "secret"
|
|
50
|
+
''',
|
|
51
|
+
'''
|
|
52
|
+
password_key = "password"
|
|
53
|
+
''',
|
|
54
|
+
)
|
|
55
|
+
|
|
32
56
|
GOOD_PARAMETERIZED_SQL = '''
|
|
33
57
|
def get_user(user_id):
|
|
34
58
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
@@ -76,6 +100,39 @@ class TestHardcodedSecrets:
|
|
|
76
100
|
violations = check_hardcoded_secrets(tree, "test.py")
|
|
77
101
|
assert len(violations) == 1
|
|
78
102
|
|
|
103
|
+
def test_secret_metadata_passes(self) -> None:
|
|
104
|
+
tree = ast.parse(METADATA_SOURCE_FIXTURE)
|
|
105
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
106
|
+
assert violations == []
|
|
107
|
+
|
|
108
|
+
def test_authorization_header_value_fails(self) -> None:
|
|
109
|
+
tree = ast.parse(HEADER_SOURCE_FIXTURE)
|
|
110
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
111
|
+
assert len(violations) == 1
|
|
112
|
+
|
|
113
|
+
def test_password_error_fails(self) -> None:
|
|
114
|
+
tree = ast.parse(ERROR_SOURCE_FIXTURE)
|
|
115
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
116
|
+
assert len(violations) == 1
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize("source_fixture", UNSAFE_KEY_METADATA_SOURCE_FIXTURES)
|
|
119
|
+
def test_sensitive_key_values_fail(self, source_fixture: str) -> None:
|
|
120
|
+
tree = ast.parse(source_fixture)
|
|
121
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
122
|
+
assert len(violations) == 1
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize(
|
|
125
|
+
"endpoint",
|
|
126
|
+
(
|
|
127
|
+
"/tokens/{token_id}?access_token=actualsecret",
|
|
128
|
+
"/tokens/{token_id}#actualsecret",
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
def test_token_endpoint_credentials_fail(self, endpoint: str) -> None:
|
|
132
|
+
tree = ast.parse(f'TOKEN_ENDPOINT_TEMPLATE = "{endpoint}"')
|
|
133
|
+
violations = check_hardcoded_secrets(tree, "test.py")
|
|
134
|
+
assert len(violations) == 1
|
|
135
|
+
|
|
79
136
|
|
|
80
137
|
class TestSqlInjection:
|
|
81
138
|
def test_parameterized_query_passes(self) -> None:
|
package/package.json
CHANGED
|
@@ -5,3 +5,5 @@ At a material fork — two or more viable paths that change the deliverable's sc
|
|
|
5
5
|
How to ask: plain language, one option per path with its tradeoff in the description, recommended path listed first and flagged "(Recommended)", hold edits to the forked area until the answer arrives.
|
|
6
6
|
|
|
7
7
|
Not a fork (just proceed): trivially reversible internal-only choices, anything the codebase / stated goal / an existing rule already determines (see `verify-before-asking`), or a single viable path.
|
|
8
|
+
|
|
9
|
+
When an option you offered names a repair and the answer leaves it undone, write it down before the turn moves on: a todo entry, a `spawn_task` chip, or a filed issue. An answer of "no preference" settles which path you take now. It leaves the repair you found still waiting.
|
package/rules/git-workflow.md
CHANGED
|
@@ -17,6 +17,18 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small
|
|
|
17
17
|
|
|
18
18
|
**ALWAYS create PRs as DRAFT:** Use `gh pr create --draft` for ALL PRs
|
|
19
19
|
|
|
20
|
+
**A release bot's PR body is machine input. Leave it alone.** Release automation reads
|
|
21
|
+
back the body of its own merged pull request to decide it owns that merge. Rewriting the
|
|
22
|
+
body, or trimming its header or footer, makes the bot treat the merge as somebody else's
|
|
23
|
+
work: it cuts no tag, the publish job skips, and it opens one more release pull request on
|
|
24
|
+
the next run. The merge stays in the repository. No tag is cut and the package never publishes.
|
|
25
|
+
|
|
26
|
+
Spot one by its head branch, which starts `release-please--branches--`, or by a body that
|
|
27
|
+
opens with the bot's own marker line. The description rules in this file, the
|
|
28
|
+
`pr-description-writer` agent, and the house wording style all step aside for it. The
|
|
29
|
+
failure signature in the release job log reads
|
|
30
|
+
`could not parse pull request body as a release PR`.
|
|
31
|
+
|
|
20
32
|
Use the `pr-description-writer` agent before creating a pull request or
|
|
21
33
|
rewriting its full description. Publish its title and body file through
|
|
22
34
|
`.agents/skills/pull-request/scripts/pull_request.py`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Pstack host mapping
|
|
2
|
+
|
|
3
|
+
Pstack skills ship with one host's tool names and paths written into their steps. The
|
|
4
|
+
active host supplies the real ones. Read a pstack step for the shape of the work, and take
|
|
5
|
+
the tool name, the skill home, and the transcript location from the host you run on.
|
|
6
|
+
|
|
7
|
+
[`pstack-models.md`](pstack-models.md) covers the model choice for each role. This rule
|
|
8
|
+
covers everything else a pstack step names.
|
|
9
|
+
|
|
10
|
+
## What each host supplies
|
|
11
|
+
|
|
12
|
+
| A pstack step names | Take from the host |
|
|
13
|
+
|---|---|
|
|
14
|
+
| A subagent tool | The native subagent tool this session exposes, with its own agent-type field |
|
|
15
|
+
| A skills directory | The skills home this host reads |
|
|
16
|
+
| A transcript directory | The session transcript location this host writes |
|
|
17
|
+
| A model identifier | The identifiers the native subagent tool accepts today |
|
|
18
|
+
|
|
19
|
+
A pstack step that names one host's directory means "the matching directory here". Resolve
|
|
20
|
+
it before the search, and search only inside the active workspace.
|
|
21
|
+
|
|
22
|
+
## The final message is the whole report
|
|
23
|
+
|
|
24
|
+
On a host where a subagent returns one message to its parent, that message is all the
|
|
25
|
+
parent receives. Working notes, tool output, and text the child printed along the way do
|
|
26
|
+
not travel.
|
|
27
|
+
|
|
28
|
+
Every pstack prompt that asks a child for a list, a set of findings, or a verdict also
|
|
29
|
+
asks for the whole thing, word for word, in the final message. A prompt without that line
|
|
30
|
+
gets back a summary such as "findings reported above", and the findings are gone. The
|
|
31
|
+
parent then reruns the child, which costs the full run a second time.
|
|
32
|
+
|
|
33
|
+
## Panel spawns
|
|
34
|
+
|
|
35
|
+
Panel size and model diversity follow `pstack-models.md`. Two host limits shape a spawn
|
|
36
|
+
beyond that:
|
|
37
|
+
|
|
38
|
+
- A spawn gate can refuse a model tier. Read the denial, take the tier it names as
|
|
39
|
+
allowed, and spawn again.
|
|
40
|
+
- A host may accept fewer distinct models than a panel asks for. A panel that requires
|
|
41
|
+
distinct models stops; a panel that treats diversity as optional runs with repeats.
|
|
42
|
+
|
|
43
|
+
## Editing pstack
|
|
44
|
+
|
|
45
|
+
Pstack installs as a plugin. A plugin update overwrites its files, so an edit inside a
|
|
46
|
+
pstack skill lasts until the next update. Durable changes belong in a rule beside this
|
|
47
|
+
one. The installer copies this package's rules into the managed rules directory and
|
|
48
|
+
generates the Cursor copy from the same source.
|
package/rules/vault-context.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
# Obsidian
|
|
1
|
+
# Obsidian vault context
|
|
2
2
|
|
|
3
|
-
An Obsidian vault holds session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects
|
|
3
|
+
An Obsidian vault holds session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects.
|
|
4
4
|
|
|
5
|
-
Before
|
|
6
|
-
|
|
7
|
-
At the end of a substantive session, offer the `session-log` skill; it owns the report format, the `vault_context_retrieved` and `session_id` frontmatter, and the closing `/rename` hand-off.
|
|
5
|
+
Before project work, search the vault with the obsidian MCP tools. Search by `project` frontmatter first, then by keyword. Search again when a component has known history, or when a task might repeat or reverse a prior decision.
|
|
@@ -8,6 +8,10 @@ A verdict that a component is fine or not the cause rests on live evidence gathe
|
|
|
8
8
|
|
|
9
9
|
Gather the probe before you write the verdict. When the probe contradicts the code (the code looks right but the port refuses the connection), report the live result and treat the component as suspect.
|
|
10
10
|
|
|
11
|
+
A status field is a report, not the effect. An exit code, a green pipeline run, and a task result all say the work finished. They do not say the work happened. Read the thing the work was meant to make.
|
|
12
|
+
|
|
13
|
+
Some evidence lasts only a moment. When a user shows you a failure, read the source they already hold: the terminal itself, and any log file the error names. A fresh probe minutes later measures a different moment, and a system that healed in between hides the failure you were asked about.
|
|
14
|
+
|
|
11
15
|
## Grounding checklist
|
|
12
16
|
|
|
13
17
|
Before stating a runtime claim, gather the matching live signal:
|
|
@@ -20,6 +24,9 @@ Before stating a runtime claim, gather the matching live signal:
|
|
|
20
24
|
| The process is running | List processes and match the name or PID. |
|
|
21
25
|
| The change took effect | Drive the flow and watch the new behavior. |
|
|
22
26
|
| The dependency is reachable | Send one real request and read the response. |
|
|
27
|
+
| The release or deploy shipped | Read what it makes: the tag, the published version, the file on disk. |
|
|
28
|
+
| The pipeline did the work | Read each job's own result. A run reports success while a job inside it is skipped. |
|
|
29
|
+
| The automation works | Drive the branch that matters. A green run of the do-nothing branch proves nothing. |
|
|
23
30
|
|
|
24
31
|
Only after a live signal backs the claim do you state it.
|
|
25
32
|
|
|
@@ -11,6 +11,7 @@ function parseArguments(argv) {
|
|
|
11
11
|
homeDirectory: null,
|
|
12
12
|
includeInstalled: false,
|
|
13
13
|
outputPath: null,
|
|
14
|
+
requireNonblocking: false,
|
|
14
15
|
repositoryRoot: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."),
|
|
15
16
|
};
|
|
16
17
|
for (let offset = 0; offset < argv.length; offset += 1) {
|
|
@@ -20,6 +21,7 @@ function parseArguments(argv) {
|
|
|
20
21
|
else if (argument === "--format") options.format = argv[++offset];
|
|
21
22
|
else if (argument === "--home") options.homeDirectory = argv[++offset];
|
|
22
23
|
else if (argument === "--output") options.outputPath = argv[++offset];
|
|
24
|
+
else if (argument === "--require-nonblocking") options.requireNonblocking = true;
|
|
23
25
|
else if (argument === "--repository-root") options.repositoryRoot = argv[++offset];
|
|
24
26
|
else throw new Error(`unknown argument ${argument}`);
|
|
25
27
|
}
|
|
@@ -37,6 +39,9 @@ function renderText(report) {
|
|
|
37
39
|
`installed Claude direct: ${summary.claudeDirectCount}`,
|
|
38
40
|
`installed Codex direct: ${summary.codexDirectCount}`,
|
|
39
41
|
`active native Git hooks: ${summary.gitHookCount}`,
|
|
42
|
+
`known native Git hooks: ${summary.knownGitHookCount}`,
|
|
43
|
+
`registration eligibility: ${report.registrationEligibility.status}`,
|
|
44
|
+
"runtime proof: separate verification required",
|
|
40
45
|
`findings: ${summary.findingCount}`,
|
|
41
46
|
].join("\n");
|
|
42
47
|
}
|
package/scripts/audit-hooks.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { KNOWN_GIT_HOOK_NAMES } from "../bin/git_hooks_installer.mjs";
|
|
7
|
+
|
|
7
8
|
const dispatcherSpecs = Object.freeze([
|
|
8
9
|
["script:blocking/pre_tool_use_dispatcher.py", "hooks_constants/pre_tool_use_dispatcher_constants.py", "ALL_HOSTED_HOOK_ENTRIES"],
|
|
9
10
|
["script:blocking/bash_pre_tool_use_dispatcher.py", "hooks_constants/bash_pre_tool_use_dispatcher_constants.py", "ALL_BASH_HOSTED_HOOK_ENTRIES"],
|
|
@@ -19,6 +20,8 @@ const lifecycleNames = new Set([
|
|
|
19
20
|
"move_to_explicit_command",
|
|
20
21
|
"move_to_linter",
|
|
21
22
|
]);
|
|
23
|
+
const nonblockingLifecycle = "keep_nonblocking_automation";
|
|
24
|
+
const blockingGitEvents = new Set(["pre-commit", "pre-push"]);
|
|
22
25
|
|
|
23
26
|
function normalizeMatcher(rawMatcher) {
|
|
24
27
|
return typeof rawMatcher === "string" && rawMatcher
|
|
@@ -269,6 +272,55 @@ function lifecycleFindings(records, lifecycle) {
|
|
|
269
272
|
];
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
function nonblockingFindings(records, lifecycle) {
|
|
276
|
+
const lifecycleByTarget = new Map(
|
|
277
|
+
lifecycle.map(({ target, lifecycle: lifecycleName }) => [target, lifecycleName]),
|
|
278
|
+
);
|
|
279
|
+
const uniqueTargets = [...new Set(records.map(({ target }) => target))];
|
|
280
|
+
const lifecycleMismatches = uniqueTargets
|
|
281
|
+
.filter((target) => !target.startsWith("git:") && lifecycleByTarget.has(target))
|
|
282
|
+
.filter((target) => lifecycleByTarget.get(target) !== nonblockingLifecycle)
|
|
283
|
+
.map((target) => ({
|
|
284
|
+
code: "NONBLOCKING_LIFECYCLE_MISMATCH",
|
|
285
|
+
message: "The registration is not eligible for nonblocking automation.",
|
|
286
|
+
scope: "catalog",
|
|
287
|
+
severity: "error",
|
|
288
|
+
target,
|
|
289
|
+
}));
|
|
290
|
+
const blockingGitFindings = records
|
|
291
|
+
.filter(({ event, scope }) => scope === "git" && blockingGitEvents.has(event))
|
|
292
|
+
.map(({ target }) => ({
|
|
293
|
+
code: "NATIVE_GIT_BLOCKING_HOOK",
|
|
294
|
+
message: "A native Git blocking hook remains active.",
|
|
295
|
+
scope: "git",
|
|
296
|
+
severity: "error",
|
|
297
|
+
target,
|
|
298
|
+
}));
|
|
299
|
+
return [...lifecycleMismatches, ...blockingGitFindings];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function registrationEligibility(records, lifecycle, isRequired) {
|
|
303
|
+
if (!isRequired) {
|
|
304
|
+
return { status: "not_checked", runtimeProof: "not_assessed" };
|
|
305
|
+
}
|
|
306
|
+
const lifecycleByTarget = new Map(
|
|
307
|
+
lifecycle.map(({ target, lifecycle: lifecycleName }) => [target, lifecycleName]),
|
|
308
|
+
);
|
|
309
|
+
const uniqueTargets = [...new Set(records.map(({ target }) => target))];
|
|
310
|
+
const hasUnclassifiedTarget = uniqueTargets.some((target) => !lifecycleByTarget.has(target));
|
|
311
|
+
const hasIneligibleTarget = uniqueTargets.some(
|
|
312
|
+
(target) => lifecycleByTarget.get(target) && lifecycleByTarget.get(target) !== nonblockingLifecycle,
|
|
313
|
+
);
|
|
314
|
+
const hasBlockingGitHook = records.some(
|
|
315
|
+
({ event, scope }) => scope === "git" && blockingGitEvents.has(event),
|
|
316
|
+
);
|
|
317
|
+
const isEligible = !hasUnclassifiedTarget && !hasIneligibleTarget && !hasBlockingGitHook;
|
|
318
|
+
return {
|
|
319
|
+
status: isEligible ? "eligible" : "ineligible",
|
|
320
|
+
runtimeProof: "not_assessed",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
272
324
|
function readInstalled(configPath, hooksRoot, scope) {
|
|
273
325
|
if (!existsSync(configPath)) return { findings: [], logical: [], records: [] };
|
|
274
326
|
const records = readDirect(configPath, scope);
|
|
@@ -295,7 +347,7 @@ function activeGitHooks(repositoryRoot) {
|
|
|
295
347
|
const hooksPath = gitQuery.stdout?.trim();
|
|
296
348
|
if (!hooksPath) return [];
|
|
297
349
|
const hooksRoot = path.isAbsolute(hooksPath) ? hooksPath : path.resolve(repositoryRoot, hooksPath);
|
|
298
|
-
return
|
|
350
|
+
return KNOWN_GIT_HOOK_NAMES
|
|
299
351
|
.filter((name) => existsSync(path.join(hooksRoot, name)))
|
|
300
352
|
.map((name, ordinal) => ({
|
|
301
353
|
event: name,
|
|
@@ -308,17 +360,16 @@ function activeGitHooks(repositoryRoot) {
|
|
|
308
360
|
}));
|
|
309
361
|
}
|
|
310
362
|
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}));
|
|
363
|
+
function knownGitHooks() {
|
|
364
|
+
return KNOWN_GIT_HOOK_NAMES.map((event, ordinal) => ({
|
|
365
|
+
event,
|
|
366
|
+
matcher: "*",
|
|
367
|
+
ordinal,
|
|
368
|
+
role: "native",
|
|
369
|
+
scope: "git",
|
|
370
|
+
target: `git:${event}`,
|
|
371
|
+
timeoutSeconds: null,
|
|
372
|
+
}));
|
|
322
373
|
}
|
|
323
374
|
|
|
324
375
|
export function auditHooks(options) {
|
|
@@ -327,16 +378,18 @@ export function auditHooks(options) {
|
|
|
327
378
|
const direct = readDirect(path.join(hooksRoot, "hooks.json"), "canonical");
|
|
328
379
|
const hosted = expandHosted(direct, hooksRoot);
|
|
329
380
|
const logical = logicalRecords(direct, hosted);
|
|
381
|
+
const knownGit = knownGitHooks();
|
|
330
382
|
const lifecycle = readLifecycle(options.catalogPath);
|
|
331
383
|
const dispatcherCount = direct.filter(({ role }) => role === "dispatcher").length;
|
|
332
384
|
const installed = options.includeInstalled
|
|
333
385
|
? installedAudit(options.homeDirectory ?? homedir(), repositoryRoot, logical)
|
|
334
386
|
: { claudeCount: 0, codexCount: 0, findings: [], git: [], logical: [], records: [] };
|
|
335
387
|
const allLogical = [...logical, ...installed.logical, ...installed.git];
|
|
388
|
+
const isNonblockingRequired = options.requireNonblocking === true;
|
|
336
389
|
const findings = [
|
|
337
|
-
...countFindings(direct.length, dispatcherCount, hosted.length),
|
|
338
390
|
...duplicateFindings(logical, "canonical"),
|
|
339
|
-
...(options.catalogPath ? lifecycleFindings(allLogical, lifecycle) : []),
|
|
391
|
+
...(options.catalogPath || isNonblockingRequired ? lifecycleFindings(allLogical, lifecycle) : []),
|
|
392
|
+
...(isNonblockingRequired ? nonblockingFindings(allLogical, lifecycle) : []),
|
|
340
393
|
...installed.findings,
|
|
341
394
|
].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
342
395
|
const targets = new Set(allLogical.map(({ target }) => target));
|
|
@@ -348,6 +401,7 @@ export function auditHooks(options) {
|
|
|
348
401
|
installedRegistrations: installed.records.sort(recordSort),
|
|
349
402
|
installedLogicalRegistrations: installed.logical.sort(recordSort),
|
|
350
403
|
gitRegistrations: installed.git,
|
|
404
|
+
knownGitRegistrations: knownGit,
|
|
351
405
|
lifecycle,
|
|
352
406
|
logicalRegistrations: logical,
|
|
353
407
|
schemaVersion: 1,
|
|
@@ -359,13 +413,15 @@ export function auditHooks(options) {
|
|
|
359
413
|
effectiveCount: hosted.length,
|
|
360
414
|
findingCount: findings.length,
|
|
361
415
|
gitHookCount: installed.git.length,
|
|
416
|
+
knownGitHookCount: knownGit.length,
|
|
362
417
|
lifecycleCount: lifecycle.length,
|
|
363
418
|
logicalAssociationCount: logical.length,
|
|
364
|
-
unclassifiedCount: options.catalogPath
|
|
419
|
+
unclassifiedCount: options.catalogPath || isNonblockingRequired
|
|
365
420
|
? [...targets].filter((target) => !classified.has(target)).length
|
|
366
421
|
: 0,
|
|
367
422
|
uniqueTargetCount: targets.size,
|
|
368
423
|
},
|
|
424
|
+
registrationEligibility: registrationEligibility(allLogical, lifecycle, isNonblockingRequired),
|
|
369
425
|
};
|
|
370
426
|
}
|
|
371
427
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Automatic local verification for explicitly registered pull requests."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .config.constants import (
|
|
7
|
+
ALL_GIT_HEAD_ARGUMENTS,
|
|
8
|
+
ALL_GIT_STATUS_ARGUMENTS,
|
|
9
|
+
GIT_BASE_REF_TEMPLATE,
|
|
10
|
+
GIT_EXECUTABLE,
|
|
11
|
+
)
|
|
12
|
+
from .model import AdvisoryRegistration, LocalCheckout
|
|
13
|
+
from .window_flags import hidden_window_creation_flags
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_local_checkout(checkout_path: Path) -> LocalCheckout:
|
|
17
|
+
"""Read the current commit and clean status for one checkout.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
checkout_path: Git working tree to inspect.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Commit and clean status, with no commit when Git cannot inspect it.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
head_process = _run_git_command(ALL_GIT_HEAD_ARGUMENTS, checkout_path)
|
|
27
|
+
status_process = _run_git_command(ALL_GIT_STATUS_ARGUMENTS, checkout_path)
|
|
28
|
+
except OSError:
|
|
29
|
+
return LocalCheckout(None, False)
|
|
30
|
+
head_sha = head_process.stdout.strip() if head_process.returncode == 0 else None
|
|
31
|
+
return LocalCheckout(
|
|
32
|
+
head_sha,
|
|
33
|
+
status_process.returncode == 0 and not status_process.stdout,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run_git_command(
|
|
38
|
+
all_arguments: tuple[str, ...], checkout_path: Path
|
|
39
|
+
) -> subprocess.CompletedProcess[str]:
|
|
40
|
+
return subprocess.run(
|
|
41
|
+
(GIT_EXECUTABLE, *all_arguments),
|
|
42
|
+
cwd=checkout_path,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
check=False,
|
|
45
|
+
text=True,
|
|
46
|
+
creationflags=hidden_window_creation_flags(),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dedicated_base_ref(registration: AdvisoryRegistration) -> str:
|
|
51
|
+
return GIT_BASE_REF_TEMPLATE.format(pull_request=registration.pull_request_number)
|