claude-dev-env 8.73.1 → 8.74.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 (116) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +23 -23
  2. package/.agents/agents-archived/clean-coder.md +5 -5
  3. package/.agents/skills/correction/playbooks/fix.md +1 -1
  4. package/.agents/skills/eli5/SKILL.md +3 -3
  5. package/.agents/skills/pr-lifecycle/SKILL.md +1 -1
  6. package/.agents/skills/recovering-codex-startup/SKILL.md +1 -1
  7. package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
  8. package/_shared/advisor/reference/third-party-bind.md +1 -1
  9. package/bin/cde.mjs +1 -0
  10. package/bin/cde.test.mjs +1 -0
  11. package/bin/install.mjs +1 -0
  12. package/bin/install.policy-lint.test.mjs +1 -0
  13. package/bin/install.profile-root.test.mjs +1 -1
  14. package/bin/install.settings-defaults.test.mjs +10 -2
  15. package/bin/install.test.mjs +1 -0
  16. package/bin/merge_settings_defaults.mjs +34 -1
  17. package/bin/merge_settings_defaults.test.mjs +53 -0
  18. package/commands/fix-followups.md +1 -1
  19. package/commands/sr-loop.md +1 -1
  20. package/docs/references/prose-style-enforcement.md +1 -1
  21. package/docs/references/working-style.md +1 -1
  22. package/docs/rule-guides/asd-ste100-language.md +9 -1
  23. package/docs/rule-guides/cleanup-temp-files.md +8 -2
  24. package/docs/rule-guides/destructive-commands.md +13 -3
  25. package/docs/rule-guides/explore-thoroughly.md +9 -1
  26. package/docs/rule-guides/features-start-with-an-eval.md +8 -2
  27. package/docs/rule-guides/filesystem-search.md +7 -1
  28. package/docs/rule-guides/flag-non-breaking-findings.md +3 -0
  29. package/docs/rule-guides/long-horizon-autonomy.md +1 -1
  30. package/docs/rule-guides/memory-stores-durable-facts.md +9 -3
  31. package/docs/rule-guides/no-contrast-framing.md +10 -4
  32. package/docs/rule-guides/prefer-existing-tools.md +2 -2
  33. package/docs/rule-guides/proof-before-pull-request.md +5 -1
  34. package/docs/rule-guides/request-scope.md +5 -1
  35. package/docs/rule-guides/research-mode.md +9 -1
  36. package/docs/rule-guides/session-title.md +7 -1
  37. package/docs/rule-guides/shell-invocation.md +9 -1
  38. package/docs/rule-guides/verify-before-asking.md +7 -1
  39. package/docs/rule-guides/verify-runtime-state.md +7 -1
  40. package/hooks/blocking/code_rules_enforcer.py +16 -6
  41. package/hooks/blocking/code_rules_shared.py +23 -4
  42. package/hooks/blocking/haiku_spawn_slim_gate.py +96 -0
  43. package/hooks/blocking/reply_length_gate.py +74 -14
  44. package/hooks/blocking/state_description_blocker.py +1 -1
  45. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +54 -1
  46. package/hooks/blocking/test_code_rules_shared.py +33 -0
  47. package/hooks/blocking/test_haiku_spawn_slim_gate.py +65 -0
  48. package/hooks/blocking/test_reply_length_gate.py +184 -5
  49. package/hooks/blocking/test_state_description_blocker.py +1 -1
  50. package/hooks/features/conduct-gates.md +3 -6
  51. package/hooks/features/spawn-routing.md +5 -1
  52. package/hooks/followup_ledger.py +52 -2
  53. package/hooks/hooks.json +11 -6
  54. package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -1
  55. package/hooks/hooks_constants/haiku_spawn_slim_gate_constants.py +29 -0
  56. package/hooks/hooks_constants/reply_length_gate_constants.py +1 -0
  57. package/hooks/hooks_constants/session_env_cleanup_constants.py +0 -2
  58. package/hooks/hooks_constants/test_visual_reply_rules_constants.py +15 -0
  59. package/hooks/hooks_constants/test_windows_filesystem.py +57 -0
  60. package/hooks/hooks_constants/visual_reply_rules_constants.py +63 -0
  61. package/hooks/hooks_constants/windows_filesystem.py +51 -0
  62. package/hooks/session/session_env_cleanup.py +4 -30
  63. package/hooks/test_followup_ledger.py +58 -0
  64. package/hooks/test_visual_reply_rules.py +164 -0
  65. package/hooks/visual_reply_rules.py +268 -0
  66. package/hooks/workflow/test_auto_formatter.py +4 -25
  67. package/package.json +1 -1
  68. package/rules/index.md +30 -0
  69. package/rules/visual-reply-rules.json +41 -0
  70. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  71. package/scripts/conftest.py +13 -5
  72. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +2 -2
  73. package/scripts/dev_env_scripts_constants/followup_constants.py +4 -1
  74. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +12 -0
  75. package/scripts/followup_cli.py +25 -1
  76. package/scripts/policy_lint/adapter_configuration.py +0 -5
  77. package/scripts/policy_lint/config/constants.py +1 -1
  78. package/scripts/repository_checks/config/constants.py +6 -0
  79. package/scripts/repository_checks/followup_ledger_duplicates.py +55 -0
  80. package/scripts/repository_checks/runner.py +7 -0
  81. package/scripts/test_active_capability_references.py +0 -5
  82. package/scripts/test_broker_state_isolation.py +3 -3
  83. package/scripts/test_digest_check.py +0 -5
  84. package/scripts/test_durable_post_lint.py +1 -5
  85. package/scripts/test_followup_cli.py +18 -6
  86. package/scripts/test_resolve_worker_spawn.py +5 -10
  87. package/scripts/test_setup_project_paths.py +0 -12
  88. package/scripts/test_shared_tree_paths.py +2 -6
  89. package/scripts/test_validate_instruction_pairs.py +0 -7
  90. package/scripts/test_verify_installable_package.py +0 -5
  91. package/scripts/tests/test_contrast_framing.py +3 -3
  92. package/scripts/tests/test_followup_ledger_duplicates.py +58 -0
  93. package/scripts/tests/test_repository_policy.py +2 -0
  94. package/scripts/tests/test_rule_index_shape.py +59 -5
  95. package/scripts/tests/test_rule_load_scopes.py +1 -16
  96. package/settings.json +9 -0
  97. package/system-prompts/software-engineer.xml +4 -4
  98. package/hooks/blocking/artifact_look_gate.py +0 -223
  99. package/hooks/blocking/test_artifact_look_gate.py +0 -153
  100. package/hooks/hooks_constants/artifact_look_gate_constants.py +0 -32
  101. package/rules/asd-ste100-language.md +0 -9
  102. package/rules/cleanup-temp-files.md +0 -9
  103. package/rules/destructive-commands.md +0 -11
  104. package/rules/explore-thoroughly.md +0 -9
  105. package/rules/features-start-with-an-eval.md +0 -9
  106. package/rules/filesystem-search.md +0 -9
  107. package/rules/memory-stores-durable-facts.md +0 -9
  108. package/rules/no-contrast-framing.md +0 -9
  109. package/rules/proof-before-pull-request.md +0 -7
  110. package/rules/request-scope.md +0 -7
  111. package/rules/research-mode.md +0 -9
  112. package/rules/session-title.md +0 -9
  113. package/rules/shell-invocation.md +0 -9
  114. package/rules/skill-pointers.md +0 -3
  115. package/rules/verify-before-asking.md +0 -9
  116. package/rules/verify-runtime-state.md +0 -9
