claude-dev-env 2.7.1 → 2.9.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 (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. package/skills/test_markdown_link_integrity.py +0 -107
@@ -874,6 +874,107 @@ def test_load_batch_spec_missing_worker_keys_raise_value_error(
874
874
  ).lower() or "must be" in str(raised_error.value).lower()
875
875
 
876
876
 
877
+ def test_load_batch_spec_rejects_an_unknown_worker_key(
878
+ tmp_path: Path,
879
+ ) -> None:
880
+ header_part, body_part = _write_prompt_parts(tmp_path)
881
+ working_directory = tmp_path / "project"
882
+ working_directory.mkdir()
883
+ worker_payload = _worker_payload(
884
+ role_name="stray-key-worker",
885
+ all_prompt_parts=[str(header_part), str(body_part)],
886
+ working_directory=working_directory,
887
+ tool_profile=TOOL_PROFILE_BUILD,
888
+ )
889
+ worker_payload["timeout_second"] = 30
890
+ specification_path = _write_batch_spec(
891
+ tmp_path, all_worker_payloads=[worker_payload]
892
+ )
893
+
894
+ with pytest.raises(ValueError) as raised_error:
895
+ batch.load_batch_spec(specification_path)
896
+
897
+ error_text = str(raised_error.value)
898
+ assert "timeout_second" in error_text
899
+ assert WORKER_SPEC_TIMEOUT_KEY in error_text
900
+
901
+
902
+ def test_load_batch_spec_names_every_unknown_worker_key(
903
+ tmp_path: Path,
904
+ ) -> None:
905
+ header_part, body_part = _write_prompt_parts(tmp_path)
906
+ working_directory = tmp_path / "project"
907
+ working_directory.mkdir()
908
+ worker_payload = _worker_payload(
909
+ role_name="two-stray-keys",
910
+ all_prompt_parts=[str(header_part), str(body_part)],
911
+ working_directory=working_directory,
912
+ tool_profile=TOOL_PROFILE_BUILD,
913
+ )
914
+ worker_payload["stray_cap"] = 5
915
+ worker_payload["notes"] = "operator scratch"
916
+ specification_path = _write_batch_spec(
917
+ tmp_path, all_worker_payloads=[worker_payload]
918
+ )
919
+
920
+ with pytest.raises(ValueError) as raised_error:
921
+ batch.load_batch_spec(specification_path)
922
+
923
+ error_text = str(raised_error.value)
924
+ assert "stray_cap" in error_text
925
+ assert "notes" in error_text
926
+
927
+
928
+ def test_load_batch_spec_rejects_the_retired_turn_cap_key(
929
+ tmp_path: Path,
930
+ ) -> None:
931
+ header_part, body_part = _write_prompt_parts(tmp_path)
932
+ working_directory = tmp_path / "project"
933
+ working_directory.mkdir()
934
+ worker_payload = _worker_payload(
935
+ role_name="retired-cap-worker",
936
+ all_prompt_parts=[str(header_part), str(body_part)],
937
+ working_directory=working_directory,
938
+ tool_profile=TOOL_PROFILE_BUILD,
939
+ )
940
+ worker_payload[RETIRED_MAX_TURNS_KEYWORD] = 5
941
+ specification_path = _write_batch_spec(
942
+ tmp_path, all_worker_payloads=[worker_payload]
943
+ )
944
+
945
+ with pytest.raises(ValueError) as raised_error:
946
+ batch.load_batch_spec(specification_path)
947
+
948
+ error_text = str(raised_error.value)
949
+ assert RETIRED_MAX_TURNS_KEYWORD in error_text
950
+ assert WORKER_SPEC_TIMEOUT_KEY in error_text
951
+
952
+
953
+ def test_load_batch_spec_accepts_every_documented_worker_key(
954
+ tmp_path: Path,
955
+ ) -> None:
956
+ header_part, body_part = _write_prompt_parts(tmp_path)
957
+ working_directory = tmp_path / "project"
958
+ working_directory.mkdir()
959
+ worker_payload: dict[str, object] = {
960
+ "role_name": "every-key-worker",
961
+ "prompt_parts": [str(header_part), str(body_part)],
962
+ "cwd": str(working_directory),
963
+ "tool_profile": TOOL_PROFILE_READONLY,
964
+ "timeout_seconds": 30,
965
+ "is_repo_only": True,
966
+ "agent_name": None,
967
+ }
968
+ specification_path = _write_batch_spec(
969
+ tmp_path, all_worker_payloads=[worker_payload]
970
+ )
971
+
972
+ batch_spec = batch.load_batch_spec(specification_path)
973
+
974
+ assert batch_spec.all_workers[0].role_name == "every-key-worker"
975
+ assert batch_spec.all_workers[0].is_repo_only is True
976
+
977
+
877
978
  def test_load_batch_spec_rejects_non_positive_timeout(
878
979
  tmp_path: Path,
879
980
  ) -> None:
@@ -1218,3 +1319,298 @@ def test_load_batch_spec_rejects_empty_agent_name(tmp_path: Path) -> None:
1218
1319
 
1219
1320
  with pytest.raises(ValueError, match=WORKER_SPEC_AGENT_NAME_KEY):
1220
1321
  batch.load_batch_spec(specification_path)
1322
+
1323
+ # --- O-02 worker advisor contract ---
1324
+
1325
+
1326
+ def test_extract_advisor_signal_accepts_four_tokens_only() -> None:
1327
+ assert batch.extract_advisor_signal("ENDORSE\nok") == "ENDORSE"
1328
+ assert batch.extract_advisor_signal("CORRECTION fix path") == "CORRECTION"
1329
+ assert batch.extract_advisor_signal("PLAN later") == "PLAN"
1330
+ assert batch.extract_advisor_signal("STOP") == "STOP"
1331
+ assert batch.extract_advisor_signal("hello ENDORSE") is None
1332
+ assert batch.extract_advisor_signal("") is None
1333
+
1334
+
1335
+ def test_load_batch_spec_parses_advisor_block(tmp_path: Path) -> None:
1336
+ header_part, body_part = _write_prompt_parts(tmp_path)
1337
+ payload = _worker_payload(
1338
+ role_name="lens",
1339
+ all_prompt_parts=[str(header_part), str(body_part)],
1340
+ working_directory=tmp_path,
1341
+ tool_profile=TOOL_PROFILE_READONLY,
1342
+ )
1343
+ specification_path = tmp_path / "batch-spec.json"
1344
+ specification_path.write_text(
1345
+ json.dumps(
1346
+ {
1347
+ "role": DEFAULT_ROLE,
1348
+ "should_ping": False,
1349
+ "workers": [payload],
1350
+ "advisor": {
1351
+ "launcher": "fixture-advisor-launcher",
1352
+ "model": "opus",
1353
+ "effort": "high",
1354
+ },
1355
+ }
1356
+ ),
1357
+ encoding=UTF8_ENCODING,
1358
+ )
1359
+ loaded = batch.load_batch_spec(specification_path)
1360
+ assert loaded.advisor is not None
1361
+ assert loaded.advisor.launcher == "fixture-advisor-launcher"
1362
+ assert loaded.advisor.model == "opus"
1363
+ assert loaded.advisor.effort == "high"
1364
+
1365
+
1366
+ def test_unique_advisor_sessions_and_completion_verdict(
1367
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1368
+ ) -> None:
1369
+ header_a, body_a = _write_prompt_parts(tmp_path, role_marker="alpha")
1370
+ header_b, body_b = _write_prompt_parts(tmp_path, role_marker="beta")
1371
+ workers = [
1372
+ _worker_payload(
1373
+ role_name="alpha",
1374
+ all_prompt_parts=[str(header_a), str(body_a)],
1375
+ working_directory=tmp_path,
1376
+ tool_profile=TOOL_PROFILE_BUILD,
1377
+ ),
1378
+ _worker_payload(
1379
+ role_name="beta",
1380
+ all_prompt_parts=[str(header_b), str(body_b)],
1381
+ working_directory=tmp_path,
1382
+ tool_profile=TOOL_PROFILE_BUILD,
1383
+ ),
1384
+ ]
1385
+ specification_path = tmp_path / "batch-spec.json"
1386
+ specification_path.write_text(
1387
+ json.dumps(
1388
+ {
1389
+ "role": DEFAULT_ROLE,
1390
+ "should_ping": False,
1391
+ "workers": workers,
1392
+ "advisor": {
1393
+ "launcher": "fixture-advisor-launcher",
1394
+ "model": "opus",
1395
+ "effort": "high",
1396
+ },
1397
+ }
1398
+ ),
1399
+ encoding=UTF8_ENCODING,
1400
+ )
1401
+ bind_count = {"n": 0}
1402
+ sessions_issued: list[str] = []
1403
+
1404
+ def fake_advisor(
1405
+ *,
1406
+ launcher: str,
1407
+ model: str,
1408
+ effort: str,
1409
+ prompt_text: str,
1410
+ session_id: str | None = None,
1411
+ ) -> tuple[str | None, str, int]:
1412
+ assert launcher == "fixture-advisor-launcher"
1413
+ assert model == "opus"
1414
+ assert effort == "high"
1415
+ if session_id is None:
1416
+ bind_count["n"] += 1
1417
+ session = f"session-{bind_count['n']}"
1418
+ sessions_issued.append(session)
1419
+ return session, "ENDORSE\npre-dispatch ok", 0
1420
+ return session_id, "ENDORSE\npost-report ok", 0
1421
+
1422
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake_advisor)
1423
+ monkeypatch.setattr(
1424
+ batch,
1425
+ "batch_preflight",
1426
+ lambda **kwargs: PreflightOutcome(is_usable=True, reason=None),
1427
+ )
1428
+ monkeypatch.setattr(batch, "batch_sleep", lambda seconds: None)
1429
+ recorder = _RunnerRecorder(
1430
+ {
1431
+ "alpha": _ok_outcome(),
1432
+ "beta": _ok_outcome(),
1433
+ }
1434
+ )
1435
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1436
+ loaded = batch.load_batch_spec(specification_path)
1437
+ summary = batch.run_grok_batch(
1438
+ batch_spec=loaded, run_state_directory=tmp_path / "run"
1439
+ )
1440
+ assert summary.is_preflight_usable
1441
+ assert len(summary.all_worker_reports) == 2
1442
+ all_session_ids = {
1443
+ each.advisor_session_id for each in summary.all_worker_reports
1444
+ }
1445
+ assert all_session_ids == {"session-1", "session-2"}
1446
+ assert all(
1447
+ each.advisor_completion_signal == "ENDORSE"
1448
+ for each in summary.all_worker_reports
1449
+ )
1450
+ assert all(
1451
+ each.classification != "advisor_blocked"
1452
+ for each in summary.all_worker_reports
1453
+ )
1454
+ for each_report in summary.all_worker_reports:
1455
+ prompt_text = Path(each_report.prompt_path).read_text(encoding=UTF8_ENCODING)
1456
+ assert each_report.advisor_session_id in prompt_text
1457
+
1458
+
1459
+ def test_advisor_failure_classifies_advisor_blocked(
1460
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1461
+ ) -> None:
1462
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="solo")
1463
+ payload = _worker_payload(
1464
+ role_name="solo",
1465
+ all_prompt_parts=[str(header_part), str(body_part)],
1466
+ working_directory=tmp_path,
1467
+ tool_profile=TOOL_PROFILE_BUILD,
1468
+ )
1469
+ specification_path = tmp_path / "batch-spec.json"
1470
+ specification_path.write_text(
1471
+ json.dumps(
1472
+ {
1473
+ "role": DEFAULT_ROLE,
1474
+ "should_ping": False,
1475
+ "workers": [payload],
1476
+ "advisor": {"launcher": "fixture-advisor-launcher"},
1477
+ }
1478
+ ),
1479
+ encoding=UTF8_ENCODING,
1480
+ )
1481
+
1482
+ def failing_advisor(**kwargs: object) -> tuple[str | None, str, int]:
1483
+ return None, "", 1
1484
+
1485
+ monkeypatch.setattr(batch, "batch_invoke_advisor", failing_advisor)
1486
+ monkeypatch.setattr(
1487
+ batch,
1488
+ "batch_preflight",
1489
+ lambda **kwargs: PreflightOutcome(is_usable=True, reason=None),
1490
+ )
1491
+ monkeypatch.setattr(batch, "batch_sleep", lambda seconds: None)
1492
+ monkeypatch.setattr(
1493
+ batch,
1494
+ "batch_headless_runner",
1495
+ _RunnerRecorder({"solo": _ok_outcome()}),
1496
+ )
1497
+ loaded = batch.load_batch_spec(specification_path)
1498
+ summary = batch.run_grok_batch(
1499
+ batch_spec=loaded, run_state_directory=tmp_path / "run"
1500
+ )
1501
+ assert len(summary.all_worker_reports) == 1
1502
+ report = summary.all_worker_reports[0]
1503
+ assert report.classification == "advisor_blocked"
1504
+ assert report.is_ok is False
1505
+
1506
+ def test_bind_unique_worker_advisor_rejects_placeholder(
1507
+ monkeypatch: pytest.MonkeyPatch,
1508
+ ) -> None:
1509
+ def boom(**kwargs: object) -> tuple[str | None, str, int]:
1510
+ raise AssertionError("should not call launcher for placeholder")
1511
+
1512
+ monkeypatch.setattr(batch, "batch_invoke_advisor", boom)
1513
+ with pytest.raises(ValueError, match="placeholder"):
1514
+ batch.bind_unique_worker_advisor(
1515
+ advisor_spec=batch.AdvisorSpec(launcher=batch.DEFAULT_ADVISOR_LAUNCHER_PLACEHOLDER),
1516
+ role_name="lens",
1517
+ all_used_session_ids=set(),
1518
+ )
1519
+
1520
+
1521
+ def test_bind_unique_worker_advisor_returns_session(
1522
+ monkeypatch: pytest.MonkeyPatch,
1523
+ ) -> None:
1524
+ def fake(**kwargs: object) -> tuple[str | None, str, int]:
1525
+ return "sess-unique-1", "ENDORSE\nok", 0
1526
+
1527
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
1528
+ session_id, signal = batch.bind_unique_worker_advisor(
1529
+ advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
1530
+ role_name="lens",
1531
+ all_used_session_ids=set(),
1532
+ )
1533
+ assert session_id == "sess-unique-1"
1534
+ assert signal == "ENDORSE"
1535
+
1536
+
1537
+ def test_obtain_advisor_completion_verdict_endorses(
1538
+ monkeypatch: pytest.MonkeyPatch,
1539
+ ) -> None:
1540
+ def fake(**kwargs: object) -> tuple[str | None, str, int]:
1541
+ return kwargs.get("session_id"), "ENDORSE\nok", 0
1542
+
1543
+ monkeypatch.setattr(batch, "batch_invoke_advisor", fake)
1544
+ signal = batch.obtain_advisor_completion_verdict(
1545
+ advisor_spec=batch.AdvisorSpec(launcher="fixture-advisor-launcher"),
1546
+ role_name="lens",
1547
+ session_id="sess-1",
1548
+ report_text="done",
1549
+ )
1550
+ assert signal == "ENDORSE"
1551
+
1552
+
1553
+ def test_invoke_advisor_launcher_builds_command(
1554
+ monkeypatch: pytest.MonkeyPatch,
1555
+ ) -> None:
1556
+ captured: dict[str, object] = {}
1557
+
1558
+ class _Completed:
1559
+ returncode = 0
1560
+ stdout = '{"session_id":"s1","result":"ENDORSE\\nok"}'
1561
+ stderr = ""
1562
+
1563
+ def fake_run(args, **kwargs): # type: ignore[no-untyped-def] # subprocess.run stub for argv capture
1564
+ captured["args"] = list(args)
1565
+ captured["input"] = kwargs.get("input")
1566
+ return _Completed()
1567
+
1568
+ monkeypatch.setattr(batch.subprocess, "run", fake_run)
1569
+ session_id, body, code = batch.invoke_advisor_launcher(
1570
+ launcher="fixture-advisor-launcher",
1571
+ model="opus",
1572
+ effort="high",
1573
+ prompt_text="hello",
1574
+ )
1575
+ assert code == 0
1576
+ assert session_id == "s1"
1577
+ assert "ENDORSE" in body
1578
+ assert captured["args"][0] == "fixture-advisor-launcher"
1579
+ assert "--model" in captured["args"]
1580
+
1581
+
1582
+ def test_invoke_advisor_launcher_missing_binary_raises_advisor_failure() -> None:
1583
+ try:
1584
+ batch.invoke_advisor_launcher(
1585
+ launcher="__no_such_advisor_launcher_xyz__",
1586
+ model="opus",
1587
+ effort="high",
1588
+ prompt_text="ping",
1589
+ )
1590
+ raise AssertionError("expected AdvisorFailureError")
1591
+ except batch.AdvisorFailureError as raised:
1592
+ assert "not found" in str(raised).lower() or "launcher" in str(raised).lower()
1593
+
1594
+
1595
+ def test_invoke_advisor_launcher_passes_timeout(
1596
+ monkeypatch: pytest.MonkeyPatch,
1597
+ ) -> None:
1598
+ captured: dict[str, object] = {}
1599
+
1600
+ class _Completed:
1601
+ returncode = 0
1602
+ stdout = '{"session_id":"s-timeout","result":"ENDORSE\\nok"}'
1603
+ stderr = ""
1604
+
1605
+ def fake_run(args, **kwargs): # type: ignore[no-untyped-def] # subprocess.run stub
1606
+ captured["timeout"] = kwargs.get("timeout")
1607
+ return _Completed()
1608
+
1609
+ monkeypatch.setattr(batch.subprocess, "run", fake_run)
1610
+ batch.invoke_advisor_launcher(
1611
+ launcher="fixture-advisor-launcher",
1612
+ model="opus",
1613
+ effort="high",
1614
+ prompt_text="hello",
1615
+ )
1616
+ assert captured["timeout"] == batch.MAXIMUM_ADVISOR_TIMEOUT_SECONDS
@@ -10,6 +10,7 @@ pytest suite for the Python scripts and Pester suite for the PowerShell scripts
10
10
  | `test_setup_project_paths_config.py` | Configuration constants used by `setup_project_paths.py` |
