Skip to content

feat(doctor): default to the whole installation, filter with --project/--type/--team - #654

Merged
fujibee merged 2 commits into
mainfrom
feat-doctor
Aug 7, 2026
Merged

feat(doctor): default to the whole installation, filter with --project/--type/--team#654
fujibee merged 2 commits into
mainfrom
feat-doctor

Conversation

@fujibee

@fujibee fujibee commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Reworks doctor's scope selection. The previous form required <project> <type> up front, which was backwards in practice: a reporter who doesn't already know which project/type to look at can't use a doctor that demands one, and there was no way to see a whole project across types, or the whole installation at once.

This addresses part of #267, which asked for both a doctor command and a viewer/status dashboard; this PR only touches the doctor half.

Interface change

doctor                    default = whole install (every team, project, type)
doctor --project <path>   narrow to one project
doctor --type <type>      narrow to one type
doctor --team <team>      narrow to one team
(all three combine; --redacted and --help unchanged)

Positional <project> <type> is dropped, not kept for compatibility — doctor has few enough callers that a stale positional form alongside flags that mean something different by default would be its own source of confusion. Argument parsing stays separate from scope-building, so a future change to the flags is a parsing-only change.

Also fixes, found by running the rebuilt scope against the real installation

  • A type with no real delivery was being queried anyway. A type whose delivery_modes manifest key is nothing but off (agmsg-app — the desktop app's own identity, which owns its own send/receive UI; hermes) has no agmsg-side delivery to ask about. Querying delivery.sh status for such a type exits 1 by design (nothing to report), and doctor was turning that into a warning — on a real installation with nothing actually wrong, this produced 9 team(s), 56 registration(s), 5 warning(s) and exit 1, breaking the exit-code contract on day one. Now checked via the manifest before querying (agmsg_type_get <type> delivery_modes, the same function/key fix(resolve): detect a type's agent process from its manifest, not a fixed list #631 already reads for a different purpose) rather than by type name, so a future no-delivery type is picked up the same way automatically.
  • Readability at real scale. A (project, type) group with nothing held (no lock owned by anyone), no warning raised, and delivery showing nothing beyond a bare idle mode: off line now collapses to one line (<project> [<type>] N registration(s), nothing to report) instead of a 6-line block. A real installation had dozens of these.
  • A global fact repeated once per group. The watch processes: N alive, M stale pidfiles line default runtime status emits scans the whole run/ directory — an installation-wide fact, not a per-(project, type) one. It (and the warning it can produce) now surface once, not once per group using default runtime status.

Unchanged

Read-only behavior (never claims/releases/removes a lock, pidfile, or registration), the exit-code contract (0 clean / 1 warnings / 2 usage-or-resolution error), and --redacted's masking behavior — including that it now works consistently across the multi-project default scope, reusing the same per-run pseudonym-table approach the existing team/agent redaction already used.

Not in this PR

SKILL.md and the per-type templates are not wired to this new interface yet — deliberately deferred to a separate PR once this scope shape is settled, so the 9 templates plus the Windows dispatcher aren't rewritten twice.

Testing

tests/test_doctor.bats: 30 tests (was 17 before this PR), covering the new default/--project/--type/--team scope selection (including that --project and --team each narrow out what they exclude, and that a project registered under two teams isn't double-counted), the dropped positional form now being a usage error, the no-delivery-type fix, the readability collapse (including that an explicitly-configured mode is never collapsed), the global-line dedup, and the full carried-over suite from the original PR.

  1. bats tests/test_doctor.bats — 30 ok, 0 not ok, exit 0.
  2. bats tests/test_delivery.bats — adjacent suite (doctor shells out to delivery.sh) — all green.
  3. Verified against the real installation directly (not just synthetic fixtures): 9 team(s), 57 registration(s), 0 warning(s), exit 0, no warnings.

fujibee added 2 commits August 7, 2026 12:58
…t/--type/--team

doctor previously required <project> <type> up front, which was backwards: a
reporter who does not already know which project/type to look at cannot use
a doctor that demands one. Positional <project> <type> is dropped, not kept
for compatibility -- doctor has few enough callers that carrying a stale
positional form alongside flags that mean something different by default
would be its own source of confusion.

New interface:
  doctor                    default = whole install (every team, project, type)
  doctor --project <path>   narrow to one project
  doctor --type <type>      narrow to one type
  doctor --team <team>      narrow to one team
  (all three combine; --redacted and --help unchanged)

Argument parsing stays separate from scope-building, so a future flag change
is a parsing-only change.

Also fixes three defects found by running the rebuilt scope against the real
installation:

- A type whose delivery_modes is nothing but "off" (agmsg-app, hermes -- the
  desktop app owns its own send/receive UI) was queried against delivery.sh
  anyway, which exits 1 by design (nothing to report); doctor turned that
  into a warning, breaking the exit-code contract on an otherwise healthy
  install. Now checked via the manifest before querying, the same
  agmsg_type_get/delivery_modes key PR #631 already reads.
- A (project, type) group with nothing held, no warning, and nothing beyond
  a bare idle mode line now collapses to one line instead of a 6-line block
  -- unreadable at real scale (dozens of such groups on a real install).
- The "watch processes: N alive, M stale pidfiles" line default runtime
  status emits is an installation-wide fact (it scans the whole run/
  directory), not a per-(project, type) fact; it and the warning it can
  produce now surface once, not once per group using default runtime status.

Read-only behavior, the 0/1/2 exit-code contract, and --redacted's masking
behavior are unchanged. SKILL.md and the per-type templates are not wired
to this yet -- deliberately deferred to a separate PR once this scope shape
is settled, so the 9 templates + the Windows dispatcher aren't rewritten
twice.

Addresses part of #267.
…gap, allow an empty install

Three defects from static review of PR #654 (co2), plus one more found
verifying the fix against the real installation:

- --team was interpolated directly into a path segment
  (teams/$FILTER_TEAM/config.json) without running it through
  agmsg_validate_team_name, the existing validator every other entry point
  that turns a team name into a path already uses. A value containing ".."
  could resolve to a config-shaped file outside teams/ entirely.

- The installation-wide "watch processes: N alive, M stale pidfiles" line
  was captured opportunistically from whichever pair's own delivery.sh call
  happened to emit it first. An installation whose registrations are ALL a
  no-delivery type (skips the call) and/or codex (overrides runtime status
  with its own per-role bridge lines) never captured it at all -- losing
  run/watch.*.pid stale-watcher detection entirely, not just deduplicating
  it. Now captured independently, once, via `delivery.sh status` with no
  <type>/<project> -- delivery.sh's own documented path for exactly this.

- An explicit --project/--type/--team matching nothing is a usage error
  (exit 2, unchanged), but a genuinely empty installation with NO filters
  given is a valid whole-install scan whose answer happens to be empty --
  that was also exiting 2, meaning diagnosing an empty installation itself
  violated the exit-code contract. Now falls through to the normal report
  path, which naturally produces a clean 0/0/0 with no special-casing.

- _team_filter_lines's own exit status leaked out of its while loop's last
  executed command: a non-matching final input line left the loop (and the
  function) returning 1, and no caller guarded the assignment. Under set -e
  this silently aborted the whole script -- with zero output -- whenever
  --team's target sorted alphabetically before some other team sharing a
  registration on the same (project, type). Found running --team against
  the real installation; none of the existing --team fixtures happened to
  hit this ordering. Fixed with an explicit trailing return 0.
@fujibee
fujibee merged commit 4d3202d into main Aug 7, 2026
15 checks passed
pull Bot pushed a commit to itsbrex/agmsg that referenced this pull request Aug 16, 2026
Brings across the two commits main had and this branch did not:

  f271cd4  fix(check-inbox): deliver with exit 0 when the poll failed
           part-way (fujibee#658) (fujibee#745)
  4d3202d  feat(doctor): default to the whole installation, narrow with
           --project/--type/--team (fujibee#654)

## check-inbox.sh, test_inbox.bats -- this branch's side

Both branches fixed fujibee#658 independently, so the conflict is two
implementations of one property set rather than a disagreement.

The properties, read from f271cd4's diff INCLUDING its removed lines --
two of them are invisible if only the additions are read:

  A  capture WHICH team failed (the break itself pre-dated this commit;
     what is new is CLAIM_FAILED_TEAM)
  B  the delivering path exits 0 and states the partial failure inside
     the payload
  C  the empty path lets the exit status carry the failure
  D  "no new messages" is printed only when the poll actually completed
     (before, the line was printed and then the status re-raised)

All four are present in this branch's implementation, checked as
behaviour rather than by variable name. Nothing in main's version exists
outside them.

My first reading of this diff listed three properties and got two of them
wrong, because I read only the added lines. Additions say what is there;
they do not say what changed. Corrected against a second reader's list.

## doctor.sh -- main's, with the collapse made reachable

doctor.sh and test_doctor.bats arrive whole; this branch had never
touched them. But main's collapse condition cannot fire here, and that is
a semantic conflict git did not mark:

  main's condition   delivery output <= 1 line AND mode = "off"
  this branch        no bare `off` exists at all -- all four assignments
                     in delivery.sh carry an annotation

  no settings file   `off (unrecognized: ...)`               1 line
  settings present   `off (no hooks...)` + path + 3 counts   5 lines

So the only state satisfying <= 1 line is the one that must NOT collapse:
`unrecognized` is a claim about what this check could not determine, not
about the configuration. Collapsing it would tell an operator delivery is
off when the truth is we could not tell.

Ruled: collapse a genuine `off`, and keep the settings path on the
collapsed line.

  nothing to report — <settings file>

Five lines become one; what goes is the mode and three "entries: 0". The
path stays because it is the only one answering a different question --
WHICH file was consulted, i.e. "looked and found nothing" versus "did not
look".

The line-count gate is removed: with it, the collapse is unreachable.

Tests: the cases that assert the collapse now build a project with a
readable settings file (a genuine off), and one new case fixes the other
half -- a project that could not be read is not called "nothing to
report" and keeps its "may not be registered" line.

## Verified

  bats test_doctor.bats + test_inbox.bats + test_delivery.bats
  222 ok, exit 0

  mutation: drop the path from the collapsed line -> the test fails
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant