claude-dev-env 2.5.0 → 2.7.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 (139) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/agents/CLAUDE.md +1 -1
  14. package/agents/code-verifier.md +36 -7
  15. package/bin/codex-compat.mjs +104 -0
  16. package/bin/codex-compat.test.mjs +51 -0
  17. package/codex-capability-map.json +13 -0
  18. package/docs/CODE_RULES.md +2 -0
  19. package/docs/codex-compatibility.md +25 -0
  20. package/docs/nas-ssh-invocation.md +96 -12
  21. package/docs/references/code-review-enforcement.md +31 -6
  22. package/hooks/blocking/CLAUDE.md +3 -0
  23. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  24. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  25. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  26. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  27. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  28. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  29. package/hooks/blocking/state_description_blocker.py +1 -1
  30. package/hooks/blocking/stop_dispatcher.py +1 -1
  31. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  32. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  33. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  34. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  35. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  36. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  37. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  38. package/hooks/git-hooks/CLAUDE.md +3 -0
  39. package/hooks/git-hooks/conftest.py +30 -0
  40. package/hooks/git-hooks/gate_utils.py +2 -2
  41. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  42. package/hooks/git-hooks/pre_push.py +75 -4
  43. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  44. package/hooks/git-hooks/test_config.py +0 -15
  45. package/hooks/git-hooks/test_gate_utils.py +3 -15
  46. package/hooks/git-hooks/test_pre_commit.py +1 -15
  47. package/hooks/git-hooks/test_pre_push.py +236 -27
  48. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  49. package/hooks/hooks.json +0 -12
  50. package/hooks/hooks_constants/CLAUDE.md +5 -1
  51. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  52. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  53. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  54. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  55. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  56. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  57. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  58. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  59. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  60. package/package.json +4 -2
  61. package/rules/CLAUDE.md +17 -23
  62. package/rules/agent-spawn-protocol.md +6 -6
  63. package/rules/anti-corollary-tests.md +1 -1
  64. package/rules/bdd.md +1 -1
  65. package/rules/cleanup-temp-files.md +10 -4
  66. package/rules/code-standards.md +7 -0
  67. package/rules/conservative-action.md +1 -5
  68. package/rules/context7.md +0 -4
  69. package/rules/destructive-commands.md +47 -0
  70. package/rules/doc-inventory-integrity.md +48 -0
  71. package/rules/doc-prose-cuts.md +58 -0
  72. package/rules/docstring-prose-matches-implementation.md +10 -2
  73. package/rules/durable-post-artifacts.md +0 -4
  74. package/rules/eli11-replies.md +31 -0
  75. package/rules/explore-thoroughly.md +4 -4
  76. package/rules/falsify-before-green.md +68 -0
  77. package/rules/file-global-constants.md +1 -1
  78. package/rules/filesystem-search.md +51 -0
  79. package/rules/gh-cli-conventions.md +27 -0
  80. package/rules/git-workflow.md +26 -0
  81. package/rules/hedging-claims.md +9 -0
  82. package/rules/long-horizon-autonomy.md +0 -4
  83. package/rules/measurement-denominators.md +48 -0
  84. package/rules/nas-ssh-invocation.md +23 -5
  85. package/rules/parallel-tools.md +2 -2
  86. package/rules/plain-illustrative-docstrings.md +3 -7
  87. package/rules/plain-language.md +2 -0
  88. package/rules/proof-of-work-pr-comments.md +0 -4
  89. package/rules/re-stage-before-commit.md +2 -0
  90. package/rules/research-mode.md +10 -0
  91. package/rules/shell-invocation.md +21 -0
  92. package/rules/testing.md +4 -0
  93. package/rules/verified-commit-gate-skip.md +3 -27
  94. package/rules/verify-before-asking.md +5 -0
  95. package/rules/windows-filesystem-safe.md +1 -1
  96. package/rules/workers-done-before-complete.md +4 -0
  97. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  98. package/scripts/codex_capability_bridge.py +171 -0
  99. package/scripts/codex_compat_materializer.py +1087 -0
  100. package/scripts/codex_compat_watcher.py +502 -0
  101. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  102. package/scripts/invoke_code_review.py +11 -4
  103. package/scripts/sync_to_cursor/rules.py +0 -10
  104. package/scripts/test_invoke_code_review.py +143 -0
  105. package/scripts/test_invoke_code_review_chain.py +1 -1
  106. package/scripts/test_invoke_code_review_contract.py +1 -1
  107. package/scripts/tests/test_code_review_constants.py +80 -0
  108. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  109. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  110. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  111. package/scripts/tests/test_sync_to_cursor.py +0 -1
  112. package/skills/autoconverge/workflow/converge.mjs +1 -1
  113. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  114. package/skills/condensing-instructions/SKILL.md +42 -51
  115. package/skills/fresh-branch/CLAUDE.md +1 -1
  116. package/skills/fresh-branch/SKILL.md +5 -6
  117. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  118. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  119. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  120. package/skills/orchestrator/SKILL.md +23 -9
  121. package/skills/orchestrator-refresh/SKILL.md +20 -1
  122. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  123. package/skills/session-log/SKILL.md +1 -1
  124. package/rules/claude-md-orphan-file.md +0 -28
  125. package/rules/cleanup-command-forms.md +0 -23
  126. package/rules/code-reviews.md +0 -11
  127. package/rules/env-var-table-code-drift.md +0 -10
  128. package/rules/gh-body-file.md +0 -5
  129. package/rules/gh-paginate.md +0 -3
  130. package/rules/hook-prose-matches-detector.md +0 -15
  131. package/rules/no-historical-clutter.md +0 -26
  132. package/rules/no-inline-destructive-literals.md +0 -9
  133. package/rules/no-justification-noise.md +0 -61
  134. package/rules/package-inventory-stale-entry.md +0 -25
  135. package/rules/right-sized-engineering.md +0 -28
  136. package/rules/self-contained-docs.md +0 -17
  137. package/rules/shell-invocation-policy.md +0 -5
  138. package/rules/state-what-is.md +0 -25
  139. package/rules/tdd.md +0 -7
@@ -245,28 +245,23 @@ class TestResolveAgentSlug:
245
245
 
246
246
 
247
247
  class TestResolveAgentWorktreeRoot:
248
- def should_use_userprofile_scratch_on_windows(
248
+ def should_nest_under_the_repository_claude_worktrees_directory(
249
249
  self,
250
- monkeypatch: pytest.MonkeyPatch,
250
+ tmp_path: Path,
251
251
  ) -> None:
252
252
  module = load_create_fresh_branch_module()
253
- monkeypatch.setattr(module.sys, "platform", "win32")
254
- monkeypatch.setenv("USERPROFILE", r"C:\Users\example")
255
- worktree_root = module.resolve_agent_worktree_root("grok")
256
- assert worktree_root == (
257
- Path(r"C:\Users\example") / "AppData" / "Local" / "Temp" / "grok"
258
- )
253
+ worktree_root = module.resolve_agent_worktree_root(tmp_path, "grok")
254
+ assert worktree_root == tmp_path / ".claude" / "worktrees" / "grok"
259
255
 
260
- def should_fall_back_to_gettempdir_off_windows(
256
+ def should_keep_each_agent_in_its_own_subdirectory(
261
257
  self,
262
- monkeypatch: pytest.MonkeyPatch,
263
258
  tmp_path: Path,
264
259
  ) -> None:
265
260
  module = load_create_fresh_branch_module()
266
- monkeypatch.setattr(module.sys, "platform", "linux")
267
- monkeypatch.setattr(module.tempfile, "gettempdir", lambda: str(tmp_path))
268
- worktree_root = module.resolve_agent_worktree_root("claude")
269
- assert worktree_root == tmp_path / "claude"
261
+ grok_root = module.resolve_agent_worktree_root(tmp_path, "grok")
262
+ claude_root = module.resolve_agent_worktree_root(tmp_path, "claude")
263
+ assert grok_root != claude_root
264
+ assert grok_root.parent == claude_root.parent
270
265
 
271
266
 
272
267
  class TestResolveUniqueWorktreePath:
@@ -301,18 +296,9 @@ class TestAgentSlugPathSafety:
301
296
  def should_reject_parent_segment_agent_slug_without_mkdir_outside(
302
297
  self,
303
298
  tmp_path: Path,
304
- monkeypatch: pytest.MonkeyPatch,
305
299
  ) -> None:
306
300
  module = load_create_fresh_branch_module()
307
- agent_scratch_parent = tmp_path / "agent-scratch"
308
- agent_scratch_parent.mkdir()
309
- escaped_agent_root = (agent_scratch_parent / ".." / "escape-agent").resolve()
310
- monkeypatch.setattr(module.sys, "platform", "linux")
311
- monkeypatch.setattr(
312
- module.tempfile,
313
- "gettempdir",
314
- lambda: str(agent_scratch_parent),
315
- )
301
+ escaped_agent_root = (tmp_path / "escape-agent").resolve()
316
302
  with pytest.raises(ValueError, match="agent slug"):
317
303
  module.create_fresh_branch(
318
304
  branch_name="fix/safe-branch",
@@ -321,22 +307,13 @@ class TestAgentSlugPathSafety:
321
307
  base_ref=DEFAULT_BASE_REF,
322
308
  )
323
309
  assert not escaped_agent_root.exists()
324
- assert not (agent_scratch_parent / "claude").exists()
310
+ assert not (tmp_path / "unused-repo").exists()
325
311
 
326
312
  def should_reject_empty_agent_slug(
327
313
  self,
328
314
  tmp_path: Path,
329
- monkeypatch: pytest.MonkeyPatch,
330
315
  ) -> None:
331
316
  module = load_create_fresh_branch_module()
332
- agent_scratch_parent = tmp_path / "agent-scratch"
333
- agent_scratch_parent.mkdir()
334
- monkeypatch.setattr(module.sys, "platform", "linux")
335
- monkeypatch.setattr(
336
- module.tempfile,
337
- "gettempdir",
338
- lambda: str(agent_scratch_parent),
339
- )
340
317
  with pytest.raises(ValueError, match="agent slug"):
341
318
  module.create_fresh_branch(
342
319
  branch_name="fix/safe-branch",
@@ -344,25 +321,16 @@ class TestAgentSlugPathSafety:
344
321
  agent_slug="",
345
322
  base_ref=DEFAULT_BASE_REF,
346
323
  )
347
- assert not any(agent_scratch_parent.iterdir())
324
+ assert not (tmp_path / "unused-repo").exists()
348
325
 
349
326
 
350
327
  class TestBranchNamePathSafety:
351
328
  def should_reject_parent_segments_without_mkdir_outside(
352
329
  self,
353
330
  tmp_path: Path,
354
- monkeypatch: pytest.MonkeyPatch,
355
331
  ) -> None:
356
332
  module = load_create_fresh_branch_module()
357
- agent_scratch_parent = tmp_path / "agent-scratch"
358
- agent_scratch_parent.mkdir()
359
- monkeypatch.setattr(module.sys, "platform", "linux")
360
- monkeypatch.setattr(
361
- module.tempfile,
362
- "gettempdir",
363
- lambda: str(agent_scratch_parent),
364
- )
365
- escaped_path = agent_scratch_parent / "escape"
333
+ escaped_path = tmp_path / "escape"
366
334
  with pytest.raises(ValueError, match="relative path"):
367
335
  module.create_fresh_branch(
368
336
  branch_name="fix/../../escape",
@@ -371,23 +339,14 @@ class TestBranchNamePathSafety:
371
339
  base_ref=DEFAULT_BASE_REF,
372
340
  )
373
341
  assert not escaped_path.exists()
374
- assert not (agent_scratch_parent / "claude").exists()
342
+ assert not (tmp_path / "unused-repo").exists()
375
343
 
376
344
  def should_reject_absolute_branch_without_mkdir_outside(
377
345
  self,
378
346
  tmp_path: Path,
379
- monkeypatch: pytest.MonkeyPatch,
380
347
  ) -> None:
381
348
  module = load_create_fresh_branch_module()
382
- agent_scratch_parent = tmp_path / "agent-scratch"
383
- agent_scratch_parent.mkdir()
384
349
  outside_target = tmp_path / "outside-evil"
385
- monkeypatch.setattr(module.sys, "platform", "linux")
386
- monkeypatch.setattr(
387
- module.tempfile,
388
- "gettempdir",
389
- lambda: str(agent_scratch_parent),
390
- )
391
350
  with pytest.raises(ValueError, match="relative path"):
392
351
  module.create_fresh_branch(
393
352
  branch_name=str(outside_target),
@@ -396,7 +355,7 @@ class TestBranchNamePathSafety:
396
355
  base_ref=DEFAULT_BASE_REF,
397
356
  )
398
357
  assert not outside_target.exists()
399
- assert not (agent_scratch_parent / "claude").exists()
358
+ assert not (tmp_path / "unused-repo").exists()
400
359
 
401
360
 
402
361
  class TestCreateFreshBranchIntegration:
@@ -409,13 +368,6 @@ class TestCreateFreshBranchIntegration:
409
368
  repository_path = build_repo_with_origin(tmp_path / "repo")
410
369
  caller_head_before = read_head_branch(repository_path)
411
370
  caller_commit_before = read_head_commit(repository_path)
412
- agent_scratch_parent = tmp_path / "agent-scratch"
413
- monkeypatch.setattr(module.sys, "platform", "linux")
414
- monkeypatch.setattr(
415
- module.tempfile,
416
- "gettempdir",
417
- lambda: str(agent_scratch_parent),
418
- )
419
371
  success_payload = module.create_fresh_branch(
420
372
  branch_name="fix/example-one",
421
373
  repo_path=repository_path,
@@ -424,7 +376,14 @@ class TestCreateFreshBranchIntegration:
424
376
  )
425
377
  worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
426
378
  assert worktree_path.is_dir()
427
- assert worktree_path == agent_scratch_parent / "grok" / "fix" / "example-one"
379
+ assert worktree_path == (
380
+ repository_path.resolve()
381
+ / ".claude"
382
+ / "worktrees"
383
+ / "grok"
384
+ / "fix"
385
+ / "example-one"
386
+ )
428
387
  assert read_head_branch(worktree_path) == "fix/example-one"
429
388
  assert read_head_commit(worktree_path) == caller_commit_before
430
389
  assert success_payload[PAYLOAD_KEY_BRANCH] == "fix/example-one"
@@ -445,14 +404,10 @@ class TestCreateFreshBranchIntegration:
445
404
  ) -> None:
446
405
  module = load_create_fresh_branch_module()
447
406
  repository_path = build_repo_with_origin(tmp_path / "repo")
448
- agent_scratch_parent = tmp_path / "agent-scratch"
449
- monkeypatch.setattr(module.sys, "platform", "linux")
450
- monkeypatch.setattr(
451
- module.tempfile,
452
- "gettempdir",
453
- lambda: str(agent_scratch_parent),
407
+ repository_worktree_root = (
408
+ repository_path.resolve() / ".claude" / "worktrees" / "claude"
454
409
  )
455
- occupied_path = agent_scratch_parent / "claude" / "fix" / "collision"
410
+ occupied_path = repository_worktree_root / "fix" / "collision"
456
411
  occupied_path.mkdir(parents=True)
457
412
  success_payload = module.create_fresh_branch(
458
413
  branch_name="fix/collision",
@@ -461,7 +416,7 @@ class TestCreateFreshBranchIntegration:
461
416
  base_ref=DEFAULT_BASE_REF,
462
417
  )
463
418
  worktree_path = Path(success_payload[PAYLOAD_KEY_WORKTREE_PATH])
464
- assert worktree_path == agent_scratch_parent / "claude" / "fix" / "collision-2"
419
+ assert worktree_path == repository_worktree_root / "fix" / "collision-2"
465
420
  assert worktree_path.is_dir()
466
421
  assert read_head_branch(worktree_path) == "fix/collision"
467
422
 
@@ -472,13 +427,6 @@ class TestCreateFreshBranchIntegration:
472
427
  ) -> None:
473
428
  module = load_create_fresh_branch_module()
474
429
  repository_path = build_repo_with_origin(tmp_path / "repo")
475
- agent_scratch_parent = tmp_path / "agent-scratch"
476
- monkeypatch.setattr(module.sys, "platform", "linux")
477
- monkeypatch.setattr(
478
- module.tempfile,
479
- "gettempdir",
480
- lambda: str(agent_scratch_parent),
481
- )
482
430
  module.create_fresh_branch(
483
431
  branch_name="fix/dup",
484
432
  repo_path=repository_path,
@@ -515,13 +463,6 @@ class TestCreateFreshBranchIntegration:
515
463
  )
516
464
  local_main_commit = read_head_commit(repository_path)
517
465
  assert local_main_commit != origin_tip_before
518
- agent_scratch_parent = tmp_path / "agent-scratch"
519
- monkeypatch.setattr(module.sys, "platform", "linux")
520
- monkeypatch.setattr(
521
- module.tempfile,
522
- "gettempdir",
523
- lambda: str(agent_scratch_parent),
524
- )
525
466
  success_payload = module.create_fresh_branch(
526
467
  branch_name="fix/bare-base",
527
468
  repo_path=repository_path,
@@ -543,13 +484,6 @@ class TestWorktreeBranchTracking:
543
484
  ) -> None:
544
485
  module = load_create_fresh_branch_module()
545
486
  repository_path = build_repo_with_origin(tmp_path / "repo")
546
- agent_scratch_parent = tmp_path / "agent-scratch"
547
- monkeypatch.setattr(module.sys, "platform", "linux")
548
- monkeypatch.setattr(
549
- module.tempfile,
550
- "gettempdir",
551
- lambda: str(agent_scratch_parent),
552
- )
553
487
  success_payload = module.create_fresh_branch(
554
488
  branch_name="fix/no-upstream",
555
489
  repo_path=repository_path,
@@ -574,13 +508,6 @@ class TestWorktreeBranchTracking:
574
508
  repository_path,
575
509
  empty_hooks_path=empty_hooks_path,
576
510
  )
577
- agent_scratch_parent = tmp_path / "agent-scratch"
578
- monkeypatch.setattr(module.sys, "platform", "linux")
579
- monkeypatch.setattr(
580
- module.tempfile,
581
- "gettempdir",
582
- lambda: str(agent_scratch_parent),
583
- )
584
511
  success_payload = module.create_fresh_branch(
585
512
  branch_name="feature/silent-push",
586
513
  repo_path=repository_path,
@@ -619,13 +546,6 @@ class TestMainCli:
619
546
  ) -> None:
620
547
  module = load_create_fresh_branch_module()
621
548
  repository_path = build_repo_with_origin(tmp_path / "repo")
622
- agent_scratch_parent = tmp_path / "agent-scratch"
623
- monkeypatch.setattr(module.sys, "platform", "linux")
624
- monkeypatch.setattr(
625
- module.tempfile,
626
- "gettempdir",
627
- lambda: str(agent_scratch_parent),
628
- )
629
549
  monkeypatch.setattr(
630
550
  sys,
631
551
  "argv",
@@ -697,15 +617,7 @@ class TestMainCli:
697
617
  ) -> None:
698
618
  module = load_create_fresh_branch_module()
699
619
  repository_path = build_repo_with_origin(tmp_path / "repo")
700
- agent_scratch_parent = tmp_path / "agent-scratch"
701
- agent_scratch_parent.mkdir()
702
- escaped_path = agent_scratch_parent / "escape"
703
- monkeypatch.setattr(module.sys, "platform", "linux")
704
- monkeypatch.setattr(
705
- module.tempfile,
706
- "gettempdir",
707
- lambda: str(agent_scratch_parent),
708
- )
620
+ escaped_path = tmp_path / "escape"
709
621
  monkeypatch.setattr(
710
622
  sys,
711
623
  "argv",
@@ -726,7 +638,7 @@ class TestMainCli:
726
638
  assert PAYLOAD_KEY_ERROR in error_payload
727
639
  assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
728
640
  assert not escaped_path.exists()
729
- assert not (agent_scratch_parent / "claude").exists()
641
+ assert not (tmp_path / "unused-repo").exists()
730
642
 
731
643
  def should_print_error_json_for_absolute_branch_name(
732
644
  self,
@@ -736,15 +648,7 @@ class TestMainCli:
736
648
  ) -> None:
737
649
  module = load_create_fresh_branch_module()
738
650
  repository_path = build_repo_with_origin(tmp_path / "repo")
739
- agent_scratch_parent = tmp_path / "agent-scratch"
740
- agent_scratch_parent.mkdir()
741
651
  outside_target = tmp_path / "outside-evil"
742
- monkeypatch.setattr(module.sys, "platform", "linux")
743
- monkeypatch.setattr(
744
- module.tempfile,
745
- "gettempdir",
746
- lambda: str(agent_scratch_parent),
747
- )
748
652
  monkeypatch.setattr(
749
653
  sys,
750
654
  "argv",
@@ -765,4 +669,4 @@ class TestMainCli:
765
669
  assert PAYLOAD_KEY_ERROR in error_payload
766
670
  assert "relative path" in error_payload[PAYLOAD_KEY_ERROR]
767
671
  assert not outside_target.exists()
768
- assert not (agent_scratch_parent / "claude").exists()
672
+ assert not (tmp_path / "unused-repo").exists()
@@ -64,6 +64,16 @@ schedule prompt must carry it: `/orchestrator-refresh --run-slug SLUG`.
64
64
 
65
65
  ### Single-pending re-arm protocol (all hosts)
66
66
 
67
+ **The re-arm never interrupts the run.** Every "stop" in the five steps
68
+ below ends the *re-arm* and nothing else: the session returns to
69
+ orchestrating in the same turn. Arming a delayed wake schedules a later
70
+ reminder; it neither ends the turn nor pauses in-flight executors, and
71
+ the session never waits for the refresh to fire before it carries on.
72
+ When a create fails, keep orchestrating and re-arm at the next natural
73
+ break. When the denial is `rearm_already_pending`, a refresh is already
74
+ queued — keep orchestrating and arm nothing further this turn; the next
75
+ refresh firing clears the latch and arms again.
76
+
67
77
  Exactly one delayed refresh may be outstanding. **Create then claim**
68
78
  (order matters on Claude: PreToolUse denies `ScheduleWakeup` when the
69
79
  slot is already pending).
@@ -105,11 +115,11 @@ pending, or when the tool is `CronCreate`.
105
115
  1. **Invocation guard.** One `/orchestrator` per session. When a refresh
106
116
  one-shot is already queued (`should-reschedule` exits 1 with
107
117
  `rearm_already_pending`), do not stack a second: reuse the live
108
- advisor bind and go to step 6 (Orchestrate). Skip steps 4–5 status
109
- is already active and a re-arm is already latched; re-registering
110
- would attempt a redundant host schedule. (Re-asserting
111
- `set --status active` preserves `rearm_pending` when already active,
112
- but still do not run step 5.)
118
+ advisor bind, skip step 4 and the re-arm half of step 5, and carry on
119
+ from step 5's dispatch — status is already active and a re-arm is
120
+ already latched, so a second registration would stack a duplicate
121
+ host schedule. (Re-asserting `set --status active` preserves
122
+ `rearm_pending` when already active, but still do not re-arm.)
113
123
  2. **Bind the shared advisor before any executor.** Follow
114
124
  [`_shared/advisor/advisor-protocol.md`](../../_shared/advisor/advisor-protocol.md)
115
125
  end to end: detect the host profile, compute the floor from the
@@ -123,11 +133,15 @@ pending, or when the tool is `CronCreate`.
123
133
  3. **Write the run artifacts** (next section) before the first spawn.
124
134
  4. **Activate status_gate** when the first open ledger task exists:
125
135
  `python scripts/status_gate.py set --status active`.
126
- 5. **Register the discipline reminder** via the single-pending re-arm
127
- protocol (cancel matching `should-reschedule` one non-recurring
128
- delayed wake → `claim-rearm`; default delay about 2700s).
136
+ 5. **Dispatch the first task with its ticket** (Spawn ticket section),
137
+ **then register the discipline reminder** via the single-pending
138
+ re-arm protocol (cancel matching → `should-reschedule` one
139
+ non-recurring delayed wake → `claim-rearm`; default delay about
140
+ 2700s). Spawn before you arm, so the run is already moving, and go
141
+ straight on to step 6 in the same turn — the armed wake is a later
142
+ reminder, not the next thing to wait for.
129
143
  6. **Orchestrate.** Hold the plan and the user conversation. Spawn each
130
- task with a ticket (Spawn ticket section), keep driving while
144
+ remaining task with a ticket (Spawn ticket section), keep driving while
131
145
  executors work, and keep the ledger reconciled (Task ledger
132
146
  discipline).
133
147
  7. **Consult the advisor at hard decisions.** The trigger list, consult
@@ -33,7 +33,7 @@ python <status_gate.py> begin-firing [--run-slug SLUG]
33
33
 
34
34
  | Exit | Action |
35
35
  |---|---|
36
- | **1** | Stop. Cancel matching host schedules for `/orchestrator-refresh` if the host allows. Report inactive/done. **Do not** re-arm. Do not spawn. |
36
+ | **1** | End the refresh. Cancel matching host schedules for `/orchestrator-refresh` if the host allows. Report inactive/done. **Do not** re-arm. Do not spawn. Any work already in flight keeps running. |
37
37
  | **0** | Latch cleared. Continue with steps 1–6. |
38
38
 
39
39
  ### 0b. Done after ledger (step 1)
@@ -47,6 +47,25 @@ python <status_gate.py> set --status done [--run-slug SLUG]
47
47
 
48
48
  Cancel matching host schedules; stop without re-arming.
49
49
 
50
+ ## The refresh never interrupts the run
51
+
52
+ A refresh firing reinforces discipline alongside work already in flight.
53
+ It never pauses, cancels, or waits on a running executor. Reconcile the
54
+ ledger, re-assert the routing, re-arm once, and hand control straight back
55
+ to the work in progress.
56
+
57
+ Inside the re-arm protocol (step 6), every "stop" ends the *re-arm* and
58
+ nothing else. A `should-reschedule` exit 1 means no schedule is created this
59
+ firing, and a `claim-rearm` exit 1 means the schedule just created is
60
+ cancelled; either way the session keeps orchestrating in the same turn.
61
+
62
+ Two stops end the whole firing, and both leave running executors alone:
63
+ `begin-firing` exit 1 (step 0a) and the done branch (step 0b). Each means the
64
+ run is finished, not active, or has no readable status file, so the refresh
65
+ reports and adds nothing further. A fail-closed advisor bind (step 3) stops
66
+ advisor consultation alone; the firing still reconciles the ledger, re-arms
67
+ once, and reports the unreachable advisor.
68
+
50
69
  ## Discipline steps
51
70
 
52
71
  1. **Reconcile the task ledger first.** Call `TaskList` after the gate.
@@ -49,7 +49,7 @@ Review each hit. Ignore:
49
49
  ## 4. Re-check before commit / post
50
50
 
51
51
  - Stage only clean files
52
- - Prefer `--body-file` for `gh` posts (also required by the gh-body-file rule)
52
+ - Prefer `--body-file` for `gh` posts (also required by the gh-cli-conventions rule)
53
53
  - Let `pii_prevention_blocker` re-run on the next Write / commit / post
54
54
 
55
55
  ## Accepted residual (do not over-scrub)
@@ -25,7 +25,7 @@ Sessions come in many shapes — convergence loops, feature builds, research div
25
25
  - **The `Artifact` tool wraps the file in a document skeleton at publish time.** It adds `<!doctype html>…<head>…</head><body>` around whatever the file holds — write page content only (a `<title>`, a `<style>` block, and the body markup). Do not include `<!DOCTYPE>`, `<html>`, `<head>`, or `<body>` tags in the file itself.
26
26
  - **The `Artifact` tool redeploys to the same URL only within the current run.** Editing the session's HTML file in a later step of this same skill run and calling `Artifact` again does not mint a new URL — it updates the same published page in place. A fresh session has no memory of an artifact's URL from an earlier run, so republishing a session report written in a prior session always mints a new URL (there is no other way to target the old one without the user supplying it).
27
27
  - **Vault paths sit outside `.claude/`.** Headless vault paths (e.g., `$OBSIDIAN_VAULT_PATH`) resolve outside the project tree. Session reports use HTML regardless of vault location — the Artifact tool needs a written file to publish.
28
- - **Sessions describe current state by convention.** The state_description_blocker hook does not scan .html, but the rule at `~/.claude/rules/no-historical-clutter.md` applies as a writing standard — skip historical and comparative language when composing the report; the rule file lists the full trigger set.
28
+ - **Sessions describe current state by convention.** The state_description_blocker hook does not scan .html, but the rule at `~/.claude/rules/doc-prose-cuts.md` applies as a writing standard — skip historical and comparative language when composing the report; the rule file lists the full trigger set.
29
29
  - **`write_existing_file_blocker` rejects Write on existing paths.** Use Write only when creating a fresh session report; use Edit for the vault-context append in step 3.
30
30
  - **Obsidian frontmatter index is HTML-blind.** Obsidian's native YAML-frontmatter parser reads only `.md` files. HTML files do not appear in Obsidian's frontmatter index. Search by content still works; search by `type: session-report` does not.
31
31
 
@@ -1,28 +0,0 @@
1
- ---
2
- paths:
3
- - "**/CLAUDE.md"
4
- ---
5
-
6
- # Orphan File Reference in a Per-Directory CLAUDE.md
7
-
8
- **When this applies:** Any Write, Edit, or MultiEdit to a file named `CLAUDE.md` that lists files in a markdown table whose first column names each file in backticks, or that shows run commands invoking those files inside fenced code blocks.
9
-
10
- ## Rule
11
-
12
- Every bare filename a per-directory `CLAUDE.md` names points at a file that exists in the directory subtree the `CLAUDE.md` describes — both the filenames its table cells list and the scripts its fenced run commands invoke (`python script.py`). A table cell or a run command naming a file that exists nowhere in that subtree points a reader at something that is not there: the doc claims a file the directory does not hold.
13
-
14
- When you add a table row or a run command, the file it names already exists in this directory or a subdirectory of it. When you remove a file, drop the row and the run command that named it.
15
-
16
- ## What the gate checks
17
-
18
- The `claude_md_orphan_file_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target basename is `CLAUDE.md`. It:
19
-
20
- 1. Reads the content the tool would leave on disk. For a Write that is the full `content`. For an Edit or MultiEdit it reconstructs the post-edit file — the existing on-disk file with the replacements applied — and also notes which orphans the file already held before the edit, so a pre-existing orphan on an untouched line is excluded and only an orphan the edit introduces is reported; when the existing file cannot be read, it scans the raw `new_string` fragment(s) instead.
21
- 2. Collects two kinds of referenced filename. Table cells: the first column of each markdown table row **outside** a fenced code block, keeping cells that name a bare filename wrapped in backticks, no path separator, not a slash-command, ending in a known file extension (`.py`, `.md`, `.json`, `.mjs`, `.js`, `.ts`, `.ps1`, `.cmd`, `.ahk`, `.yml`, `.yaml`, `.sh`, `.txt`, `.cfg`, `.toml`, `.ini`). Run commands: each line **inside** a fenced code block (between a ``` or `~~~` fence pair) that invokes an interpreter (`python`, `python.exe`, `python3`, `node`, `pwsh`, `powershell`, `bash`, `sh`, `ruby`, `perl`) on a script, taking that script's basename when it ends in `.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`, `.rb`, or `.pl`. A fenced *table row* is an example, not a live listing, so it contributes no table-cell filename; a fenced *run command* is the contract a reader runs, so its script filename is checked.
22
- 3. Blocks the write when a referenced filename — from a table cell or a fenced run command — exists nowhere under the scan root — the `CLAUDE.md` directory's parent, which covers the directory, its subdirectories, and its siblings. A filesystem error that halts the whole subtree walk fails open (the write proceeds), so an unreadable tree never blocks a write.
23
-
24
- The check stays quiet for a target that is not a `CLAUDE.md`, for a table cell that holds a path, a subdirectory ending in `/`, or a slash-command, for a table row inside a fenced code block, for an inline `python x.py` mention outside a fence (prose, not a runnable contract), and for a table whose content names an explicit relative-path source (a `../` token), since that table documents files that sit outside the subtree by design.
25
-
26
- ## Why this is a hook, not a lint pass
27
-
28
- A table row or a run command that names an absent file reads as a contract: a reader trusts the listing to map the directory and trusts the shown command to run. A wrong row sends the reader looking for a file that is not there; a stale run command fails the moment the reader runs it. Both erode trust in every other entry. Catching them as each line is written keeps the doc and the directory in step.
@@ -1,23 +0,0 @@
1
- # Cleanup Command Forms
2
-
3
- Never use bash `rm` in any form to clean up. The `destructive_command_blocker` hook watches every Bash-tool command and matches `rm -rf` (and the rest of the destructive patterns) as raw text. It allows an `rm` without a prompt only for a narrow set of shapes it can prove safe: an `rm` whose every target is an absolute path under the OS temp root, `/tmp`, `/temp`, or a worktrees directory — standalone, or in a chain whose other segments are plain reporting commands such as `echo` or `cat`; an `rm` run from an ephemeral working directory; and an `rm` whose every target sits inside `~/.claude`. It falls through to a permission prompt on anything outside that set: a `$`, `$(...)`, or backtick expansion whose value it cannot resolve, a target it cannot place in a safe directory, a glob basename, or a string-executing wrapper (`bash -c 'rm -rf …'`). In a background or auto-mode run no human can answer that prompt, so the call stalls.
4
-
5
- Remove files with these forms, which the hook never prompts on:
6
-
7
- - **Scratch and probe files:** the PowerShell tool — `Remove-Item -Recurse -Force -Confirm:$false <absolute path>`. The hook watches only the Bash tool, so a PowerShell removal never reaches it. A file left in the OS temp dir or `$CLAUDE_JOB_DIR/tmp` is ephemeral and needs no explicit removal.
8
- - **Worktrees:** `git worktree remove --force <path>`. This matches no destructive pattern.
9
- - **When bash `rm` is unavoidable:** one standalone `rm` command, an absolute literal path under the OS temp root or a worktrees directory, no chaining, no variables, no globs. The hook auto-allows this shape without a prompt.
10
-
11
- ## Every subagent prompt carries the rule
12
-
13
- A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers — review lenses, fix agents, verifiers — copies this line into every subagent prompt it issues, so a grandchild cleaning up its own probe file uses an allowed form:
14
-
15
- > Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
16
-
17
- Prefer that a child leaves its scratch files in place for the parent to remove at teardown with `Remove-Item`.
18
-
19
- ## Sibling rules
20
-
21
- - [`no-inline-destructive-literals`](no-inline-destructive-literals.md) — keep a destructive literal out of the Bash command string even when it rides only as data.
22
- - [`cleanup-temp-files`](cleanup-temp-files.md) — remove the scratch files a task created once the task is done.
23
- - [`windows-filesystem-safe`](windows-filesystem-safe.md) — the safe `rmtree` / `force_rmtree` patterns for read-only Windows files.
@@ -1,11 +0,0 @@
1
- # Responding to Code Reviews
2
-
3
- **When this applies:** GitHub PR review feedback on a branch you are fixing.
4
-
5
- **MANDATORY PROTOCOL:**
6
-
7
- 1. Fetch ALL reviewer comments BEFORE any fixes
8
- 2. Create TodoWrite checklist - One item per comment
9
- 3. Fix systematically - Mark each todo complete
10
- 4. Reply to EACH comment inline
11
- 5. Create ONE review fix commit - DO NOT squash with original
@@ -1,10 +0,0 @@
1
- ---
2
- paths:
3
- - "**/*.md"
4
- ---
5
-
6
- # Env-Var Summary Table Names a Code File That Reads the Variable
7
-
8
- Every row in an env-var summary table pairs an UPPER_SNAKE variable with a code-file path that reads it — written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | ... | ``. When a code change removes the last read of a variable from a file, the same change drops or corrects the table row that names that file.
9
-
10
- `env_var_table_code_drift_blocker.py` (PreToolUse on Write|Edit|MultiEdit of `.md`) blocks a row whose named code file exists yet never references the variable, and names the fix. For an Edit, drift a file already held on an untouched row is excluded; a row whose code file resolves nowhere stays quiet (the hook cannot prove the drift).
@@ -1,5 +0,0 @@
1
- # gh --body-file Rule
2
-
3
- Every `gh` command carrying markdown body content (`gh pr create/edit/comment/review`, `gh issue create/edit/comment`) uses `--body-file <path>` with a temp file — never a `--body`/`-b` string, where backticks land on GitHub as literal `\``. Write the temp file BOM-free: `[IO.File]::WriteAllText($bodyPath, $body, [Text.UTF8Encoding]::new($false))`. MCP GitHub tools take `body` as a structured parameter and are unaffected.
4
-
5
- `gh_body_arg_blocker.py` (PreToolUse on Bash) blocks `--body <arg>` and returns the corrective message.
@@ -1,3 +0,0 @@
1
- # gh API Pagination
2
-
3
- Every `gh api` read of a paginated GitHub list endpoint (PR `reviews`/`comments`/`files`, issue `comments`, `pulls`, `issues`) uses `--paginate --slurp` piped to **external** `jq` — `gh`'s built-in `--jq` runs per page, so cross-page operations like `sort_by | last` give wrong-but-confident results. Single-object endpoints (`pulls/<n>`, `issues/<n>`) skip pagination and may use `--jq` directly. For a newest-first walk, sort the slurped array and take the last element; for single-page bounds, cap with a `per_page` query parameter.
@@ -1,15 +0,0 @@
1
- ---
2
- paths: **/hooks/**/*.py
3
- ---
4
-
5
- # Hook Prose Matches Its Detector
6
-
7
- A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags — no broader trigger surface than the regex enforces.
8
-
9
- `hook_prose_detector_consistency` (PreToolUse on Write|Edit of hook modules and `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
10
-
11
- ## Judgment
12
-
13
- After writing a hook, ask: would a token that matches every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches.
14
-
15
- The path-shape case is the common overstatement: a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
@@ -1,26 +0,0 @@
1
- ---
2
- paths:
3
- - "**/*.md"
4
- - "**/*.py"
5
- - "**/*.mjs"
6
- - "**/*.js"
7
- - "**/*.ts"
8
- - "**/*.ps1"
9
- - "**/*.sh"
10
- ---
11
-
12
- # No Historical Clutter in Documentation or Comments
13
-
14
- Never reference removed implementations, old defaults, prior behaviors, or earlier contracts when updating documentation or comments. The current state is all that matters. A module or function docstring carries the same describe-current-state-only contract as a `.md` file.
15
-
16
- `state_description_blocker` (PreToolUse on Write|Edit) blocks historical and comparative phrases in `.md` prose, code comments, and Python docstrings; a phrase wrapped in double quotes or backticks inside a docstring counts as a mention and is skipped. The denial names the matched phrases and shows a rewrite example.
17
-
18
- ## What stays allowed
19
-
20
- - Comparisons to alternatives that still exist (for example, "use `--paginate --slurp | jq`, not `--jq` alone")
21
- - Rationale that explains why a pattern is wrong in terms of present behavior (for example, "`--jq` runs per-page, so cross-page operations produce wrong results")
22
- - References to external sources for defects that still exist (for example, gh CLI #10459)
23
-
24
- ## The test
25
-
26
- After writing, ask: if someone reads this a year from now with no knowledge of earlier states, does every sentence still make sense and add value? If a sentence only helps someone who knew an earlier state, delete it.
@@ -1,9 +0,0 @@
1
- # No Inline Destructive-Command Literals in Bash
2
-
3
- The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as data (a commit message, a PR/issue body, an echoed string, a `python -c` / `node -e` / `awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
4
-
5
- Keep destructive literals out of the Bash command string:
6
-
7
- - Bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
8
- - To exercise or verify the blocker (or any hook), run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data — never an inline `python -c` harness.
9
- - Genuine cleanup targets the OS temp dir or `$CLAUDE_JOB_DIR/tmp` (auto-allowed as ephemeral), never a repository or worktree path.
@@ -1,61 +0,0 @@
1
- ---
2
- paths:
3
- - "**/*.md"
4
- ---
5
-
6
- # No Justification Noise in Documentation
7
-
8
- **When this applies:** Any Write or Edit to a `.md` file.
9
-
10
- ## Rule
11
-
12
- Markdown states what the system is and does, in facts a reader can act on. Cut any sentence whose only job is to say why a choice is good, or to restate a gain the reader already works out from the stated behavior or from a rule a hook or another file enforces. A sentence like that carries no new fact — it repeats one the reader already holds, so it earns no space.
13
-
14
- ## The test
15
-
16
- For each sentence, ask: **does it state a fact the reader can act on that they could not already work out from the behavior around it or from a rule enforced elsewhere?** If no, cut the sentence.
17
-
18
- ## Two shapes to catch
19
-
20
- ### Pure noise — cut the whole sentence
21
-
22
- A sentence whose only job is to point out a result that follows from a fact the doc already states.
23
-
24
- > Autoconverge's bug-audit and self-review lenses and pr-converge's CODE_REVIEW step point at this file and read it when they run; they do not carry its text in their spawn prompts, so the checklist stays out of the per-round token budget.
25
-
26
- The doc already states the lenses read the file. The "so it stays out of the token budget" tail is a result the reader works out alone, so it carries nothing. Cut the sentence.
27
-
28
- ### Load-bearing first, noise after — keep the fact, cut the rest
29
-
30
- A sentence that states a real fact, trailed by prose that only re-argues it or restates its payoff.
31
-
32
- > The pre-catch stage drives the code to clean against these five lanes before any external reviewer sees it. External reviewers (Cursor Bugbot, GitHub Copilot) are terminal confirmation gates that run only after every lane below is clean, and they are expected to return zero findings.
33
-
34
- Keep the first sentence — it states what the stage does. Cut the second: "run only after every lane below is clean" repeats the first sentence's claim, and "expected to return zero findings" is a hoped-for result, not a fact the reader acts on.
35
-
36
- ## What stays
37
-
38
- - A fact the reader acts on: an input a piece of code takes, an order a producer emits, a path a script writes.
39
- - A rule's one-line reason stated in terms of present behavior — `--jq` runs per page, so cross-page sorts give wrong results — because that reason names a fact the reader needs to pick the right call.
40
- - A tradeoff or a constraint the reader weighs before choosing a path.
41
-
42
- ## Sibling rules
43
-
44
- This rule sits beside three others; each cuts a different kind of dead prose.
45
-
46
- | Rule | Cuts |
47
- |---|---|
48
- | `no-historical-clutter.md` | references to old state (`previously`, `migrated from`) |
49
- | `self-contained-docs.md` | references to the chat that produced the doc |
50
- | `plain-language.md` | heavy words with an everyday swap |
51
- | `no-justification-noise.md` | a present-tense sentence that only justifies or restates a fact the reader already holds |
52
-
53
- `no-historical-clutter.md` keeps a rule's reason when the reason names present behavior; this rule keeps the same reason for the same test. The two agree: a reason that names a fact the reader acts on stays, and a sentence that only re-argues a stated fact goes.
54
-
55
- ## Enforcement
56
-
57
- The AI review lane carries this rule: `AGENTS.md` names it as a finding an agent applies to the `.md` lines a PR changes. No hook backs it, because telling a justification sentence from a load-bearing one needs meaning a regex cannot read.
58
-
59
- ## Why
60
-
61
- A sentence that repeats a fact the reader already holds costs reading time and pays back nothing.