claude-dev-env 8.15.0 → 8.16.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/agents/test_agent_frontmatter.py +0 -72
- package/.agents/agents-archived/code-quality-agent.md +16 -16
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
- package/.agents/skills/e-code-review/SKILL.md +1 -3
- package/.agents/skills/e-simplify/SKILL.md +6 -6
- package/.agents/skills/eli5/SKILL.md +4 -14
- package/.agents/skills/eli5/reference/task-seeds.md +3 -4
- package/.agents/skills/everything-search/SKILL.md +2 -2
- package/.agents/skills/fresh-branch/SKILL.md +2 -3
- package/.agents/skills/grok-spawn/SKILL.md +3 -8
- package/.agents/skills/issue-tracker/SKILL.md +4 -6
- package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
- package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
- package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/pull-request/SKILL.md +11 -14
- package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
- package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
- package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
- package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
- package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
- package/.agents/skills/second-account-workers/SKILL.md +26 -8
- package/.agents/skills/syncing-submodules/SKILL.md +7 -17
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
- package/.agents/skills/task-build/SKILL.md +1 -1
- package/.agents/skills/task-build/reference/tool-routing.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -4
- package/.agents/skills/usage-pause/SKILL.md +1 -1
- package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/advisor/advisor-protocol.md +4 -4
- package/_shared/advisor/reference/consult-format.md +1 -1
- package/_shared/advisor/reference/identity.md +2 -2
- package/_shared/advisor/reference/warm-up.md +1 -1
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
- package/bin/install.mjs +1 -0
- package/bin/install.prompt-context.test.mjs +7 -3
- package/bin/install.test.mjs +3 -2
- package/commands/sr-loop.md +2 -2
- package/docs/CODE_RULES.md +2 -2
- package/docs/DJANGO_PATTERNS.md +3 -3
- package/docs/REACT_PATTERNS.md +6 -6
- package/docs/TEST_QUALITY.md +10 -16
- package/docs/agent-spawn-protocol.md +4 -4
- package/docs/codex-compatibility.md +4 -33
- package/docs/references/team-advisor-skill.md +0 -1
- package/docs/worker-completion-gate.md +0 -1
- package/hooks/advisory/migration_safety_advisor.py +3 -2
- package/hooks/advisory/refactor_guard.py +3 -3
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
- package/package.json +1 -1
- package/rules/explore-thoroughly.md +0 -1
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +5 -5
- package/rules/long-horizon-autonomy.md +1 -5
- package/rules/re-stage-before-commit.md +0 -2
- package/rules/shell-invocation.md +1 -1
- package/scripts/claude_account_choice.py +183 -52
- package/scripts/claude_account_profile.py +81 -49
- package/scripts/claude_account_worker.py +48 -15
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +36 -1
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +2 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/policy_lint/engine.py +5 -2
- package/scripts/repository_checks/config/constants.py +1 -1
- package/scripts/spawn_grok_batch.py +12 -1
- package/scripts/test_claude_account_choice.py +127 -17
- package/scripts/test_claude_account_profile.py +63 -16
- package/scripts/test_claude_account_worker.py +118 -0
- package/scripts/test_spawn_grok_batch.py +22 -0
- package/scripts/tests/test_banned_prose_words.py +1 -1
- package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
- package/scripts/tests/test_rules.py +9 -9
- package/system-prompts/software-engineer.xml +15 -30
- package/audit-rubrics/audit-categories.json +0 -700
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
- package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
- package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
- package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
- package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
- package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
- package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
- package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
- package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
- package/hooks/session/style_reminder_prompt.py +0 -55
- package/hooks/session/test_style_reminder_prompt.py +0 -86
package/docs/TEST_QUALITY.md
CHANGED
|
@@ -4,37 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
## Test Infrastructure Anti-Patterns
|
|
6
6
|
|
|
7
|
-
Test helpers drift toward over-engineering faster than production code, because nothing reviews them for size. Keep a helper to one file of plain functions with the fewest abstractions the current tests need. Add a package, a cache class, or an abstraction only when a second test needs it today.
|
|
8
|
-
|
|
9
|
-
**Pre-check (MANDATORY):**
|
|
10
|
-
- [ ] ONE file?
|
|
11
|
-
- [ ] Functions not classes?
|
|
12
|
-
- [ ] Solving problem not building infrastructure?
|
|
13
|
-
- [ ] Junior-dev-understandable?
|
|
7
|
+
Test helpers drift toward over-engineering faster than production code, because nothing reviews them for size. Keep a helper to one file of plain functions with the fewest abstractions the current tests need. Add a package, a cache class, or an abstraction only when a second test needs it today. A junior developer reads the helper on the first pass.
|
|
14
8
|
|
|
15
9
|
## Delete Useless Tests
|
|
16
10
|
|
|
17
11
|
**Tests must add value.** Delete these types of tests:
|
|
18
12
|
|
|
19
|
-
###
|
|
13
|
+
### Tests to delete
|
|
20
14
|
|
|
21
15
|
**Function existence** - "Testing that the function exists doesn't add value. If the function doesn't exist, the code will not run."
|
|
22
16
|
- Bad: `test_public_api_exports_download_function()` only verified `callable(func)`
|
|
23
|
-
- Action:
|
|
17
|
+
- Action: delete
|
|
24
18
|
|
|
25
19
|
**Constant values** - "It's silly to test that constant values have not changed."
|
|
26
20
|
- Bad: `assert CACHE_DIR == "cache"`
|
|
27
|
-
- Action:
|
|
21
|
+
- Action: delete
|
|
28
22
|
|
|
29
23
|
**Duplicate coverage** - "Isn't this test case the same as test_X?"
|
|
30
|
-
- Action:
|
|
24
|
+
- Action: delete the redundant test
|
|
31
25
|
|
|
32
|
-
##
|
|
26
|
+
## Missing dependencies fail the test
|
|
33
27
|
|
|
34
28
|
"The tests should fail if they can't run. Missing system dependencies should make the test fail."
|
|
35
29
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
30
|
+
- Leave `@skip_if_missing_dependency` and other skip decorators out of test files. The code-rules lint flags each one.
|
|
31
|
+
- A missing dependency fails the test with a clear error, which prompts the install.
|
|
38
32
|
|
|
39
33
|
## Core Testing Principles
|
|
40
34
|
|
|
@@ -46,7 +40,7 @@ Test helpers drift toward over-engineering faster than production code, because
|
|
|
46
40
|
|
|
47
41
|
## React Testing Patterns
|
|
48
42
|
|
|
49
|
-
###
|
|
43
|
+
### Patterns
|
|
50
44
|
|
|
51
45
|
**Test behavior, not implementation** - What user sees/does, not internal state
|
|
52
46
|
- Bad: `expect(component.state.isOpen).toBe(true)`
|
|
@@ -65,7 +59,7 @@ Test helpers drift toward over-engineering faster than production code, because
|
|
|
65
59
|
- Use `userEvent` over `fireEvent` (more realistic)
|
|
66
60
|
- `await userEvent.click(button)` then assert result
|
|
67
61
|
|
|
68
|
-
###
|
|
62
|
+
### Anti-patterns
|
|
69
63
|
|
|
70
64
|
**Snapshot test everything** - Only for stable, visual components
|
|
71
65
|
- Snapshots break on any change, creating noise
|
|
@@ -13,9 +13,9 @@ Before writing any agent prompt, confirm you can answer all of these:
|
|
|
13
13
|
|
|
14
14
|
When any answer is "I don't know", investigate first (read files, search code) or ask the user. Do not spawn with incomplete context.
|
|
15
15
|
|
|
16
|
-
## Step 2:
|
|
16
|
+
## Step 2: Write the prompt
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Write the brief yourself. Include:
|
|
19
19
|
|
|
20
20
|
- The task description and goal
|
|
21
21
|
- Target files and directories found in Step 1
|
|
@@ -23,7 +23,7 @@ Run the `/prompt-generator` skill to produce a structured prompt. Feed it:
|
|
|
23
23
|
- Expected output format
|
|
24
24
|
- Acceptance criteria
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Use the brief as the agent's `prompt` parameter.
|
|
27
27
|
|
|
28
28
|
## Step 3: Spawn the agent
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ Pass the structured prompt from Step 2 to the Agent or Task tool.
|
|
|
31
31
|
|
|
32
32
|
## Why
|
|
33
33
|
|
|
34
|
-
An agent that receives a vague prompt wastes tokens exploring in circles, produces code that misses constraints, and needs a second pass. A small investment in prompt quality
|
|
34
|
+
An agent that receives a vague prompt wastes tokens exploring in circles, produces code that misses constraints, and needs a second pass. A small investment in prompt quality saves a long agent failure. This holds for Explore agents, which waste context on unfocused searches, and for execution agents, which write wrong code.
|
|
35
35
|
|
|
36
36
|
## Relationship to other rules
|
|
37
37
|
|
|
@@ -36,39 +36,10 @@ The default `.claude` target uses `CODEX_HOME`. A named profile or another
|
|
|
36
36
|
target uses `<managed-root>/.codex`, so launch that profile with the same
|
|
37
37
|
`CODEX_HOME` value.
|
|
38
38
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
works for both, as is.
|
|
44
|
-
|
|
45
|
-
The install already points `$CODEX_HOME/hooks` at the shared hooks folder.
|
|
46
|
-
Once installed, the script sits at
|
|
47
|
-
`$CODEX_HOME/hooks/session/style_reminder_prompt.py`.
|
|
48
|
-
|
|
49
|
-
Add this by hand to `$CODEX_HOME/hooks.json` (merge it into an existing file):
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"hooks": {
|
|
54
|
-
"UserPromptSubmit": [
|
|
55
|
-
{
|
|
56
|
-
"command": "python3 $CODEX_HOME/hooks/session/style_reminder_prompt.py"
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
The package installer does not register this optional context hook. Add it by
|
|
64
|
-
hand when the host should receive that context.
|
|
65
|
-
|
|
66
|
-
## Luna fast-mode guard
|
|
67
|
-
|
|
68
|
-
Commit `0f21faf8` retired `luna_fast_mode_gate.py` with the other blocking
|
|
69
|
-
policy hooks. The package ships no replacement, so a Codex `hooks.json` needs
|
|
70
|
-
no entry for it, and the spawn tier a Luna spawn asks for is the caller's to
|
|
71
|
-
hold.
|
|
39
|
+
## Luna spawn tier
|
|
40
|
+
|
|
41
|
+
No hook gates the tier a Luna spawn asks for, so a Codex `hooks.json` needs no
|
|
42
|
+
entry for it, and the caller holds that tier.
|
|
72
43
|
|
|
73
44
|
## Roots and safety
|
|
74
45
|
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
| `.agents/skills/team-advisor/SKILL.md` | Sole-consumer bind wiring and constraints |
|
|
10
10
|
| `advisor-tool.md` | Consult timing, hard rule, how to treat advice |
|
|
11
11
|
| `~/.claude/_shared/advisor/advisor-protocol.md` | Session identity, host bind, floor, lifecycle |
|
|
12
|
-
| `agents/session-advisor.md` | ENDORSE / CORRECTION / PLAN / STOP |
|
|
13
12
|
|
|
14
13
|
## When to use
|
|
15
14
|
|
|
@@ -30,4 +30,3 @@ Mark `completed` only when every row passes.
|
|
|
30
30
|
## Relationship to other rules
|
|
31
31
|
|
|
32
32
|
- `long-horizon-autonomy` covers acting on what you have and not ending a turn on a promise. This gate names the specific completion condition: workers finished and their results merged.
|
|
33
|
-
- `skills/pr-converge/reference/state-schema.md` defines the run-state records a worker's result lands in before the task closes.
|
|
@@ -52,8 +52,9 @@ def main() -> None:
|
|
|
52
52
|
operations = ", ".join(found_unsafe)
|
|
53
53
|
advisory_message = (
|
|
54
54
|
f"MIGRATION SAFETY: Contains {operations}. "
|
|
55
|
-
"
|
|
56
|
-
"
|
|
55
|
+
"After launch, model changes stay backwards-compatible, because running "
|
|
56
|
+
"instances keep reading the old schema until the deployment finishes. "
|
|
57
|
+
"Check that this migration keeps those instances working."
|
|
57
58
|
)
|
|
58
59
|
advisory_payload = {
|
|
59
60
|
"systemMessage": advisory_message,
|
|
@@ -264,9 +264,9 @@ def build_refactor_advisory_context(refactor_description: str, file_path: str) -
|
|
|
264
264
|
"""Build guidance that names the Edit stage and changed-surface rule."""
|
|
265
265
|
return (
|
|
266
266
|
f"[HOOK ADVISORY] Refactor guard — {refactor_description} in {file_path}. "
|
|
267
|
-
"Edit-stage guidance:
|
|
268
|
-
"
|
|
269
|
-
"
|
|
267
|
+
"Edit-stage guidance: This edit went through, and it changes lines outside the "
|
|
268
|
+
"current git diff. Keep further edits to lines the task already changes, and ask "
|
|
269
|
+
"the user before refactoring code outside them."
|
|
270
270
|
)
|
|
271
271
|
|
|
272
272
|
|
package/hooks/hooks.json
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"hooks": [
|
|
70
70
|
{
|
|
71
71
|
"type": "command",
|
|
72
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/
|
|
72
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
|
|
73
73
|
"timeout": 10
|
|
74
74
|
}
|
|
75
75
|
]
|
|
@@ -18,6 +18,5 @@ ISSUE_TRACKER_STARTER_TIMEOUT_MILLISECONDS: int = 50
|
|
|
18
18
|
ISSUE_TRACKER_SESSION_START_DIRECTIVE: str = (
|
|
19
19
|
"SessionStart issue-tracker opt-in is active for this registered repository. "
|
|
20
20
|
"Load the issue-tracker skill when the session needs one GitHub issue action "
|
|
21
|
-
"(open epic, file sub-issue, update status, refresh checklist, close sub-issue).
|
|
22
|
-
"Manual issue-tracker agent spawn remains available unchanged."
|
|
21
|
+
"(open epic, file sub-issue, update status, refresh checklist, close sub-issue)."
|
|
23
22
|
)
|
|
@@ -18,6 +18,5 @@ ORCHESTRATOR_STARTER_TIMEOUT_MILLISECONDS: int = 50
|
|
|
18
18
|
ORCHESTRATOR_SESSION_START_DIRECTIVE: str = (
|
|
19
19
|
"SessionStart orchestrator opt-in is active. Load the orchestrator skill "
|
|
20
20
|
"and run in executor-advisor mode for this session's multi-step work. "
|
|
21
|
-
"Do not invent a second orchestrator path; follow the skill.
|
|
22
|
-
"/orchestrator remains available unchanged."
|
|
21
|
+
"Do not invent a second orchestrator path; follow the skill."
|
|
23
22
|
)
|
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@ Note: This deliberately chooses exploration depth over the "commit and execute q
|
|
|
9
9
|
- Read the relevant files. Understand what exists before proposing what to change.
|
|
10
10
|
- Map the existing patterns: naming conventions, file organization, architectural decisions.
|
|
11
11
|
- Identify constraints that could invalidate an approach before investing effort in it.
|
|
12
|
-
- For unfamiliar codebases or high-stakes changes, invest more time exploring than feels necessary.
|
|
13
12
|
|
|
14
13
|
## Exploration scales with risk
|
|
15
14
|
|
|
@@ -47,5 +47,5 @@ Issue one shell search at a time when the walk is large. Parallel full-tree sear
|
|
|
47
47
|
|
|
48
48
|
## Search controls
|
|
49
49
|
|
|
50
|
-
- No hook denies a walk from an unscoped root.
|
|
50
|
+
- No hook denies a walk from an unscoped root. The scope invariant above is guidance a reader follows.
|
|
51
51
|
- For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
|
package/rules/git-workflow.md
CHANGED
|
@@ -15,7 +15,7 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh`. Small
|
|
|
15
15
|
|
|
16
16
|
## Pull request submission rules
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
**Create every pull request as a draft.** Use `gh pr create --draft`.
|
|
19
19
|
|
|
20
20
|
**A release bot's PR body is machine input. Leave it alone.** Release automation reads
|
|
21
21
|
back the body of its own merged pull request to decide it owns that merge. Rewriting the
|
|
@@ -47,7 +47,7 @@ untouched.
|
|
|
47
47
|
|
|
48
48
|
## Git golden rules
|
|
49
49
|
|
|
50
|
-
1. **
|
|
50
|
+
1. **Draft before push.** Put a pull request in draft state before you push to it.
|
|
51
51
|
- Before push: `gh pr ready --undo`
|
|
52
52
|
- After review approved: `gh pr ready`
|
|
53
53
|
|
|
@@ -101,7 +101,7 @@ before you ask anyone to read or merge it.
|
|
|
101
101
|
|
|
102
102
|
## Never commit working documents or images
|
|
103
103
|
|
|
104
|
-
**
|
|
104
|
+
**Keep these files out of the repository:**
|
|
105
105
|
|
|
106
106
|
| Pattern | Reason |
|
|
107
107
|
|---------|--------|
|
|
@@ -117,10 +117,10 @@ An image a PR needs as visual evidence is not an exception to that row. Upload i
|
|
|
117
117
|
**When this applies:** GitHub PR review feedback on a branch you are fixing.
|
|
118
118
|
|
|
119
119
|
1. Fetch every reviewer comment before making any fix.
|
|
120
|
-
2. Create a
|
|
120
|
+
2. Create a checklist in the session's task tool with one item per comment.
|
|
121
121
|
3. Fix systematically, marking each todo complete.
|
|
122
122
|
4. Reply to each comment inline.
|
|
123
123
|
|
|
124
|
-
Repair only reported findings
|
|
124
|
+
Repair only the reported findings.
|
|
125
125
|
|
|
126
126
|
Every `gh` post in this workflow uses `--body-file` per `gh-cli-conventions.md` and keeps volatile scratch paths out per `durable-post-artifacts.md`. Stage session edits per `re-stage-before-commit.md` before each commit.
|
|
@@ -35,11 +35,7 @@ Authority the task already granted stays granted. A later preference about tone,
|
|
|
35
35
|
|
|
36
36
|
## Delegate and keep working
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Verify your work at intervals
|
|
41
|
-
|
|
42
|
-
On a long build, set a checkpoint cadence and hold to it. At each interval, check the work so far against the task's stated goals with a fresh-context verifier subagent. A separate verifier in a clean context catches what self-review misses.
|
|
38
|
+
Delegate a large, independent track to a subagent, and keep working while it runs in the background. Do work you can finish in a few tool calls yourself: a few reads, a handful of edits, or a simple check. Reuse a long-lived subagent across related subtasks so its context carries forward and saves repeated reads. Step in when a subagent drifts off track or is missing context.
|
|
43
39
|
|
|
44
40
|
## Ground every progress claim
|
|
45
41
|
|
|
@@ -12,5 +12,3 @@ Staging covers tracked files you edited. Do not commit untracked files unless th
|
|
|
12
12
|
- **A preceding `git add` or `git stage`.** `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
|
|
13
13
|
|
|
14
14
|
A `--amend` carries the same risk. An amend records the staged snapshot too, so an unstaged session edit is dropped the same way a plain commit drops it.
|
|
15
|
-
|
|
16
|
-
`session_file_edit_tracker` (PostToolUse) still records each Write/Edit/MultiEdit path, and `session_edit_tracker_cleanup` (SessionStart, SessionEnd) still clears the session's tracker. No gate reads that record today, so it serves as history rather than a precondition.
|
|
@@ -18,7 +18,7 @@ When a script file's literal body needs `$(...)`, author it with the Write tool,
|
|
|
18
18
|
|
|
19
19
|
## Enforcement
|
|
20
20
|
|
|
21
|
-
No PreToolUse hook denies a Bash command.
|
|
21
|
+
No PreToolUse hook denies a Bash command. The substitution constraint above is guidance a reader follows, and a permission prompt on a wrapped command is the signal that one slipped through.
|
|
22
22
|
|
|
23
23
|
One PreToolUse hook does run on a Bash command, and it only rewrites. `blocking/msys_rev_path_rewriter.py` (PreToolUse on Bash, hosted by `bash_pre_tool_use_dispatcher`) reads a git command before it runs and keeps Git Bash from converting a `<rev>:<path>` argument. That roster holds this one hook, and a test asserts its whole content, so a blocking hook added beside it fails the suite.
|
|
24
24
|
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Choose a Claude account for a runner job while protecting the main account.
|
|
3
3
|
|
|
4
|
-
The main account
|
|
5
|
-
|
|
6
|
-
account while that account has room, and waits when it has none::
|
|
7
|
-
|
|
8
|
-
main week resets in 5 hours, 20% left, 5-hour window 10% used -> main
|
|
9
|
-
main week resets in 3 days -> second
|
|
10
|
-
second at 99% of its week, main not spendable -> wait
|
|
11
|
-
main meter unread -> never main
|
|
12
|
-
|
|
13
|
-
::
|
|
14
|
-
|
|
15
|
-
python claude_account_choice.py --main-config-dir C:/Users/me/.claude
|
|
16
|
-
--second-config-dir C:/Users/me/.claude-profiles/ev
|
|
17
|
-
{"account": "second", "config_dir": "C:/Users/me/.claude-profiles/ev",
|
|
18
|
-
"reason": "second account has 60% of its week and 90% of its 5-hour window left"}
|
|
4
|
+
The main account takes a job only to spend usage that expires soon. Otherwise,
|
|
5
|
+
the picker tries extra profiles in order and waits when every one is full.
|
|
19
6
|
"""
|
|
20
7
|
|
|
21
8
|
from __future__ import annotations
|
|
@@ -23,6 +10,7 @@ from __future__ import annotations
|
|
|
23
10
|
import argparse
|
|
24
11
|
import json
|
|
25
12
|
import sys
|
|
13
|
+
from collections.abc import Sequence
|
|
26
14
|
from dataclasses import dataclass
|
|
27
15
|
from datetime import datetime
|
|
28
16
|
from pathlib import Path
|
|
@@ -52,6 +40,9 @@ from dev_env_scripts_constants.claude_account_constants import (
|
|
|
52
40
|
MAIN_SPEND_WINDOW,
|
|
53
41
|
MAIN_WEEKLY_USED_CEILING_PERCENT,
|
|
54
42
|
REASON_MAIN_EXPIRING_TEMPLATE,
|
|
43
|
+
REASON_ALL_EXTRAS_WAIT_TEMPLATE,
|
|
44
|
+
REASON_EXTRA_HAS_ROOM_TEMPLATE,
|
|
45
|
+
REASON_EXTRA_UNREAD_TEMPLATE,
|
|
55
46
|
REASON_SECOND_HAS_ROOM_TEMPLATE,
|
|
56
47
|
REASON_SECOND_UNREAD,
|
|
57
48
|
REASON_WAIT_TEMPLATE,
|
|
@@ -70,6 +61,15 @@ class AccountDecision:
|
|
|
70
61
|
reason: str
|
|
71
62
|
|
|
72
63
|
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ExtraAccount:
|
|
66
|
+
"""One ordered extra profile and its usage reading."""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
meters: AccountUsageMeters | None
|
|
70
|
+
config_dir: Path | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
73
|
def _main_spendable_reason(
|
|
74
74
|
main_meters: AccountUsageMeters | None, now: datetime
|
|
75
75
|
) -> str | None:
|
|
@@ -92,14 +92,23 @@ def _main_spendable_reason(
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
def _blocking_reset(meters: AccountUsageMeters) -> datetime | None:
|
|
96
|
+
all_blocking_resets: list[datetime] = []
|
|
97
|
+
weekly_used = meters.weekly_utilization
|
|
98
|
+
session_used = meters.session_utilization
|
|
99
|
+
if weekly_used is not None and weekly_used >= SECOND_WEEKLY_USED_CEILING_PERCENT:
|
|
100
|
+
if meters.weekly_resets_at is None:
|
|
101
|
+
return None
|
|
102
|
+
all_blocking_resets.append(meters.weekly_resets_at)
|
|
103
|
+
if session_used is not None and session_used >= SECOND_SESSION_USED_CEILING_PERCENT:
|
|
104
|
+
if meters.session_resets_at is None:
|
|
105
|
+
return None
|
|
106
|
+
all_blocking_resets.append(meters.session_resets_at)
|
|
107
|
+
return max(all_blocking_resets, default=None)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _wait_decision(second_meters: AccountUsageMeters) -> AccountDecision:
|
|
111
|
+
blocking_reset = _blocking_reset(second_meters)
|
|
103
112
|
return AccountDecision(
|
|
104
113
|
account=CHOICE_WAIT,
|
|
105
114
|
reason=REASON_WAIT_TEMPLATE.format(
|
|
@@ -112,28 +121,83 @@ def _wait_decision(
|
|
|
112
121
|
)
|
|
113
122
|
|
|
114
123
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
def _extra_account_decision(extra_account: ExtraAccount) -> AccountDecision | None:
|
|
125
|
+
meters = extra_account.meters
|
|
126
|
+
unread_reason = (
|
|
127
|
+
REASON_SECOND_UNREAD
|
|
128
|
+
if extra_account.name == CHOICE_SECOND
|
|
129
|
+
else REASON_EXTRA_UNREAD_TEMPLATE.format(account=extra_account.name)
|
|
130
|
+
)
|
|
131
|
+
if meters is None:
|
|
132
|
+
return AccountDecision(account=extra_account.name, reason=unread_reason)
|
|
133
|
+
session_used = meters.session_utilization
|
|
134
|
+
weekly_used = meters.weekly_utilization
|
|
122
135
|
if session_used is None or weekly_used is None:
|
|
123
|
-
return AccountDecision(account=
|
|
136
|
+
return AccountDecision(account=extra_account.name, reason=unread_reason)
|
|
124
137
|
is_week_blocked = weekly_used >= SECOND_WEEKLY_USED_CEILING_PERCENT
|
|
125
138
|
is_session_blocked = session_used >= SECOND_SESSION_USED_CEILING_PERCENT
|
|
126
139
|
if is_week_blocked or is_session_blocked:
|
|
127
|
-
return
|
|
140
|
+
return None
|
|
141
|
+
reason_template = (
|
|
142
|
+
REASON_SECOND_HAS_ROOM_TEMPLATE
|
|
143
|
+
if extra_account.name == CHOICE_SECOND
|
|
144
|
+
else REASON_EXTRA_HAS_ROOM_TEMPLATE
|
|
145
|
+
)
|
|
128
146
|
return AccountDecision(
|
|
129
|
-
account=
|
|
130
|
-
reason=
|
|
147
|
+
account=extra_account.name,
|
|
148
|
+
reason=reason_template.format(
|
|
149
|
+
account=extra_account.name,
|
|
131
150
|
weekly_remaining_percent=FULL_PERCENT - weekly_used,
|
|
132
151
|
session_remaining_percent=FULL_PERCENT - session_used,
|
|
133
152
|
),
|
|
134
153
|
)
|
|
135
154
|
|
|
136
155
|
|
|
156
|
+
def choose_account_from_extras(
|
|
157
|
+
*,
|
|
158
|
+
main_meters: AccountUsageMeters | None,
|
|
159
|
+
all_extra_accounts: Sequence[ExtraAccount],
|
|
160
|
+
now: datetime,
|
|
161
|
+
) -> AccountDecision:
|
|
162
|
+
"""Choose main or the first extra profile with room in list order.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
main_meters: The main account's meters, or None when unread.
|
|
166
|
+
all_extra_accounts: The extra accounts in the order they are tried.
|
|
167
|
+
now: The time the reset windows are measured from.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The chosen account and the reason, or a wait naming the soonest reset.
|
|
171
|
+
"""
|
|
172
|
+
main_reason = _main_spendable_reason(main_meters, now)
|
|
173
|
+
if main_reason is not None:
|
|
174
|
+
return AccountDecision(account=CHOICE_MAIN, reason=main_reason)
|
|
175
|
+
for each_extra_account in all_extra_accounts:
|
|
176
|
+
decision = _extra_account_decision(each_extra_account)
|
|
177
|
+
if decision is not None:
|
|
178
|
+
return decision
|
|
179
|
+
return _all_extras_wait_decision(all_extra_accounts)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _all_extras_wait_decision(
|
|
183
|
+
all_extra_accounts: Sequence[ExtraAccount],
|
|
184
|
+
) -> AccountDecision:
|
|
185
|
+
if len(all_extra_accounts) == 1 and all_extra_accounts[0].meters is not None:
|
|
186
|
+
return _wait_decision(all_extra_accounts[0].meters)
|
|
187
|
+
all_resets = (
|
|
188
|
+
_blocking_reset(each_extra_account.meters)
|
|
189
|
+
for each_extra_account in all_extra_accounts
|
|
190
|
+
if each_extra_account.meters is not None
|
|
191
|
+
)
|
|
192
|
+
next_reset = min((reset for reset in all_resets if reset is not None), default=None)
|
|
193
|
+
return AccountDecision(
|
|
194
|
+
account=CHOICE_WAIT,
|
|
195
|
+
reason=REASON_ALL_EXTRAS_WAIT_TEMPLATE.format(
|
|
196
|
+
next_reset=next_reset.isoformat() if next_reset else UNKNOWN_RESET_TEXT
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
137
201
|
def choose_account(
|
|
138
202
|
*,
|
|
139
203
|
main_meters: AccountUsageMeters | None,
|
|
@@ -159,10 +223,11 @@ def choose_account(
|
|
|
159
223
|
Returns:
|
|
160
224
|
The chosen account and the reason.
|
|
161
225
|
"""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
226
|
+
return choose_account_from_extras(
|
|
227
|
+
main_meters=main_meters,
|
|
228
|
+
all_extra_accounts=(ExtraAccount(name=CHOICE_SECOND, meters=second_meters),),
|
|
229
|
+
now=now,
|
|
230
|
+
)
|
|
166
231
|
|
|
167
232
|
|
|
168
233
|
def read_account_meters(credentials_path: Path) -> AccountUsageMeters | None:
|
|
@@ -180,6 +245,64 @@ def read_account_meters(credentials_path: Path) -> AccountUsageMeters | None:
|
|
|
180
245
|
return None
|
|
181
246
|
|
|
182
247
|
|
|
248
|
+
def read_extra_accounts(
|
|
249
|
+
all_config_directories: Sequence[Path],
|
|
250
|
+
) -> tuple[ExtraAccount, ...]:
|
|
251
|
+
"""Read ordered extra profiles and reject duplicate directories.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
all_config_directories: The extra profile homes in the order they are tried.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
One extra account per directory, with its meters.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValueError: When two entries name the same directory.
|
|
261
|
+
"""
|
|
262
|
+
all_resolved_directories = [
|
|
263
|
+
str(each_directory.resolve()).casefold()
|
|
264
|
+
for each_directory in all_config_directories
|
|
265
|
+
]
|
|
266
|
+
if len(set(all_resolved_directories)) != len(all_config_directories):
|
|
267
|
+
raise ValueError("extra account config directories must be distinct")
|
|
268
|
+
return tuple(
|
|
269
|
+
ExtraAccount(
|
|
270
|
+
name=CHOICE_SECOND if each_index == 1 else f"extra_{each_index}",
|
|
271
|
+
meters=read_account_meters(each_directory / CREDENTIALS_FILE_NAME),
|
|
272
|
+
config_dir=each_directory,
|
|
273
|
+
)
|
|
274
|
+
for each_index, each_directory in enumerate(all_config_directories, start=1)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def config_directory_for_decision(
|
|
279
|
+
decision: AccountDecision,
|
|
280
|
+
*,
|
|
281
|
+
main_config_dir: Path,
|
|
282
|
+
all_extra_accounts: Sequence[ExtraAccount],
|
|
283
|
+
) -> Path | None:
|
|
284
|
+
"""Find the selected account's config directory.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
decision: The chosen account and reason.
|
|
288
|
+
main_config_dir: The main account's Claude home.
|
|
289
|
+
all_extra_accounts: The extra accounts the decision chose from.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The chosen account's Claude home, or None on wait.
|
|
293
|
+
"""
|
|
294
|
+
if decision.account == CHOICE_MAIN:
|
|
295
|
+
return main_config_dir
|
|
296
|
+
return next(
|
|
297
|
+
(
|
|
298
|
+
each_extra_account.config_dir
|
|
299
|
+
for each_extra_account in all_extra_accounts
|
|
300
|
+
if each_extra_account.name == decision.account
|
|
301
|
+
),
|
|
302
|
+
None,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
183
306
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
184
307
|
parser = argparse.ArgumentParser(
|
|
185
308
|
description="Name the Claude account a runner job uses."
|
|
@@ -194,6 +317,7 @@ def _build_argument_parser() -> argparse.ArgumentParser:
|
|
|
194
317
|
type=Path,
|
|
195
318
|
default=default_profile_home(),
|
|
196
319
|
)
|
|
320
|
+
parser.add_argument("--extra-config-dir", type=Path, action="append", default=[])
|
|
197
321
|
return parser
|
|
198
322
|
|
|
199
323
|
|
|
@@ -253,7 +377,7 @@ def meters_payload(
|
|
|
253
377
|
|
|
254
378
|
|
|
255
379
|
def main(all_command_arguments: list[str]) -> int:
|
|
256
|
-
"""Print the chosen account and
|
|
380
|
+
"""Print the chosen account and every account's meters as JSON.
|
|
257
381
|
|
|
258
382
|
Args:
|
|
259
383
|
all_command_arguments: Command-line arguments after the program name.
|
|
@@ -261,26 +385,33 @@ def main(all_command_arguments: list[str]) -> int:
|
|
|
261
385
|
Returns:
|
|
262
386
|
Zero once the choice is printed.
|
|
263
387
|
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
388
|
+
parser = _build_argument_parser()
|
|
389
|
+
arguments = parser.parse_args(all_command_arguments)
|
|
390
|
+
try:
|
|
391
|
+
extra_accounts = read_extra_accounts(
|
|
392
|
+
(arguments.second_config_dir, *arguments.extra_config_dir)
|
|
393
|
+
)
|
|
394
|
+
except ValueError as error:
|
|
395
|
+
parser.error(str(error))
|
|
269
396
|
main_meters = read_account_meters(arguments.main_config_dir / CREDENTIALS_FILE_NAME)
|
|
270
|
-
|
|
271
|
-
arguments.second_config_dir / CREDENTIALS_FILE_NAME
|
|
272
|
-
)
|
|
273
|
-
decision = choose_account(
|
|
397
|
+
decision = choose_account_from_extras(
|
|
274
398
|
main_meters=main_meters,
|
|
275
|
-
|
|
399
|
+
all_extra_accounts=extra_accounts,
|
|
276
400
|
now=datetime.now().astimezone(),
|
|
277
401
|
)
|
|
278
|
-
config_directory =
|
|
402
|
+
config_directory = config_directory_for_decision(
|
|
403
|
+
decision,
|
|
404
|
+
main_config_dir=arguments.main_config_dir,
|
|
405
|
+
all_extra_accounts=extra_accounts,
|
|
406
|
+
)
|
|
279
407
|
report = {
|
|
280
408
|
**decision_payload(decision, config_directory=config_directory),
|
|
281
409
|
JSON_METERS_KEY: {
|
|
282
410
|
CHOICE_MAIN: meters_payload(main_meters),
|
|
283
|
-
|
|
411
|
+
**{
|
|
412
|
+
extra_account.name: meters_payload(extra_account.meters)
|
|
413
|
+
for extra_account in extra_accounts
|
|
414
|
+
},
|
|
284
415
|
},
|
|
285
416
|
}
|
|
286
417
|
print(json.dumps(report))
|