Skip to content

feat: add desktop:doctor self-diagnosis command for generated apps#14

Merged
Christian-Katzmann merged 1 commit into
mainfrom
feat/desktop-doctor
Jun 1, 2026
Merged

feat: add desktop:doctor self-diagnosis command for generated apps#14
Christian-Katzmann merged 1 commit into
mainfrom
feat/desktop-doctor

Conversation

@Christian-Katzmann
Copy link
Copy Markdown
Owner

@Christian-Katzmann Christian-Katzmann commented Jun 1, 2026

What

Adds desktop:doctor — a command that makes any generated app-it app self-diagnosing. A user can run npm run desktop:doctor long after the build session (no agent in the loop) and get a short, issue-ready report on one launcher.

It's the user-facing embodiment of Core principle #8 — runtime truth beats build-time guess. The same server.port-first, descendant-walk-ownership, read-the-runtime checks the agent runs in Phase 4, packaged as a command on the user's machine.

What it checks

config + placeholder leakage · bundle id shape (rejects com.$(id -un).*) · installed/build .app · Info.plist identity · run + Mach-O wrapper · icon · ad-hoc signature · quarantine / iCloud signature-breaking xattrs · preferred-vs-runtime port · stale PID · whether the process on the runtime port is actually in the recorded supervisor's descendant tree (reuses the launcher's reattach gate) · start-command binary resolution on the launcher's augmented PATH · log/state paths · template drift (feature-probes the installed wrapper/run against the current templates via the grep -qboa idiom — no version stamp needed). --tail[=N] appends the launcher log.

Hard rules (honored structurally)

  • Diagnostic, not fixer. Read-only by default; deterministic and local (no network, no new dependencies); says "probably" when a check can't be certain.
  • --fix-safe is the only mutating mode and is deliberately narrow — it touches only app-it's own generated state: stale pid/port files (only when the recorded process is dead), this bundle's stale LaunchServices registration, the rebuilt icon, and quarantine on the generated .app. It never touches the user's product code, dependencies, framework config, or anything outside app-it's artifacts, and never kills a running server (that's desktop:quit).
  • Scoped to the macOS app-it plugin. app-it-static has a different runtime model (no dev-server daemon, no PID/port) and would need its own tailored checks — left as a clean follow-up.

Design notes

  • Plain bash, like every other desktop-*.sh — no new dependency.
  • Drift via feature-probe, not a new version stamp. Generated apps carry no version marker; rather than add build-pipeline machinery, the doctor probes the installed artifacts against the current templates, reusing the skill's documented grep -qboa <marker> wrapper idiom.
  • Reads scripts/app-it.config.json the same way desktop-build.sh / desktop-quit.sh do — no APPS-table drift.

Testing

./scripts/validate.sh passes (exit 0; the new template is required and covered by the bash -n sweep). Smoke-tested across 5 scenarios — no-app, --help, unknown-app selector, a fully built/signed/quarantined/stale-PID/markerless-wrapper bundle, and --fix-safe — verifying the fixes land and stay inside generated state, and that uncertain checks hedge with "probably".

Docs

SKILL.md (templates list, Phase 3, script naming, report, a new Diagnosing a generated app section, frontmatter), the user-facing desktop-launcher.md.template, docs/TROUBLESHOOTING.md (leads with it), and CHANGELOG.md.

🤖 Generated with Claude Code

Summary by Sourcery

Add a self-diagnosing desktop:doctor command for macOS app-it launchers and document how to use it.

New Features:

  • Introduce desktop-doctor.sh template and wire it as the desktop:doctor npm script for generated app-it apps, enabling runtime self-diagnosis of a single launcher and an optional --fix-safe cleanup mode.

Enhancements:

  • Update app-it skill documentation to describe the desktop:doctor command, its scope, and how it fits into the launcher lifecycle, including multi-app selection semantics.
  • Extend validation to require the new desktop-doctor.sh template so generated projects consistently include the diagnostic command.

Documentation:

  • Expand SKILL.md, the desktop launcher docs template, and TROUBLESHOOTING.md to highlight desktop:doctor as the first-line diagnostic tool and explain its read-only checks and narrow --fix-safe behavior.
  • Record the new desktop:doctor capability and its guarantees in the changelog for the unreleased version.

Generated app-it apps can now self-diagnose long after the build session,
with no agent in the loop. `scripts/desktop-doctor.sh` (wired as
`desktop:doctor`) inspects one launcher and prints a short, issue-ready
report on the things app-it actually cares about: config + placeholder
leakage, installed/build .app, Info.plist identity, ad-hoc signature,
quarantine/iCloud xattrs, preferred-vs-runtime port, stale PID, whether the
process on the runtime port is genuinely in the recorded supervisor's
descendant tree (reuses the launcher's reattach gate), start-command binary
resolution on the launcher's PATH, log/state paths, and template drift
(feature-probes the installed wrapper/run against the current templates via
the `grep -qboa` idiom — no version stamp needed). `--tail[=N]` appends the
launcher log.

This is the user-facing embodiment of Core principle #8 (runtime truth beats
build-time guess).

Hard rules honored structurally:
- Diagnostic, not fixer: read-only by default; deterministic and local (no
  network, no new dependencies); says "probably" when a check can't be certain.
- `--fix-safe` is the only mutating mode and is deliberately narrow — it
  touches ONLY app-it's own generated state (stale pid/port files, this
  bundle's stale LaunchServices registration, the rebuilt icon, quarantine on
  the generated .app). Never the user's product code, dependencies, framework
  config, or a running server.