@@ -0,0 +1,55 @@
1
+ """Committed follow-up ledger check for exact repeated lines.
2
+
3
+ GitHub's server-side merge ignores ``merge=union``, so repeated lines arrive
4
+ only through a local union merge. This check stops them at the merge queue.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+
13
+ from policy_lint.config import constants as policy_constants
14
+
15
+ from repository_checks.config import constants as repository_constants
16
+ from repository_checks.hook_modules import load_hooks_module
17
+ from repository_checks.models import RepositoryFinding
18
+
19
+
20
+ def collect_followup_ledger_duplicate_findings(
21
+ repository_root: Path, all_tracked_paths: Sequence[str]
22
+ ) -> list[RepositoryFinding]:
23
+ """Return a finding when the tracked ledger repeats a line.
24
+
25
+ Args:
26
+ repository_root: Git repository root.
27
+ all_tracked_paths: Repository-relative tracked paths.
28
+
29
+ Returns:
30
+ One finding naming the repeated-line count, or no findings.
31
+ """
32
+ ledger = load_hooks_module(repository_constants.FOLLOWUP_LEDGER_MODULE_NAME)
33
+ ledger_path = ledger.followup_ledger_path(repository_root)
34
+ relative_path = ledger_path.relative_to(repository_root).as_posix()
35
+ if relative_path not in all_tracked_paths:
36
+ return []
37
+ repeated_line_count = _repeated_line_count(ledger, ledger_path)
38
+ if not repeated_line_count:
39
+ return []
40
+ return [
41
+ RepositoryFinding(
42
+ repository_constants.CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
43
+ relative_path,
44
+ repository_constants.FOLLOWUP_LEDGER_DUPLICATE_MESSAGE_TEMPLATE.format(
45
+ repeated_line_count=repeated_line_count
46
+ ),
47
+ )
48
+ ]
49
+
50
+
51
+ def _repeated_line_count(ledger: ModuleType, ledger_path: Path) -> int:
52
+ all_ledger_lines = ledger_path.read_text(
53
+ encoding=policy_constants.UTF8_ENCODING
54
+ ).splitlines()
55
+ return len(all_ledger_lines) - len(ledger.deduplicated_lines(all_ledger_lines))
@@ -11,6 +11,9 @@ from repository_checks.config import constants as repository_constants
11
11
  from repository_checks.env_var_documentation import (
12
12
  collect_env_var_documentation_findings,
13
13
  )
14
+ from repository_checks.followup_ledger_duplicates import (
15
+ collect_followup_ledger_duplicate_findings,
16
+ )
14
17
  from repository_checks.models import RepositoryCheckReport, RepositoryFinding
15
18
  from repository_checks.package_inventory import collect_package_inventory_findings
16
19
  from repository_checks.pytest_testpaths import collect_pytest_testpath_findings
@@ -81,6 +84,10 @@ def _all_collectors() -> tuple[tuple[str, RepositoryCollector], ...]:
81
84
  repository_constants.CHECK_ID_ENV_VAR_DOCUMENTATION,
82
85
  collect_env_var_documentation_findings,
83
86
  ),
87
+ (
88
+ repository_constants.CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
89
+ collect_followup_ledger_duplicate_findings,
90
+ ),
84
91
  (
85
92
  repository_constants.CHECK_ID_PACKAGE_INVENTORY,
86
93
  collect_package_inventory_findings,
@@ -2,13 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
6
5
  from pathlib import Path
7
6
 
8
- _SCRIPTS = Path(__file__).resolve().parent
9
- if str(_SCRIPTS) not in sys.path:
10
- sys.path.insert(0, str(_SCRIPTS))
11
-
12
7
  from active_capability_references import (
13
8
  build_capability_inventory,
14
9
  classify_capability_reference,
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import subprocess
6
7
  from datetime import datetime, timezone
7
8
  from pathlib import Path
@@ -43,6 +44,5 @@ def test_meter_cache_write_lands_outside_the_home_directory(
43
44
 
44
45
  home_state_path = fake_home / ".claude" / "account-broker" / "state.json"
45
46
  assert not home_state_path.exists()
46
- assert f"claude:only:{account.home}" in isolated_broker_state_path.read_text(
47
- encoding="utf-8"
48
- )
47
+ state = json.loads(isolated_broker_state_path.read_text(encoding="utf-8"))
48
+ assert f"claude:only:{account.home}" in state["meters"]
@@ -2,15 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import sys
6
5
  from pathlib import Path
7
6
 
8
7
  import pytest
9
8
 
10
- _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
11
- if str(_SCRIPTS_DIRECTORY) not in sys.path:
12
- sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
13
-
14
9
  import digest_check
15
10
  from dev_env_scripts_constants.digest_check_constants import (
16
11
  CARD_WORD_BUDGET,
@@ -9,14 +9,10 @@ from pathlib import Path
9
9
 
10
10
  import pytest
11
11
 
12
- _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
13
- if str(_SCRIPTS_DIRECTORY) not in sys.path:
14
- sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
15
-
16
12
  import durable_post_lint
17
13
  from dev_env_scripts_constants.private_term_constants import PrivateTermDigest
18
14
 
19
- SCRIPT_PATH = _SCRIPTS_DIRECTORY / "durable_post_lint.py"
15
+ SCRIPT_PATH = Path(__file__).resolve().parent / "durable_post_lint.py"
20
16
  VALID_PR_BODY = """## Why
