claude-dev-env 2.7.1 → 2.8.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 (85) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/process-tree/CLAUDE.md +41 -0
  6. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  8. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  9. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  10. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  11. package/agents/code-quality-agent.md +6 -5
  12. package/agents/deep-research.md +7 -24
  13. package/agents/docs-agent.md +1 -27
  14. package/agents/issue-tracker.md +1 -7
  15. package/agents/skill-writer-agent.md +1 -2
  16. package/agents/test_agent_frontmatter.py +309 -12
  17. package/hooks/blocking/CLAUDE.md +2 -0
  18. package/hooks/blocking/fable_spawn_gate.py +187 -0
  19. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  20. package/hooks/blocking/plain_language_blocker.py +287 -15
  21. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  22. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  23. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  24. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  25. package/hooks/git-hooks/CLAUDE.md +2 -2
  26. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  27. package/hooks/git-hooks/pre_push.py +343 -54
  28. package/hooks/git-hooks/test_pre_push.py +852 -6
  29. package/hooks/hooks.json +9 -19
  30. package/hooks/hooks_constants/CLAUDE.md +2 -0
  31. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  32. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  33. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  34. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  36. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  37. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  39. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  40. package/package.json +1 -1
  41. package/rules/CLAUDE.md +1 -0
  42. package/rules/ask-user-question-required.md +26 -0
  43. package/rules/claims-as-quotes.md +65 -0
  44. package/scripts/CLAUDE.md +1 -1
  45. package/scripts/_code_review_test_support.py +6 -0
  46. package/scripts/check.ps1 +18 -5
  47. package/scripts/claude_chain_runner.py +203 -31
  48. package/scripts/codec_forwarding_test_support.py +2 -0
  49. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  50. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  51. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  52. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
  53. package/scripts/grok_headless_runner.py +16 -83
  54. package/scripts/spawn_grok_batch.py +38 -1
  55. package/scripts/test_claude_chain_runner.py +358 -0
  56. package/scripts/test_grok_headless_runner.py +18 -63
  57. package/scripts/test_invoke_code_review.py +298 -0
  58. package/scripts/test_resolve_worker_spawn.py +6 -0
  59. package/scripts/test_spawn_grok_batch.py +101 -0
  60. package/scripts/tests/CLAUDE.md +1 -0
  61. package/scripts/tests/test_grok_worker_constants.py +59 -0
  62. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  63. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  64. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  65. package/skills/autoconverge/workflow/converge.mjs +66 -33
  66. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  67. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  68. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  69. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  70. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  71. package/skills/e-code-review/SKILL.md +9 -8
  72. package/skills/e-code-review/reference/fix.md +29 -7
  73. package/skills/e-code-review/reference/loop.md +230 -14
  74. package/skills/e-code-review/reference/low.md +33 -15
  75. package/skills/e-code-review/reference/medium.md +55 -21
  76. package/skills/e-code-review/reference/xhigh.md +30 -12
  77. package/skills/fresh-branch/CLAUDE.md +5 -5
  78. package/skills/fresh-branch/SKILL.md +14 -6
  79. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  80. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  81. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  82. package/skills/grok-spawn/SKILL.md +5 -0
  83. package/skills/orchestrator/SKILL.md +4 -1
  84. package/skills/orchestrator-refresh/SKILL.md +5 -1
  85. package/skills/team-advisor/SKILL.md +4 -1
@@ -17,9 +17,12 @@ if str(SCRIPTS_DIRECTORY) not in sys.path:
17
17
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
18
18
 