Scoped to the macOS app-it plugin; the app-it-static companion has a different
runtime model and would need its own checks.

Docs: SKILL.md (templates list, Phase 3, script naming, report, a new
"Diagnosing a generated app" section, frontmatter), the user-facing
desktop-launcher doc template, docs/TROUBLESHOOTING.md (leads with it), and
CHANGELOG. validate.sh requires the new template and bash -n covers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jun 1, 2026

Reviewer's Guide

Implements a new desktop:doctor diagnostic command for macOS app-it launchers, wiring it into the app-it template/tooling/docs and providing an optional --fix-safe path that only mutates app-it–generated artifacts.

File-Level Changes

Change Details Files
Add desktop:doctor diagnostic script for app-it launchers, including runtime checks and optional safe fixes limited to generated artifacts.
  • Introduce scripts/desktop-doctor.sh as a bash-based self-diagnosis tool that reads app-it.config.json at runtime and inspects one launcher’s config, bundle, signature, runtime ports/PIDs, ownership, logs, and template drift.
  • Implement structured, colorized reporting with ok/warn/fail/info counters, careful handling of uncertain checks ("probably" wording), and non-fatal probing of system tools like lsof, codesign, curl, and PlistBuddy.
  • Add feature-probe based template drift detection comparing the installed wrapper/run against current templates via grep -qboa markers instead of version stamping.
  • Provide a --tail[=N] option to append recent server.log lines to the report and a --fix-safe mode that can clear stale pid/port files, refresh LaunchServices registration, rebuild/copy icons, and clear quarantine on the generated .app without touching user code or config.
plugins/app-it/skills/app-it/templates/desktop-doctor.sh
Wire desktop:doctor into the app-it skill documentation, templates list, and package script recommendations, including multi-app usage.
  • Document desktop:doctor in SKILL.md as a generated command, describe its checks, and add a dedicated "Diagnosing a generated app" section with behavior, scope, and safety contract.
  • List desktop-doctor.sh alongside other desktop-* helper scripts in the templates section and clarify that it is copied verbatim like the others.
  • Extend recommended package.json scripts for single-app and multi-app setups to include desktop:doctor, explaining multi-app selection via slug and default behavior.
  • Update quickstart/usage sections to mention pnpm desktop:doctor and its flags for diagnosis and cleanup.
