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.
- package/.agents/agents/poteto-agent.md +6 -0
- package/.agents/agents/test_agent_frontmatter.py +9 -9
- package/.agents/agents/test_pr_description_writer_style.py +1 -1
- package/.agents/agents-archived/ARCHIVE-MANIFEST.md +15 -0
- package/.agents/{agents → agents-archived}/code-quality-agent.md +1 -1
- package/.agents/{agents → agents-archived}/session-advisor.md +1 -1
- package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +2 -2
- package/.agents/skills/grok-spawn/SKILL.md +9 -6
- package/.agents/skills/grok-spawn/reference/flag-profiles.md +12 -8
- package/.agents/skills/issue-tracker/SKILL.md +2 -2
- package/.agents/skills/orchestrator/SKILL.md +16 -15
- package/.agents/skills/orchestrator-refresh/SKILL.md +2 -0
- package/.agents/skills/pull-request/SKILL.md +14 -5
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +13 -8
- package/_shared/advisor/reference/spawn-walk-log.md +2 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +169 -14
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +20 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +99 -0
- package/bin/install.agents-home.test.mjs +54 -94
- package/bin/install.mjs +31 -24
- package/bin/install.test.mjs +35 -0
- package/bin/resolve-package-managed-directory.test.mjs +1 -1
- package/bin/subagent-model-policy.test.mjs +15 -8
- package/docs/references/advisor-tool.md +32 -0
- package/docs/references/team-advisor-skill.md +1 -1
- package/docs/subagent-model-routing.md +6 -5
- package/hooks/blocking/tdd_enforcer.py +2 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +7 -24
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -3
- package/hooks/hooks_constants/hosted_hook_runner.py +1 -1
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +3 -35
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +2 -10
- package/hooks/routing/subagent_model_routing.mjs +15 -0
- package/hooks/routing/subagent_model_routing.test.mjs +58 -25
- package/hooks/validators/README.md +0 -1
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +1 -2
- package/package.json +1 -1
- package/rules/git-workflow.md +3 -3
- package/rules/shell-invocation.md +0 -2
- package/rules/subagent-model-policy.json +5 -13
- package/rules/testing.md +0 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +3 -3
- package/scripts/grok_headless_runner.py +19 -1
- package/scripts/grok_worker_preflight.py +67 -1
- package/scripts/policy_lint/config/approved_test_pairs.py +9 -0
- package/scripts/resolve_advisor_model_route.mjs +2 -2
- package/scripts/subagent_model_policy.mjs +4 -1
- package/scripts/test_grok_headless_runner.py +35 -1
- package/scripts/test_grok_worker_preflight.py +73 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +3 -3
- package/scripts/tests/test_policy_lint_rules_pairing.py +16 -0
- package/scripts/tests/test_rules.py +5 -5
- package/scripts/windows/pstack/pstack-command-shim.ps1 +41 -0
- package/scripts/windows/pstack/ship-pr.cmd +3 -0
- package/scripts/windows/pstack/watch-pr.cmd +3 -0
- package/hooks/advisory/msys_path_conversion_advisor.py +0 -200
- package/hooks/advisory/test_msys_path_conversion_advisor.py +0 -223
- package/hooks/hooks_constants/test_failure_recorder_constants.py +0 -34
- package/hooks/observability/test_failure_recorder.py +0 -221
- package/hooks/observability/test_test_failure_recorder.py +0 -173
- package/hooks/validators/VALIDATION_REPORT.md +0 -287
- package/hooks/validators/validator_defaults.py +0 -5
- /package/.agents/{agents → agents-archived}/clean-coder.md +0 -0
- /package/.agents/{agents → agents-archived}/git-commit-crafter.md +0 -0
- /package/.agents/{agents → agents-archived}/issue-tracker.md +0 -0
- /package/.agents/{agents → agents-archived}/plan-packet-validator.md +0 -0
- /package/.agents/{agents → agents-archived}/pr-description-writer.md +0 -0
- /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.
|
|
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
|
|
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
|
-
|
|
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 = '
|
|
38
|
-
const
|
|
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('
|
|
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
|
|
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
|
-
|
|
295
|
-
`${eachInstallCase.name}: agent is
|
|
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 {
|