21
17
 
22
18
  Why text.
@@ -4,24 +4,20 @@ from __future__ import annotations
4
4
 
5
5
  import io
6
6
  import json
7
- import sys
8
7
  from pathlib import Path
9
8
 
10
9
  from dev_env_scripts_constants.followup_constants import (
11
10
  BACKLOG_EXCEEDED_EXIT_CODE,
12
11
  BACKLOG_EXCEEDED_TEMPLATE,
13
12
  BACKLOG_WITHIN_TEMPLATE,
13
+ DEDUPE_RESULT_TEMPLATE,
14
14
  FOLLOWUP_BACKLOG_THRESHOLD,
15
15
  )
16
16
  from followup_cli import main
17
-
18
- _hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
19
- if _hooks_directory not in sys.path:
20
- sys.path.insert(0, _hooks_directory)
21
-
22
17
  from followup_ledger import (
23
18
  FollowupFinding,
24
19
  all_recorded_findings,
20
+ followup_ledger_path,
25
21
  record_followup_finding,
26
22
  )
27
23
 
@@ -309,3 +305,19 @@ def test_list_names_the_check_behind_each_finding(tmp_path: Path) -> None:
309
305
 
310
306
  assert "code-rules/constant-outside-config" in output_text
311
307
  assert "f00dcafe" in output_text