plugins/app-it/skills/app-it/SKILL.md
Surface desktop:doctor prominently in user-facing docs and changelog as the first troubleshooting step for desktop issues.
  • Add a "Diagnose It First" section to docs/TROUBLESHOOTING.md that instructs users to run desktop:doctor (with --tail and slug selection) and explains what it inspects and how --fix-safe behaves.
  • Update CHANGELOG.md Unreleased section with a detailed entry describing desktop:doctor’s purpose, checks, safety guarantees, and scope (macOS app-it only).
  • Ensure messaging emphasizes that desktop:doctor is diagnostic by default, local, deterministic, and uses "probably" where checks are uncertain.
docs/TROUBLESHOOTING.md
CHANGELOG.md
Guard the new doctor template in validation to prevent drift or accidental removal.
  • Update scripts/validate.sh to require the presence of plugins/app-it/skills/app-it/templates/desktop-doctor.sh, aligning it with other critical templates like desktop-build.sh and wrapper.swift.
scripts/validate.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The LATEST_NVM_NODE selection uses sort -V, which is not supported by BSD sort on macOS and will fail on many default setups; consider switching to a portable sort (e.g., sort -t. -k1,1 -k2,2 -k3,3 or using ls -t and picking the first directory) or delegating to a small Node/Python helper.
  • The walk_descendants helper assigns potentially multiple child PIDs into current and then passes that string to pgrep -P, but pgrep expects a comma-separated list, not a space-separated string; this can cause the descendant walk to stop after the first generation, so it would be safer to iterate per PID or normalize to the expected format.
  • The launcher PATH emulation in LAUNCHER_PATH appends the current shell’s $PATH, which may differ substantially from the bare PATH used by the real desktop launcher; to avoid false positives/negatives in the start-command resolution check, consider deriving this exactly from the run-template.sh implementation rather than duplicating it with $PATH appended.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `LATEST_NVM_NODE` selection uses `sort -V`, which is not supported by BSD `sort` on macOS and will fail on many default setups; consider switching to a portable sort (e.g., `sort -t. -k1,1 -k2,2 -k3,3` or using `ls -t` and picking the first directory) or delegating to a small Node/Python helper.
- The `walk_descendants` helper assigns potentially multiple child PIDs into `current` and then passes that string to `pgrep -P`, but `pgrep` expects a comma-separated list, not a space-separated string; this can cause the descendant walk to stop after the first generation, so it would be safer to iterate per PID or normalize to the expected format.
- The launcher PATH emulation in `LAUNCHER_PATH` appends the current shell’s `$PATH`, which may differ substantially from the bare PATH used by the real desktop launcher; to avoid false positives/negatives in the start-command resolution check, consider deriving this exactly from the `run-template.sh` implementation rather than duplicating it with `$PATH` appended.

## Individual Comments

