claude-dev-env 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/CLAUDE.md +3 -2
  2. package/_shared/advisor/advisor-protocol.md +2 -2
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
  9. package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
  10. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  11. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
  12. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
  13. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
  14. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
  16. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
  17. package/_shared/pr-loop/worker-spawn.md +1 -1
  18. package/agents/CLAUDE.md +1 -0
  19. package/agents/code-verifier.md +4 -4
  20. package/agents/skill-writer-agent.md +84 -0
  21. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
  22. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  23. package/docs/CLAUDE.md +1 -0
  24. package/docs/references/CLAUDE.md +1 -0
  25. package/docs/references/code-review-enforcement.md +97 -0
  26. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  27. package/hooks/blocking/CLAUDE.md +8 -1
  28. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  29. package/hooks/blocking/code_review_gate_deny.py +74 -0
  30. package/hooks/blocking/code_review_pr_create_gate.py +194 -0
  31. package/hooks/blocking/code_review_push_gate.py +140 -0
  32. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
  33. package/hooks/blocking/code_review_stamp_store.py +233 -0
  34. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  35. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  36. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  37. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  38. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  39. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  40. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  41. package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
  42. package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
  43. package/hooks/blocking/config/verified_commit_constants.py +24 -9
  44. package/hooks/blocking/conftest.py +2 -0
  45. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  46. package/hooks/blocking/destructive_command_blocker.py +19 -6
  47. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  48. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  49. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  50. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  51. package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
  52. package/hooks/blocking/test_code_review_push_gate.py +189 -0
  53. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
  54. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  55. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  56. package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
  57. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  58. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  59. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  60. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  61. package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
  62. package/hooks/blocking/test_verification_verdict_store.py +920 -810
  63. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  64. package/hooks/blocking/verification_verdict_store.py +118 -5
  65. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  66. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  67. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  68. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  69. package/hooks/git-hooks/pre_push.py +89 -2
  70. package/hooks/git-hooks/test_pre_push.py +103 -0
  71. package/hooks/hooks.json +16 -1
  72. package/hooks/hooks_constants/CLAUDE.md +1 -0
  73. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  74. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  75. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  76. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  77. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  78. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  79. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  80. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  81. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  82. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  83. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  84. package/hooks/validators/CLAUDE.md +1 -0
  85. package/hooks/validators/mypy_integration.py +63 -50
  86. package/hooks/validators/pyproject_config_discovery.py +101 -0
  87. package/hooks/validators/ruff_integration.py +211 -23
  88. package/hooks/validators/run_all_validators.py +21 -14
  89. package/hooks/validators/test_mypy_integration.py +32 -0
  90. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  91. package/hooks/validators/test_ruff_integration.py +68 -1
  92. package/hooks/validators/test_run_all_validators.py +25 -0
  93. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  94. package/package.json +1 -1
  95. package/rules/docstring-prose-matches-implementation.md +20 -43
  96. package/rules/durable-post-artifacts.md +7 -0
  97. package/scripts/CLAUDE.md +2 -1
  98. package/scripts/claude-chain.example.json +15 -3
  99. package/scripts/claude_chain_runner.py +130 -27
  100. package/scripts/claude_chain_usage.py +346 -0
  101. package/scripts/codec_forwarding_test_support.py +83 -0
  102. package/scripts/conftest.py +8 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
  104. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
  105. package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
  106. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  107. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  108. package/scripts/dev_env_scripts_constants/timing.py +1 -1
  109. package/scripts/invoke_code_review.py +550 -38
  110. package/scripts/resolve_worker_spawn.py +8 -1
  111. package/scripts/test_claude_chain_runner.py +412 -6
  112. package/scripts/test_claude_chain_usage.py +534 -0
  113. package/scripts/test_invoke_code_review.py +298 -4
  114. package/scripts/test_invoke_code_review_codec.py +77 -0
  115. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  116. package/skills/autoconverge/SKILL.md +9 -3
  117. package/skills/autoconverge/reference/convergence.md +2 -1
  118. package/skills/autoconverge/reference/multi-pr.md +6 -1
  119. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  120. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
  121. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
  123. package/skills/autoconverge/workflow/converge.mjs +24 -8
  124. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  125. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  126. package/skills/fresh-branch/CLAUDE.md +2 -0
  127. package/skills/fresh-branch/SKILL.md +2 -0
  128. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  129. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  131. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  132. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  133. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  134. package/skills/orchestrator/SKILL.md +1 -20
  135. package/skills/orchestrator-refresh/SKILL.md +1 -1
  136. package/skills/pr-converge/SKILL.md +3 -3
  137. package/skills/pr-converge/reference/examples.md +3 -3
  138. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  139. package/skills/pr-converge/reference/ground-rules.md +3 -3
  140. package/skills/pr-converge/reference/per-tick.md +5 -5
  141. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  142. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  143. package/skills/prototype/SKILL.md +86 -0
  144. package/skills/prototype/reference/honest-limitations.md +23 -0
  145. package/skills/prototype/reference/promotion-tasks.md +23 -0
  146. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  147. package/skills/prototype/scripts/conftest.py +15 -0
  148. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  149. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  150. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  151. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  152. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  153. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  154. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  155. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  156. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  157. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  158. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  159. package/skills/prototype/workflows/promotion.md +27 -0
  160. package/skills/prototype/workflows/sandbox.md +35 -0
  161. package/skills/skill-builder/CLAUDE.md +3 -3
  162. package/skills/skill-builder/SKILL.md +5 -5
  163. package/skills/skill-builder/references/CLAUDE.md +1 -1
  164. package/skills/skill-builder/references/delegation-map.md +3 -3
  165. package/skills/skill-builder/references/description-field.md +1 -1
  166. package/skills/skill-builder/references/skill-modularity.md +2 -3
  167. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  168. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  169. package/skills/skill-builder/workflows/new-skill.md +2 -2
  170. package/skills/team-advisor/SKILL.md +5 -4
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: prototype
3
+ description: >-
4
+ Isolated hookless worktree sandbox for zero-friction proof-of-concept builds, then a clean-room re-verification that promotes a successful POC into a real deploy. Triggers: prototype, /prototype, proof of concept, POC, spike, throwaway build, build without hooks, sandbox this idea, hookless worktree, prototype then ship, promote the prototype.
5
+ ---
6
+
7
+ # Prototype
8
+
9
+ ## Principle
10
+
11
+ Give a build the freedom to move fast, then make it earn the right to ship. Two phases, one hard wall between them:
12
+
13
+ - **Sandbox** — an isolated worktree where an agent runs under `claude --bare`, so none of the standards gates (TDD, code rules, verified-commit, plain-language, stage) fire. The agent builds a proof-of-concept with zero friction.
14
+ - **Promotion** — back in the normal, fully-hooked session, the successful POC goes through a clean-room re-verification before it becomes a commit and a pull request. Nothing from the sandbox rides along un-checked.
15
+
16
+ Two safety gates stay live even in the sandbox: personal-data blocking and destructive-command blocking. A worktree shares the real repo's `.git` store and `rm` reaches the whole disk, so these are containment, not the "delays" the sandbox is meant to shed.
17
+
18
+ ## Gotchas
19
+
20
+ Highest-signal content. Append a bullet each time a run fails in a new way.
21
+
22
+ - A `--settings` file's hooks still load under `--bare` (the file is passed explicitly). That is the mechanism for keeping the two safety gates, not a bug.
23
+ - The two safety hooks must point at the real installed scripts and import their `*_constants` packages at runtime. `scripts/build_sandbox_settings.py` resolves each hook's command from the live `~/.claude/settings.json` and registers it on the matchers the sandbox needs — personal-data on Write, Edit, MultiEdit, and Bash; destructive-command on Bash — so the paths stay correct on any machine and both gates cover the write surface. Do not hand-write the hook paths or matchers, and do not inherit whatever matcher the live config happens to use (a personal-data gate wired only to a narrow tool leaves disk writes ungated).
24
+ - Under `--dangerously-skip-permissions` an `ask` decision is auto-resolved, so only a hard `deny` blocks a destructive command. The settings file carries an `env` block that sets `CLAUDE_DESTRUCTIVE_DENY_MODE`, which turns the destructive gate's terminal `ask` into a `deny`. The probe runs the gate under that env block and passes only on the hard deny.
25
+
26
+ **Refusal cases — first match wins:**
27
+
28
+ - **Not in a git repository.** Respond: `Prototype needs a git repo to branch a worktree from. Run this from inside one.`
29
+ - **The `fresh-branch` skill is not installed.** Respond: `Prototype composes fresh-branch to make the sandbox worktree, and it is not installed. Install claude-dev-env first.`
30
+ - **The `claude` CLI is not on PATH.** Respond: `The sandbox launches a headless claude session, and the claude CLI is not on PATH.`
31
+
32
+ ## Process
33
+
34
+ Two phases. Run the sandbox phase first; run the promotion phase only when the POC succeeds and the user wants it shipped.
35
+
36
+ ### Phase 1 — Sandbox
37
+
38
+ Follow `workflows/sandbox.md`. In short:
39
+
40
+ 1. Invoke the `fresh-branch` skill to create an isolated worktree off `origin/main`. Keep its returned `worktree_path` and `base_commit`.
41
+ 2. Run `scripts/build_sandbox_settings.py` to emit the minimal safety settings (personal-data and destructive-command gates only).
42
+ 3. Run `scripts/probe_sandbox_safety.py --settings <path>` and confirm both gates block. Do not continue on a non-zero exit.
43
+ 4. Run `scripts/launch_sandbox.py` to start the hookless `claude -p --bare` session in the worktree with those settings and the POC task.
44
+ 5. Read what the sandbox built. Decide whether the POC proves the idea.
45
+
46
+ ### Phase 2 — Promotion
47
+
48
+ Run only in the normal, fully-hooked session — never inside the sandbox. Follow `workflows/promotion.md`, which drives the clean-room task seeds in `reference/promotion-tasks.md`: fresh branch off live `origin/main`, POC content as an uncommitted diff, cleanup and privacy sweep, `code-verifier` in a fresh context, then `/commit` and a draft PR handed to a PR-loop skill. State the two honest limitations from `reference/honest-limitations.md`.
49
+
50
+ ## Task seeding
51
+
52
+ At the start of Phase 2, register every item in `reference/promotion-tasks.md` as a session task (`TaskCreate`, or `TodoWrite` if that is the host tool). Work only from the task list. Mark each complete with evidence. Do not track promotion as a markdown checklist.
53
+
54
+ ## Sub-skills
55
+
56
+ | Skill / agent | When | Produces | If missing |
57
+ |---|---|---|---|
58
+ | `fresh-branch` | Sandbox step 1; Promotion step 2 | isolated worktree JSON (`worktree_path`, `base_commit`, `repo_root`) | Refuse — see refusal cases |
59
+ | `privacy-hygiene` | Promotion step 5 | personal-data and secret sweep of the diff | Warn; do a manual review before continuing |
60
+ | `code-verifier` (agent) | Promotion step 6 | fresh-context verdict against the real diff; mints the commit-gate verdict | Stop; the commit gate will block anyway |
61
+ | `/commit` (command) | Promotion step 7 | conventional commit + push | Commit and push by hand per `git-workflow` |
62
+ | `autoconverge` (default; `pr-converge` or `bugteam` as alternatives) | Promotion step 9 | the PR converged to ready | Stop after the draft PR; tell the user to converge manually |
63
+
64
+ ## Degree of freedom
65
+
66
+ Low on the skill's own mechanics — the launch flags and the promotion order are fragile with cliffs, so they live in scripts and a fixed task list, not in prose the agent reconstructs. High inside the sandbox — the sandboxed agent's build freedom is the whole point.
67
+
68
+ ## File index
69
+
70
+ | File | Purpose |
71
+ |---|---|
72
+ | `SKILL.md` | This hub — principle, gotchas, when-applies, process, sub-skills, file index |
73
+ | `workflows/sandbox.md` | Phase 1 steps: worktree, safety settings, probe, hookless launch |
74
+ | `workflows/promotion.md` | Phase 2 steps: the clean-room re-verification that drives the promotion task seeds |
75
+ | `reference/promotion-tasks.md` | Task-seed catalog for the clean-room protocol (register via the task tool) |
76
+ | `reference/honest-limitations.md` | The two fixed statements to make on every promotion |
77
+ | `scripts/build_sandbox_settings.py` | Emit the minimal safety `--settings`: resolve each hook's command from live settings, register it on the required matchers |
78
+ | `scripts/launch_sandbox.py` | Launch the hookless `claude -p --bare` sandbox session in the worktree |
79
+ | `scripts/probe_sandbox_safety.py` | Prove both safety gates block before trusting the sandbox |
80
+
81
+ ## Folder map
82
+
83
+ - `SKILL.md` — hub.
84
+ - `workflows/` — the two phase workflows.
85
+ - `reference/` — promotion task seeds and the honest-limitation statements.
86
+ - `scripts/` — the settings builder, the launcher, the safety probe, their `prototype_scripts_constants` package, and paired tests.
@@ -0,0 +1,23 @@
1
+ # Honest limitations of a promoted prototype
2
+
3
+ State both of these to the user, in these terms, whenever a proof-of-concept is promoted. Do not soften or drop them. They are the price of building without standards gates.
4
+
5
+ ## 1. Write-time code rules never ran on this code
6
+
7
+ `code_rules_enforcer` is a Write/Edit gate: it checks content as it is written. Prototype code is built under `--bare`, so that gate never fired, and content brought into promotion as a git diff (apply, checkout, cherry-pick) does not pass through it either.
8
+
9
+ Standards re-engage on promotion through three surfaces that stand in for the write-time hook:
10
+
11
+ - the `code-verifier` agent, in a fresh context, deriving and running the named gates against the real diff;
12
+ - the `privacy-hygiene` sweep for personal data and secrets;
13
+ - the pull-request review (AGENTS.md criteria and any PR-loop reviewers).
14
+
15
+ Say plainly: the write-time rule engine did not see this code; the verifier and review are what cover it.
16
+
17
+ ## 2. TDD ordering is waived on promoted prototype lines
18
+
19
+ The sandbox agent wrote code first and tests, if any, after. Red-green-refactor ordering did not happen. So the honest claim on promoted prototype code is exactly this, and nothing more:
20
+
21
+ > code-verifier passed, privacy swept, review passed — TDD ordering waived.
22
+
23
+ Do not claim red-green compliance on these lines. A prototype is a reference build, not a test-first build. Fred Brooks: plan to throw one away. Promotion re-verifies the code and often rewrites it to standard; expect real work in the verifier repair loop, not a rubber stamp.
@@ -0,0 +1,23 @@
1
+ # Promotion task seeds (clean-room protocol)
2
+
3
+ Register every numbered item below as a session task (`TaskCreate`, or `TodoWrite` if that is the host tool) at the start of the promotion phase. Work only from the task list. Mark each complete only with evidence — a command result, a path, a verdict, or a skill's return. This is not a checkbox board; it is a seed catalog for the task tool.
4
+
5
+ Promotion runs in the **normal, fully-hooked session** — never inside the `--bare` sandbox. Nothing from the sandbox history is carried; only its file content is re-applied and re-verified.
6
+
7
+ 1. **Confirm the prototype is worth promoting.** The sandbox build works and the user (or the standing goal) wants it shipped. Evidence: the working behavior observed, one sentence on what the POC proves.
8
+
9
+ 2. **Fresh branch off live upstream.** Re-fetch `origin/main` and branch from it via the `fresh-branch` skill. Evidence: the returned `base_commit` matches current `origin/main`. This starts clean history and keeps the work based on live upstream.
10
+
11
+ 3. **Bring prototype content as an uncommitted working-tree diff, by allowlist.** Take the sandbox diff against its `base_commit` and copy only the product files you intend to ship into the fresh branch's working tree. Do NOT cherry-pick or merge the sandbox commits, and do NOT bulk-copy the worktree. Exclude the sandbox settings file (`.prototype-sandbox-settings.json`) and every scratch, debug, or artifact file the POC produced — name the files you are bringing, not the ones you are dropping. Evidence: the allowlist of copied paths; `git status` shows only those unstaged; `git log` shows no sandbox commits.
12
+
13
+ 4. **Cleanup pass.** Remove every scratch file, debug dump, and temp helper the prototype created (see the `cleanup-temp-files` rule). Evidence: the removed paths, or a stated "none created".
14
+
15
+ 5. **Privacy sweep.** Run the `privacy-hygiene` skill over the full applied working tree, not only the diff — a POC that pulled live data can leave a secret in a file the diff view hides. Evidence: its clean report, or the leak it found and how it was removed. If the skill is missing, do a manual PII and secret review and say so.
16
+
17
+ 6. **Verify in a fresh context.** Spawn the `code-verifier` agent against the real diff. Expect findings and a repair loop — the code was un-TDD'd. Evidence: the verifier's clean verdict, and a note of what it made you fix. Do not skip this on the belief that the sandbox agent already tested it.
18
+
19
+ 7. **Commit and open a draft PR.** Only on a clean verdict, run `/commit` (which mints the commit-gate verdict and pushes), then open a draft PR per the `git-workflow` rule. Evidence: the commit hash and the PR URL.
20
+
21
+ 8. **State the honest limitations.** Post the two statements from `reference/honest-limitations.md` — write-time rules never ran; TDD ordering waived — in the PR body or to the user. Evidence: the text was included.
22
+
23
+ 9. **Hand to a PR-loop skill.** Hand the PR to `autoconverge` by default — one autonomous run to ready. Reach for `pr-converge` when paced ticks fit better, or `bugteam` for an open-loop audit-fix. Evidence: the skill was invoked, or the user chose to converge manually.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """Emit a minimal ``--settings`` file carrying only the two sandbox safety hooks.
3
+
4
+ ::
5
+
6
+ python build_sandbox_settings.py --out sandbox-settings.json
7
+ sandbox-settings.json -> {"env": {deny-mode override},
8
+ "hooks": {"PreToolUse": [
9
+ {"matcher": "Bash", "hooks": [pii_prevention, destructive_command]},
10
+ {"matcher": "Edit", "hooks": [pii_prevention]},
11
+ {"matcher": "MultiEdit", "hooks": [pii_prevention]},
12
+ {"matcher": "Write", "hooks": [pii_prevention]}]}}
13
+
14
+ The builder resolves each safety hook's command entry from the live settings
15
+ source, which carries the per-machine interpreter, absolute script path, and
16
+ timeout. It then registers each entry on the matchers the sandbox requires.
17
+ The personal-data gate covers every write surface and the command line. The
18
+ destructive-command gate covers the command line, and the emitted ``env`` block
19
+ runs that gate in deny mode so its block holds under
20
+ ``--dangerously-skip-permissions``. When either safety hook is absent from the
21
+ source, the builder exits without writing, because a sandbox cannot be
22
+ contained without both.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import logging
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ from prototype_scripts_constants.config.build_sandbox_settings_constants import (
34
+ ALL_REQUIRED_MATCHERS_BY_SAFETY_BASENAME,
35
+ ALL_SAFETY_HOOK_SCRIPT_BASENAMES,
36
+ BUILD_SUCCESS_EXIT_CODE,
37
+ COMMAND_KEY,
38
+ DEFAULT_SETTINGS_SOURCE,
39
+ DESTRUCTIVE_DENY_MODE_ENV_VALUE,
40
+ DESTRUCTIVE_DENY_MODE_ENV_VAR,
41
+ ENV_KEY,
42
+ HOOKS_KEY,
43
+ JSON_INDENT_SPACES,
44
+ MATCHER_KEY,
45
+ MISSING_BASENAMES_JOIN_SEPARATOR,
46
+ PRE_TOOL_USE_KEY,
47
+ SETTINGS_MISSING_SAFETY_HOOK_EXIT_CODE,
48
+ SETTINGS_SOURCE_UNREADABLE_EXIT_CODE,
49
+ )
50
+ from prototype_scripts_constants.config.prototype_common_constants import (
51
+ LOGGING_FORMAT,
52
+ TEXT_ENCODING_UTF8,
53
+ )
54
+
55
+ logger = logging.getLogger("build_sandbox_settings")
56
+
57
+
58
+ def read_settings_document(source_path: Path) -> dict:
59
+ """Load the live settings JSON the sandbox settings are resolved from.
60
+
61
+ ::
62
+
63
+ ~/.claude/settings.json -> parsed dict with hooks.PreToolUse blocks
64
+ """
65
+ return json.loads(source_path.read_text(encoding=TEXT_ENCODING_UTF8))
66
+
67
+
68
+ def _first_subhook_running(
69
+ pre_tool_use_block: dict, script_basename: str
70
+ ) -> dict | None:
71
+ """Return the block's first sub-hook whose command runs the named script."""
72
+ for each_subhook in pre_tool_use_block.get(HOOKS_KEY, []):
73
+ if script_basename in each_subhook.get(COMMAND_KEY, ""):
74
+ return each_subhook
75
+ return None
76
+
77
+
78
+ def _first_block_running(
79
+ all_pre_tool_use_blocks: list[dict], script_basename: str
80
+ ) -> dict | None:
81
+ """Return the first block's sub-hook entry that runs the named safety script."""
82
+ for each_block in all_pre_tool_use_blocks:
83
+ resolved_entry = _first_subhook_running(each_block, script_basename)
84
+ if resolved_entry is not None:
85
+ return resolved_entry
86
+ return None
87
+
88
+
89
+ def resolve_safety_hook_entries(settings_document: dict) -> dict[str, dict]:
90
+ """Resolve each safety hook's live sub-hook entry, keyed by script basename.
91
+
92
+ ::
93
+
94
+ live PreToolUse blocks
95
+ -> {"pii_prevention_blocker.py": {type, command, timeout},
96
+ "destructive_command_blocker.py": {type, command, timeout}}
97
+
98
+ The captured entry carries the per-machine interpreter, absolute script
99
+ path, and timeout, so the sandbox settings stay correct on any machine.
100
+
101
+ Args:
102
+ settings_document: the live settings JSON with hooks.PreToolUse blocks.
103
+
104
+ Returns:
105
+ The first matching sub-hook entry per safety basename; a basename with
106
+ no matching sub-hook is absent from the mapping.
107
+ """
108
+ pre_tool_use_blocks = settings_document.get(HOOKS_KEY, {}).get(PRE_TOOL_USE_KEY, [])
109
+ entry_by_basename: dict[str, dict] = {}
110
+ for each_basename in ALL_SAFETY_HOOK_SCRIPT_BASENAMES:
111
+ resolved_entry = _first_block_running(pre_tool_use_blocks, each_basename)
112
+ if resolved_entry is not None:
113
+ entry_by_basename[each_basename] = resolved_entry
114
+ return entry_by_basename
115
+
116
+
117
+ def find_unresolved_safety_hook_basenames(
118
+ entry_by_basename: dict[str, dict],
119
+ ) -> list[str]:
120
+ """List the safety hook basenames the live source resolved no command for.
121
+
122
+ ::
123
+
124
+ both resolved -> []
125
+ destructive unresolved -> ["destructive_command_blocker.py"]
126
+
127
+ Args:
128
+ entry_by_basename: the resolved sub-hook entry per safety basename.
129
+
130
+ Returns:
131
+ The safety hook basenames absent from the mapping, empty when both
132
+ resolved.
133
+ """
134
+ return [
135
+ each_basename
136
+ for each_basename in ALL_SAFETY_HOOK_SCRIPT_BASENAMES
137
+ if each_basename not in entry_by_basename
138
+ ]
139
+
140
+
141
+ def _build_pre_tool_use_blocks(entry_by_basename: dict[str, dict]) -> list[dict]:
142
+ """Build one PreToolUse block per required matcher, in sorted matcher order."""
143
+ entries_by_matcher: dict[str, list[dict]] = {}
144
+ for each_basename in ALL_REQUIRED_MATCHERS_BY_SAFETY_BASENAME:
145
+ resolved_entry = entry_by_basename[each_basename]
146
+ for each_matcher in ALL_REQUIRED_MATCHERS_BY_SAFETY_BASENAME[each_basename]:
147
+ entries_by_matcher.setdefault(each_matcher, []).append(resolved_entry)
148
+ return [
149
+ {MATCHER_KEY: each_matcher, HOOKS_KEY: entries_by_matcher[each_matcher]}
150
+ for each_matcher in sorted(entries_by_matcher)
151
+ ]
152
+
153
+
154
+ def _build_deny_mode_env_block() -> dict[str, str]:
155
+ """Build the env block that runs the destructive gate in deny mode.
156
+
157
+ The single key is the deny-mode env variable, mapped to its truthy value,
158
+ so the sandbox session runs the destructive gate with a hard block.
159
+
160
+ Returns:
161
+ The single-key env override the sandbox session applies.
162
+ """
163
+ return {DESTRUCTIVE_DENY_MODE_ENV_VAR: DESTRUCTIVE_DENY_MODE_ENV_VALUE}
164
+
165
+
166
+ def build_minimal_settings(entry_by_basename: dict[str, dict]) -> dict:
167
+ """Assemble the minimal settings document from the resolved safety entries.
168
+
169
+ ::
170
+
171
+ resolved entries -> {"env": {deny-mode override},
172
+ "hooks": {"PreToolUse": [required-matcher blocks]}}
173
+
174
+ The ``env`` block runs the destructive gate in deny mode, so its block
175
+ holds under ``--dangerously-skip-permissions``, where an ask decision is
176
+ auto-resolved.
177
+
178
+ Args:
179
+ entry_by_basename: the resolved sub-hook entry per safety basename.
180
+
181
+ Returns:
182
+ The minimal settings document registering each safety entry on the
183
+ matchers the sandbox requires, with the deny-mode env override.
184
+ """
185
+ return {
186
+ ENV_KEY: _build_deny_mode_env_block(),
187
+ HOOKS_KEY: {PRE_TOOL_USE_KEY: _build_pre_tool_use_blocks(entry_by_basename)},
188
+ }
189
+
190
+
191
+ def write_minimal_settings(minimal_settings: dict, out_path: Path) -> None:
192
+ """Write the minimal settings document to the output path as pretty JSON."""
193
+ serialized_settings = json.dumps(minimal_settings, indent=JSON_INDENT_SPACES)
194
+ out_path.write_text(serialized_settings + "\n", encoding=TEXT_ENCODING_UTF8)
195
+
196
+
197
+ def _parse_arguments(all_arguments: list[str] | None) -> argparse.Namespace:
198
+ parser = argparse.ArgumentParser(description=__doc__)
199
+ parser.add_argument("--out", required=True, help="where to write the settings")
200
+ parser.add_argument(
201
+ "--settings-source",
202
+ default=DEFAULT_SETTINGS_SOURCE,
203
+ help="the live settings JSON to resolve the safety hook commands from",
204
+ )
205
+ return parser.parse_args(all_arguments)
206
+
207
+
208
+ def _load_settings_or_none(source_path: Path) -> dict | None:
209
+ """Read and parse the settings source, logging and returning None on failure."""
210
+ try:
211
+ return read_settings_document(source_path)
212
+ except (OSError, json.JSONDecodeError) as read_error:
213
+ logger.error("cannot read settings source %s: %s", source_path, read_error)
214
+ return None
215
+
216
+
217
+ def main(all_arguments: list[str] | None = None) -> int:
218
+ """Build the minimal safety settings and write them to the output path.
219
+
220
+ Args:
221
+ all_arguments: the command-line arguments, or None to read sys.argv.
222
+
223
+ Returns:
224
+ 0 after writing the settings, 2 when the source is unreadable, and 2
225
+ when either safety hook is unresolved from the source.
226
+ """
227
+ logging.basicConfig(format=LOGGING_FORMAT)
228
+ arguments = _parse_arguments(all_arguments)
229
+ source_path = Path(arguments.settings_source).expanduser()
230
+ settings_document = _load_settings_or_none(source_path)
231
+ if settings_document is None:
232
+ return SETTINGS_SOURCE_UNREADABLE_EXIT_CODE
233
+ entry_by_basename = resolve_safety_hook_entries(settings_document)
234
+ all_unresolved_basenames = find_unresolved_safety_hook_basenames(entry_by_basename)
235
+ if all_unresolved_basenames:
236
+ logger.error(
237
+ "safety hook missing from settings source: %s",
238
+ MISSING_BASENAMES_JOIN_SEPARATOR.join(all_unresolved_basenames),
239
+ )
240
+ return SETTINGS_MISSING_SAFETY_HOOK_EXIT_CODE
241
+ minimal_settings = build_minimal_settings(entry_by_basename)
242
+ out_path = Path(arguments.out).expanduser()
243
+ write_minimal_settings(minimal_settings, out_path)
244
+ sys.stdout.write(str(out_path) + "\n")
245
+ return BUILD_SUCCESS_EXIT_CODE
246
+
247
+
248
+ if __name__ == "__main__":
249
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,15 @@
1
+ """Put this worktree's prototype scripts directory on the import path.
2
+
3
+ The scripts import their ``prototype_scripts_constants`` package by name.
4
+ Inserting this directory lets the tests resolve that package and the
5
+ script modules from the local worktree rather than an installed copy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
14
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
15
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env python3
2
+ """Launch the hookless ``claude -p --bare`` sandbox session in a worktree.
3
+
4
+ ::
5
+
6
+ python launch_sandbox.py --worktree ./wt --settings ./s.json --task-file ./t.txt
7
+ {"worktree": "./wt", "settings": "./s.json", "exit_code": 0}
8
+
9
+ The launcher validates the three paths, reads the proof-of-concept task from
10
+ the task file, and runs a headless ``claude`` session in the worktree under
11
+ the minimal safety settings. The session runs with the standards gates
12
+ stripped, so the two safety hooks in the settings are its only containment.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import logging
20
+ import subprocess
21
+ import sys
22
+ from collections.abc import Callable
23
+ from pathlib import Path
24
+
25
+ from prototype_scripts_constants.config.launch_sandbox_constants import (
26
+ BARE_FLAG,
27
+ CLAUDE_EXECUTABLE_NAME,
28
+ DEFAULT_TIMEOUT_SECONDS,
29
+ LAUNCH_MISSING_PATH_EXIT_CODE,
30
+ LAUNCH_TIMEOUT_EXIT_CODE,
31
+ PROMPT_FLAG,
32
+ SETTINGS_FLAG,
33
+ SKIP_PERMISSIONS_FLAG,
34
+ SUMMARY_KEY_EXIT_CODE,
35
+ SUMMARY_KEY_SETTINGS,
36
+ SUMMARY_KEY_WORKTREE,
37
+ )
38
+ from prototype_scripts_constants.config.prototype_common_constants import (
39
+ LOGGING_FORMAT,
40
+ TEXT_ENCODING_UTF8,
41
+ )
42
+
43
+ logger = logging.getLogger("launch_sandbox")
44
+
45
+ SandboxCommandRunner = Callable[[list[str], Path, "int | None"], int]
46
+
47
+
48
+ def build_sandbox_command(task_text: str, settings_path: Path) -> list[str]:
49
+ """Build the exact headless ``claude`` argument vector for the sandbox.
50
+
51
+ ::
52
+
53
+ "build a spike", settings.json
54
+ -> ["claude", "-p", "build a spike", "--bare",
55
+ "--dangerously-skip-permissions", "--settings", "settings.json"]
56
+
57
+ Args:
58
+ task_text: the proof-of-concept task the session builds.
59
+ settings_path: the minimal safety settings file.
60
+
61
+ Returns:
62
+ The ordered argument vector for the headless claude session.
63
+ """
64
+ return [
65
+ CLAUDE_EXECUTABLE_NAME,
66
+ PROMPT_FLAG,
67
+ task_text,
68
+ BARE_FLAG,
69
+ SKIP_PERMISSIONS_FLAG,
70
+ SETTINGS_FLAG,
71
+ str(settings_path),
72
+ ]
73
+
74
+
75
+ def validate_sandbox_paths(
76
+ worktree_path: Path, settings_path: Path, task_file_path: Path
77
+ ) -> str | None:
78
+ """Report the first path that does not fit the sandbox launch contract.
79
+
80
+ Args:
81
+ worktree_path: the isolated worktree the session runs in.
82
+ settings_path: the minimal safety settings file.
83
+ task_file_path: the file holding the proof-of-concept task text.
84
+
85
+ Returns:
86
+ An error message for the first path that is absent or the wrong
87
+ kind, or None when the worktree is a directory and both files exist.
88
+ """
89
+ if not worktree_path.is_dir():
90
+ return f"worktree is not a directory: {worktree_path}"
91
+ if not settings_path.is_file():
92
+ return f"settings file not found: {settings_path}"
93
+ if not task_file_path.is_file():
94
+ return f"task file not found: {task_file_path}"
95
+ return None
96
+
97
+
98
+ def _run_via_subprocess(
99
+ all_command_tokens: list[str], working_directory: Path, timeout_seconds: int | None
100
+ ) -> int:
101
+ try:
102
+ completed_process = subprocess.run(
103
+ all_command_tokens,
104
+ cwd=working_directory,
105
+ timeout=timeout_seconds,
106
+ check=False,
107
+ )
108
+ except subprocess.TimeoutExpired:
109
+ logger.error(
110
+ "sandbox session exceeded the %s-second wall-clock limit; terminated",
111
+ timeout_seconds,
112
+ )
113
+ return LAUNCH_TIMEOUT_EXIT_CODE
114
+ except OSError as launch_error:
115
+ logger.error(
116
+ "cannot launch the claude executable %s: %s",
117
+ all_command_tokens[0],
118
+ launch_error,
119
+ )
120
+ return LAUNCH_MISSING_PATH_EXIT_CODE
121
+ return completed_process.returncode
122
+
123
+
124
+ def run_sandbox(
125
+ worktree_path: Path,
126
+ settings_path: Path,
127
+ task_text: str,
128
+ timeout_seconds: int | None,
129
+ command_runner: SandboxCommandRunner,
130
+ ) -> int:
131
+ """Run the headless sandbox command in the worktree and return its code.
132
+
133
+ Args:
134
+ worktree_path: the isolated worktree the session runs in.
135
+ settings_path: the minimal safety settings file.
136
+ task_text: the proof-of-concept task the session builds.
137
+ timeout_seconds: the wall-clock limit, or None for no limit.
138
+ command_runner: the callable that runs the command vector.
139
+
140
+ Returns:
141
+ The exit code the command runner reports for the session, which is
142
+ the timeout exit code when the session outran its wall-clock limit.
143
+ """
144
+ sandbox_command = build_sandbox_command(task_text, settings_path)
145
+ return command_runner(sandbox_command, worktree_path, timeout_seconds)
146
+
147
+
148
+ def _emit_sandbox_summary(
149
+ worktree_path: Path, settings_path: Path, exit_code: int
150
+ ) -> None:
151
+ summary = {
152
+ SUMMARY_KEY_WORKTREE: str(worktree_path),
153
+ SUMMARY_KEY_SETTINGS: str(settings_path),
154
+ SUMMARY_KEY_EXIT_CODE: exit_code,
155
+ }
156
+ sys.stdout.write(json.dumps(summary) + "\n")
157
+
158
+
159
+ def _parse_arguments(all_arguments: list[str] | None) -> argparse.Namespace:
160
+ parser = argparse.ArgumentParser(description=__doc__)
161
+ parser.add_argument("--worktree", required=True, help="the sandbox worktree")
162
+ parser.add_argument("--settings", required=True, help="the safety settings file")
163
+ parser.add_argument("--task-file", required=True, help="the task text file")
164
+ parser.add_argument(
165
+ "--timeout-seconds",
166
+ type=int,
167
+ default=DEFAULT_TIMEOUT_SECONDS,
168
+ help="the wall-clock limit for the session",
169
+ )
170
+ return parser.parse_args(all_arguments)
171
+
172
+
173
+ def main(all_arguments: list[str] | None = None) -> int:
174
+ """Resolve and validate the paths, run the session, and print a summary.
175
+
176
+ Args:
177
+ all_arguments: the command-line arguments, or None to read sys.argv.
178
+
179
+ Returns:
180
+ The sandbox exit code, 2 when a required path is missing, or the
181
+ timeout exit code when the session outran its wall-clock limit.
182
+ """
183
+ logging.basicConfig(format=LOGGING_FORMAT)
184
+ arguments = _parse_arguments(all_arguments)
185
+ worktree_path = Path(arguments.worktree).expanduser().resolve()
186
+ settings_path = Path(arguments.settings).expanduser().resolve()
187
+ task_file_path = Path(arguments.task_file).expanduser().resolve()
188
+ path_error = validate_sandbox_paths(worktree_path, settings_path, task_file_path)
189
+ if path_error is not None:
190
+ logger.error("%s", path_error)
191
+ return LAUNCH_MISSING_PATH_EXIT_CODE
192
+ task_text = task_file_path.read_text(encoding=TEXT_ENCODING_UTF8)
193
+ exit_code = run_sandbox(
194
+ worktree_path,
195
+ settings_path,
196
+ task_text,
197
+ arguments.timeout_seconds,
198
+ _run_via_subprocess,
199
+ )
200
+ _emit_sandbox_summary(worktree_path, settings_path, exit_code)
201
+ return exit_code
202
+
203
+
204
+ if __name__ == "__main__":
205
+ raise SystemExit(main(sys.argv[1:]))