claude-dev-env 8.2.0 → 8.4.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 (70) hide show
  1. package/.agents/agents/poteto-agent.md +6 -0
  2. package/.agents/agents/test_agent_frontmatter.py +9 -9
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -1
  4. package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
  5. package/.agents/{agents → agents-archived}/code-quality-agent.md +1 -1
  6. package/.agents/{agents → agents-archived}/session-advisor.md +1 -1
  7. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
  8. package/.agents/skills/grok-spawn/SKILL.md +9 -6
  9. package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
  10. package/.agents/skills/issue-tracker/SKILL.md +2 -2
  11. package/.agents/skills/orchestrator/SKILL.md +16 -15
  12. package/.agents/skills/orchestrator-refresh/SKILL.md +2 -0
  13. package/.agents/skills/pull-request/SKILL.md +14 -5
  14. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  15. package/.agents/skills/team-advisor/SKILL.md +13 -8
  16. package/_shared/advisor/reference/spawn-walk-log.md +2 -1
  17. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -0
  18. package/_shared/advisor/scripts/model_tier_run_validator.py +169 -14
  19. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +20 -0
  20. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +99 -0
  21. package/bin/install.agents-home.test.mjs +54 -94
  22. package/bin/install.mjs +31 -24
  23. package/bin/install.test.mjs +35 -0
  24. package/bin/resolve-package-managed-directory.test.mjs +1 -1
  25. package/bin/subagent-model-policy.test.mjs +15 -8
  26. package/docs/references/advisor-tool.md +32 -0
  27. package/docs/references/team-advisor-skill.md +1 -1
  28. package/docs/subagent-model-routing.md +6 -5
  29. package/hooks/blocking/tdd_enforcer.py +2 -3
  30. package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
  31. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
  32. package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
  33. package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
  34. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
  35. package/hooks/routing/subagent_model_routing.mjs +15 -0
  36. package/hooks/routing/subagent_model_routing.test.mjs +58 -25
  37. package/hooks/validators/README.md +0 -1
  38. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +1 -2
  39. package/package.json +1 -1
  40. package/rules/git-workflow.md +3 -3
  41. package/rules/shell-invocation.md +0 -2
  42. package/rules/subagent-model-policy.json +5 -13
  43. package/rules/testing.md +0 -2
  44. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
  45. package/scripts/grok_headless_runner.py +19 -1
  46. package/scripts/grok_worker_preflight.py +67 -1
  47. package/scripts/policy_lint/config/approved_test_pairs.py +9 -0
  48. package/scripts/resolve_advisor_model_route.mjs +2 -2
  49. package/scripts/subagent_model_policy.mjs +4 -1
  50. package/scripts/test_grok_headless_runner.py +35 -1
  51. package/scripts/test_grok_worker_preflight.py +73 -0
  52. package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
  53. package/scripts/tests/test_policy_lint_rules_pairing.py +16 -0
  54. package/scripts/tests/test_rules.py +5 -5
  55. package/scripts/windows/pstack/pstack-command-shim.ps1 +41 -0
  56. package/scripts/windows/pstack/ship-pr.cmd +3 -0
  57. package/scripts/windows/pstack/watch-pr.cmd +3 -0
  58. package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
  59. package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
  60. package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
  61. package/hooks/observability/test_failure_recorder.py +0 -221
  62. package/hooks/observability/test_test_failure_recorder.py +0 -173
  63. package/hooks/validators/VALIDATION_REPORT.md +0 -287
  64. package/hooks/validators/validator_defaults.py +0 -5
  65. /package/.agents/{agents → agents-archived}/clean-coder.md +0 -0
  66. /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
  67. /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
  68. /package/.agents/{agents → agents-archived}/plan-packet-validator.md +0 -0
  69. /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
  70. /package/.agents/{agents → agents-archived}/skill-writer-agent.md +0 -0
@@ -42,6 +42,7 @@ from __future__ import annotations
42
42
 
43
43
  import json
44
44
  import sys
45
+ from collections.abc import Mapping
45
46
  from dataclasses import dataclass
46
47
  from pathlib import Path
47
48
 
@@ -52,7 +53,21 @@ if _config_directory not in sys.path:
52
53
  if _scripts_directory not in sys.path:
53
54
  sys.path.insert(0, _scripts_directory)
54
55
 