### Comment 1
<location path="plugins/app-it/skills/app-it/templates/desktop-doctor.sh" line_range="167-169" />
<code_context>
+# walk_descendants PID — echo the PID and up to 4 generations of children,
+# space-separated. Mirrors run-template.sh's reattach gate so "does the running
+# server belong to this launcher" uses the SAME ownership test the launcher does.
+walk_descendants() {
+    local root="$1" current="$1" tree="$1" gen
+    for _ in 1 2 3 4; do
+        gen="$(pgrep -P "$current" 2>/dev/null | tr '\n' ' ')"
+        [ -z "$gen" ] && break
</code_context>
<issue_to_address>
**issue (bug_risk):** walk_descendants may pass multiple PIDs to `pgrep -P`, which expects a single parent PID

After the first iteration, `current` can hold multiple space-separated PIDs, but `pgrep -P` expects a single PID or a comma-separated list, not a space-delimited string. This can make the descendant walk incorrect or brittle and undermine the ownership check. Consider iterating over each PID in `current` and aggregating children, or using a queue-based traversal so you always pass a single parent PID per `pgrep` call.
</issue_to_address>

### Comment 2
<location path="plugins/app-it/skills/app-it/templates/desktop-doctor.sh" line_range="479-50" />
<code_context>
+    if [ -n "$ICON_SRC" ] && [ -x "$SCRIPT_DIR/desktop-icons.sh" ]; then
</code_context>
<issue_to_address>
**suggestion:** The icon rebuild skip message conflates missing source icon with missing desktop-icons.sh

The `else` branch is taken when either `ICON_SRC` is empty or `desktop-icons.sh` is not executable, but the message only mentions a missing source icon. This can misdirect debugging when the script file is the real problem. Consider splitting the checks so each failure case (no `ICON_SRC` vs. non-executable/missing `desktop-icons.sh`) has its own, accurate log message.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +167 to +169
walk_descendants() {
local root="$1" current="$1" tree="$1" gen
for _ in 1 2 3 4; do
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): walk_descendants may pass multiple PIDs to pgrep -P, which expects a single parent PID

After the first iteration, current can hold multiple space-separated PIDs, but pgrep -P expects a single PID or a comma-separated list, not a space-delimited string. This can make the descendant walk incorrect or brittle and undermine the ownership check. Consider iterating over each PID in current and aggregating children, or using a queue-based traversal so you always pass a single parent PID per pgrep call.

if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_FAIL=$'\033[31m'
C_INFO=$'\033[36m'; C_DIM=$'\033[2m'; C_BOLD=$'\033[1m'; C_OFF=$'\033[0m'
else
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The icon rebuild skip message conflates missing source icon with missing desktop-icons.sh

The else branch is taken when either ICON_SRC is empty or desktop-icons.sh is not executable, but the message only mentions a missing source icon. This can misdirect debugging when the script file is the real problem. Consider splitting the checks so each failure case (no ICON_SRC vs. non-executable/missing desktop-icons.sh) has its own, accurate log message.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f894ff1cc8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Backend (A3.2 multi-server) — only when the config declares one.
if [ -n "$BACKEND_PORT" ]; then
BRUNTIME=""; [ -f "$BPORT_FILE" ] && BRUNTIME="$(cat "$BPORT_FILE" 2>/dev/null || true)"
if [ -n "$BRUNTIME" ] && lsof -ti tcp:"$BRUNTIME" >/dev/null 2>&1; then ok "backend listening on :$BRUNTIME"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify backend listener ownership for multi-server apps

For A3.2 multi-server launchers, this reports the backend as healthy whenever any process is bound to the recorded backend port. If backend.pid is stale or another local service has taken backend.port, desktop:doctor prints [ ok ] backend listening even though the multi-server launcher would not reattach because its descendant_holds_port gate requires the backend listener to be in the recorded backend supervisor's tree. Please mirror the frontend/launcher ownership check here before marking the backend healthy.

Useful? React with 👍 / 👎.


# Launch-time binary preflight — catches "works in my terminal, dead from Dock"
# because Finder launches with a bare PATH. Uses the launcher's augmented PATH.
CMD="$START_COMMAND"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the backend start command on PATH

In a multi-server config, BACKEND_START is parsed but never preflighted, so desktop:doctor can report the start command's binary as resolvable when only the frontend binary is present. The generated multi-server launcher checks both START_COMMAND and BACKEND_START_COMMAND before launching, so a missing backend runner (for example uvicorn or python) will still make Dock launch fail while the doctor report misses the actionable PATH problem.

Useful? React with 👍 / 👎.

@Christian-Katzmann Christian-Katzmann merged commit f894ff1 into main Jun 1, 2026
5 checks passed
@Christian-Katzmann Christian-Katzmann deleted the feat/desktop-doctor branch June 1, 2026 20:37
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