11
11
  | `test_sweep_empty_dirs.py` | `sweep_empty_dirs.py` — age check, one-shot mode, and continuous-watch behavior |
12
12
  | `test_sync_to_cursor.py` | `sync_to_cursor/` package — mapping, hashing, manifest, and path resolution |
13
+ | `test_grok_worker_constants.py` | `grok_worker_constants.py` — the accepted batch worker-key set stays in step with the worker key constants, and the unknown-key message names both its placeholders |
13
14
 
14
15
  ## PowerShell test files
15
16
 
@@ -0,0 +1,59 @@
1
+ """Specifications for the accepted-key set on a batch worker entry.
2
+
3
+ The batch launcher rejects a worker entry carrying any key outside
4
+ ``ALL_KNOWN_WORKER_SPEC_KEYS``. That makes the set a gate on the published
5
+ JSON contract: a worker key the module defines but the set omits is rejected
6
+ from every spec that uses it, and a member the module declares nowhere keeps
7
+ accepting a key the launcher drops. Both drifts are silent, so they are
8
+ pinned here.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dev_env_scripts_constants import grok_worker_constants
14
+
15
+ WORKER_SPEC_KEY_NAME_PREFIX: str = "WORKER_SPEC_"
16
+ WORKER_SPEC_KEY_NAME_SUFFIX: str = "_KEY"
17
+
18
+
19
+ def _all_declared_worker_key_values() -> set[str]:
20
+ """Read every ``WORKER_SPEC_*_KEY`` value the constants module declares."""
21
+ return {
22
+ getattr(grok_worker_constants, each_name)
23
+ for each_name in dir(grok_worker_constants)
24
+ if each_name.startswith(WORKER_SPEC_KEY_NAME_PREFIX)
25
+ and each_name.endswith(WORKER_SPEC_KEY_NAME_SUFFIX)
26
+ }
27
+
28
+
29
+ def test_should_accept_every_declared_worker_spec_key() -> None:
30
+ all_declared_key_values = _all_declared_worker_key_values()
31
+ all_missing_key_values = (
32
+ all_declared_key_values - grok_worker_constants.ALL_KNOWN_WORKER_SPEC_KEYS
33
+ )
34
+
35
+ assert not all_missing_key_values, (
36
+ "worker key constants missing from the accepted set: "
37
+ f"{sorted(all_missing_key_values)}"
38
+ )
39
+
40
+
41
+ def test_should_accept_only_keys_the_module_declares() -> None:
42
+ all_declared_key_values = _all_declared_worker_key_values()
43
+ all_stale_key_values = (
44
+ grok_worker_constants.ALL_KNOWN_WORKER_SPEC_KEYS - all_declared_key_values
45
+ )
46
+
47
+ assert not all_stale_key_values, (
48
+ f"accepted keys with no worker key constant: {sorted(all_stale_key_values)}"
49
+ )
50
+
51
+
52
+ def test_should_name_both_placeholders_in_the_unknown_key_message() -> None:
53
+ formatted_message = grok_worker_constants.UNKNOWN_WORKER_KEY_ERROR_TEMPLATE.format(
54
+ unknown_keys="timeout_second",
55
+ accepted_keys="timeout_seconds",
56
+ )
57
+
58
+ assert "timeout_second;" in formatted_message
59
+ assert formatted_message.endswith("timeout_seconds")
package/skills/CLAUDE.md CHANGED
@@ -17,7 +17,9 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
17
17
 
18
18
  ## Shared support code
19
19
 
20
- `_shared/`support code used by more than one skill. It holds `pr-loop/`, which provides prompt templates and Python helper scripts shared across the PR-loop skills, and `advisor/`, which provides the shared warm-advisor protocol used by `team-advisor`, `orchestrator`, and `orchestrator-refresh`.
20
+ **`skills/_shared/`** — skill-local PR-loop helpers plus `@` stubs that name
21
+ canonical homes under **`@~/.claude/_shared/`** (advisor protocol, PR-loop
22
+ contracts, runtime scripts). Map: `skills/_shared/CLAUDE.md`. End-of-run gotchas: `skills/_shared/end-of-run-gotcha-recommendations.md`.
21
23
 
22
24
  ## Skill groups
23
25
 
@@ -25,7 +27,7 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
25
27
  - `anthropic-plan` — creates a source-grounded plan packet before any code changes
26
28
  - `orchestrator` — turns the session into the orchestrator: it spawns executor subagents to do the code edits and test runs; hard decisions go to a shared advisor (Claude warm `session-advisor` via SendMessage; a third-party host: max-tier Claude via CLI Claude-chain)
27
29
  - `orchestrator-refresh` — sub-skill fired by the `/orchestrator` loop to re-assert the host-matched shared-advisor discipline mid-run (Claude SendMessage; a third-party host's Claude CLI chain, no Agent-tool advisor spawn)
28
- - `team-advisor` — binds one advisor at the strongest reachable tier (Claude warm agent; a third-party host: max-tier Claude via CLI Claude-chain, fail closed when unreachable) and consults it for a second opinion before a big decision, at completion, when stuck, or when reconsidering the approach
30
+ - `team-advisor` — binds one advisor at the strongest reachable tier (Claude warm agent; a third-party host: max-tier Claude via CLI Claude-chain, fail closed when unreachable); consult cadence and weight live in `docs/references/advisor-tool.md`
29
31
  - `grokify` — builds a paste-ready Grok Build handoff with a Claude advisor charter
30
32
  - `grok-spawn` — orchestrator playbook for fleets of headless grok CLI workers (preflight, batch spec, `spawn_grok_batch.py`)
31
33
 
@@ -1,11 +1,44 @@
1
- # _shared
1
+ # skills/_shared
2
2
 
3
- Support code shared across multiple skills. Each subdirectory targets a specific cross-skill concern.
3
+ **Map** for skill-install shared assets. Open a stub, then load the `@` target.
4
+
5
+ ## Two homes
6
+
7
+ | Home | Path | Holds |
8
+ |---|---|---|
9
+ | **Skills shared** | `~/.claude/skills/_shared/` | Converge helpers, end-of-run gotcha ref, and `@` stubs |
10
+ | **Top-level shared** | `~/.claude/_shared/` | Advisor protocol, PR-loop contracts, runtime scripts |
11
+
12
+
13
+ ## Reference docs (this tree)
14
+
15
+ | File | Role |
16
+ |---|---|
17
+ | **`end-of-run-gotcha-recommendations.md`** | End-of-run pasteable gotcha protocol for every skill |
4
18
 
5
19
  ## Subdirectories
6
20
 
7
21
  | Directory | Role |
8
22
  |---|---|
9
- | `pr-loop/` | Prompt templates and Python helper scripts used by both `bugteam` and `pr-converge` for their audit-fix loop. |
23
+ | **`advisor/`** | Stubs `@~/.claude/_shared/advisor/` |
24
+ | **`pr-loop/`** | Local converge helpers + stubs → `@~/.claude/_shared/pr-loop/` |
25
+
26
+ ## Canonical-path stubs
27
+
28
+ | Stub here | Load |
29
+ |---|---|
30
+ | `advisor/advisor-protocol.md` | `@~/.claude/_shared/advisor/advisor-protocol.md` |
31
+ | `advisor/CLAUDE.md` | `@~/.claude/_shared/advisor/CLAUDE.md` |
32
+ | `advisor/scripts/README.md` | `@~/.claude/_shared/advisor/scripts/` |
33
+ | `pr-loop/audit-contract.md` | `@~/.claude/_shared/pr-loop/audit-contract.md` |
34
+ | `pr-loop/audit-reply-template.md` | `@~/.claude/_shared/pr-loop/audit-reply-template.md` |
35
+ | `pr-loop/code-rules-gate.md` | `@~/.claude/_shared/pr-loop/code-rules-gate.md` |
36
+ | `pr-loop/fix-protocol.md` | `@~/.claude/_shared/pr-loop/fix-protocol.md` |
37
+ | `pr-loop/gh-payloads.md` | `@~/.claude/_shared/pr-loop/gh-payloads.md` |
38
+ | `pr-loop/post-audit-thread-contract.md` | `@~/.claude/_shared/pr-loop/post-audit-thread-contract.md` |
39
+ | `pr-loop/precatch-rubric.md` | `@~/.claude/_shared/pr-loop/precatch-rubric.md` |
40
+ | `pr-loop/state-schema.md` | `@~/.claude/_shared/pr-loop/state-schema.md` |
41
+ | `pr-loop/worker-spawn.md` | `@~/.claude/_shared/pr-loop/worker-spawn.md` |
42
+ | `pr-loop/scripts/RUNTIME_SCRIPTS.md` | `@~/.claude/_shared/pr-loop/scripts/` |
10
43
 
11
- Files here are not skills themselves and have no `SKILL.md`. They install alongside each consuming skill via the install pipeline in `packages/claude-dev-env/bin/install.mjs`.
44
+ Install via `packages/claude-dev-env/bin/install.mjs`.
@@ -0,0 +1,9 @@
1
+ # Advisor
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/CLAUDE.md
6
+
7
+ @~/.claude/_shared/advisor/advisor-protocol.md
8
+
9
+ Scripts: `~/.claude/_shared/advisor/scripts/` (`model_tier_run_validator.py`, `tier_model_ids.py`, constants under `scripts/config/`).
@@ -0,0 +1,5 @@
1
+ # Advisor protocol
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/advisor-protocol.md
@@ -0,0 +1,9 @@
1
+ # Advisor scripts
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/scripts/model_tier_run_validator.py
6
+
7
+ @~/.claude/_shared/advisor/scripts/tier_model_ids.py
8
+
9
+ Full tree: `~/.claude/_shared/advisor/scripts/`.