claude-dev-env 8.21.0 → 8.23.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 (45) hide show
  1. package/.agents/skills/eli5/SKILL.md +7 -3
  2. package/.agents/skills/eli5/reference/task-seeds.md +5 -4
  3. package/.agents/skills/sync-dev-env/SKILL.md +51 -0
  4. package/.agents/skills/sync-dev-env/scripts/link_plugin_skills.py +211 -0
  5. package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/__init__.py +0 -0
  6. package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/__init__.py +1 -0
  7. package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/constants.py +26 -0
  8. package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/config/test_constants.py +19 -0
  9. package/.agents/skills/sync-dev-env/scripts/sync_dev_env_constants/test___init__.py +5 -0
  10. package/.agents/skills/sync-dev-env/scripts/test_link_plugin_skills.py +142 -0
  11. package/bin/ever-shipped-skills.mjs +1 -0
  12. package/bin/merge_settings_defaults.test.mjs +43 -0
  13. package/hooks/blocking/edit_marker_gate.py +74 -0
  14. package/hooks/blocking/reply_length_gate.py +119 -0
  15. package/hooks/blocking/test_edit_marker_gate.py +110 -0
  16. package/hooks/blocking/test_reply_length_gate.py +193 -0
  17. package/hooks/hooks.json +25 -0
  18. package/hooks/hooks_constants/advisor_rules_prompt_constants.py +43 -0
  19. package/hooks/hooks_constants/edit_marker_gate_constants.py +18 -0
  20. package/hooks/hooks_constants/reply_length_gate_constants.py +28 -0
  21. package/hooks/hooks_constants/test_edit_marker_gate_constants.py +9 -0
  22. package/hooks/hooks_constants/test_reply_length_gate_constants.py +15 -0
  23. package/hooks/session/advisor_rules_prompt.py +76 -0
  24. package/hooks/session/test_advisor_rules_prompt.py +86 -0
  25. package/package.json +1 -1
  26. package/rules/asd-ste100-language.md +1 -0
  27. package/rules/ci-owns-the-gate.md +28 -0
  28. package/rules/memory-stores-durable-facts.md +57 -0
  29. package/rules/review-closure-is-a-check.md +1 -0
  30. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +4 -0
  31. package/scripts/dev_env_scripts_constants/review_closure_constants.py +13 -0
  32. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -0
  33. package/scripts/invoke_code_review.py +4 -1
  34. package/scripts/local_report_cli.py +50 -53
  35. package/scripts/policy_lint/adapter_configuration.py +8 -0
  36. package/scripts/policy_lint/config/constants.py +2 -0
  37. package/scripts/resolve_worker_spawn.py +4 -1
  38. package/scripts/review_closure_model.py +16 -3
  39. package/scripts/shared_tree_paths.py +27 -0
  40. package/scripts/test_resolve_worker_spawn.py +28 -0
  41. package/scripts/test_shared_tree_paths.py +27 -0
  42. package/scripts/tests/test_adapter_configuration.py +16 -0
  43. package/scripts/tests/test_local_report_publisher_cli.py +54 -0
  44. package/scripts/tests/test_review_closure.py +1 -0
  45. package/scripts/tests/test_review_closure_model.py +70 -0
@@ -21,6 +21,10 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
21
21
  SCRIPTS_DIRECTORY_NAME,
22
22
  SHARED_PACKAGE_DIRECTORY_NAME,
23
23
  )
24
+ from dev_env_scripts_constants.shared_tree_constants import (
25
+ ADVISOR_DIRECTORY_NAME,
26
+ TIER_MODEL_IDS_MODULE_FILENAME,
27
+ )
24
28
 
25
29
 
26
30
  def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
@@ -143,6 +147,29 @@ def test_missing_module_returns_unresolved_candidate(tmp_path: Path) -> None:
143
147
  ).is_file()
144
148
 
145
149
 
150
+ def test_junction_scripts_dir_finds_advisor_scripts_on_unresolved_parent(
151
+ tmp_path: Path,
152
+ ) -> None:
153
+ module_file = _build_junction_scripts_layout(tmp_path)
154
+ expected_scripts_directory = (
155
+ tmp_path
156
+ / "root"
157
+ / SHARED_PACKAGE_DIRECTORY_NAME
158
+ / ADVISOR_DIRECTORY_NAME
159
+ / SCRIPTS_DIRECTORY_NAME
160
+ )
161
+ expected_scripts_directory.mkdir(parents=True)
162
+ (expected_scripts_directory / TIER_MODEL_IDS_MODULE_FILENAME).write_text(
163
+ "", encoding="utf-8"
164
+ )
165
+ resolved_scripts_directory = (
166
+ shared_tree_paths.resolve_shared_advisor_scripts_directory(
167
+ module_file, all_environment={}
168
+ )
169
+ )
170
+ assert resolved_scripts_directory == expected_scripts_directory
171
+
172
+
146
173
  def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
