feat(doctor): nomarchy-doctor — read-only health sheet + VM check (item 10)
Some checks failed
Check / eval (push) Has been cancelled

The things that actually break user machines, one command (and menu ›
System › Doctor): failed system/user units, disk space (real
filesystems only — fstype allowlist makes it VM/live safe, device
dedupe collapses / and /nix), theme-state.json parses + is
git-tracked, flake dirty/behind as warnings, generation age (profile
symlink mtime — store paths are epoch-1), snapper timeline when
enabled. Read-only by contract; every ✖ prints the one fixing command;
exit 1 on any problem. Ships as pkgs/nomarchy-doctor so the VM check
runs on a minimal node.

Verified: V1 package build (shellcheck-gated) + a real-hardware smoke
that correctly flagged a genuinely failed user unit on the dev box;
V2 green — checks.doctor VM test: induced failed unit flips the sheet
to exit-1 naming the unit and its fix, reset-failed returns healthy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-07-04 20:34:11 +01:00
parent 1b8eccbdca
commit 2a23e82169
10 changed files with 383 additions and 10 deletions

View File

@@ -0,0 +1,168 @@
---
name: use-codex
display_name: Use Codex
description: Spawn one or more OpenAI Codex CLI subagents from bash to offload context-heavy work from the parent Claude. Use when the user says "use codex", "build with codex", "ask codex", "get a codex second opinion", "compare codex vs claude", "have two agents try it", or otherwise asks to delegate or parallelize work to Codex. Also use whenever a user asks for a large coding task — research, multi-file refactors, large codebase analysis.
builtIn: true
---
# Codex Subagent Skill
Spawn autonomous Codex CLI subagents to offload context-heavy work. Subagents burn their own tokens and return only their final message, so the parent's context stays clean.
**Golden Rule:** If task + intermediate work would add 3,000+ tokens to parent context → use a subagent.
## Instructions
When invoking this skill:
1. **Clarify intent.** Figure out what the user wants. If unclear, ask. Inline args ("use codex to review my auth plan") usually carry the intent — infer from them. Common buckets: second-opinion code review, refactor, plan validation, implementation of features, fresh perspective on a stuck bug, parallel comparison of approaches.
2. **Pick the right reasoning tier** Use GPT 5.5 model with either low or xhigh reasoning
3. **Spawn the subagent** using the canonical invocation (Basic Usage). Pipe long prompts via stdin.
4. **Act autonomously while it runs.** Don't ask for permission mid-flight; the parent only sees the final result, so mid-task pauses waste tokens. Pause only for genuinely destructive operations (data loss, external impact, security).
5. **Monitor, don't fire-and-forget.** Check completion, verify quality, retry on failure, answer follow-ups if blocked. Feel free to run multiple sequential or parallel codex subagents.
6. **Present results, don't dump them.** Summarize what Codex said in your own words, surface concrete code changes or recommendations, and leave the next move to the user. Subagent output is *input* for your synthesis. Keep your response to the user concise and specific.
## Model + Reasoning Selection
Always use GPT 5.5 (latest), but choose from low or xhigh reasoning. You may use other reasoning but usually simple, short, or basic tasks are best done by low reasoning and hard, large, or long tasks are done by xhigh reasoning.
## Intelligent Prompting
Subagents only see what you give them. Be specific:
1. **Context** — what they're analyzing, where it lives.
2. **Objectives** — numbered, concrete.
3. **Constraints** — what to focus on, what to ignore.
4. **Output format** — exact shape the parent wants back.
5. **Success criteria** — when the task is done.
Template:
```
[TASK CONTEXT]
You are researching/analyzing/coding [TOPIC/EXPLANATION].
[OBJECTIVES]
1. ...
2. ...
[CONSTRAINTS]
- Focus on: ...
- Ignore: ...
[OUTPUT FORMAT]
Return: ...
[SUCCESS CRITERIA]
Complete when: ...
```
### Good vs. Bad Prompts
Vague prompts produce vague work; specific prompts produce useful work.
**Research**
❌ "Research authentication"
✅ "Research authentication in this Next.js codebase. Focus on: 1) Session management strategy (JWT vs session cookies), 2) Auth provider integration (NextAuth, Clerk, etc), 3) Protected route patterns. Check /app, /lib/auth, and middleware files. Return architecture summary with code examples."
**Web search / docs lookup**
❌ "Search for Codex SDK"
✅ "Find the most recent Codex SDK documentation using the web and summarize key updates. Focus on: 1) Installation/quickstart, 2) Core API methods and parameters, 3) Breaking changes or deprecations. Prioritize official OpenAI docs and release notes. Return a concise summary with citations."
**API discovery**
❌ "Find API endpoints"
✅ "Find all REST API endpoints in this Express.js app. Look in /routes, /api, and /controllers directories. For each endpoint document: method (GET/POST/etc), path, auth requirements, request/response schemas. Return as markdown table."
## Basic Usage
**Default: pipe the prompt via stdin using `-` as the positional argument.** Inline string prompts work for short ones, but anything with newlines, quotes, backticks, or `$` should be piped to avoid shell-escaping bugs.
Canonical invocation (capture output to file, GPT 5.5 with xhigh reasoning):
```bash
cat <<'EOF' | codex exec --yolo --skip-git-repo-check \
-m gpt-5.5 -c 'model_reasoning_effort="xhigh"' \
-o /tmp/codex-result.txt -
[TASK CONTEXT]
You are analyzing /path/to/repo.
[OBJECTIVES]
1. Do X
2. Do Y
[OUTPUT FORMAT]
Return: path - purpose
EOF
result=$(cat /tmp/codex-result.txt)
```
**Variants** (only what changes from the canonical form):
- **Low-effort task** (simple search, short fetch, basic lookup): swap `-c 'model_reasoning_effort="xhigh"'``-c 'model_reasoning_effort="low"'`. Model stays `gpt-5.5`.
- **Machine-parsable output:** swap `-o /tmp/codex-result.txt``--json`, then pipe through `jq -r 'select(.event=="turn.completed") | .content'`. Prefer `-o` whenever possible — it skips JSON parsing and avoids terminal truncation on long outputs.
## Parallel Subagents
Spawn multiple subagents for independent tasks — research two topics in parallel, or compare two approaches to the same problem. Each writes to its own `-o` file; `wait` for all, then read:
```bash
cat <<'EOF' | codex exec --yolo --skip-git-repo-check \
-m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -o /tmp/agent-a.txt - &
Approach A: solve [problem] using [strategy A]. Return diff + rationale.
EOF
cat <<'EOF' | codex exec --yolo --skip-git-repo-check \
-m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -o /tmp/agent-b.txt - &
Approach B: solve [problem] using [strategy B]. Return diff + rationale.
EOF
wait
RESULT_A=$(cat /tmp/agent-a.txt)
RESULT_B=$(cat /tmp/agent-b.txt)
```
The canonical compare-two-approaches pattern: give both subagents the same problem under different framings, wait, then synthesize the better answer in the parent.
## Sequential Subagents
When the parent is driving a multi-step engagement — each next call is a *judgment* based on what just came back, not a mechanical retry. Spawn one subagent, read its output, decide what to do next (next slice of the work, a review pass, a redo, a different angle, a verification step), spawn the next one, repeat. The parent stays in the driver's seat the whole time:
```bash
# Step 1: dispatch the first subagent
cat <<'EOF' | codex exec --yolo --skip-git-repo-check \
-m gpt-5.5 -c 'model_reasoning_effort="xhigh"' \
-o /tmp/codex-step-1.txt -
Implement the auth middleware in src/middleware/auth.ts per the spec at docs/auth.md.
Return: summary of changes + files touched.
EOF
# Step 2: parent reads /tmp/codex-step-1.txt, decides what's needed next.
# Could be: next part of the task, a critique pass, a redo, a verification — whatever fits.
# Always use a quoted heredoc (<<'EOF') for the template and `cat` the prior
# output on stdin — never interpolate $STEP_N into an unquoted heredoc.
# Codex output frequently contains backticks and $() sequences (code blocks,
# command examples), and bash would execute those during expansion.
{
cat <<'EOF'
Review the auth middleware changes summarized below for security holes
(token leakage, timing attacks, missing CSRF). Return: issues + line refs.
[PRIOR WORK]
EOF
cat /tmp/codex-step-1.txt
} | codex exec --yolo --skip-git-repo-check \
-m gpt-5.5 -c 'model_reasoning_effort="xhigh"' \
-o /tmp/codex-step-2.txt -
# Step 3: parent reads /tmp/codex-step-2.txt, dispatches a fix pass — or moves
# on to the next slice of work entirely. Keep going until the parent decides done.
```
The shape: **call → read → decide → call → read → decide**. Each prompt is composed fresh in the parent based on what just landed. There's no fixed loop count and no shared template — every step can be a totally different task (implement, review, refactor, sanity-check, redo from scratch). Feed prior outputs into later prompts whenever the next subagent needs that context to do its job.

