Skip to content

Commit bf015f8

Browse files
fix(doctor): treat unspecified harness as unknown, label adapter checks (#449)
Stop defaulting unconfigured targets to Claude in build_context so doctor only runs harness-native checks when a harness is declared. Prefix handoff, skills, and orphan projection checks with adapter: for clear labeling. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 64c4131 commit bf015f8

2 files changed

Lines changed: 96 additions & 20 deletions

File tree

src/brigade/doctor.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@
2626
MANUAL = "MANUAL"
2727
INFO = "INFO"
2828
DEFAULT_TEXT_CHECK_LIMIT = 50
29+
ADAPTER_CHECK_PREFIX = "adapter: "
30+
31+
32+
def _adapter_check_name(name: str) -> str:
33+
"""Label Brigade adapter/projection checks distinctly from native harness checks."""
34+
return f"{ADAPTER_CHECK_PREFIX}{name}"
2935

3036

3137
def build_context(target: Path, harness: str = "generic") -> DoctorContext:
3238
target = target.expanduser().resolve()
3339
from .config import load_config
3440

3541
sel = None
42+
harnesses: list[str] = []
3643
try:
3744
cfg = load_config(target)
3845
except (ValueError, json.JSONDecodeError):
@@ -41,9 +48,7 @@ def build_context(target: Path, harness: str = "generic") -> DoctorContext:
4148
sel = cfg.selection
4249
harnesses = list(sel.harnesses)
4350
elif harness in ("openclaw", "hermes"):
44-
harnesses = ["claude", harness]
45-
else:
46-
harnesses = ["claude"]
51+
harnesses = [harness]
4752
return DoctorContext(target=target, selection=sel, harnesses=harnesses)
4853

4954

@@ -232,7 +237,10 @@ def run(target: Path, harness: str = "generic", *, json_output: bool = False, fu
232237
sel = ctx.selection
233238
print(f" harnesses: {', '.join(sel.harnesses) or '(none)'} (owner={sel.owner}, depth={sel.depth})")
234239
else:
235-
print(f" harnesses: (legacy target, no config; assuming {', '.join(ctx.harnesses)})")
240+
if ctx.harnesses:
241+
print(f" harnesses: (legacy target, no config; declared {', '.join(ctx.harnesses)})")
242+
else:
243+
print(" harnesses: (unspecified; no Brigade config and no explicit --harness)")
236244
return _report(checks, full=full)
237245

238246

@@ -363,7 +371,7 @@ def _check_default_wired_skills(target: Path, selected_harnesses: List[str]) ->
363371
missing.append((skill_id, str(rel_file)))
364372
if not present and not missing:
365373
continue
366-
name = f"skills: {harness} default wired"
374+
name = _adapter_check_name(f"skills: {harness} default wired")
367375
if missing:
368376
for skill_id, rel_file in missing:
369377
results.append(
@@ -427,19 +435,19 @@ def _check_handoff_inboxes(target: Path, sel, selected_harnesses: List[str]) ->
427435
continue # reader harness, no inbox
428436
inbox = target / rel
429437
if inbox.is_dir():
430-
results.append((OK, f"handoff: {h} inbox", str(inbox)))
438+
results.append((OK, _adapter_check_name(f"handoff: {h} inbox"), str(inbox)))
431439
else:
432-
results.append((FAIL, f"handoff: {h} inbox", f"missing at {inbox}"))
440+
results.append((FAIL, _adapter_check_name(f"handoff: {h} inbox"), f"missing at {inbox}"))
433441
tmpl = inbox / "TEMPLATE.md"
434442
if tmpl.is_file():
435-
results.append((OK, f"handoff: {h} TEMPLATE.md", str(tmpl)))
443+
results.append((OK, _adapter_check_name(f"handoff: {h} TEMPLATE.md"), str(tmpl)))
436444
else:
437-
results.append((WARN, f"handoff: {h} TEMPLATE.md", f"missing at {tmpl}"))
445+
results.append((WARN, _adapter_check_name(f"handoff: {h} TEMPLATE.md"), f"missing at {tmpl}"))
438446
processed = inbox / "processed"
439447
if processed.is_dir():
440-
results.append((OK, f"handoff: {h} processed/", str(processed)))
448+
results.append((OK, _adapter_check_name(f"handoff: {h} processed/"), str(processed)))
441449
else:
442-
results.append((WARN, f"handoff: {h} processed/", f"missing at {processed}"))
450+
results.append((WARN, _adapter_check_name(f"handoff: {h} processed/"), f"missing at {processed}"))
443451
cards = target / "memory" / "cards"
444452
if cards.is_dir():
445453
card_count = len([path for path in cards.rglob("*.md") if path.is_file()])
@@ -569,7 +577,7 @@ def _check_orphan_inboxes(target: Path, selected_harnesses: List[str]) -> List[C
569577
results.append(
570578
(
571579
WARN,
572-
f"orphan: {h} inbox",
580+
_adapter_check_name(f"orphan: {h} inbox"),
573581
f"{inbox} exists but {h} is not in config; remove or add to config (unselected harness)",
574582
)
575583
)

tests/test_doctor.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,11 @@ def test_doctor_checks_codex_inbox_when_selected(tmp_target: Path, capsys):
619619
# or on check ordering, which the managed-tool additions can shift.
620620
doctor_mod.run(tmp_target, json_output=True)
621621
payload = json.loads(capsys.readouterr().out)
622-
inbox_checks = {check["name"]: check for check in payload["checks"] if check["name"].startswith("handoff: codex")}
623-
assert "handoff: codex inbox" in inbox_checks
624-
codex_inbox = inbox_checks["handoff: codex inbox"]
622+
inbox_checks = {
623+
check["name"]: check for check in payload["checks"] if check["name"].startswith("adapter: handoff: codex")
624+
}
625+
assert "adapter: handoff: codex inbox" in inbox_checks
626+
codex_inbox = inbox_checks["adapter: handoff: codex inbox"]
625627
assert codex_inbox["status"] == doctor_mod.OK
626628
assert ".codex/memory-handoffs" in codex_inbox["detail"]
627629

@@ -641,10 +643,10 @@ def test_doctor_reports_default_wired_skills_for_selected_harnesses(tmp_target:
641643
checks = {item["name"]: item for item in payload["checks"]}
642644

643645
assert rc == 0
644-
assert checks["skills: claude default wired"]["status"] == "OK"
645-
assert "brigade-work" in checks["skills: claude default wired"]["detail"]
646-
assert checks["skills: codex default wired"]["status"] == "OK"
647-
assert ".codex/skills" in checks["skills: codex default wired"]["detail"]
646+
assert checks["adapter: skills: claude default wired"]["status"] == "OK"
647+
assert "brigade-work" in checks["adapter: skills: claude default wired"]["detail"]
648+
assert checks["adapter: skills: codex default wired"]["status"] == "OK"
649+
assert ".codex/skills" in checks["adapter: skills: codex default wired"]["detail"]
648650

649651

650652
def test_doctor_warns_when_default_wired_skill_is_missing(tmp_target: Path, capsys):
@@ -661,7 +663,7 @@ def test_doctor_warns_when_default_wired_skill_is_missing(tmp_target: Path, caps
661663
rc = doctor_mod.run(tmp_target, json_output=True)
662664
payload = json.loads(capsys.readouterr().out)
663665
checks = {item["name"]: item for item in payload["checks"]}
664-
skill_check = checks["skills: codex default wired: ultra-work-scout"]
666+
skill_check = checks["adapter: skills: codex default wired: ultra-work-scout"]
665667

666668
assert rc == 0
667669
assert skill_check["status"] == "WARN"
@@ -713,6 +715,72 @@ def test_doctor_falls_back_to_v0_2_behavior_when_no_config(tmp_target: Path, cap
713715
assert "doctor" in out
714716

715717

718+
def test_build_context_unspecified_harness_without_config(tmp_target: Path, capsys):
719+
"""Unconfigured targets must not inherit Claude harness checks by default."""
720+
tmp_target.mkdir()
721+
(tmp_target / "AGENTS.md").write_text("# Agents")
722+
723+
ctx = doctor_mod.build_context(tmp_target)
724+
assert ctx.harnesses == []
725+
assert ctx.selection is None
726+
727+
doctor_mod.run(target=tmp_target, harness="generic", json_output=True)
728+
payload = json.loads(capsys.readouterr().out)
729+
assert payload["harnesses"] == []
730+
731+
names = {check["name"] for check in payload["checks"]}
732+
assert "claude work loop" not in names
733+
assert not any(name.startswith("adapter: handoff:") for name in names)
734+
assert not any(name.startswith("adapter: skills:") for name in names)
735+
736+
capsys.readouterr()
737+
doctor_mod.run(target=tmp_target, harness="generic")
738+
out = capsys.readouterr().out
739+
assert "unspecified" in out.lower()
740+
assert "assuming claude" not in out.lower()
741+
742+
743+
def test_build_context_claude_declared_labels_adapter_checks(tmp_target: Path, capsys):
744+
"""Explicit Claude selection keeps native checks and labels adapter projections."""
745+
install_selection(
746+
tmp_target,
747+
Selection(depth="repo", harnesses=["claude"], owner="claude", includes=[]),
748+
)
749+
capsys.readouterr()
750+
751+
ctx = doctor_mod.build_context(tmp_target)
752+
assert ctx.harnesses == ["claude"]
753+
754+
doctor_mod.run(target=tmp_target, json_output=True)
755+
payload = json.loads(capsys.readouterr().out)
756+
names = {check["name"] for check in payload["checks"]}
757+
758+
assert "adapter: handoff: claude inbox" in names
759+
assert "adapter: skills: claude default wired" in names
760+
assert not any(name.startswith("adapter: handoff: codex") for name in names)
761+
762+
763+
def test_build_context_non_claude_harness_labels_adapter_checks_only(tmp_target: Path, capsys):
764+
"""Non-Claude harness selection must not run Claude-native doctor checks."""
765+
install_selection(
766+
tmp_target,
767+
Selection(depth="repo", harnesses=["codex"], owner="codex", includes=[]),
768+
)
769+
capsys.readouterr()
770+
771+
ctx = doctor_mod.build_context(tmp_target)
772+
assert ctx.harnesses == ["codex"]
773+
774+
doctor_mod.run(target=tmp_target, json_output=True)
775+
payload = json.loads(capsys.readouterr().out)
776+
names = {check["name"] for check in payload["checks"]}
777+
778+
assert "claude work loop" not in names
779+
assert "adapter: handoff: codex inbox" in names
780+
assert "adapter: skills: codex default wired" in names
781+
assert not any(name.startswith("adapter: handoff: claude") for name in names)
782+
783+
716784
def test_doctor_includes_embedded_content_guard_without_external_binary(monkeypatch, tmp_target, capsys):
717785
from brigade.install import install_selection
718786
from brigade.selection import Selection

0 commit comments

Comments
 (0)