claude-dev-env 2.35.0 → 4.0.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 (99) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/autoconverge/SKILL.md +19 -15
  3. package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
  4. package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
  5. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
  6. package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
  7. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
  8. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
  9. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
  10. package/.agents/skills/team-advisor/SKILL.md +11 -11
  11. package/_shared/advisor/advisor-protocol.md +51 -51
  12. package/_shared/advisor/reference/advisor-block.md +9 -9
  13. package/_shared/advisor/reference/astra-rung.md +39 -0
  14. package/_shared/advisor/reference/identity.md +4 -4
  15. package/_shared/advisor/reference/lifecycle.md +6 -6
  16. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  17. package/_shared/advisor/reference/third-party-bind.md +11 -11
  18. package/_shared/advisor/reference/warm-up.md +13 -13
  19. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  20. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  21. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  22. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  24. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  25. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  26. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  28. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  29. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  30. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  31. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  32. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  33. package/bin/cde.mjs +175 -0
  34. package/bin/cde.test.mjs +222 -0
  35. package/bin/install-constants.mjs +4 -0
  36. package/bin/install.cursor-rules.test.mjs +233 -2
  37. package/bin/install.mjs +49 -3
  38. package/bin/pstack-model-preferences.codex.json +23 -0
  39. package/bin/select-pstack-models.test.mjs +378 -0
  40. package/docs/references/team-advisor-skill.md +3 -3
  41. package/hooks/hooks_constants/tool_names.py +20 -0
  42. package/hooks/validators/README.md +35 -1
  43. package/hooks/validators/fast_save_validators.py +63 -89
  44. package/hooks/validators/run_all_validators.py +16 -8
  45. package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
  46. package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
  47. package/package.json +3 -2
  48. package/rules/pstack-models.md +119 -0
  49. package/scripts/cde_lint.py +258 -0
  50. package/scripts/check.ps1 +18 -6
  51. package/scripts/policy_lint/__init__.py +41 -0
  52. package/scripts/policy_lint/adapter_configuration.py +91 -0
  53. package/scripts/policy_lint/adapter_detectors.py +368 -0
  54. package/scripts/policy_lint/adapter_pairing.py +145 -0
  55. package/scripts/policy_lint/adapter_support.py +97 -0
  56. package/scripts/policy_lint/adapters.py +376 -0
  57. package/scripts/policy_lint/config/__init__.py +1 -0
  58. package/scripts/policy_lint/config/constants.py +77 -0
  59. package/scripts/policy_lint/engine.py +224 -0
  60. package/scripts/policy_lint/model.py +388 -0
  61. package/scripts/policy_lint/registry.py +171 -0
  62. package/scripts/policy_lint/render.py +91 -0
  63. package/scripts/policy_lint/selection.py +372 -0
  64. package/scripts/policy_lint/selection_git.py +147 -0
  65. package/scripts/repository_checks/__init__.py +1 -0
  66. package/scripts/repository_checks/claude_md.py +108 -0
  67. package/scripts/repository_checks/config/__init__.py +1 -0
  68. package/scripts/repository_checks/config/constants.py +150 -0
  69. package/scripts/repository_checks/env_var_documentation.py +243 -0
  70. package/scripts/repository_checks/hook_modules.py +34 -0
  71. package/scripts/repository_checks/models.py +37 -0
  72. package/scripts/repository_checks/package_inventory.py +118 -0
  73. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  74. package/scripts/repository_checks/reporting.py +39 -0
  75. package/scripts/repository_checks/runner.py +93 -0
  76. package/scripts/repository_checks/tracked_secrets.py +147 -0
  77. package/scripts/repository_checks/tracked_tree.py +28 -0
  78. package/scripts/repository_policy.py +120 -0
  79. package/scripts/select_pstack_models.mjs +304 -0
  80. package/scripts/tests/repository_policy_test_support.py +89 -0
  81. package/scripts/tests/test_cde_lint.py +398 -0
  82. package/scripts/tests/test_claude_md.py +68 -0
  83. package/scripts/tests/test_env_var_documentation.py +133 -0
  84. package/scripts/tests/test_hook_modules.py +17 -0
  85. package/scripts/tests/test_models.py +26 -0
  86. package/scripts/tests/test_package_inventory.py +96 -0
  87. package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
  88. package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
  89. package/scripts/tests/test_policy_lint_selection.py +373 -0
  90. package/scripts/tests/test_pytest_testpaths.py +66 -0
  91. package/scripts/tests/test_reporting.py +54 -0
  92. package/scripts/tests/test_repository_policy.py +71 -0
  93. package/scripts/tests/test_runner.py +59 -0
  94. package/scripts/tests/test_tracked_secrets.py +175 -0
  95. package/scripts/tests/test_tracked_tree.py +28 -0
  96. package/_shared/advisor/reference/sol-rung.md +0 -39
  97. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  98. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  99. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