View File

@@ -369,6 +369,7 @@ nomarchy-theme-sync get colors.accent
sys-update # update inputs + rebuild the system (snapshots first)
sys-rebuild # rebuild the system, current lock (no update)
home-update # rebuild just the desktop layer
nomarchy-doctor # read-only health sheet (also: menu System Doctor)
```
Something broke anyway? Every rebuild is a generation and (on BTRFS)

View File

@@ -26,16 +26,6 @@ next, in what order*.
## NEXT
### 10. `nomarchy-doctor` — one-shot health check
New. A single command (+ System-menu entry) that checks the things that
actually break user machines and prints a themed pass/fail sheet:
failed systemd units (system + user), disk space (/, /boot, the nix
store), state-file parses + is git-tracked, downstream flake dirty/
diverged, last rebuild generation age, snapper timeline running (when
enabled). Read-only, no auto-fixing; each failure prints the one command
that fixes it. Optionally later: a self-gating Waybar warning fed by the
same script. **Verify:** V2 — a VM check with an induced failure.
### 11. State-file validation & friendly errors
New; "the user never has to master Nix" must include *error messages*.
A hand-edited theme-state.json (trailing comma, wrong type, unknown
@@ -126,6 +116,9 @@ earns ≥3 entries — keep the root at six.
- **MIPI/IPU software-ISP camera** support (no-UVC machines).
- **OCR screenshot-to-text**: a Capture entry (grim region → tesseract
→ clipboard) — cheap once recording (#12) reshapes the submenu.
- **Doctor Waybar warning**: a self-gating bar indicator fed by
`nomarchy-doctor` (shipped 2026-07-04) — appears only when the sheet
has a ✖; click opens the sheet.
- **Auto-timezone Waybar tooltip** showing the detected zone (optional).
- **VPN exit-node richer display** (country/city) (optional).
- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never

View File

@@ -86,6 +86,10 @@ QA machine), the **T14s** (webcam case).
`sudo nomarchy-snapshots`, restore a single file (`undochange`)
and walk a root-config rollback up to (or through) the
typed-`yes` gate.
- [ ] **Doctor (item 10)** — menu System Doctor opens the sheet in
a terminal; `nomarchy-doctor` over SSH shows the same minus user
units. On a healthy machine everything is ✔/ (dev-box run
2026-07-04 correctly flagged a genuinely failed user unit).
- [ ] **Rollback menu (item 9b)** — after a couple of theme changes,
menu System Rollback: recent desktop generations listed
(newest marked current); picking an older one opens a terminal,

View File

@@ -17,6 +17,27 @@ Template:
---
## 2026-07-04 — nomarchy-doctor (iteration #20, item 10)
- **Task:** BACKLOG NEXT#10 — one-shot read-only health check.
- **Did:** pkgs/nomarchy-doctor (writeShellApplication → shellcheck-
gated; a package so the VM check runs on a minimal node): failed
system+user units (user bus self-skips), disk space on real
filesystems only (fstype allowlist skips tmpfs/9p → VM-safe; device
dedupe), state-file parses + git-tracked, flake dirty/behind
(warn-level), generation age via the profile SYMLINK mtime (store
paths are epoch-1), snapper timer when enabled. Every ✖ prints its
fix; exit 1 on any ✖. Wired: overlay, systemPackages, System
Doctor menu row (terminal), README §5.
- **Verified:** V0; V1 package build (shellcheck) + real-hardware
smoke: correctly flagged a genuinely failed user unit on the dev box;
V2 GREEN — VM test: induced failed unit → exit 1 + names it + prints
fix; reset-failed → healthy exit 0. Menu rebuild bash -n ok.
- **Pending:** V3 menu-row check queued. Waybar-warning follow-up →
LATER.
- **Next suggestion:** NEXT#11 (state-file validation) — doctor's
"parses" check is its little sibling; 11 adds validate-before-write
+ eval-time schema messages.
## 2026-07-04 — System Rollback menu (iteration #19, item 9b → item 9 done)
- **Task:** BACKLOG NEXT#9 half (b) — undo one menu away.
- **Did:** new `rollback` case in nomarchy-menu (System submenu row):

View File

@@ -83,6 +83,7 @@
# works on machines that don't check out this repo.
themesDir = ./themes;
};
nomarchy-doctor = final.callPackage ./pkgs/nomarchy-doctor { };
};
nixosModules.nomarchy = {
@@ -183,6 +184,34 @@
touch $out
'';
# nomarchy-doctor's contract: an induced failed unit flips the
# sheet to ✖/exit-1 and names the unit; with the failure
# cleared it reports healthy/exit-0. Minimal node (just the
# package) — the disk/flake/snapper checks self-skip in a VM.
doctor = pkgs.testers.runNixOSTest {
name = "nomarchy-doctor";
nodes.machine = { ... }: {
environment.systemPackages = [ pkgs.nomarchy-doctor ];
systemd.services.doomed = {
description = "deliberately failing unit (doctor test)";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/false";
};
wantedBy = [ "multi-user.target" ];
};
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.wait_until_succeeds("systemctl is-failed doomed.service")
out = machine.fail("nomarchy-doctor 2>&1")
assert "doomed.service" in out, f"doctor did not name the failed unit:\n{out}"
assert "journalctl" in out, f"doctor did not print a fix:\n{out}"
machine.succeed("systemctl reset-failed")
machine.succeed("nomarchy-doctor")
'';
};
# Config-level VM assertions for the nomarchy.hardware.* toggles —
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
# behaviour (firmware/driver/device) needs bare metal and is out of

View File

@@ -434,6 +434,12 @@ ${themeRows}
|| { notify-send "Snapshots" "Snapshot tools unavailable (nomarchy.system.snapper off?)."; exit 0; }
exec ${cfg.terminal} -e sudo nomarchy-snapshots ;;
doctor)
# Read-only health sheet in a terminal; exit code doesn't matter
# here the sheet itself is the product.
exec ${cfg.terminal} -e sh -c "nomarchy-doctor
printf '\nEnter to close.'; read -r _" ;;
rollback)
# Undo, one menu away (informative + rock-stable). Desktop =
# Home Manager generations: theme/config history is already one
@@ -528,6 +534,7 @@ ${themeRows}
fi
command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift
row "Rollback" edit-undo
command -v nomarchy-doctor >/dev/null 2>&1 && row "Doctor" utilities-system-monitor
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
row "Power profile" preferences-system-power
fi
@@ -547,6 +554,7 @@ ${themeRows}
*"Auto-commit"*) exec "$0" autocommit ;;
*Snapshots*) exec "$0" snapshot ;;
*Rollback*) exec "$0" rollback ;;
*Doctor*) exec "$0" doctor ;;
*"Power profile"*) exec "$0" power-profile ;;
esac ;;

View File

@@ -293,6 +293,7 @@ in
# ── Essential packages ───────────────────────────────────────────
environment.systemPackages = with pkgs; [
nomarchy-theme-sync # provided by overlays.default
nomarchy-doctor # read-only health check (System Doctor)
# Friendly wrappers for the two rebuild paths (README §3). Run as
# your user: `nix flake update` must NOT run as root (libgit2

View File

@@ -0,0 +1,10 @@
# One-shot read-only health check (System Doctor / `nomarchy-doctor`).
# A package (not an inline script) so the VM check can exercise it on a
# minimal node without pulling in the whole distro module.
{ writeShellApplication, coreutils, gawk, git, gnugrep, jq, systemd }:
writeShellApplication {
name = "nomarchy-doctor";
runtimeInputs = [ coreutils gawk git gnugrep jq systemd ];
text = builtins.readFile ./nomarchy-doctor.sh;
}

View File

@@ -0,0 +1,138 @@
# nomarchy-doctor — one-shot, READ-ONLY health check: the things that
# actually break user machines, as a pass/fail sheet where every
# failure prints the one command that fixes it. It never changes
# anything itself. Exit 0 = no failures (warnings allowed), 1 = at
# least one ✖.
flake="${NOMARCHY_PATH:-$HOME/.nomarchy}"
fails=0
warns=0
grn=$'\033[32m'; red=$'\033[31m'; ylw=$'\033[33m'; dim=$'\033[2m'; rst=$'\033[0m'
ok() { printf ' %b✔%b %s\n' "$grn" "$rst" "$1"; }
bad() { printf ' %b✖%b %s\n' "$red" "$rst" "$1"
printf ' %bfix: %s%b\n' "$dim" "$2" "$rst"
fails=$((fails + 1)); }
warn() { printf ' %b●%b %s\n' "$ylw" "$rst" "$1"
if [ $# -gt 1 ]; then printf ' %b%s%b\n' "$dim" "$2" "$rst"; fi
warns=$((warns + 1)); }
skip() { printf ' %b %s%b\n' "$dim" "$1" "$rst"; }
printf 'nomarchy-doctor — %s\n\n' "$(date '+%Y-%m-%d %H:%M')"
# ── systemd units ────────────────────────────────────────────────────
mapfile -t sysfailed < <(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [ "${#sysfailed[@]}" -eq 0 ]; then
ok "no failed system units"
else
bad "failed system unit(s): ${sysfailed[*]}" \
"journalctl -b -u <unit> (read why; sys-rebuild after fixing)"
fi
# A user bus only exists inside a session (not over bare SSH/root).
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
mapfile -t usrfailed < <(systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
if [ "${#usrfailed[@]}" -eq 0 ]; then
ok "no failed user units"
else
bad "failed user unit(s): ${usrfailed[*]}" \
"journalctl --user -b -u <unit>"
fi
else
skip "user units (no user session bus here)"
fi
# ── disk space ───────────────────────────────────────────────────────
# Only real on-disk filesystems; skips the tmpfs/9p/overlay mounts of
# live systems and test VMs (where usage numbers mean nothing).
seen=""
for mp in / /boot /nix; do
[ -d "$mp" ] || continue
line=$(df -PT "$mp" 2>/dev/null | awk 'NR==2 {gsub("%","",$6); print $1, $2, $6, $5}')
[ -n "$line" ] || continue
read -r dev fstype use availkb <<<"$line"
case "$fstype" in
ext2|ext3|ext4|btrfs|xfs|vfat|f2fs|zfs) ;;
*) continue ;;
esac
case " $seen " in *" $dev "*) continue ;; esac
seen="$seen $dev"
avail=$(numfmt --to=iec $((availkb * 1024)))
if [ "$use" -ge 90 ]; then
bad "$mp is ${use}% full (${avail} free)" \
"sudo nix-collect-garbage --delete-older-than 14d (then sys-rebuild to prune old boot entries)"
else
ok "$mp has space (${use}% used, ${avail} free)"
fi
done
# ── the flake + state file ───────────────────────────────────────────
if [ -f "$flake/theme-state.json" ]; then
if jq empty "$flake/theme-state.json" 2>/dev/null; then
ok "theme-state.json parses"
else
bad "theme-state.json is not valid JSON (rebuilds will fail)" \
"jq . $flake/theme-state.json (shows the bad spot; fix it, or re-apply a theme)"
fi
if git -C "$flake" ls-files --error-unmatch theme-state.json >/dev/null 2>&1; then
ok "theme-state.json is git-tracked"
else
bad "theme-state.json is NOT git-tracked (flake evaluation can't see it)" \
"git -C $flake add theme-state.json"
fi
else
skip "theme-state.json (no flake checkout at $flake)"
fi
if [ -d "$flake/.git" ]; then
dirty=$(git -C "$flake" status --porcelain 2>/dev/null | wc -l)
if [ "$dirty" -gt 0 ]; then
warn "$dirty uncommitted change(s) in $flake (normal — theme writes land there)" \
"commit when happy: git -C $flake add -A && git -C $flake commit -m settings"
else
ok "flake checkout is clean"
fi
behind=$(git -C "$flake" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
if [ "$behind" -gt 0 ]; then
warn "flake is $behind commit(s) behind its upstream" "git -C $flake pull, then sys-rebuild + home-update"
fi
fi
# ── generation age ───────────────────────────────────────────────────
# The profile SYMLINK's own mtime is the generation's creation time
# (store paths themselves are all epoch-1).
link=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)
if [ -n "$link" ] && [ -e "/nix/var/nix/profiles/$link" ]; then
ts=$(stat -c %Y "/nix/var/nix/profiles/$link" 2>/dev/null || echo 0)
days=$(( ($(date +%s) - ts) / 86400 ))
if [ "$days" -gt 30 ]; then
warn "last system rebuild was $days days ago" \
"sys-update when convenient (security fixes arrive with input bumps)"
else
ok "system generation is $days day(s) old"
fi
else
skip "system generation age (no system profile)"
fi
# ── snapper timeline (when enabled) ──────────────────────────────────
if [ -n "$(systemctl list-unit-files snapper-timeline.timer --no-legend --plain 2>/dev/null)" ]; then
if systemctl is-active --quiet snapper-timeline.timer; then
ok "snapper timeline snapshots are running"
else
bad "snapper is enabled but the timeline timer is not active" \
"systemctl status snapper-timeline.timer (then journalctl -u snapper-timeline.service)"
fi
else
skip "snapper (not enabled on this machine)"
fi
# ── verdict ──────────────────────────────────────────────────────────
echo
if [ "$fails" -eq 0 ]; then
printf '%b✔ healthy%b — %d warning(s)\n' "$grn" "$rst" "$warns"
exit 0
else
printf '%b✖ %d problem(s)%b, %d warning(s) — each ✖ above shows its fix\n' "$red" "$fails" "$rst" "$warns"
exit 1
fi