19
19
  from fresh_branch_scripts_constants.fresh_branch_cli_constants import (
20
+ ALL_REPOSITORY_WORKTREE_ROOT_PARTS,
21
+ CLI_FLAG_WORKTREE_ROOT,
20
22
  DEFAULT_AGENT_SLUG,
21
23
  DEFAULT_BASE_REF,
22
24
  ERROR_CLI_ARGUMENTS,
25
+ ERROR_WORKTREE_ROOT_NOT_ABSOLUTE,
23
26
  EXIT_CODE_FAILURE,
24
27
  EXIT_CODE_SUCCESS,
25
28
  FRESH_BRANCH_AGENT_ENV_VAR,
@@ -244,6 +247,40 @@ class TestResolveAgentSlug:
244
247
  module.resolve_agent_slug("../evil")
245
248
 
246
249
 
250
+ class TestResolveConfiguredWorktreeRoot:
251
+ def should_default_to_repo_claude_worktrees(
252
+ self,
253
+ tmp_path: Path,
254
+ ) -> None:
255
+ module = load_create_fresh_branch_module()
256
+ configured_root = module.resolve_configured_worktree_root(tmp_path)
257
+ expected_root = tmp_path.joinpath(*ALL_REPOSITORY_WORKTREE_ROOT_PARTS)
258
+ assert configured_root == expected_root.resolve()
259
+
260
+ def should_accept_an_absolute_root_outside_the_repo(
261
+ self,
262
+ tmp_path: Path,
263
+ ) -> None:
264
+ module = load_create_fresh_branch_module()
265
+ external_root = (tmp_path / "external-isolated").resolve()
266
+ configured_root = module.resolve_configured_worktree_root(
267
+ tmp_path / "repo",
268
+ maybe_worktree_root=str(external_root),
269
+ )
270
+ assert configured_root == external_root
271
+
272
+ def should_reject_a_relative_worktree_root(
273
+ self,
274
+ tmp_path: Path,
275
+ ) -> None:
276
+ module = load_create_fresh_branch_module()
277
+ with pytest.raises(ValueError, match="absolute path"):
278
+ module.resolve_configured_worktree_root(
279
+ tmp_path,
280
+ maybe_worktree_root="relative/worktrees",
281
+ )
282
+
283
+
247
284
  class TestResolveAgentWorktreeRoot:
248
285
  def should_nest_under_the_repository_claude_worktrees_directory(
249
286
  self,
@@ -263,6 +300,19 @@ class TestResolveAgentWorktreeRoot:
263
300
  assert grok_root != claude_root
264
301
  assert grok_root.parent == claude_root.parent
265
302
 
303
+ def should_nest_agent_under_an_explicit_configured_root(
304
+ self,
305
+ tmp_path: Path,
306
+ ) -> None:
307
+ module = load_create_fresh_branch_module()
308
+ external_root = (tmp_path / "isolated-root").resolve()
309
+ agent_root = module.resolve_agent_worktree_root(
310
+ tmp_path / "repo",
311
+ "grok",
312
+ maybe_worktree_root=str(external_root),
313
+ )
314
+ assert agent_root == external_root / "grok"
315
+
266
316
 
267
317
  class TestResolveUniqueWorktreePath:
268
318
  def should_return_preferred_when_missing(self, tmp_path: Path) -> None:
@@ -358,6 +408,146 @@ class TestBranchNamePathSafety:
358
408
  assert not (tmp_path / "unused-repo").exists()
359
409
 
360
410
 
411
+ class TestConfiguredWorktreeRootIntegration:
412
+ def should_create_worktree_under_explicit_absolute_root(
413
+ self,
414
+ tmp_path: Path,
415
+ monkeypatch: pytest.MonkeyPatch,
416
+ ) -> None:
417
+ module = load_create_fresh_branch_module()
418
+ repository_path = build_repo_with_origin(tmp_path / "repo")
419
+ configured_root = (tmp_path / "isolated-worktrees").resolve()
420
+ caller_head_before = read_head_branch(repository_path)
421
+ caller_commit_before = read_head_commit(repository_path)
422
+ success_payload = module.create_fresh_branch(
423
+ branch_name="fix/external-root",
424
+ repo_path=repository_path,
425
+ agent_slug="grok",
426
+ base_ref=DEFAULT_BASE_REF,
427
+ maybe_worktree_root=str(configured_root),
428
+ )
429
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
430
+ assert worktree_path == configured_root / "grok" / "fix" / "external-root"
431
+ assert worktree_path.is_dir()
432
+ assert worktree_path.resolve().is_relative_to(configured_root)
433
+ assert not (
434
+ repository_path.resolve()
435
+ / ".claude"
436
+ / "worktrees"
437
+ / "grok"
438
+ / "fix"
439
+ / "external-root"
440
+ ).exists()
441
+ assert read_head_branch(repository_path) == caller_head_before
442
+ assert read_head_commit(repository_path) == caller_commit_before
443
+
444
+ def should_suffix_when_path_exists_under_configured_root(
445
+ self,
446
+ tmp_path: Path,
447
+ monkeypatch: pytest.MonkeyPatch,
448
+ ) -> None:
449
+ module = load_create_fresh_branch_module()
450
+ repository_path = build_repo_with_origin(tmp_path / "repo")
451
+ configured_root = (tmp_path / "isolated-worktrees").resolve()
452
+ occupied_path = configured_root / "claude" / "fix" / "collision"
453
+ occupied_path.mkdir(parents=True)
454
+ success_payload = module.create_fresh_branch(
455
+ branch_name="fix/collision",
456
+ repo_path=repository_path,
457
+ agent_slug="claude",
458
+ base_ref=DEFAULT_BASE_REF,
459
+ maybe_worktree_root=str(configured_root),
460
+ )
461
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
462
+ assert worktree_path == configured_root / "claude" / "fix" / "collision-2"
463
+ assert worktree_path.is_dir()
464
+
465
+ def should_reject_relative_worktree_root_without_creating_worktree(
466
+ self,
467
+ tmp_path: Path,
468
+ monkeypatch: pytest.MonkeyPatch,
469
+ ) -> None:
470
+ module = load_create_fresh_branch_module()
471
+ repository_path = build_repo_with_origin(tmp_path / "repo")
472
+ with pytest.raises(ValueError, match="absolute path") as raised_error:
473
+ module.create_fresh_branch(
474
+ branch_name="fix/relative-root",
475
+ repo_path=repository_path,
476
+ agent_slug="claude",
477
+ base_ref=DEFAULT_BASE_REF,
478
+ maybe_worktree_root="relative/worktrees",
479
+ )
480
+ assert str(raised_error.value) == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
481
+ assert not (repository_path / ".claude" / "worktrees").exists()
482
+
483
+ def should_reject_relative_worktree_root_before_fetch(
484
+ self,
485
+ tmp_path: Path,
486
+ monkeypatch: pytest.MonkeyPatch,
487
+ ) -> None:
488
+ module = load_create_fresh_branch_module()
489
+ repository_path = build_repo_with_origin(tmp_path / "repo")
490
+ all_fetch_calls: list[str] = []
491
+
492
+ def record_fetch(repo_root: Path, base_ref: str) -> None:
493
+ all_fetch_calls.append(base_ref)
494
+ raise AssertionError("fetch must not run for a relative worktree root")
495
+
496
+ monkeypatch.setattr(module, "fetch_base_ref", record_fetch)
497
+ with pytest.raises(ValueError, match="absolute path") as raised_error:
498
+ module.create_fresh_branch(
499
+ branch_name="fix/relative-root-before-fetch",
500
+ repo_path=repository_path,
501
+ agent_slug="claude",
502
+ base_ref=DEFAULT_BASE_REF,
503
+ maybe_worktree_root="relative/worktrees",
504
+ )
505
+ assert str(raised_error.value) == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
506
+ assert all_fetch_calls == []
507
+
508
+ def should_fetch_base_before_creating_worktree(
509
+ self,
510
+ tmp_path: Path,
511
+ monkeypatch: pytest.MonkeyPatch,
512
+ ) -> None:
513
+ module = load_create_fresh_branch_module()
514
+ repository_path = build_repo_with_origin(tmp_path / "repo")
515
+ all_call_names: list[str] = []
516
+ real_fetch = module.fetch_base_ref
517
+ real_create = module.create_worktree_branch
518
+
519
+ def record_fetch(
520
+ repo_root: Path,
521
+ base_ref: str,
522
+ ) -> None:
523
+ all_call_names.append("fetch")
524
+ real_fetch(repo_root, base_ref)
525
+
526
+ def record_create(
527
+ repo_root: Path,
528
+ branch_name: str,
529
+ worktree_path: Path,
530
+ base_ref: str,
531
+ ) -> None:
532
+ all_call_names.append("create")
533
+ real_create(
534
+ repo_root,
535
+ branch_name=branch_name,
536
+ worktree_path=worktree_path,
537
+ base_ref=base_ref,
538
+ )
539
+
540
+ monkeypatch.setattr(module, "fetch_base_ref", record_fetch)
541
+ monkeypatch.setattr(module, "create_worktree_branch", record_create)
542
+ module.create_fresh_branch(
543
+ branch_name="fix/fetch-order",
544
+ repo_path=repository_path,
545
+ agent_slug="claude",
546
+ base_ref=DEFAULT_BASE_REF,
547
+ )
548
+ assert all_call_names == ["fetch", "create"]
549
+
550
+
361
551
  class TestCreateFreshBranchIntegration:
362
552
  def should_create_worktree_branch_without_moving_caller_head(
363
553
  self,
@@ -670,3 +860,64 @@ class TestMainCli:
670
860
  assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
671
861
  assert not outside_target.exists()
672
862
  assert not (tmp_path / "unused-repo").exists()
863
+
864
+ def should_print_error_json_for_relative_worktree_root(
865
+ self,
866
+ tmp_path: Path,
867
+ monkeypatch: pytest.MonkeyPatch,
868
+ capsys: pytest.CaptureFixture[str],
869
+ ) -> None:
870
+ module = load_create_fresh_branch_module()
871
+ repository_path = build_repo_with_origin(tmp_path / "repo")
872
+ monkeypatch.setattr(
873
+ sys,
874
+ "argv",
875
+ [
876
+ "create_fresh_branch.py",
877
+ "--branch-name",
878
+ "feat/relative-root",
879
+ "--repo",
880
+ str(repository_path),
881
+ "--agent",
882
+ "claude",
883
+ CLI_FLAG_WORKTREE_ROOT,
884
+ "relative/worktrees",
885
+ ],
886
+ )
887
+ exit_code = module.main()
888
+ captured = capsys.readouterr()
889
+ assert exit_code == EXIT_CODE_FAILURE
890
+ error_payload = json.loads(captured.out)
891
+ assert error_payload[PAYLOAD_KEY_ERROR] == ERROR_WORKTREE_ROOT_NOT_ABSOLUTE
892
+
893
+ def should_print_success_json_for_absolute_worktree_root(
894
+ self,
895
+ tmp_path: Path,
896
+ monkeypatch: pytest.MonkeyPatch,
897
+ capsys: pytest.CaptureFixture[str],
898
+ ) -> None:
899
+ module = load_create_fresh_branch_module()
900
+ repository_path = build_repo_with_origin(tmp_path / "repo")
901
+ configured_root = (tmp_path / "cli-isolated").resolve()
902
+ monkeypatch.setattr(
903
+ sys,
904
+ "argv",
905
+ [
906
+ "create_fresh_branch.py",
907
+ "--branch-name",
908
+ "feat/cli-root",
909
+ "--repo",
910
+ str(repository_path),
911
+ "--agent",
912
+ "grok",
913
+ CLI_FLAG_WORKTREE_ROOT,
914
+ str(configured_root),
915
+ ],
916
+ )
917
+ exit_code = module.main()
918
+ captured = capsys.readouterr()
919
+ assert exit_code == EXIT_CODE_SUCCESS
920
+ success_payload = json.loads(captured.out)
921
+ worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
922
+ assert worktree_path == configured_root / "grok" / "feat" / "cli-root"
923
+ assert worktree_path.is_dir()
@@ -148,6 +148,11 @@ Workers run with no turn cap. The timeout is the only bound on a worker's
148
148
  length, and a worker that hits it is killed with its whole process tree and
149
149
  reported as `timeout`.
150
150
 
151
+ A worker entry accepts these fields and no others. Any other key fails the
152
+ load with an error naming the stray key and listing the accepted set, so a
153
+ misspelled field shows up at startup rather than passing for a setting that
154
+ took effect.
155
+
151
156
  Put the spec file under the run state directory (or any path you pass to
152
157
  `--spec`).
153
158
 
@@ -129,7 +129,10 @@ pending, or when the tool is `CronCreate`.
129
129
  whole lifecycle (its Lifecycle ownership section); executors only ever
130
130
  message the warm agent or report here, and an executor that finds the
131
131
  advisor unreachable reports that upward — it never spawns a
132
- replacement itself.
132
+ replacement itself. A **Fable**-tier attempt carries the exact token
133
+ `FABLE-SPAWN-AUTHORIZED` in its spawn prompt, as the protocol's
134
+ warm-up rule states; `hooks/blocking/fable_spawn_gate.py` denies a
135
+ fable spawn whose prompt lacks it.
133
136
  3. **Write the run artifacts** (next section) before the first spawn.
134
137
  4. **Activate status_gate** when the first open ledger task exists:
135
138
  `python scripts/status_gate.py set --status active`.
@@ -89,7 +89,11 @@ once, and reports the unreachable advisor.
89
89
  - **Claude host:** executors consult the warm `session-advisor` via
90
90
  `SendMessage` (ENDORSE / CORRECTION / PLAN / STOP). This session
91
91
  routes the same way; keep tool use to orchestration and light
92
- verification reads.
92
+ verification reads. A drift re-spawn at the **Fable** tier carries
93
+ the exact token `FABLE-SPAWN-AUTHORIZED` in its fresh prompt, as
94
+ the protocol's warm-up rule states;
95
+ `hooks/blocking/fable_spawn_gate.py` denies a fable spawn whose
96
+ prompt lacks it.
93
97
  - **Third-party host:** advisor is a max-tier Claude CLI bind owned
94
98
  by this session (`claude_chain_runner.py`, Fable high then Opus
95
99
  max). Do **not** spawn `session-advisor` via Agent. Executors
@@ -27,7 +27,10 @@ simply this session's own tier — no routing table to take a max against.
27
27
  warm-up spawn and charter, the consult format and cadence, drift-respawn, and
28
28
  the CLI fallback — using `team-advisor-agent` as the name and this session as
29
29
  the only consumer (skip the "who you are and your assignment" opener in each
30
- consult; a single-consumer session doesn't need it).
30
+ consult; a single-consumer session doesn't need it). A **Fable**-tier attempt
31
+ carries the exact token `FABLE-SPAWN-AUTHORIZED` in its spawn prompt, as the
32
+ protocol's warm-up rule states; `hooks/blocking/fable_spawn_gate.py` denies a
33
+ fable spawn whose prompt lacks it.
31
34
 
32
35
  **Third-party host:** bind a max-tier Claude advisor through the shared CLI Claude-chain
33
36
  in the protocol (Fable max, then Opus max; `claude_chain_runner.py` ranks