55
- from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
56
+ from advisor_scripts_constants.advisor_route_constants import (
57
+ ADVISOR_FALLBACK_TIER,
58
+ ADVISOR_MODEL_TIER,
59
+ CLI_BIND_SUCCESS_TOKEN,
60
+ CODEX_BIND_SUCCESS_TOKEN,
61
+ SPAWN_OUTCOME_KEY,
62
+ SPAWN_SUCCESS_TOKEN,
63
+ TIER_KEY,
64
+ )
65
+ from advisor_scripts_constants.astra_advisor_constants import (
66
+ ASTRA_FALLBACK_KIND_BROKEN,
67
+ ASTRA_FALLBACK_KIND_DECLINED,
68
+ )
69
+ from advisor_scripts_constants.model_tier_run_validator_constants import (
70
+ ALL_ADVISOR_REPLY_PATHS,
56
71
  ATTEMPT_ORDER_MISMATCH_MESSAGE,
57
72
  ATTEMPT_TIER_OUT_OF_SLICE_MESSAGE,
58
73
  CANDIDATE_TIERS_MISMATCH_MESSAGE,
@@ -73,16 +88,7 @@ from advisor_scripts_constants.model_tier_run_validator_constants import ( # no
73
88
  UNKNOWN_HOST_PROFILE_ERROR,
74
89
  UNKNOWN_OWN_TIER_MESSAGE,
75
90
  )
76
- from advisor_scripts_constants.advisor_route_constants import ( # noqa: E402
77
- ADVISOR_FALLBACK_TIER,
78
- ADVISOR_MODEL_TIER,
79
- CODEX_BIND_SUCCESS_TOKEN,
80
- CLI_BIND_SUCCESS_TOKEN,
81
- SPAWN_OUTCOME_KEY,
82
- SPAWN_SUCCESS_TOKEN,
83
- TIER_KEY,
84
- )
85
- from tier_model_ids import canonical_host_profile, canonical_tier_name # noqa: E402
91
+ from tier_model_ids import canonical_host_profile, canonical_tier_name
86
92
 
87
93
 
88
94
  @dataclass(frozen=True)
@@ -94,12 +100,157 @@ class ModelTierRun:
94
100
  fallback_reason: str | None = None
95
101
  is_astra_enabled: bool = False
96
102
  host_profile: str = HOST_PROFILE_CLAUDE
103
+ evidence: dict[str, object] | None = None
97
104
 
98
105
 
99
106
  class ModelTierRunError(ValueError):
100
107
  """Raised when a model-tier spawn-walk log violates an invariant."""
101
108
 
102
109
 
110
+ def _required_evidence_text(
111
+ fields_by_name: Mapping[str, object], section_name: str, field_name: str
112
+ ) -> str:
113
+ raw_field_text = fields_by_name.get(field_name)
114
+ if not isinstance(raw_field_text, str) or not raw_field_text.strip():
115
+ raise ModelTierRunError(
116
+ f"evidence.{section_name}.{field_name} must be a non-empty string"
117
+ )
118
+ return raw_field_text.strip()
119
+
120
+
121
+ def _required_evidence_list(
122
+ fields_by_name: Mapping[str, object], section_name: str, field_name: str
123
+ ) -> list[str]:
124
+ raw_field_entries = fields_by_name.get(field_name)
125
+ if not isinstance(raw_field_entries, list) or not raw_field_entries:
126
+ raise ModelTierRunError(
127
+ f"evidence.{section_name}.{field_name} must be a non-empty list"
128
+ )
129
+ if any(
130
+ not isinstance(each_entry, str) or not each_entry.strip()
131
+ for each_entry in raw_field_entries
132
+ ):
133
+ raise ModelTierRunError(
134
+ f"evidence.{section_name}.{field_name} must contain non-empty strings"
135
+ )
136
+ return [each_entry.strip() for each_entry in raw_field_entries]
137
+
138
+
139
+ def _required_evidence_section(
140
+ sections_by_name: Mapping[str, object], section_name: str
141
+ ) -> Mapping[str, object]:
142
+ raw_section = sections_by_name.get(section_name)
143
+ if not isinstance(raw_section, Mapping):
144
+ raise ModelTierRunError(f"evidence.{section_name} must be an object")
145
+ return raw_section
146
+
147
+
148
+ def _validate_reference_evidence(sections_by_name: Mapping[str, object]) -> None:
149
+ reference = _required_evidence_section(sections_by_name, "reference")
150
+ _required_evidence_text(reference, "reference", "path")
151
+ reference_status = _required_evidence_text(reference, "reference", "status")
152
+ if reference_status not in {"missing", "read"}:
153
+ raise ModelTierRunError("evidence.reference.status must be missing or read")
154
+ _required_evidence_text(reference, "reference", "repair_action")
155
+ _required_evidence_text(reference, "reference", "repair_result")
156
+
157
+
158
+ def _validate_fallback_tier(
159
+ fallback_by_name: Mapping[str, object], run: ModelTierRun
160
+ ) -> str | None:
161
+ raw_selected_tier = fallback_by_name.get("selected_tier")
162
+ if raw_selected_tier is not None and not isinstance(raw_selected_tier, str):
163
+ raise ModelTierRunError("evidence.fallback.selected_tier must be a string or null")
164
+ maybe_selected_tier = (
165
+ canonical_tier_name(raw_selected_tier)
166
+ if isinstance(raw_selected_tier, str)
167
+ else None
168
+ )
169
+ maybe_run_selected_tier = (
170
+ canonical_tier_name(run.selected_tier)
171
+ if run.selected_tier is not None
172
+ else None
173
+ )
174
+ if maybe_selected_tier != maybe_run_selected_tier:
175
+ raise ModelTierRunError(
176
+ "evidence.fallback.selected_tier must match selected_tier"
177
+ )
178
+ return maybe_run_selected_tier
179
+
180
+
181
+ def _validate_fallback_kind(fallback_by_name: Mapping[str, object]) -> None:
182
+ raw_fallback_kind = fallback_by_name.get("fallback_kind")
183
+ if raw_fallback_kind not in {
184
+ None,
185
+ ASTRA_FALLBACK_KIND_BROKEN,
186
+ ASTRA_FALLBACK_KIND_DECLINED,
187
+ }:
188
+ raise ModelTierRunError(
189
+ "evidence.fallback.fallback_kind must be declined, broken, or null"
190
+ )
191
+ raw_fallback_reason = fallback_by_name.get("fallback_reason")
192
+ if raw_fallback_reason is not None and (
193
+ not isinstance(raw_fallback_reason, str) or not raw_fallback_reason.strip()
194
+ ):
195
+ raise ModelTierRunError(
196
+ "evidence.fallback.fallback_reason must be a non-empty string or null"
197
+ )
198
+ if raw_fallback_kind is not None and raw_fallback_reason is None:
199
+ raise ModelTierRunError(
200
+ "evidence.fallback.fallback_reason is required for a fallback kind"
201
+ )
202
+
203
+
204
+ def _validate_fallback_reply_path(
205
+ fallback_by_name: Mapping[str, object],
206
+ run: ModelTierRun,
207
+ maybe_run_selected_tier: str | None,
208
+ ) -> None:
209
+ reply_path = _required_evidence_text(fallback_by_name, "fallback", "reply_path")
210
+ if reply_path not in ALL_ADVISOR_REPLY_PATHS:
211
+ raise ModelTierRunError(
212
+ "evidence.fallback.reply_path is not a known advisor path"
213
+ )
214
+ if (
215
+ canonical_host_profile(run.host_profile) == HOST_PROFILE_CODEX
216
+ and maybe_run_selected_tier == ADVISOR_MODEL_TIER
217
+ and reply_path != "native"
218
+ ):
219
+ raise ModelTierRunError(
220
+ "Codex Astra success requires the native reply path"
221
+ )
222
+
223
+
224
+ def _validate_fallback_evidence(
225
+ sections_by_name: Mapping[str, object], run: ModelTierRun
226
+ ) -> None:
227
+ fallback = _required_evidence_section(sections_by_name, "fallback")
228
+ maybe_run_selected_tier = _validate_fallback_tier(fallback, run)
229
+ _validate_fallback_kind(fallback)
230
+ _validate_fallback_reply_path(fallback, run, maybe_run_selected_tier)
231
+
232
+
233
+ def _validate_consult_evidence(sections_by_name: Mapping[str, object]) -> None:
234
+ consult = _required_evidence_section(sections_by_name, "consult")
235
+ for each_field_name in ("changed_evidence", "validation", "unresolved_risks"):
236
+ _required_evidence_list(consult, "consult", each_field_name)
237
+ _required_evidence_text(consult, "consult", "report_back_status")
238
+
239
+
240
+ def _validate_advisor_evidence(
241
+ evidence: object,
242
+ run: ModelTierRun,
243
+ ) -> None:
244
+ if not isinstance(evidence, Mapping):
245
+ raise ModelTierRunError("evidence must be an object")
246
+ raw_schema_version = evidence.get("schema_version")
247
+ if isinstance(raw_schema_version, bool) or raw_schema_version != 1:
248
+ raise ModelTierRunError("evidence.schema_version must be 1")
249
+ _validate_reference_evidence(evidence)
250
+ _validate_fallback_evidence(evidence, run)
251
+ _validate_consult_evidence(evidence)
252
+
253
+
103
254
  def _canonical_tier_list(all_tier_names: list[str]) -> list[str] | None:
104
255
  all_canonical_tiers: list[str] = []
105
256
  for each_tier_name in all_tier_names:
@@ -149,9 +300,7 @@ def _is_successful_attempt_outcome(
149
300
  return False
150
301
  if outcome_token == SPAWN_SUCCESS_TOKEN:
151
302
  return True
152
- if outcome_token == CLI_BIND_SUCCESS_TOKEN:
153
- return True
154
- return False
303
+ return outcome_token == CLI_BIND_SUCCESS_TOKEN
155
304
 
156
305
 
157
306
  def validate_model_tier_run(run: ModelTierRun) -> None:
@@ -205,6 +354,8 @@ def validate_model_tier_run(run: ModelTierRun) -> None:
205
354
  all_attempted_tiers=all_attempted_tiers,
206
355
  all_expected_candidates=all_expected_candidates,
207
356
  )
357
+ if run.evidence is not None:
358
+ _validate_advisor_evidence(run.evidence, run)
208
359
 
209
360
 
210
361
  def _validate_selected_tier(
@@ -262,6 +413,9 @@ def load_model_tier_run_from_json_path(from_path: Path) -> ModelTierRun:
262
413
  raw_host_profile = parsed_payload.get(HOST_PROFILE_JSON_KEY, HOST_PROFILE_CLAUDE)
263
414
  if not isinstance(raw_host_profile, str):
264
415
  raise TypeError(HOST_PROFILE_MUST_BE_STRING_MESSAGE)
416
+ raw_evidence = parsed_payload.get("evidence")
417
+ if raw_evidence is not None and not isinstance(raw_evidence, dict):
418
+ raise TypeError("evidence must be an object")
265
419
  return ModelTierRun(
266
420
  own_tier=parsed_payload["own_tier"],
267
421
  candidate_tiers=list(parsed_payload["candidate_tiers"]),
@@ -270,6 +424,7 @@ def load_model_tier_run_from_json_path(from_path: Path) -> ModelTierRun:
270
424
  fallback_reason=parsed_payload.get("fallback_reason"),
271
425
  is_astra_enabled=raw_astra_enabled,
272
426
  host_profile=raw_host_profile,
427
+ evidence=raw_evidence,
273
428
  )
274
429
 
275
430
 
@@ -220,6 +220,12 @@ def test_bind_runs_probe_then_codex() -> None:
220
220
  _two_step_runner(calls),
221
221
  )
222
222
  assert reply.successful
223
+ assert reply.selected_tier == "Astra"
224
+ assert reply.outcome == "codex"
225
+ assert reply.session_id == "thread-1"
226
+ assert reply.guidance == "PLAN\ninspect"
227
+ assert reply.signal == "PLAN"
228
+ assert reply.fallback_kind is None
223
229
  assert calls[1] == astra_advisor.build_codex_arguments("codex")
224
230
 
225
231
 
@@ -311,6 +317,20 @@ def test_active_docs_use_astra_names() -> None:
311
317
  assert all(name not in content for name in legacy_names)
312
318
 
313
319
 
320
+ def test_team_advisor_source_references_resolve_from_the_package_tree() -> None:
321
+ package_root = SCRIPTS_ROOT.parents[2]
322
+ skill_path = package_root / ".agents" / "skills" / "team-advisor" / "SKILL.md"
323
+ all_reference_targets = (
324
+ "../../../docs/references/advisor-tool.md",
325
+ "../../../_shared/advisor/advisor-protocol.md",
326
+ "../../../_shared/advisor/reference/third-party-bind.md",
327
+ "../../../_shared/advisor/reference/astra-rung.md",
328
+ "../../../_shared/advisor/reference/consult-format.md",
329
+ )
330
+ for each_target in all_reference_targets:
331
+ assert (skill_path.parent / each_target).is_file(), each_target
332
+
333
+
314
334
  def test_astra_docs_name_shared_effort_and_helper() -> None:
315
335
  advisor_root = SCRIPTS_ROOT.parent
316
336
  rung = (advisor_root / "reference" / "astra-rung.md").read_text(encoding="utf-8")
@@ -37,6 +37,105 @@ ADVISOR_MODEL_TIER = model_tier_run_validator.ADVISOR_MODEL_TIER
37
37
  CODEX_BIND_SUCCESS_TOKEN = model_tier_run_validator.CODEX_BIND_SUCCESS_TOKEN
38
38
 
39
39
 
40
+ def _native_codex_evidence(
41
+ *,
42
+ reply_path: str = "native",
43
+ reference_status: str = "missing",
44
+ ) -> dict[str, object]:
45
+ return {
46
+ "schema_version": 1,
47
+ "reference": {
48
+ "path": "~/.claude/docs/references/advisor-tool.md",
49
+ "status": reference_status,
50
+ "repair_action": "use the projected docs root",
51
+ "repair_result": "read",
52
+ },
53
+ "fallback": {
54
+ "selected_tier": "Astra",
55
+ "fallback_kind": None,
56
+ "fallback_reason": None,
57
+ "reply_path": reply_path,
58
+ },
59
+ "consult": {
60
+ "changed_evidence": ["native bind returned a reply"],
61
+ "validation": ["signal and session id read back"],
62
+ "unresolved_risks": ["optional reference projection"],
63
+ "report_back_status": "recorded",
64
+ },
65
+ }
66
+
67
+
68
+ def test_codex_native_bind_and_reply_readback_validate() -> None:
69
+ run = ModelTierRun(
70
+ own_tier="Opus",
71
+ candidate_tiers=[ADVISOR_MODEL_TIER],
72
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
73
+ selected_tier=ADVISOR_MODEL_TIER,
74
+ host_profile="Codex",
75
+ evidence=_native_codex_evidence(),
76
+ )
77
+
78
+ assert validate_model_tier_run(run) is None
79
+
80
+
81
+ def test_codex_native_success_rejects_non_native_reply_path() -> None:
82
+ evidence = _native_codex_evidence(reply_path="cli")
83
+ run = ModelTierRun(
84
+ own_tier="Opus",
85
+ candidate_tiers=[ADVISOR_MODEL_TIER],
86
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
87
+ selected_tier=ADVISOR_MODEL_TIER,
88
+ host_profile="Codex",
89
+ evidence=evidence,
90
+ )
91
+
92
+ with pytest.raises(ModelTierRunError, match="native reply path"):
93
+ validate_model_tier_run(run)
94
+
95
+
96
+ def test_broken_bind_keeps_fallback_evidence_separate_from_reference_gap() -> None:
97
+ evidence = _native_codex_evidence()
98
+ evidence["fallback"] = {
99
+ "selected_tier": None,
100
+ "fallback_kind": "broken",
101
+ "fallback_reason": "native Astra bind failed",
102
+ "reply_path": "none",
103
+ }
104
+ run = ModelTierRun(
105
+ own_tier="Opus",
106
+ candidate_tiers=[ADVISOR_MODEL_TIER],
107
+ attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "unavailable"}],
108
+ selected_tier=None,
109
+ fallback_reason="native Astra bind failed",
110
+ host_profile="Codex",
111
+ evidence=evidence,
112
+ )
113
+
114
+ assert validate_model_tier_run(run) is None
115
+
116
+
117
+ def test_cli_reads_versioned_advisor_evidence(tmp_path: Path) -> None:
118
+ log_path = tmp_path / "model-tier-run.json"
119
+ log_path.write_text(
120
+ json.dumps(
121
+ {
122
+ "own_tier": "Opus",
123
+ "candidate_tiers": [ADVISOR_MODEL_TIER],
124
+ "attempts": [{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
125
+ "selected_tier": ADVISOR_MODEL_TIER,
126
+ "host_profile": "Codex",
127
+ "evidence": _native_codex_evidence(reference_status="read"),
128
+ }
129
+ ),
130
+ encoding="utf-8",
131
+ )
132
+
133
+ assert main([str(log_path)]) == 0
134
+ loaded_run = load_model_tier_run_from_json_path(from_path=log_path)
135
+ assert loaded_run.evidence is not None
136
+ assert loaded_run.evidence["consult"]["report_back_status"] == "recorded"
137
+
138
+
40
139
  def test_clean_single_spawn_at_top_of_slice_passes() -> None:
41
140
  run = ModelTierRun(
42
141
  own_tier="Opus",
@@ -12,6 +12,7 @@ import {
12
12
  mkdirSync,
13
13
  mkdtempSync,
14
14
  readFileSync,
15
+ readdirSync,
15
16
  realpathSync,
16
17
  rmSync,
17
18
  writeFileSync,
@@ -33,39 +34,10 @@ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
33
34
  const SHIPPED_SKILL_NAME = 'privacy-hygiene';
34
35
  const ELI5_SKILL_NAME = 'eli5';
35
36
  const E_CODE_REVIEW_SKILL_NAME = 'e-code-review';
37
+ const TEAM_ADVISOR_SKILL_NAME = 'team-advisor';
36
38
  const REFERENCE_DIRECTORY_NAME = 'reference';
37
- const SHIPPED_AGENT_FILE_NAME = 'clean-coder.md';
38
- const SHIPPED_AGENT_FILE_NAMES = [
39
- SHIPPED_AGENT_FILE_NAME,
40
- 'code-quality-agent.md',
41
- 'pr-description-writer.md',
42
- ];
43
- const CLEAN_CODER_POLICY_REFERENCES = [
44
- [
45
- '<managed-root>/docs/CODE_RULES.md',
46
- 'packages/claude-dev-env/docs/CODE_RULES.md',
47
- ],
48
- [
49
- '<managed-root>/hooks/blocking/code_rules_enforcer.py',
50
- 'packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py',
51
- ],
52
- [
53
- '<managed-root>/rules/code-standards.md',
54
- 'packages/claude-dev-env/rules/code-standards.md',
55
- ],
56
- [
57
- '<managed-root>/rules/windows-filesystem-safe.md',
58
- 'packages/claude-dev-env/rules/windows-filesystem-safe.md',
59
- ],
60
- [
61
- '<managed-root>/rules/gh-cli-conventions.md',
62
- 'packages/claude-dev-env/rules/gh-cli-conventions.md',
63
- ],
64
- [
65
- '<managed-root>/rules/plain-illustrative-docstrings.md',
66
- 'packages/claude-dev-env/rules/plain-illustrative-docstrings.md',
67
- ],
68
- ];
39
+ const SHIPPED_AGENT_FILE_NAME = 'AGENTS.md';
40
+ const RETIRED_AGENT_FILE_NAMES = ['clean-coder.md', 'code-quality-agent.md', 'pr-description-writer.md'];
69
41
  const PERSONAL_SKILL_NAME = 'my-notes';
70
42
  const PREFLIGHT_PROPOSAL_FILE_NAME = 'preflight-proposal.md';
71
43
 
@@ -116,37 +88,48 @@ function assertProposalContractInstallation(installationPaths) {
116
88
  assert.equal(readFileSync(installedContractPath, 'utf8'), readFileSync(sourceContractPath, 'utf8'));
117
89
  }
118
90
 
119
- /**
120
- * @param {string} agentFilePath
121
- * @param {string} layoutName
122
- * @param {string} managedRoot
123
- * @returns {string[]}
124
- */
125
- function cleanCoderPolicyReferenceProblems(agentFilePath, layoutName, managedRoot) {
126
- const agentBody = readFileSync(agentFilePath, 'utf8');
127
- const missingReferences = [];
128
- for (const [installedReference, sourceReference] of CLEAN_CODER_POLICY_REFERENCES) {
129
- const eachReference = layoutName === 'source'
130
- ? sourceReference
131
- : installedReference;
132
- const eachTargetPath = sourceReference.replace('packages/claude-dev-env/', '');
133
- if (!agentBody.includes(eachReference)) {
134
- missingReferences.push(eachReference + ' is absent');
135
- }
136
- const resolvedPath = layoutName === 'source'
137
- ? join(PACKAGE_DIRECTORY, eachTargetPath)
138
- : join(managedRoot, eachTargetPath);
139
- if (!existsSync(resolvedPath)) {
140
- missingReferences.push(layoutName + ': ' + resolvedPath);
141
- }
142
- }
143
- return missingReferences.map((reference) => layoutName + ': ' + reference);
144
- }
145
91
 
146
92
  test('CONTENT_DIRECTORIES omits agents because that tree installs to the agents home', () => {
147
93
  assert.equal(CONTENT_DIRECTORIES.includes(MANAGED_AGENTS_DIRECTORY_NAME), false);
148
94
  });
149
95
 
96
+ test('full install archives recorded retired agents and preserves third-party agents', () => {
97
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agent-retirement-'));
98
+ try {
99
+ runInstaller(homeDirectory, []);
100
+ const agentsDirectory = join(homeDirectory, '.agents', 'agents');
101
+ const retiredPath = join(agentsDirectory, 'clean-coder.md');
102
+ const pluginPath = join(agentsDirectory, 'poteto-agent.md');
103
+ const activePath = join(agentsDirectory, 'AGENTS.md');
104
+ const sourcePluginPath = join(PACKAGE_DIRECTORY, '.agents', 'agents', 'poteto-agent.md');
105
+ assert.equal(readFileSync(pluginPath, 'utf8'), readFileSync(sourcePluginPath, 'utf8'));
106
+ writeFileSync(retiredPath, 'recover this retired definition\n');
107
+ writeFileSync(pluginPath, 'preserve this plugin definition\n');
108
+ const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
109
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
110
+ manifest.files.push(retiredPath);
111
+ manifest.files.push(activePath);
112
+ writeFileSync(manifestPath, JSON.stringify(manifest));
113
+
114
+ runInstaller(homeDirectory, []);
115
+
116
+ assert.equal(existsSync(retiredPath), false);
117
+ assert.ok(existsSync(activePath));
118
+ const backupRoot = join(homeDirectory, '.claude', '.claude-dev-env-pruned');
119
+ const backupNames = readdirSync(backupRoot, { recursive: true });
120
+ const retiredBackup = backupNames.find(name => name.endsWith('clean-coder.md'));
121
+ assert.ok(retiredBackup);
122
+ assert.equal(readFileSync(join(backupRoot, retiredBackup), 'utf8'), 'recover this retired definition\n');
123
+ assert.equal(readFileSync(pluginPath, 'utf8'), 'preserve this plugin definition\n');
124
+ const updatedManifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
125
+ assert.equal(updatedManifest.files.includes(retiredPath), false);
126
+ runInstaller(homeDirectory, []);
127
+ assert.equal(readFileSync(pluginPath, 'utf8'), 'preserve this plugin definition\n');
128
+ } finally {
129
+ rmSync(homeDirectory, { recursive: true, force: true });
130
+ }
131
+ });
132
+
150
133
  test('a full install writes skills and agents under .agents and points .claude at them', () => {
151
134
  const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-agents-home-'));
152
135
  const claudeHome = join(homeDirectory, '.claude');
@@ -197,22 +180,19 @@ test('a full install writes skills and agents under .agents and points .claude a
197
180
  assert.equal(realpathSync(lookupSkillFile), realpathSync(canonicalSkillFile));
198
181
  assert.equal(realpathSync(lookupEli5SkillFile), realpathSync(canonicalEli5SkillFile));
199
182
  assertProposalContractInstallation({ skillsInstallDirectory });
183
+ const projectedAdvisorPaths = [
184
+ join(skillsInstallDirectory, TEAM_ADVISOR_SKILL_NAME, 'SKILL.md'),
185
+ join(claudeHome, 'docs', 'references', 'advisor-tool.md'),
186
+ join(claudeHome, '_shared', 'advisor', 'advisor-protocol.md'),
187
+ join(agentsInstallDirectory, 'poteto-agent.md'),
188
+ ];
189
+ for (const eachPath of projectedAdvisorPaths) {
190
+ assert.ok(existsSync(eachPath), eachPath);
191
+ }
200
192
  assert.equal(
201
193
  readFileSync(lookupAgentFile, 'utf8'),
202
194
  readFileSync(canonicalAgentFile, 'utf8'),
203
195
  );
204
- const sourceAgentFile = join(
205
- resolvePackageManagedDirectory(
206
- PACKAGE_DIRECTORY,
207
- MANAGED_AGENTS_DIRECTORY_NAME,
208
- ),
209
- SHIPPED_AGENT_FILE_NAME,
210
- );
211
- const brokenPolicyReferences = [
212
- ...cleanCoderPolicyReferenceProblems(sourceAgentFile, 'source', claudeHome),
213
- ...cleanCoderPolicyReferenceProblems(canonicalAgentFile, 'installed', claudeHome),
214
- ];
215
- assert.deepEqual(brokenPolicyReferences, [], 'Clean Coder has broken policy references');
216
196
  assert.equal(realpathSync(lookupAgentFile), realpathSync(canonicalAgentFile));
217
197
  assert.equal(
218
198
  lstatSync(skillsInstallDirectory).isSymbolicLink(),
@@ -229,7 +209,7 @@ test('a full install writes skills and agents under .agents and points .claude a
229
209
  }
230
210
  });
231
211
 
232
- test('real installs place the Clean Coder in each active agents home', () => {
212
+ test('installs omit retired agents in each active agents home', () => {
233
213
  const runRoot = mkdtempSync(join(tmpdir(), 'cdev-active-roots-'));
234
214
  const homeDirectory = join(runRoot, 'home');
235
215
  const configRoot = join(runRoot, 'config-profile');
@@ -283,7 +263,7 @@ test('real installs place the Clean Coder in each active agents home', () => {
283
263
  ...eachInstallCase.environment,
284
264
  },
285
265
  );
286
- for (const eachAgentFileName of SHIPPED_AGENT_FILE_NAMES) {
266
+ for (const eachAgentFileName of RETIRED_AGENT_FILE_NAMES) {
287
267
  const installedAgentPath = join(
288
268
  eachInstallCase.agentsHome,
289
269
  MANAGED_AGENTS_DIRECTORY_NAME,
@@ -291,30 +271,10 @@ test('real installs place the Clean Coder in each active agents home', () => {
291
271
  );
292
272
  assert.equal(
293
273
  existsSync(installedAgentPath),
294
- true,
295
- `${eachInstallCase.name}: agent is under the active agents home`,
274
+ false,
275
+ `${eachInstallCase.name}: retired agent is absent`,
296
276
  );
297
- const installedAgentText = readFileSync(installedAgentPath, 'utf8');
298
- assert.match(installedAgentText, /active managed root/i);
299
- assert.match(installedAgentText, /active agents home/i);
300
- assert.match(installedAgentText, /<managed-root>\//);
301
- assert.match(installedAgentText, /<agents-home>\//);
302
277
  }
303
- const installedCleanCoderPath = join(
304
- eachInstallCase.agentsHome,
305
- MANAGED_AGENTS_DIRECTORY_NAME,
306
- SHIPPED_AGENT_FILE_NAME,
307
- );
308
- const policyReferenceProblems = cleanCoderPolicyReferenceProblems(
309
- installedCleanCoderPath,
310
- 'installed',
311
- eachInstallCase.managedRoot,
312
- );
313
- assert.deepEqual(
314
- policyReferenceProblems,
315
- [],
316
- `${eachInstallCase.name}: Clean Coder has broken policy references`,
317
- );
318
278
  assert.equal(
319
279
  isDirectoryPointerTo(
320
280
  join(eachInstallCase.managedRoot, MANAGED_AGENTS_DIRECTORY_NAME),
package/bin/install.mjs CHANGED
@@ -1135,6 +1135,7 @@ function renameCaseOnlyMatchToShippedName(destinationFilePath, entryNamesByDirec
1135
1135
 
1136
1136
  export function copyTree(sourceBase, destBase, options = {}) {
1137
1137
  const excludedFileNames = new Set(options.excludeFileNames ?? []);
1138
+ const preservedFileNames = new Set(options.preserveExistingFileNames ?? []);
1138
1139
  const files = collectFiles(sourceBase).filter(
1139
1140
  sourceFile => !excludedFileNames.has(basename(sourceFile)),
1140
1141
  );
@@ -1144,6 +1145,10 @@ export function copyTree(sourceBase, destBase, options = {}) {
1144
1145
  const relativePath = relative(sourceBase, sourceFile);
1145
1146
  const destFile = join(destBase, relativePath);
1146
1147
  mkdirSync(dirname(destFile), { recursive: true });
1148
+ if (preservedFileNames.has(basename(sourceFile)) && existsSync(destFile)) {
1149
+ stats.paths.push(destFile);
1150
+ continue;
1151
+ }
1147
1152
  const existed = existsSync(destFile);
1148
1153
  renameCaseOnlyMatchToShippedName(destFile, entryNamesByDirectory, options);
1149
1154
  copyFileSync(sourceFile, destFile);
@@ -1223,6 +1228,8 @@ export const POST_FOLDED_HOOK_RELATIVE_PATHS = new Set([
1223
1228
  ]);
1224
1229
 
1225
1230
  export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
1231
+ 'observability/test_failure_recorder.py',
1232
+ 'advisory/msys_path_conversion_advisor.py',
1226
1233
  'session/untracked_repo_detector.py',
1227
1234
  'session/gh_pr_author_session_cleanup.py',
1228
1235
  'observability/pr_description_writer_spawn_tracker.py',
@@ -2326,29 +2333,6 @@ function pruneRetiredSkills(installedSkillNames, priorManifestSkills) {
2326
2333
  return movedDirectoryCount;
2327
2334
  }
2328
2335
 
2329
- /**
2330
- * Move every file a prior install wrote under a managed root that this run leaves
2331
- * unwritten into the run's backup root, one call per root.
2332
- *
2333
- * `copyTree` adds and overwrites but never removes, so every managed root carries
2334
- * the same drift the skills root does. One call per root hands the containment
2335
- * guard and the emptied-parent walk the root that owns each file, and each root's
2336
- * content lands under `<backupRoot>/<root-name>/<relative>`, so the recovery point
2337
- * mirrors ~/.claude.
2338
- *
2339
- * Nothing moves unless a prior install recorded it. A user-authored file, a
2340
- * runtime artifact, and a recorded path under no managed root — ~/.claude/CLAUDE.md,
2341
- * settings.json, the manifest itself, and ~/.mypy.ini outside the home — all sit
2342
- * outside every root's diff and stay where they are. ~/.claude/_shared and
2343
- * ~/.claude/skills/_shared are distinct absolute paths, so the `_shared` root call
2344
- * and the skills root call each see their own files and neither sees the other's.
2345
- *
2346
- * @param {string[]|null} priorInstalledFiles Files the prior manifest recorded, or null when unknown.
2347
- * @param {string[]} currentInstalledFiles Every file this run copied.
2348
- * @param {string} backupRoot The run's timestamped backup directory.
2349
- * @returns {{prunedCount: number, skillsPrunedCount: number, failedPaths: string[]}}
2350
- * The summed count, the skills root's own count, and every path whose move failed.
2351
- */
2352
2336
  function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalledFiles, backupRoot) {
2353
2337
  let prunedCount = 0;
2354
2338
  let skillsPrunedCount = 0;
@@ -2366,6 +2350,27 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
2366
2350
  skillsPrunedCount = rootOutcome.prunedCount;
2367
2351
  }
2368
2352
  }
2353
+ for (const rootName of [MANAGED_AGENTS_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME]) {
2354
+ const lookupRoot = join(CLAUDE_HOME, rootName);
2355
+ const canonicalRoot = join(AGENTS_HOME, rootName);
2356
+ const canonicalCurrentFiles = currentInstalledFiles.map(filePath =>
2357
+ isInsideDirectory(filePath, lookupRoot)
2358
+ ? join(canonicalRoot, relative(lookupRoot, filePath))
2359
+ : filePath,
2360
+ );
2361
+ const rootOutcome = pruneStaleInstalledFiles(
2362
+ priorInstalledFiles,
2363
+ canonicalCurrentFiles,
2364
+ canonicalRoot,
2365
+ join(backupRoot, 'agents-home', rootName),
2366
+ { managedHomeDirectory: AGENTS_HOME },
2367
+ );
2368
+ prunedCount += rootOutcome.prunedCount;
2369
+ failedPaths.push(...rootOutcome.failedPaths);
2370
+ if (rootName === MANAGED_SKILLS_DIRECTORY_NAME) {
2371
+ skillsPrunedCount += rootOutcome.prunedCount;
2372
+ }
2373
+ }
2369
2374
  return { prunedCount, skillsPrunedCount, failedPaths };
2370
2375
  }
2371
2376
 
@@ -2655,7 +2660,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2655
2660
  );
2656
2661
  if (!existsSync(agentsSource)) continue;
2657
2662
  const agentsDestination = INSTALL_ROOT_RESOLUTION.agentsLookupDirectory;
2658
- const stats = copyTree(agentsSource, agentsDestination);
2663
+ const stats = copyTree(agentsSource, agentsDestination, {
2664
+ preserveExistingFileNames: ['poteto-agent.md'],
2665
+ });
2659
2666
  if (!summary[MANAGED_AGENTS_DIRECTORY_NAME]) {
2660
2667
  summary[MANAGED_AGENTS_DIRECTORY_NAME] = stats;
2661
2668
  } else {