claude-dev-env 1.82.0 → 1.84.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/CLAUDE.md +16 -13
- package/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -4
- package/hooks/blocking/code_rules_constants_config.py +164 -1
- package/hooks/blocking/code_rules_docstrings.py +428 -15
- package/hooks/blocking/code_rules_enforcer.py +33 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +152 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -4
- package/hooks/hooks_constants/blocking_check_limits.py +46 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +5 -2
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +40 -1
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +7 -2
- package/skills/autoconverge/SKILL.md +48 -61
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +167 -106
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/everything-search/SKILL.md +5 -0
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# Audit contract
|
|
2
|
-
|
|
3
|
-
Shared output schema and audit-loop contract used by `/bugteam`, `/qbug`, `/findbugs`, and `/fixbugs`. Changing a shape here is a breaking change for every consuming skill.
|
|
4
|
-
|
|
5
|
-
## Contents
|
|
6
|
-
|
|
7
|
-
- Finding schema (Shape A, Shape B)
|
|
8
|
-
- Adversarial second pass
|
|
9
|
-
- Haiku secondary auditor
|
|
10
|
-
- Post-fix self-audit
|
|
11
|
-
- Persistence (loop-<L>-audit.json, loop-<L>-diagnostics.json)
|
|
12
|
-
|
|
13
|
-
## Finding schema
|
|
14
|
-
|
|
15
|
-
Each finding an audit produces MUST be one of exactly two shapes.
|
|
16
|
-
|
|
17
|
-
### Shape A — structured finding
|
|
18
|
-
|
|
19
|
-
```json
|
|
20
|
-
{
|
|
21
|
-
"id": "loop<L>-<K>",
|
|
22
|
-
"file": "path/relative/to/repo/root.py",
|
|
23
|
-
"line": 123,
|
|
24
|
-
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
|
|
25
|
-
"severity": "P0 | P1 | P2",
|
|
26
|
-
"excerpt": "verbatim code snippet from the offending line(s)",
|
|
27
|
-
"failure_mode": "one sentence describing what goes wrong and when",
|
|
28
|
-
"evidence_files": ["additional/files/opened.py"]
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`id` is `loop<L>-<K>` where `L` is the loop counter (1-based) and `K` is the 1-based index within the loop. For `/findbugs` which runs once, use `find<K>`.
|
|
33
|
-
|
|
34
|
-
### Shape B — structured proof-of-absence
|
|
35
|
-
|
|
36
|
-
Used when an audit investigates a category and does NOT find a bug. Bare "verified clean" claims are REJECTED because they hide shallow reading.
|
|
37
|
-
|
|
38
|
-
```json
|
|
39
|
-
{
|
|
40
|
-
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
|
|
41
|
-
"files_opened": ["file1.py", "file2.py"],
|
|
42
|
-
"lines_quoted": [
|
|
43
|
-
{"file": "file1.py", "line": 88, "text": "verbatim line content"}
|
|
44
|
-
],
|
|
45
|
-
"adversarial_probes": [
|
|
46
|
-
"what failure mode was tested for and how it was ruled out"
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Every category an audit touches MUST have either at least one Shape A finding OR at least one Shape B proof-of-absence entry. A category with neither is a protocol violation.
|
|
52
|
-
|
|
53
|
-
### Example — Shape A
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"id": "loop1-1",
|
|
58
|
-
"file": "scripts/db/neon.py",
|
|
59
|
-
"line": 43,
|
|
60
|
-
"category": "C",
|
|
61
|
-
"severity": "P1",
|
|
62
|
-
"excerpt": "load_dotenv(env_path, override=False)",
|
|
63
|
-
"failure_mode": "Called on every connect() — repeats file I/O per connection in scripts that open multiple short-lived connections.",
|
|
64
|
-
"evidence_files": ["scripts/db/neon.py", "scripts/update_new_releases.py"]
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Example — Shape B
|
|
69
|
-
|
|
70
|
-
```json
|
|
71
|
-
{
|
|
72
|
-
"category": "H",
|
|
73
|
-
"files_opened": ["scripts/db/neon.py", "scripts/db/config.py"],
|
|
74
|
-
"lines_quoted": [
|
|
75
|
-
{"file": "scripts/db/neon.py", "line": 30, "text": "dsn = os.environ.get(\"DATABASE_URL\")"}
|
|
76
|
-
],
|
|
77
|
-
"adversarial_probes": [
|
|
78
|
-
"Checked whether DATABASE_URL is interpolated into a shell — it is passed to psycopg.connect() directly with no shell involvement.",
|
|
79
|
-
"Checked whether the env path is user-controlled — it is derived from a fixed Y: drive constant, not user input."
|
|
80
|
-
]
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Adversarial second pass
|
|
85
|
-
|
|
86
|
-
After the primary finding list is complete, every audit runs a second pass against itself with the prompt:
|
|
87
|
-
|
|
88
|
-
> Assume your first pass missed at least 3 P1 bugs. Where are they?
|
|
89
|
-
|
|
90
|
-
The audit must either produce new Shape A findings citing new file:line references not present in the first pass, or cite explicit Shape B adversarial-probe entries for each category it re-examined. An adversarial pass that returns "nothing new, confident first pass was complete" is REJECTED — produce evidence or findings, not confidence.
|
|
91
|
-
|
|
92
|
-
For `/bugteam`, the single audit agent provides per-category coverage by walking all A–P rubrics in one invocation.
|
|
93
|
-
|
|
94
|
-
## Merge rules
|
|
95
|
-
|
|
96
|
-
When the LEAD combines findings from multiple sources (primary auditor, Haiku secondary auditor, adversarial pass), it applies these rules:
|
|
97
|
-
|
|
98
|
-
- **De-dup key**: `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple are the same finding and collapse into one entry.
|
|
99
|
-
- **Severity conflict**: max wins (`P0 > P1 > P2`). When sources disagree on severity for the same de-dup key, the merged entry keeps the highest severity.
|
|
100
|
-
- **Unique-to-secondary findings**: added to the merged set with the secondary's severity and source annotation.
|
|
101
|
-
- **Unique-to-primary findings**: kept as-is.
|
|
102
|
-
- **Zero secondary findings**: primary set trusted; proceed.
|
|
103
|
-
- **Malformed or non-parseable secondary output**: lead trusts the primary set and logs the event in `loop-<L>-diagnostics.json` under `haiku_findings` as `[{"parse_error": "<message>"}]`.
|
|
104
|
-
|
|
105
|
-
## Post-fix self-audit
|
|
106
|
-
|
|
107
|
-
Audit-and-fix skills (`/qbug`, `/bugteam`) MUST re-audit modified files between `py_compile` and `git add`. This catches fix-induced regressions in the same loop that introduced them rather than on loop N+1.
|
|
108
|
-
|
|
109
|
-
Sequence:
|
|
110
|
-
|
|
111
|
-
1. Capture pre-fix file contents for every file this FIX will touch.
|
|
112
|
-
2. Apply edits.
|
|
113
|
-
3. Run `py_compile` (or language-equivalent) on each modified file.
|
|
114
|
-
4. Compute `fix_diff` against pre-fix contents for the modified set.
|
|
115
|
-
5. Run `bugteam_code_rules_gate.py` with explicit paths for every modified file.
|
|
116
|
-
6. Spawn a scoped audit of `fix_diff` with full A–P rigor, Shape A/B contract, adversarial pass, AND Haiku secondary in parallel (paranoid mode on post-fix).
|
|
117
|
-
7. Any new findings become same-loop fix-targets. Internal iteration count increments by one.
|
|
118
|
-
8. After 3 internal iterations with fresh findings each time, exit `stuck: post-fix audit not converging`.
|
|
119
|
-
9. Only when `gate_findings` empty AND `post_fix_findings` empty: `git add`, commit, push.
|
|
120
|
-
|
|
121
|
-
`converged` exit condition: `primary_audit_clean AND post_fix_audit_clean` for the committing loop.
|
|
122
|
-
|
|
123
|
-
## De-dup and merge
|
|
124
|
-
|
|
125
|
-
Findings from primary, adversarial, Haiku secondary, and post-fix passes are merged into a single deduped finding list before persistence.
|
|
126
|
-
|
|
127
|
-
- **De-dup key:** `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple collapse into a single deduped entry.
|
|
128
|
-
- **Severity conflict resolution:** `max wins`. When merged findings disagree on severity, the deduped entry carries the maximum severity (`P0 > P1 > P2`).
|
|
129
|
-
- **Excerpt and failure_mode:** the deduped entry inherits these fields from the highest-severity contributing finding. Ties keep the first observed contributor.
|
|
130
|
-
- **`evidence_files`:** the deduped entry carries the union of every contributor's `evidence_files`, deduplicated and sorted.
|
|
131
|
-
|
|
132
|
-
The merged list lands in `loop-<L>-diagnostics.json` under both `merged` (one entry per contributing finding) and `deduped` (one entry per unique `(file, line, category)` tuple).
|
|
133
|
-
|
|
134
|
-
## Persistence
|
|
135
|
-
|
|
136
|
-
Every audit loop writes two JSON files under the skill's scoped temp directory (resolved via `tempfile.gettempdir()`):
|
|
137
|
-
|
|
138
|
-
### `loop-<L>-audit.json`
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{
|
|
142
|
-
"findings": [],
|
|
143
|
-
"proof_of_absence": [],
|
|
144
|
-
"source": "primary | haiku | adversarial | merged"
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### `loop-<L>-diagnostics.json`
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{
|
|
152
|
-
"loop": 1,
|
|
153
|
-
"gate_findings": [],
|
|
154
|
-
"primary_findings": [],
|
|
155
|
-
"adversarial_findings": [],
|
|
156
|
-
"haiku_findings": [],
|
|
157
|
-
"post_fix_findings": [],
|
|
158
|
-
"merged": [],
|
|
159
|
-
"deduped": []
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
All eight keys MUST be present. Missing keys break convergence debugging.
|
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
"""Shared helpers for grant_project_claude_permissions and revoke_project_claude_permissions.
|
|
2
|
-
|
|
3
|
-
Writes to ~/.claude/settings.json are atomic and permission-preserving: the
|
|
4
|
-
target file's existing POSIX mode is captured, a sibling temp file is
|
|
5
|
-
created via os.open with O_CREAT | O_EXCL and the preserved mode, content
|
|
6
|
-
is written, then os.replace swaps it into place. Output is serialized with
|
|
7
|
-
sort_keys=True for a stable on-disk layout; the first run on a hand-ordered
|
|
8
|
-
settings file produces a one-time re-sort diff, subsequent writes are stable.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import stat
|
|
14
|
-
import sys
|
|
15
|
-
from collections.abc import Callable
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import NoReturn
|
|
18
|
-
|
|
19
|
-
from bugteam_scripts_constants.claude_permissions_common_constants import (
|
|
20
|
-
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
|
|
21
|
-
ATOMIC_WRITE_TEMPORARY_SUFFIX,
|
|
22
|
-
DEFAULT_SETTINGS_FILE_MODE,
|
|
23
|
-
TEXT_FILE_ENCODING,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def exit_with_error(message: str) -> NoReturn:
|
|
28
|
-
print(f"Error: {message}", file=sys.stderr)
|
|
29
|
-
raise SystemExit(1)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def path_contains_glob_metacharacters(candidate_path: str) -> bool:
|
|
33
|
-
"""Check whether a path contains characters reserved for glob patterns.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
candidate_path: The file path string to inspect.
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
True when any glob metacharacter is present in the path.
|
|
40
|
-
"""
|
|
41
|
-
glob_metacharacters_in_path: tuple[str, ...] = (
|
|
42
|
-
"*",
|
|
43
|
-
"?",
|
|
44
|
-
"[",
|
|
45
|
-
"]",
|
|
46
|
-
"(",
|
|
47
|
-
")",
|
|
48
|
-
"{",
|
|
49
|
-
"}",
|
|
50
|
-
",",
|
|
51
|
-
)
|
|
52
|
-
return any(
|
|
53
|
-
each_character in candidate_path
|
|
54
|
-
for each_character in glob_metacharacters_in_path
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def get_current_project_path() -> str:
|
|
59
|
-
"""Return the normalized current working directory path.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
The cwd as a POSIX-style path string.
|
|
63
|
-
|
|
64
|
-
Raises:
|
|
65
|
-
ValueError: When the path contains glob metacharacters.
|
|
66
|
-
"""
|
|
67
|
-
normalized_project_path = str(Path.cwd()).replace("\\", "/")
|
|
68
|
-
if path_contains_glob_metacharacters(normalized_project_path):
|
|
69
|
-
raise ValueError(
|
|
70
|
-
f"Current directory path contains glob metacharacters and cannot "
|
|
71
|
-
f"be used to build permission rules safely: {normalized_project_path}"
|
|
72
|
-
)
|
|
73
|
-
return normalized_project_path
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def build_permission_rule(tool_name: str, project_path: str) -> str:
|
|
77
|
-
return f"{tool_name}({project_path}/.claude/**)"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def build_permission_rules(
|
|
81
|
-
project_path: str, all_permission_allow_tools: tuple[str, ...]
|
|
82
|
-
) -> list[str]:
|
|
83
|
-
"""Construct permission rule strings for each tool.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
project_path: The POSIX-style project root path.
|
|
87
|
-
all_permission_allow_tools: Tool names to build rules for.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
List of permission rule strings for the given project path.
|
|
91
|
-
"""
|
|
92
|
-
return [
|
|
93
|
-
build_permission_rule(each_tool, project_path)
|
|
94
|
-
for each_tool in all_permission_allow_tools
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def build_agent_config_deny_rule(
|
|
99
|
-
tool_name: str, project_path: str, agent_config_path_pattern: str
|
|
100
|
-
) -> str:
|
|
101
|
-
"""Construct a deny rule for a single agent-config path pattern.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
tool_name: The permission tool name (e.g., "Edit", "Write", "Read").
|
|
105
|
-
project_path: The POSIX-style project root path.
|
|
106
|
-
agent_config_path_pattern: The agent-config path pattern under .claude/.
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
The deny rule string Claude Code matches against tool invocations.
|
|
110
|
-
"""
|
|
111
|
-
return f"{tool_name}({project_path}/.claude/{agent_config_path_pattern})"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def build_agent_config_deny_rules(
|
|
115
|
-
project_path: str,
|
|
116
|
-
all_permission_allow_tools: tuple[str, ...],
|
|
117
|
-
all_agent_config_path_patterns: tuple[str, ...],
|
|
118
|
-
) -> list[str]:
|
|
119
|
-
"""Construct deny rules covering every tool and pattern pair.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
project_path: The POSIX-style project root path.
|
|
123
|
-
all_permission_allow_tools: Tool names to build deny rules for.
|
|
124
|
-
all_agent_config_path_patterns: Agent-config path patterns to deny under .claude/.
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
List of deny rule strings, one per tool/pattern combination.
|
|
128
|
-
"""
|
|
129
|
-
return [
|
|
130
|
-
build_agent_config_deny_rule(each_tool, project_path, each_pattern)
|
|
131
|
-
for each_tool in all_permission_allow_tools
|
|
132
|
-
for each_pattern in all_agent_config_path_patterns
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _is_project_path_token_at_word_boundary(
|
|
137
|
-
body_after_prefix: str, token_position: int
|
|
138
|
-
) -> bool:
|
|
139
|
-
if token_position == 0:
|
|
140
|
-
return True
|
|
141
|
-
preceding_character = body_after_prefix[token_position - 1]
|
|
142
|
-
if preceding_character.isspace():
|
|
143
|
-
return True
|
|
144
|
-
return preceding_character in ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def is_trust_entry_for_project(
|
|
148
|
-
candidate_entry: object, project_path: str, prefix: str
|
|
149
|
-
) -> bool:
|
|
150
|
-
"""Detect whether an autoMode.environment entry is a trust entry for the project.
|
|
151
|
-
|
|
152
|
-
The predicate matches any string entry whose prefix matches the trust-entry
|
|
153
|
-
marker and that contains the project's .claude/** path token anchored on a
|
|
154
|
-
non-path boundary (the start of the body after the prefix, a whitespace
|
|
155
|
-
character, or a quote character). The boundary anchor prevents
|
|
156
|
-
cross-project false positives where the current project's path is a path
|
|
157
|
-
suffix of an unrelated entry's path. The exact wording after the prefix is
|
|
158
|
-
allowed to vary between template revisions.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
candidate_entry: The autoMode.environment list value to inspect.
|
|
162
|
-
project_path: The POSIX-style project root path.
|
|
163
|
-
prefix: The literal prefix that marks a trust entry.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
True when the entry is a prior trust entry for this project.
|
|
167
|
-
"""
|
|
168
|
-
if not isinstance(candidate_entry, str):
|
|
169
|
-
return False
|
|
170
|
-
if not candidate_entry.startswith(prefix):
|
|
171
|
-
return False
|
|
172
|
-
project_path_token = f"{project_path}/.claude/**"
|
|
173
|
-
body_after_prefix = candidate_entry[len(prefix):]
|
|
174
|
-
token_position = body_after_prefix.find(project_path_token)
|
|
175
|
-
while token_position != -1:
|
|
176
|
-
if _is_project_path_token_at_word_boundary(body_after_prefix, token_position):
|
|
177
|
-
return True
|
|
178
|
-
next_search_start = token_position + 1
|
|
179
|
-
token_position = body_after_prefix.find(project_path_token, next_search_start)
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def remove_matching_entries_from_list(
|
|
184
|
-
all_target_list: list[object],
|
|
185
|
-
match_predicate: Callable[[object], bool],
|
|
186
|
-
) -> int:
|
|
187
|
-
"""Remove every entry from a list that satisfies the predicate.
|
|
188
|
-
|
|
189
|
-
Args:
|
|
190
|
-
all_target_list: The list to filter in place.
|
|
191
|
-
match_predicate: Function returning True for entries to remove.
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
Number of entries removed.
|
|
195
|
-
"""
|
|
196
|
-
original_length = len(all_target_list)
|
|
197
|
-
all_target_list[:] = [
|
|
198
|
-
each_value
|
|
199
|
-
for each_value in all_target_list
|
|
200
|
-
if not match_predicate(each_value)
|
|
201
|
-
]
|
|
202
|
-
return original_length - len(all_target_list)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def load_settings(settings_path: Path) -> dict[str, object]:
|
|
206
|
-
"""Read and parse a JSON settings file from disk.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
settings_path: Path to the JSON settings file.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Parsed settings dictionary. Returns an empty dict when the file
|
|
213
|
-
does not exist.
|
|
214
|
-
|
|
215
|
-
Raises:
|
|
216
|
-
SystemExit: When the file exists but is not valid JSON, or when
|
|
217
|
-
its root value is not a JSON object.
|
|
218
|
-
"""
|
|
219
|
-
if not settings_path.exists():
|
|
220
|
-
return {}
|
|
221
|
-
parsed_settings: dict[str, object] = {}
|
|
222
|
-
try:
|
|
223
|
-
raw_text = settings_path.read_text(encoding=TEXT_FILE_ENCODING)
|
|
224
|
-
except OSError as read_error:
|
|
225
|
-
exit_with_error(f"Failed to read {settings_path}: {read_error}")
|
|
226
|
-
try:
|
|
227
|
-
parsed_settings = json.loads(raw_text)
|
|
228
|
-
except json.JSONDecodeError as decode_error:
|
|
229
|
-
exit_with_error(
|
|
230
|
-
f"Refusing to modify {settings_path}: existing file is not valid JSON "
|
|
231
|
-
f"({decode_error}). Fix or back up the file manually, then re-run."
|
|
232
|
-
)
|
|
233
|
-
if not isinstance(parsed_settings, dict):
|
|
234
|
-
exit_with_error(
|
|
235
|
-
f"Refusing to modify {settings_path}: existing file's root is "
|
|
236
|
-
f"{type(parsed_settings).__name__}, not a JSON object. Fix or back up "
|
|
237
|
-
f"the file manually, then re-run."
|
|
238
|
-
)
|
|
239
|
-
return parsed_settings
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
|
|
243
|
-
"""Serialize a settings dictionary to JSON text with stable formatting.
|
|
244
|
-
|
|
245
|
-
Args:
|
|
246
|
-
all_settings: The settings dictionary to serialize.
|
|
247
|
-
|
|
248
|
-
Returns:
|
|
249
|
-
Pretty-printed JSON string with sorted keys.
|
|
250
|
-
"""
|
|
251
|
-
json_indent_width_columns: int = len(" ")
|
|
252
|
-
return json.dumps(
|
|
253
|
-
all_settings,
|
|
254
|
-
indent=json_indent_width_columns,
|
|
255
|
-
sort_keys=True,
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def get_mode_to_preserve(settings_path: Path) -> int:
|
|
260
|
-
"""Return the file permission bits from an existing settings file.
|
|
261
|
-
|
|
262
|
-
Args:
|
|
263
|
-
settings_path: Path to the target settings file to stat.
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
The permission bits from the file mode (lower portion).
|
|
267
|
-
Returns DEFAULT_SETTINGS_FILE_MODE when the file does not exist.
|
|
268
|
-
"""
|
|
269
|
-
try:
|
|
270
|
-
stat_result = os.stat(settings_path)
|
|
271
|
-
except FileNotFoundError:
|
|
272
|
-
return DEFAULT_SETTINGS_FILE_MODE
|
|
273
|
-
except OSError as stat_error:
|
|
274
|
-
exit_with_error(f"Failed to stat {settings_path}: {stat_error}")
|
|
275
|
-
return stat.S_IMODE(stat_result.st_mode)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def write_atomically_with_mode(
|
|
279
|
-
temporary_path: Path, serialized_content: str, file_mode: int
|
|
280
|
-
) -> None:
|
|
281
|
-
"""Create and write to a temporary file with the given mode.
|
|
282
|
-
|
|
283
|
-
Uses os.open with O_CREAT | O_EXCL to create the file securely,
|
|
284
|
-
then writes the serialized content. The caller is responsible for
|
|
285
|
-
replacing the target file with os.replace afterward.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
temporary_path: Path for the temporary file (sibling of target).
|
|
289
|
-
serialized_content: The content to write to the temporary file.
|
|
290
|
-
file_mode: Unix permission bits for the new file (e.g., 0o600).
|
|
291
|
-
|
|
292
|
-
Raises:
|
|
293
|
-
OSError: When os.open or os.fdopen fails. The raw file descriptor
|
|
294
|
-
is closed before re-raising so the FD does not leak.
|
|
295
|
-
MemoryError: When os.fdopen runs out of buffer memory; the FD is
|
|
296
|
-
closed before re-raising.
|
|
297
|
-
"""
|
|
298
|
-
file_descriptor = os.open(
|
|
299
|
-
str(temporary_path),
|
|
300
|
-
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
301
|
-
file_mode,
|
|
302
|
-
)
|
|
303
|
-
try:
|
|
304
|
-
writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
|
|
305
|
-
except (OSError, MemoryError):
|
|
306
|
-
os.close(file_descriptor)
|
|
307
|
-
try:
|
|
308
|
-
os.unlink(str(temporary_path))
|
|
309
|
-
except OSError:
|
|
310
|
-
pass
|
|
311
|
-
raise
|
|
312
|
-
with writer:
|
|
313
|
-
writer.write(serialized_content)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
|
|
317
|
-
"""Write settings to a JSON file atomically with permission preservation.
|
|
318
|
-
|
|
319
|
-
Creates a temporary sibling file, writes content, then atomically
|
|
320
|
-
replaces the target. Cleans up the temporary file in a finally block.
|
|
321
|
-
|
|
322
|
-
Args:
|
|
323
|
-
settings_path: Path to the target settings JSON file.
|
|
324
|
-
all_settings: The settings dictionary to serialize and save.
|
|
325
|
-
"""
|
|
326
|
-
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
327
|
-
serialized_settings = serialize_settings_to_json_text(all_settings)
|
|
328
|
-
process_keyed_temporary_suffix = (
|
|
329
|
-
f"{ATOMIC_WRITE_TEMPORARY_SUFFIX}.{os.getpid()}"
|
|
330
|
-
)
|
|
331
|
-
temporary_path = settings_path.with_suffix(
|
|
332
|
-
settings_path.suffix + process_keyed_temporary_suffix
|
|
333
|
-
)
|
|
334
|
-
mode_to_preserve = get_mode_to_preserve(settings_path)
|
|
335
|
-
is_temp_owned_by_this_invocation = False
|
|
336
|
-
try:
|
|
337
|
-
try:
|
|
338
|
-
write_atomically_with_mode(
|
|
339
|
-
temporary_path, serialized_settings, mode_to_preserve
|
|
340
|
-
)
|
|
341
|
-
is_temp_owned_by_this_invocation = True
|
|
342
|
-
os.replace(str(temporary_path), str(settings_path))
|
|
343
|
-
is_temp_owned_by_this_invocation = False
|
|
344
|
-
except OSError as os_error:
|
|
345
|
-
exit_with_error(
|
|
346
|
-
f"Failed to write settings atomically to {settings_path}: {os_error}"
|
|
347
|
-
)
|
|
348
|
-
finally:
|
|
349
|
-
if is_temp_owned_by_this_invocation and temporary_path.exists():
|
|
350
|
-
try:
|
|
351
|
-
temporary_path.unlink()
|
|
352
|
-
except OSError as unlink_error:
|
|
353
|
-
print(
|
|
354
|
-
f"Warning: could not remove temp file {temporary_path}: "
|
|
355
|
-
f"{type(unlink_error).__name__}: {unlink_error}",
|
|
356
|
-
file=sys.stderr,
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
|
|
361
|
-
"""Add a value to a list if it is not already present.
|
|
362
|
-
|
|
363
|
-
Args:
|
|
364
|
-
all_target_list: The list to potentially append to.
|
|
365
|
-
new_value: The string value to add if missing.
|
|
366
|
-
|
|
367
|
-
Returns:
|
|
368
|
-
True when the value was appended, False when it already existed.
|
|
369
|
-
"""
|
|
370
|
-
if new_value in all_target_list:
|
|
371
|
-
return False
|
|
372
|
-
all_target_list.append(new_value)
|
|
373
|
-
return True
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def ensure_dict_section(
|
|
377
|
-
all_settings: dict[str, object], section_name: str
|
|
378
|
-
) -> dict[str, object]:
|
|
379
|
-
"""Return an existing dict section or create an empty one if absent.
|
|
380
|
-
|
|
381
|
-
A missing key and an explicit JSON null are treated identically: both
|
|
382
|
-
produce a fresh empty dict stored back into settings. Any other non-dict
|
|
383
|
-
value (string, list, number, bool) calls exit_with_error to avoid
|
|
384
|
-
overwriting user data.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
all_settings: The parsed settings dictionary.
|
|
388
|
-
section_name: Key name of the section to retrieve or create.
|
|
389
|
-
|
|
390
|
-
Returns:
|
|
391
|
-
The existing or newly created section dictionary.
|
|
392
|
-
"""
|
|
393
|
-
existing_section = all_settings.get(section_name)
|
|
394
|
-
if existing_section is None:
|
|
395
|
-
replacement_section: dict[str, object] = {}
|
|
396
|
-
all_settings[section_name] = replacement_section
|
|
397
|
-
return replacement_section
|
|
398
|
-
if not isinstance(existing_section, dict):
|
|
399
|
-
exit_with_error(
|
|
400
|
-
f"Refusing to modify settings key {section_name!r}: existing value "
|
|
401
|
-
f"is {type(existing_section).__name__}, not a JSON object. Fix or "
|
|
402
|
-
f"remove the key manually, then re-run."
|
|
403
|
-
)
|
|
404
|
-
return existing_section
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def ensure_list_entry(
|
|
408
|
-
all_section: dict[str, object], entry_name: str
|
|
409
|
-
) -> list[object]:
|
|
410
|
-
"""Return an existing list entry or create an empty one if absent.
|
|
411
|
-
|
|
412
|
-
A missing key and an explicit JSON null are treated identically: both
|
|
413
|
-
produce a fresh empty list stored back into the section. Any other
|
|
414
|
-
non-list value (string, dict, number, bool) calls exit_with_error to
|
|
415
|
-
avoid overwriting user data.
|
|
416
|
-
|
|
417
|
-
Args:
|
|
418
|
-
all_section: The parent dictionary section.
|
|
419
|
-
entry_name: Key name of the list entry to retrieve or create.
|
|
420
|
-
|
|
421
|
-
Returns:
|
|
422
|
-
The existing or newly created list entry.
|
|
423
|
-
"""
|
|
424
|
-
existing_entry = all_section.get(entry_name)
|
|
425
|
-
if existing_entry is None:
|
|
426
|
-
replacement_entry: list[object] = []
|
|
427
|
-
all_section[entry_name] = replacement_entry
|
|
428
|
-
return replacement_entry
|
|
429
|
-
if not isinstance(existing_entry, list):
|
|
430
|
-
exit_with_error(
|
|
431
|
-
f"Refusing to modify settings entry {entry_name!r}: existing value "
|
|
432
|
-
f"is {type(existing_entry).__name__}, not a JSON array. Fix or "
|
|
433
|
-
f"remove the entry manually, then re-run."
|
|
434
|
-
)
|
|
435
|
-
return existing_entry
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def prune_empty_list_then_empty_section(
|
|
439
|
-
all_settings: dict[str, object], section_key: str, list_key: str
|
|
440
|
-
) -> None:
|
|
441
|
-
"""Remove an empty list key and its parent section if both are empty.
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
all_settings: The parsed settings dictionary to prune in place.
|
|
445
|
-
section_key: Key of the parent section to check.
|
|
446
|
-
list_key: Key of the list entry within the section.
|
|
447
|
-
"""
|
|
448
|
-
section = all_settings.get(section_key)
|
|
449
|
-
if not isinstance(section, dict):
|
|
450
|
-
return
|
|
451
|
-
list_entry = section.get(list_key)
|
|
452
|
-
if isinstance(list_entry, list) and len(list_entry) == 0:
|
|
453
|
-
del section[list_key]
|
|
454
|
-
if len(section) == 0:
|
|
455
|
-
del all_settings[section_key]
|
package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""Configuration constants for claude_permissions_common shared helpers."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
TEXT_FILE_ENCODING: str = "utf-8"
|
|
6
|
-
ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
|
|
7
|
-
ALL_AGENT_CONFIG_DENY_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read", "Glob")
|
|
8
|
-
ALL_AGENT_CONFIG_PATH_PATTERNS: tuple[str, ...] = (
|
|
9
|
-
"settings*.json",
|
|
10
|
-
"hooks/**",
|
|
11
|
-
"commands/**",
|
|
12
|
-
"agents/**",
|
|
13
|
-
"skills/**",
|
|
14
|
-
"mcp.json",
|
|
15
|
-
"CLAUDE.md",
|
|
16
|
-
)
|
|
17
|
-
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX: str = "Trusted local workspace:"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _describe_agent_config_pattern_for_humans(agent_config_path_pattern: str) -> str:
|
|
21
|
-
glob_suffix_under_directory = "/**"
|
|
22
|
-
file_name_for_special_phrasing = "mcp.json"
|
|
23
|
-
if agent_config_path_pattern.endswith(glob_suffix_under_directory):
|
|
24
|
-
directory_name = agent_config_path_pattern[
|
|
25
|
-
: -len(glob_suffix_under_directory)
|
|
26
|
-
]
|
|
27
|
-
return f"anything under {directory_name}/"
|
|
28
|
-
if agent_config_path_pattern == file_name_for_special_phrasing:
|
|
29
|
-
return f"the {file_name_for_special_phrasing} file"
|
|
30
|
-
return agent_config_path_pattern
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _build_agent_config_pattern_phrase(
|
|
34
|
-
all_agent_config_path_patterns: tuple[str, ...],
|
|
35
|
-
) -> str:
|
|
36
|
-
all_described_patterns: list[str] = [
|
|
37
|
-
_describe_agent_config_pattern_for_humans(each_pattern)
|
|
38
|
-
for each_pattern in all_agent_config_path_patterns
|
|
39
|
-
]
|
|
40
|
-
if len(all_described_patterns) <= 1:
|
|
41
|
-
return ", ".join(all_described_patterns)
|
|
42
|
-
leading_phrase_parts = ", ".join(all_described_patterns[:-1])
|
|
43
|
-
final_phrase_part = all_described_patterns[-1]
|
|
44
|
-
return f"{leading_phrase_parts}, and {final_phrase_part}"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
_AGENT_CONFIG_PATTERN_PHRASE: str = _build_agent_config_pattern_phrase(
|
|
48
|
-
ALL_AGENT_CONFIG_PATH_PATTERNS
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
|
|
52
|
-
f"Trusted local workspace: Files under {{project_path}}/.claude/** inherit "
|
|
53
|
-
f"the workspace's trust for Edit, Write, Read, and Glob operations EXCEPT "
|
|
54
|
-
f"for agent-config files: {_AGENT_CONFIG_PATTERN_PHRASE}. Edits to those "
|
|
55
|
-
f"agent-config files always require explicit per-edit user approval."
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS: tuple[str, ...] = ('"', "'")
|
|
59
|
-
ATOMIC_WRITE_TEMPORARY_SUFFIX: str = ".tmp"
|
|
60
|
-
GIT_DIRECTORY_MARKER: str = ".git"
|
|
61
|
-
CLAUDE_DIRECTORY_MARKER: str = ".claude"
|
|
62
|
-
CLAUDE_USER_SETTINGS_FILENAME: str = "settings.json"
|
|
63
|
-
DEFAULT_SETTINGS_FILE_MODE: int = 0o600
|
|
64
|
-
SETTINGS_PERMISSIONS_KEY: str = "permissions"
|
|
65
|
-
SETTINGS_ALLOW_KEY: str = "allow"
|
|
66
|
-
SETTINGS_DENY_KEY: str = "deny"
|
|
67
|
-
SETTINGS_ADDITIONAL_DIRECTORIES_KEY: str = "additionalDirectories"
|
|
68
|
-
SETTINGS_AUTO_MODE_KEY: str = "autoMode"
|
|
69
|
-
SETTINGS_ENVIRONMENT_KEY: str = "environment"
|