147
174
  """A caller three levels under the root resolves its own sub-package.
148
175
 
@@ -58,6 +58,22 @@ def test_new_step_note_gate_registration_is_clean(tmp_path: Path) -> None:
58
58
  assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
59
59
 
60
60
 
61
+ def test_new_reply_length_gate_registration_is_clean(tmp_path: Path) -> None:
62
+ current_document = _hook_document(
63
+ ["hooks/blocking/reply_length_gate.py"],
64
+ '{"hooks": {}}',
65
+ )
66
+ assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
67
+
68
+
69
+ def test_new_edit_marker_gate_registration_is_clean(tmp_path: Path) -> None:
70
+ current_document = _hook_document(
71
+ ["hooks/blocking/edit_marker_gate.py"],
72
+ '{"hooks": {}}',
73
+ )
74
+ assert adapters.hook_configuration_diagnostics(current_document, tmp_path) == ()
75
+
76
+
61
77
  def test_new_blocker_in_staged_change_is_rejected(tmp_path: Path) -> None:
62
78
  current_document = _hook_document(
63
79
  ["hooks/blocking/new.py"],
@@ -6,6 +6,8 @@ import subprocess
6
6
  import sys
7
7
  from pathlib import Path
8
8
 
9
+ import pytest
10
+
9
11
  SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
10
12
  if str(SCRIPTS_DIRECTORY) not in sys.path:
11
13
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
@@ -83,3 +85,55 @@ def test_publisher_script_displays_help_without_authentication() -> None:
83
85
 
84
86
  assert completed_process.returncode == 0
85
87
  assert "usage:" in completed_process.stdout
88
+
89
+
90
+ def _token_arguments(tmp_path: Path, variable_name: str) -> list[str]:
91
+ return [
92
+ "--api-url",
93
+ "http://127.0.0.1:9",
94
+ "--token-environment",
95
+ variable_name,
96
+ "--repository",
97
+ "owner/repository",
98
+ "--pull-number",
99
+ "7",
100
+ "--local-repo",
101
+ str(tmp_path),
102
+ "--manifest",
103
+ str(tmp_path / "manifest.json"),
104
+ "--report",
105
+ str(tmp_path / "report.json"),
106
+ ]
107
+
108
+
109
+ def test_cli_names_missing_token_variable_without_network_access(
110
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
111
+ ) -> None:
112
+ monkeypatch.delenv("PUBLISHER_TEST_TOKEN", raising=False)
113
+ standard_output = io.StringIO()
114
+ standard_error = io.StringIO()
115
+
116
+ exit_code = main(
117
+ _token_arguments(tmp_path, "PUBLISHER_TEST_TOKEN"),
118
+ stdout=standard_output,
119
+ stderr=standard_error,
120
+ )
121
+
122
+ assert exit_code == 3
123
+ assert standard_output.getvalue() == ""
124
+ assert "PUBLISHER_TEST_TOKEN" in standard_error.getvalue()
125
+
126
+
127
+ def test_cli_token_path_reaches_the_api_without_app_settings(
128
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
129
+ ) -> None:
130
+ monkeypatch.setenv("PUBLISHER_TEST_TOKEN", "token")
131
+ standard_error = io.StringIO()
132
+
133
+ exit_code = main(
134
+ _token_arguments(tmp_path, "PUBLISHER_TEST_TOKEN"), stderr=standard_error
135
+ )
136
+
137
+ assert exit_code == 3
138
+ assert "Missing publisher setting" not in standard_error.getvalue()
139
+ assert "refused" in standard_error.getvalue().lower()
@@ -140,6 +140,7 @@ def should_print_the_url_of_a_waiting_top_level_comment(
140
140
  identifier=1,
141
141
  author_login="qodo-merge-pro[bot]",
142
142
  is_bot=True,
143
+ is_notice=False,
143
144
  created_at=posted,
144
145
  updated_at=posted,
145
146
  url=summary_url,
@@ -4,6 +4,8 @@ import sys
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
6
 
7
+ import pytest
8
+
7
9
  SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
8
10
  if str(SCRIPTS_DIRECTORY) not in sys.path:
9
11
  sys.path.insert(0, str(SCRIPTS_DIRECTORY))
@@ -315,11 +317,13 @@ def top_level_comment(
315
317
  edited_minute: int | None = None,
316
318
  url: str = BOT_SUMMARY_URL,
317
319
  is_bot: bool = True,
320
+ is_notice: bool = False,
318
321
  ) -> model.TopLevelComment:
319
322
  return model.TopLevelComment(
320
323
  identifier=posted_minute,
321
324
  author_login=author_login,
322
325
  is_bot=is_bot,
326
+ is_notice=is_notice,
323
327
  created_at=at_minute(posted_minute),
324
328
  updated_at=at_minute(posted_minute if edited_minute is None else edited_minute),
325
329
  url=url,
@@ -445,6 +449,72 @@ def should_open_a_bot_comment_posted_after_the_driver_time() -> None:
445
449
  )
446
450
 
447
451
 
452
+ def should_close_a_bot_notice_posted_after_the_driver_time() -> None:
453
+ comment = top_level_comment(REVIEW_BOT, 5, is_notice=True)
454
+
455
+ assert model.top_level_finding(comment, DRIVING_AGENT, at_minute(4)) is None
456
+
457
+
458
+ @pytest.mark.parametrize(
459
+ "body",
460
+ [
461
+ "No code changes since the last review \u2014 review skipped",
462
+ "[Code review](https://example.test/pull/7#issuecomment-1) by qodo was "
463
+ "updated up to the latest commit https://example.test/commit/abc",
464
+ "<!-- graphite-review-comment -->\n\n### Graphite AI review",
465
+ "<h3>PR Summary by Qodo</h3>\n\nBuild the default map per instance",
466
+ "\n<h3>Qodo is busy working</h3>\n\nCheck back in a few minutes.",
467
+ "\n<h3>Code Review by Qodo</h3>\n<code>\U0001f41e Bugs (0)</code>\n\n"
468
+ "<h3>Great, no issues found!</h3>\nQodo reviewed your code",
469
+ ],
470
+ )
471
+ def should_read_a_bot_notice_as_a_notice(body: str) -> None:
472
+ comment = model.parse_top_level_comment(notice_record(body, "Bot"))
473
+
474
+ assert comment.is_notice
475
+ assert model.top_level_findings((comment,), DRIVING_AGENT) == ()
476
+
477
+
478
+ def should_read_a_person_pasting_notice_text_as_a_comment() -> None:
479
+ comment = model.parse_top_level_comment(
480
+ notice_record("No code changes since the last review", "User")
481
+ )
482
+
483
+ assert not comment.is_notice
484
+ assert len(model.top_level_findings((comment,), DRIVING_AGENT)) == 1
485
+
486
+
487
+ def should_read_a_bot_finding_as_a_comment() -> None:
488
+ comment = model.parse_top_level_comment(
489
+ notice_record("A finding: the loop never ends.", "Bot")
490
+ )
491
+
492
+ assert not comment.is_notice
493
+
494
+
495
+ def should_read_a_qodo_review_with_bugs_as_a_comment() -> None:
496
+ comment = model.parse_top_level_comment(
497
+ notice_record(
498
+ "\n<h3>Code Review by Qodo</h3>\n<code>\U0001f41e Bugs (2)</code>\n\n"
499
+ "1. The loop never ends.",
500
+ "Bot",
501
+ )
502
+ )
503
+
504
+ assert not comment.is_notice
505
+
506
+
507
+ def notice_record(body: str, user_type: str) -> dict[str, object]:
508
+ return {
509
+ "id": 1,
510
+ "user": {"login": REVIEW_BOT, "type": user_type},
511
+ "body": body,
512
+ "created_at": "2026-09-26T12:05:00+00:00",
513
+ "updated_at": "2026-09-26T12:05:00+00:00",
514
+ "html_url": BOT_SUMMARY_URL,
515
+ }
516
+
517
+
448
518
  def should_count_waiting_top_level_comments_among_the_findings() -> None:
449
519
  all_findings = model.all_open_findings(
450
520
  (), (top_level_comment(REVIEW_BOT, 1),), DRIVING_AGENT, None