308
+
309
+
310
+ def test_dedupe_removes_repeated_ledger_lines_and_keeps_the_order(
311
+ tmp_path: Path,
312
+ ) -> None:
313
+ ledger_path = followup_ledger_path(tmp_path)
314
+ ledger_path.parent.mkdir(parents=True)
315
+ ledger_path.write_text("first\nsecond\nfirst\n", encoding="utf-8")
316
+
317
+ exit_code, output_text = run_command(
318
+ ["dedupe", "--repository-root", str(tmp_path)]
319
+ )
320
+
321
+ assert exit_code == 0
322
+ assert output_text == DEDUPE_RESULT_TEMPLATE.format(removed_count=1) + "\n"
323
+ assert ledger_path.read_text(encoding="utf-8") == "first\nsecond\n"
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import subprocess
7
- import sys
8
7
  import threading
9
8
  from dataclasses import dataclass, field
10
9
  from datetime import datetime, timezone
@@ -12,13 +11,9 @@ from pathlib import Path
12
11
 
13
12
  import pytest
14
13
 
15
- _SCRIPTS_DIR = Path(__file__).resolve().parent
16
- if str(_SCRIPTS_DIR) not in sys.path:
17
- sys.path.insert(0, str(_SCRIPTS_DIR))
18
-
19
14
  import account_broker
20
15
  import invoke_code_review as invoker