@@ -0,0 +1,369 @@
1
+ """Registry, grouping, and configuration tests for policy lint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import types
8
+ from pathlib import Path, PurePosixPath
9
+
10
+ import pytest
11
+ from policy_lint import adapters, registry
12
+ from policy_lint.engine import lint
13
+ from policy_lint.model import (
14
+ ChangeSetRule,
15
+ Document,
16
+ DocumentRule,
17
+ DocumentSet,
18
+ LintRequest,
19
+ SelectionKind,
20
+ )
21
+
22
+ _FUNCTION_SOURCE = "def work() -> None:\n pass\n"
23
+ _CHANGED_RULE_SETS = frozenset({"changed"})
24
+
25
+
26
+ def _rule_named(rule_id: str) -> DocumentRule:
27
+ for each_rule in registry.default_registry():
28
+ if each_rule.rule_id == rule_id and isinstance(each_rule, DocumentRule):
29
+ return each_rule
30
+ raise AssertionError(rule_id)
31
+
32
+
33
+ def _ensure_git_repository(repository_root: Path) -> None:
34
+ git_directory = repository_root / ".git"
35
+ if git_directory.exists():
36
+ return
37
+ subprocess.run(
38
+ ("git", "init"),
39
+ cwd=repository_root,
40
+ check=True,
41
+ capture_output=True,
42
+ )
43
+
44
+
45
+ def _commit_repository(repository_root: Path, message: str) -> None:
46
+ subprocess.run(
47
+ ("git", "config", "user.email", "test@example.com"),
48
+ cwd=repository_root,
49
+ check=True,
50
+ capture_output=True,
51
+ )
52
+ subprocess.run(
53
+ ("git", "config", "user.name", "Test"),
54
+ cwd=repository_root,
55
+ check=True,
56
+ capture_output=True,
57
+ )
58
+ subprocess.run(
59
+ ("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
60
+ )
61
+ subprocess.run(
62
+ ("git", "commit", "-m", message),
63
+ cwd=repository_root,
64
+ check=True,
65
+ capture_output=True,
66
+ )
67
+
68
+
69
+ def _stage_terminology_change(
70
+ repository_root: Path,
71
+ base_source: str,
72
+ current_source: str,
73
+ documentation_text: str,
74
+ ) -> None:
75
+ _ensure_git_repository(repository_root)
76
+ source_path = repository_root / "src" / "quota.py"
77
+ source_path.parent.mkdir()
78
+ source_path.write_text(base_source, encoding="utf-8")
79
+ _commit_repository(repository_root, "base")
80
+ source_path.write_text(current_source, encoding="utf-8")
81
+ documentation_path = repository_root / "docs" / "README.md"
82
+ documentation_path.parent.mkdir()
83
+ documentation_path.write_text(documentation_text, encoding="utf-8")
84
+ subprocess.run(
85
+ ("git", "add", "-A"), cwd=repository_root, check=True, capture_output=True
86
+ )
87
+
88
+
89
+ def _terminology_rule() -> ChangeSetRule:
90
+ terminology_rule = next(
91
+ each_rule
92
+ for each_rule in registry.default_registry()
93
+ if each_rule.rule_id == "terminology-sweep"
94
+ )
95
+ assert isinstance(terminology_rule, ChangeSetRule)
96
+ return terminology_rule
97
+
98
+
99
+ def test_hook_format_accepts_claude_settings() -> None:
100
+ settings_document = Document.from_text(".claude/settings.json", "{}")
101
+ hooks_document = Document.from_text("hooks/hooks.json", "{}")
102
+ format_rule = _rule_named("hook-format")
103
+ configuration_rule = _rule_named("hook-configuration")
104
+ assert format_rule.accepts(settings_document) is True
105
+ assert format_rule.accepts(hooks_document) is False
106
+ assert configuration_rule.accepts(hooks_document) is True
107
+ assert configuration_rule.accepts(settings_document) is False
108
+
109
+
110
+ def test_workflow_substitution_accepts_workflow_js_suffix() -> None:
111
+ workflow_document = Document.from_text("packages/foo/bar.workflow.js", "loop")
112
+ directory_document = Document.from_text("docs/workflow/readme.md", "loop")
113
+ assert adapters.accepts_workflow(workflow_document) is True
114
+ assert adapters.accepts_workflow(directory_document) is False
115
+
116
+
117
+ def test_open_questions_cover_repository_and_claude_plans() -> None:
118
+ docs_plan = Document.from_text("docs/plans/packet.md", "# Plan\n")
119
+ claude_plan = Document.from_text(".claude/plans/packet.md", "# Plan\n")
120
+ other_markdown = Document.from_text("docs/notes.md", "# Notes\n")
121
+ assert adapters.accepts_plans(docs_plan) is True
122
+ assert adapters.accepts_plans(claude_plan) is True
123
+ assert adapters.accepts_plans(other_markdown) is False
124
+
125
+
126
+ def test_open_question_diagnostics_scan_claude_plans(
127
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
128
+ ) -> None:
129
+ def fake_hooks_module(module_name: str) -> object:
130
+ if not module_name:
131
+ raise AssertionError("missing module")
132
+ return types.SimpleNamespace(_content_has_open_questions=lambda text: True)
133
+
134
+ monkeypatch.setattr(adapters, "_hooks_module", fake_hooks_module)
135
+ all_diagnostics = adapters.open_question_diagnostics(
136
+ Document.from_text(".claude/plans/packet.md", "## Open Questions\n"),
137
+ tmp_path,
138
+ )
139
+ assert len(all_diagnostics) == 1
140
+ assert all_diagnostics[0].rule_id == "open-questions"
141
+
142
+
143
+ def test_test_pairing_diagnostics_reports_unmatched_changed_production_file(
144
+ tmp_path: Path,
145
+ ) -> None:
146
+ document_set = DocumentSet(
147
+ (Document.from_text("src/feature.py", _FUNCTION_SOURCE),),
148
+ SelectionKind.STAGED,
149
+ tmp_path,
150
+ )
151
+ all_diagnostics = adapters.test_pairing_diagnostics(document_set)
152
+ assert len(all_diagnostics) == 1
153
+ assert all_diagnostics[0].location is not None
154
+ assert all_diagnostics[0].location.path == PurePosixPath("src/feature.py")
155
+
156
+
157
+ def test_test_pairing_diagnostics_exempts_constants_only_python_modules(
158
+ tmp_path: Path,
159
+ ) -> None:
160
+ document_set = DocumentSet(
161
+ (
162
+ Document.from_text(
163
+ "hooks_constants/tool_names.py",
164
+ "TOOL_NAME: str = \"tool\"\n",
165
+ ),
166
+ ),
167
+ SelectionKind.STAGED,
168
+ tmp_path,
169
+ )
170
+ assert adapters.test_pairing_diagnostics(document_set) == ()
171
+
172
+
173
+ def test_test_pairing_diagnostics_accepts_changed_matching_test(tmp_path: Path) -> None:
174
+ document_set = DocumentSet(
175
+ (
176
+ Document.from_text("src/feature.py", _FUNCTION_SOURCE),
177
+ Document.from_text("tests/test_feature.py", _FUNCTION_SOURCE),
178
+ ),
179
+ SelectionKind.BASE,
180
+ tmp_path,
181
+ )
182
+ assert adapters.test_pairing_diagnostics(document_set) == ()
183
+
184
+
185
+ @pytest.mark.parametrize(
186
+ ("production_path", "test_path"),
187
+ (
188
+ ("scripts/policy_lint/adapters.py", "scripts/tests/test_policy_lint_rules.py"),
189
+ (
190
+ "scripts/policy_lint/selection.py",
191
+ "scripts/tests/test_policy_lint_selection.py",
192
+ ),
193
+ (
194
+ "hooks/validators/run_all_validators.py",
195
+ "hooks/validators/test_run_all_validators_integration.py",
196
+ ),
197
+ ("bin/install-plan.mjs", "bin/install.plan.test.mjs"),
198
+ ),
199
+ )
200
+ def test_test_pairing_diagnostics_accepts_grouped_families(
201
+ tmp_path: Path, production_path: str, test_path: str
202
+ ) -> None:
203
+ document_set = DocumentSet(
204
+ (
205
+ Document.from_text(production_path, _FUNCTION_SOURCE),
206
+ Document.from_text(test_path, _FUNCTION_SOURCE),
207
+ ),
208
+ SelectionKind.STAGED,
209
+ tmp_path,
210
+ )
211
+ assert adapters.test_pairing_diagnostics(document_set) == ()
212
+
213
+
214
+ def test_test_pairing_diagnostics_rejects_a_different_family(tmp_path: Path) -> None:
215
+ document_set = DocumentSet(
216
+ (
217
+ Document.from_text("src/feature.py", _FUNCTION_SOURCE),
218
+ Document.from_text("tests/test_feature_helper.py", _FUNCTION_SOURCE),
219
+ ),
220
+ SelectionKind.STAGED,
221
+ tmp_path,
222
+ )
223
+ all_diagnostics = adapters.test_pairing_diagnostics(document_set)
224
+ assert len(all_diagnostics) == 1
225
+
226
+
227
+ def test_test_pairing_diagnostics_recognizes_spec_test_paths(tmp_path: Path) -> None:
228
+ document_set = DocumentSet(
229
+ (
230
+ Document.from_text("src/widget.ts", _FUNCTION_SOURCE),
231
+ Document.from_text("tests/widget.spec.ts", _FUNCTION_SOURCE),
232
+ ),
233
+ SelectionKind.STAGED,
234
+ tmp_path,
235
+ )
236
+ assert adapters.test_pairing_diagnostics(document_set) == ()
237
+
238
+
239
+ def test_hook_configuration_diagnostics_rejects_registered_action_boundary_path(
240
+ tmp_path: Path,
241
+ ) -> None:
242
+ configuration_document = Document.from_text(
243
+ "hooks/hooks.json",
244
+ json.dumps(
245
+ {
246
+ "hooks": {
247
+ "PreToolUse": [
248
+ {"hooks": [{"command": "hooks/blocking/deny.py"}]}
249
+ ]
250
+ }
251
+ }
252
+ ),
253
+ )
254
+ all_diagnostics = adapters.hook_configuration_diagnostics(
255
+ configuration_document, tmp_path
256
+ )
257
+ assert len(all_diagnostics) == 1
258
+ assert all_diagnostics[0].rule_id == "hook-configuration"
259
+
260
+
261
+ def test_hook_configuration_diagnostics_ignore_description_text(tmp_path: Path) -> None:
262
+ configuration_document = Document.from_text(
263
+ "hooks/hooks.json",
264
+ json.dumps({"description": "blocking safety controls", "hooks": {}}),
265
+ )
266
+ assert adapters.hook_configuration_diagnostics(configuration_document, tmp_path) == ()
267
+
268
+
269
+ def test_hook_configuration_diagnostics_ignore_task_path(tmp_path: Path) -> None:
270
+ configuration_document = Document.from_text(
271
+ "hooks/hooks.json",
272
+ json.dumps({"hooks": {"PostToolUse": [{"command": "task_runner.py"}]}}),
273
+ )
274
+ assert adapters.hook_configuration_diagnostics(configuration_document, tmp_path) == ()
275
+
276
+
277
+ def test_registry_omits_native_ruff_mypy_javascript_adapters() -> None:
278
+ all_rule_ids = {each_rule.rule_id for each_rule in registry.default_registry()}
279
+ assert "ruff" not in all_rule_ids
280
+ assert "mypy" not in all_rule_ids
281
+ assert "eslint" not in all_rule_ids
282
+ assert "javascript" not in all_rule_ids
283
+
284
+
285
+ def test_registry_registers_unique_python_validator_rule() -> None:
286
+ validator_rule = _rule_named("validators")
287
+ assert validator_rule.accepts(Document.from_text("module.py", _FUNCTION_SOURCE))
288
+ assert not validator_rule.accepts(Document.from_text("module.ts", _FUNCTION_SOURCE))
289
+
290
+
291
+ def test_registry_registers_terminology_change_set_rule() -> None:
292
+ terminology_rule = _terminology_rule()
293
+ assert terminology_rule.rule_sets == _CHANGED_RULE_SETS
294
+ assert terminology_rule.selections == frozenset({SelectionKind.STAGED})
295
+
296
+
297
+ def test_registry_runs_test_pairing_for_staged_and_base_changes() -> None:
298
+ test_pairing_rule = next(
299
+ each_rule
300
+ for each_rule in registry.default_registry()
301
+ if each_rule.rule_id == "test-pairing"
302
+ )
303
+ assert isinstance(test_pairing_rule, ChangeSetRule)
304
+ assert test_pairing_rule.selections == frozenset(
305
+ {SelectionKind.STAGED, SelectionKind.BASE}
306
+ )
307
+
308
+
309
+ def test_terminology_rule_reports_cross_surface_path_and_line(
310
+ tmp_path: Path,
311
+ ) -> None:
312
+ _stage_terminology_change(
313
+ tmp_path,
314
+ "baseline = 1\n",
315
+ "premium_request_interactions = 5\n",
316
+ "Quota fields use one name.\n"
317
+ "The premium-request-budget field gates the run.\n",
318
+ )
319
+ lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
320
+
321
+ assert len(lint_report.diagnostics) == 1
322
+ diagnostic = lint_report.diagnostics[0]
323
+ assert diagnostic.rule_id == "terminology-sweep"
324
+ assert diagnostic.location is not None
325
+ assert diagnostic.location.path == PurePosixPath("docs/README.md")
326
+ assert diagnostic.location.start_line == 2
327
+
328
+
329
+ def test_terminology_rule_suppresses_identifier_present_in_prior_tree(
330
+ tmp_path: Path,
331
+ ) -> None:
332
+ _stage_terminology_change(
333
+ tmp_path,
334
+ "premium_request_interactions = 5\n",
335
+ "premium_request_interactions = 5\n"
336
+ "premium_request_interactions = 6\n",
337
+ "The premium-request-budget field gates the run.\n",
338
+ )
339
+ lint_report = lint(LintRequest.staged(tmp_path), all_registry=(_terminology_rule(),))
340
+
341
+ assert lint_report.diagnostics == ()
342
+
343
+
344
+ def test_terminology_rule_skips_file_selection(tmp_path: Path) -> None:
345
+ document_set = DocumentSet(
346
+ (Document.from_text("docs/README.md", "text\n"),),
347
+ SelectionKind.FILES,
348
+ tmp_path,
349
+ )
350
+ assert adapters.terminology_diagnostics(document_set) == ()
351
+
352
+
353
+ def test_lint_keeps_skipped_rules_out_of_executed_rules(tmp_path: Path) -> None:
354
+ skipped_rule = DocumentRule(
355
+ "skipped",
356
+ _CHANGED_RULE_SETS,
357
+ lambda _document: False,
358
+ lambda _document, _root: (),
359
+ )
360
+ _ensure_git_repository(tmp_path)
361
+ document_set = DocumentSet(
362
+ (Document.from_text("pkg/mod.py", "x = 1\n"),),
363
+ SelectionKind.TEXT,
364
+ tmp_path,
365
+ )
366
+ request = LintRequest(tmp_path, document_set)
367
+ all_rules_report = lint(request, all_registry=(skipped_rule,))
368
+ assert all_rules_report.executed_rules == ()
369
+ assert all_rules_report.skipped_rules == ("skipped",)