21
- import resolve_worker_spawn as dispatcher # noqa: E402
16
+ import resolve_worker_spawn as dispatcher
22
17
  from dev_env_scripts_constants.account_broker_constants import (
23
18
  Account,
24
19
  BrokerConfigurationError,
@@ -28,7 +23,7 @@ from dev_env_scripts_constants.account_broker_constants import (
28
23
  ProductAdapter,
29
24
  WAIT_EXIT_CODE,
30
25
  )
31
- from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
26
+ from dev_env_scripts_constants.grok_worker_constants import (
32
27
  AGENT_FLAG,
33
28
  CLI_AGENT_FLAG,
34
29
  ALL_AGENT_FILENAMES_BY_ROLE,
@@ -67,12 +62,12 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
67
62
  TIER_CLAUDE_HEADLESS,
68
63
  TIER_GROK,
69
64
  )
70
- import grok_headless_runner # noqa: E402
71
- from grok_headless_runner import ( # noqa: E402
65
+ import grok_headless_runner
66
+ from grok_headless_runner import (
72
67
  GrokRunnerOutcome,
73
68
  run_headless_worker,
74
69
  )
75
- from grok_worker_preflight import PreflightOutcome # noqa: E402
70
+ from grok_worker_preflight import PreflightOutcome
76
71
 
77
72
  HOST_PROFILE_CLAUDE = "Claude"
78
73
  HOST_PROFILE_THIRD_PARTY = "ThirdParty"
@@ -3,23 +3,11 @@
3
3
  import inspect
4
4
  import json
5
5
  import subprocess
6
- import sys
7
6
  from pathlib import Path
8
7
  from unittest.mock import patch
9
8
 
10
9
  import pytest
11
10
 
12
- _SCRIPTS_DIR = Path(__file__).resolve().parent
13
- _HOOKS_DIR = _SCRIPTS_DIR.parent / "hooks"
14
- _SESSION_HOOKS_PACKAGE_DIR = _SCRIPTS_DIR.parent / "hooks" / "session"
15
- for each_sys_path_entry in (
16
- str(_SCRIPTS_DIR),
17
- str(_HOOKS_DIR),
18
- str(_SESSION_HOOKS_PACKAGE_DIR),
19
- ):
20
- if each_sys_path_entry not in sys.path:
21
- sys.path.insert(0, each_sys_path_entry)
22
-
23
11
  import setup_project_paths as setup
24
12
  import untracked_repo_detector as detector_module
25
13
  from hooks_constants.project_paths_reader import registry_file_path
@@ -9,12 +9,8 @@ from pathlib import Path
9
9
 
10
10
  import pytest
11
11
 
12
- _SCRIPTS_DIR = Path(__file__).resolve().parent
13
- if str(_SCRIPTS_DIR) not in sys.path:
14
- sys.path.insert(0, str(_SCRIPTS_DIR))
15
-
16
- import shared_tree_paths # noqa: E402
17
- from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
12
+ import shared_tree_paths
13
+ from dev_env_scripts_constants.grok_worker_constants import (
18
14
  CLAUDE_CONFIG_DIR_ENV_VAR,
19
15
  PROCESS_TREE_DIRECTORY_NAME,
20
16
  PROCESS_TREE_KILL_MODULE_FILENAME,
@@ -1,8 +1,6 @@
1
1
  import subprocess
2
2
  from pathlib import Path
3
3
 
4
- import sys
5
-
6
4
  from validate_instruction_pairs import (
7
5
  GATE_FAILED_EXIT_CODE,
8
6
  GATE_PASSED_EXIT_CODE,
@@ -10,11 +8,6 @@ from validate_instruction_pairs import (
10
8
  run_gate,
11
9
  validate_repository,
12
10
  )
13
-
14
- _hooks_directory = str(Path(__file__).resolve().parents[1] / "hooks")
15
- if _hooks_directory not in sys.path:
16
- sys.path.insert(0, _hooks_directory)
17
-
18
11
  from followup_ledger import all_recorded_findings, head_commit
19
12
  from hooks_constants.followup_ledger_constants import SEVERITY_SMELL
20
13
 
@@ -6,14 +6,9 @@ command walking without requiring a full npm pack on every case.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import sys
10
9
  import tarfile
11
10
  from pathlib import Path
12
11
 
13
- _SCRIPTS_DIR = Path(__file__).resolve().parent
14
- if str(_SCRIPTS_DIR) not in sys.path:
15
- sys.path.insert(0, str(_SCRIPTS_DIR))
16
-
17
12
  import verify_installable_package as mod
18
13
  from dev_env_scripts_constants.verify_installable_package_constants import (
19
14
  CLASS_PACKAGED,
@@ -89,12 +89,12 @@ def test_form_named_rejects_an_unknown_name() -> None:
89
89
  form_named("no-such-form")
90
90
 
91
91
 
92
- def test_every_form_is_named_in_the_rule_document() -> None:
93
- rule_text = (PACKAGE_ROOT / CONTRAST_FRAMING_RULE_DOCUMENT).read_text(
92
+ def test_every_form_is_named_in_the_rule_guide() -> None:
93
+ guide_text = (PACKAGE_ROOT / CONTRAST_FRAMING_RULE_DOCUMENT).read_text(
94
94
  encoding="utf-8"
95
95
  )
96
96
  for each_form in ALL_CONTRAST_FRAMING_FORMS:
97
- assert f"`{each_form.name}`" in rule_text
97
+ assert f"`{each_form.name}`" in guide_text
98
98
 
99
99
 
100
100
  def test_a_list_of_not_items_stays_quiet() -> None:
@@ -0,0 +1,58 @@
1
+ """Behavior tests for the committed follow-up ledger duplicate check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _TESTS_DIRECTORY = Path(__file__).resolve().parent
9
+ if str(_TESTS_DIRECTORY) not in sys.path:
10
+ sys.path.insert(0, str(_TESTS_DIRECTORY))
11
+
12
+ from repository_checks.config.constants import (
13
+ CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
14
+ FINDINGS_EXIT_CODE,
15
+ SUCCESS_EXIT_CODE,
16
+ )
17
+ from repository_policy_test_support import (
18
+ commit_tracked_files,
19
+ initialize_repository,
20
+ run_policy,
21
+ write_text,
22
+ )
23
+
24
+ _LEDGER_RELATIVE_PATH = ".claude/followups/smells.jsonl"
25
+
26
+
27
+ def test_should_flag_a_committed_ledger_with_a_repeated_line(tmp_path: Path) -> None:
28
+ repository_root = tmp_path / "repo"
29
+ initialize_repository(repository_root)
30
+ write_text(
31
+ repository_root / _LEDGER_RELATIVE_PATH,
32
+ '{"rule_id": "a"}\n{"rule_id": "b"}\n{"rule_id": "a"}\n',
33
+ )
34
+ commit_tracked_files(repository_root)
35
+
36
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
37
+
38
+ assert exit_code == FINDINGS_EXIT_CODE
39
+ assert (
40
+ f"{CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES}: {_LEDGER_RELATIVE_PATH}:"
41
+ in stdout_text
42
+ )
43
+ assert "cde followup dedupe" in stdout_text
44
+
45
+
46
+ def test_should_pass_a_committed_ledger_without_repeats(tmp_path: Path) -> None:
47
+ repository_root = tmp_path / "repo"
48
+ initialize_repository(repository_root)
49
+ write_text(
50
+ repository_root / _LEDGER_RELATIVE_PATH,
51
+ '{"rule_id": "a"}\n{"rule_id": "b"}\n',
52
+ )
53
+ commit_tracked_files(repository_root)
54
+
55
+ exit_code, stdout_text, _stderr_text = run_policy(repository_root)
56
+
57
+ assert exit_code == SUCCESS_EXIT_CODE
58
+ assert stdout_text == ""
@@ -17,6 +17,7 @@ import repository_policy
17
17
  from repository_checks.config.constants import (
18
18
  CHECK_ID_CLAUDE_MD_ORPHANS,
19
19
  CHECK_ID_ENV_VAR_DOCUMENTATION,
20
+ CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
20
21
  CHECK_ID_PACKAGE_INVENTORY,
21
22
  CHECK_ID_PYTEST_TESTPATHS,
22
23
  CHECK_ID_TRACKED_PERSONAL_DATA,
@@ -50,6 +51,7 @@ def test_should_keep_stable_check_identifiers() -> None:
50
51
  assert repository_policy.ALL_CHECK_IDS == (
51
52
  CHECK_ID_CLAUDE_MD_ORPHANS,
52
53
  CHECK_ID_ENV_VAR_DOCUMENTATION,
54
+ CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
53
55
  CHECK_ID_PACKAGE_INVENTORY,
54
56
  CHECK_ID_PYTEST_TESTPATHS,
55
57
  CHECK_ID_TRACKED_PERSONAL_DATA,
@@ -1,4 +1,4 @@
1
- """Each rule entry keeps the index shape, and the always-loaded entries stay inside a byte budget."""
1
+ """The rules index and each rule entry keep their shape, and the always-loaded rules stay inside a byte budget."""
2
2
 
3
3
  import re
4
4
  import sys
@@ -28,11 +28,16 @@ DOUBLED_WHEN_LEAD_IN_PATTERN = re.compile(
28
28
  r"^\*\*When(?: this applies)?:\*\* When\b", re.MULTILINE
29
29
  )
30
30
 
31
+ INDEX_LINE_PATTERN = re.compile(r"^- \*\*[^*]+\*\*(?: \(\[guide\]\((\.\./docs/rule-guides/[^)#\s]+\.md)\)\))?\. \S")
32
+ INDEX_SKILL_POINTER_PATTERN = re.compile(r"the `[a-z-]+` skill")
33
+
34
+ INDEX_FILE_NAME = "index.md"
35
+ INDEX_PATH = RULES_DIRECTORY / INDEX_FILE_NAME
31
36
  MAXIMUM_ENTRY_BYTES = 1_500
32
- MAXIMUM_ALWAYS_ON_BYTES = 12_000
33
- POINTER_ENTRY_NAMES = frozenset({"skill-pointers.md"})
37
+ MAXIMUM_INDEX_BYTES = 4_000
38
+ MAXIMUM_ALWAYS_ON_BYTES = 6_000
34
39
  CODEX_VERBATIM_ENTRY_NAMES = frozenset({"question-presentation.md"})
35
- INDEX_SHAPE_EXEMPT_ENTRY_NAMES = POINTER_ENTRY_NAMES | CODEX_VERBATIM_ENTRY_NAMES
40
+ INDEX_SHAPE_EXEMPT_ENTRY_NAMES = CODEX_VERBATIM_ENTRY_NAMES
36
41
  CODEX_MATERIALIZED_GUIDE_NAMES = frozenset(
37
42
  Path(each_path).name for each_path in codex_instruction_rule_relative_paths
38
43
  )
@@ -43,6 +48,7 @@ def _entry_paths() -> list[Path]:
43
48
  each_path
44
49
  for each_path in RULES_DIRECTORY.glob("*.md")
45
50
  if each_path.name not in instruction_alias_filenames
51
+ and each_path.name != INDEX_FILE_NAME
46
52
  )
47
53
 
48
54
 
@@ -183,11 +189,59 @@ def test_each_entry_links_a_full_text_guide_that_exists() -> None:
183
189
  assert all_problems == []
184
190
 
185
191
 
192
+ def _index_lines() -> list[str]:
193
+ return [
194
+ each_line
195
+ for each_line in INDEX_PATH.read_text(encoding="utf-8").splitlines()
196
+ if each_line.startswith("- ")
197
+ ]
198
+
199
+
200
+ def _index_guide_names() -> list[str]:
201
+ return [
202
+ Path(each_match.group(1)).name
203
+ for each_line in _index_lines()
204
+ if (each_match := INDEX_LINE_PATTERN.match(each_line)) and each_match.group(1)
205
+ ]
206
+
207
+
208
+ def test_each_index_line_names_a_rule_and_a_trigger() -> None:
209
+ malformed = [
210
+ each_line for each_line in _index_lines() if not INDEX_LINE_PATTERN.match(each_line)
211
+ ]
212
+ assert malformed == []
213
+
214
+
215
+ def test_each_index_line_links_a_guide_or_names_a_skill() -> None:
216
+ unlinked = [
217
+ each_line
218
+ for each_line in _index_lines()
219
+ if not INDEX_LINE_PATTERN.match(each_line).group(1)
220
+ and not INDEX_SKILL_POINTER_PATTERN.search(each_line)
221
+ ]
222
+ assert unlinked == []
223
+
224
+
225
+ def test_each_index_guide_exists() -> None:
226
+ missing = [
227
+ each_guide_name
228
+ for each_guide_name in _index_guide_names()
229
+ if not (RULE_GUIDES_DIRECTORY / each_guide_name).is_file()
230
+ ]
231
+ assert missing == []
232
+
233
+
234
+ def test_index_stays_inside_its_byte_budget() -> None:
235
+ assert len(INDEX_PATH.read_bytes()) <= MAXIMUM_INDEX_BYTES
236
+
237
+
186
238
  def test_each_guide_has_exactly_one_owner() -> None:
187
239
  owners_by_guide: dict[str, list[str]] = {}
188
240
  for each_path in _entry_paths():
189
241
  for each_guide_name in _linked_guide_names(_entry_text(each_path)):
190
242
  owners_by_guide.setdefault(each_guide_name, []).append(each_path.name)
243
+ for each_guide_name in _index_guide_names():
244
+ owners_by_guide.setdefault(each_guide_name, []).append(INDEX_FILE_NAME)
191
245
  unowned = sorted(
192
246
  each_guide.name
193
247
  for each_guide in RULE_GUIDES_DIRECTORY.glob("*.md")
@@ -204,7 +258,7 @@ def test_each_guide_has_exactly_one_owner() -> None:
204
258
 
205
259
 
206
260
  def test_always_loaded_entries_stay_inside_the_total_byte_budget() -> None:
207
- always_on_bytes = sum(
261
+ always_on_bytes = len(INDEX_PATH.read_bytes()) + sum(
208
262
  len(each_path.read_bytes())
209
263
  for each_path in _entry_paths()
210
264
  if _loads_in_every_session(_entry_text(each_path))
@@ -8,24 +8,9 @@ SKILLS_DIRECTORY = PACKAGE_ROOT / ".agents" / "skills"
8
8
 
9
9
  ALWAYS_ON_RULE_NAMES = frozenset(
10
10
  {
11
- "asd-ste100-language.md",
12
- "cleanup-temp-files.md",
13
11
  "correction-lens.md",
14
- "destructive-commands.md",
15
- "explore-thoroughly.md",
16
- "features-start-with-an-eval.md",
17
- "filesystem-search.md",
18
- "memory-stores-durable-facts.md",
19
- "no-contrast-framing.md",
20
- "proof-before-pull-request.md",
12
+ "index.md",
21
13
  "question-presentation.md",
22
- "research-mode.md",
23
- "request-scope.md",
24
- "session-title.md",
25
- "skill-pointers.md",
26
- "shell-invocation.md",
27
- "verify-before-asking.md",
28
- "verify-runtime-state.md",
29
14
  }
30
15
  )
31
16
 
package/settings.json CHANGED
@@ -7,6 +7,15 @@
7
7
  ]
8
8
  },
9
9
  "advisorModel": "fable",
10
+ "autoCompactWindow": 350000,
11
+ "modelSettings": {
12
+ "claude-haiku-5-5": {
13
+ "autoCompactWindow": 100000
14
+ },
15
+ "claude-haiku-4-5": {
16
+ "autoCompactWindow": 100000
17
+ }
18
+ },
10
19
  "autoMode": {
11
20
  "allow": [
12
21
  "$defaults",
@@ -7,7 +7,7 @@
7
7
  </role>
8
8
 
9
9
  <task_scope>
10
- Apply rules/asd-ste100-language.md for user-facing word choice, sentence style, tone, punctuation,
10
+ Apply docs/rule-guides/asd-ste100-language.md for user-facing word choice, sentence style, tone, punctuation,
11
11
  and prose form. Keep the behavior contracts below within their named scope.
12
12
  Defer to the user's judgment when they indicate a task is too ambitious or complex to pursue.
13
13
  Match the scope of every action to what the user explicitly requested: a bug fix delivers the
@@ -28,7 +28,7 @@
28
28
  <output_style>
29
29
  When a user-facing response needs beginner presentation, use ELI5: beginner framing, large
30
30
  visuals, minimal text, one stable self-contained HTML artifact, update-in-place continuity,
31
- and sharing. Apply rules/asd-ste100-language.md to sentence-level prose inside that artifact.
31
+ and sharing. Apply docs/rule-guides/asd-ste100-language.md to sentence-level prose inside that artifact.
32
32
  Lead every response with the direct answer or result. Open with the substance — the
33
33
  conclusion, the fix, the decision — before any supporting context or explanation. Expand
34
34
  depth in proportion to the complexity of the ask: a direct question earns a sentence; a
@@ -67,7 +67,7 @@
67
67
  Reserve user questions for preferences, missing context the user holds, judgment calls, and
68
68
  scope decisions.
69
69
 
70
- Source every claim as rules/research-mode.md describes.
70
+ Source every claim as docs/rule-guides/research-mode.md describes.
71
71
  </investigation>
72
72
 
73
73
  <problem_solving>
@@ -363,7 +363,7 @@
363
363
  Visible output is concise by default. The first progress update is one sentence.
364
364
  Later updates mark only important discoveries or direction changes. The final
365
365
  response begins with the outcome.
366
- Language policy: rules/asd-ste100-language.md. This section owns visible progress,
366
+ Language policy: docs/rule-guides/asd-ste100-language.md. This section owns visible progress,
367
367
  tool narration, and outcome-first final structure.
368
368
  </communication_contract>
369
369