Implement the v0.8.1 fix in the spt-core Rust workspace (cwd C:\Users\decid\Documents\projects\spt-core, branch v0.8.1-livehost-bootrace, already checked out). This is a well-scoped, gater-approved spec. Do NOT run the full test suite (it hangs in preflight) — run ONLY the specific targeted tests named below, each bounded.

CONTEXT (already established, do not re-investigate): A daemon-hosted live agent that goes status=online but whose Psyche fails to spawn (e.g. the adapter's psyche binary is absent from its install dir) is left online with NO Psyche and NO harness-visible error — `LIVEHOST_PSYCHE_FAIL` is only an `eprintln!` on the brain's invisible stderr (crates/spt-daemon/src/livehost.rs ~:227). A harness (and a human via `spt endpoint list`) reads PERCH STATE, never brain stderr, so the failure is silent. The REAL daemon E2E already exists at crates/spt/tests/livehost_bootrace_e2e.rs (it launches a real `spt daemon run` broker → real brain child, net-less, real psyche binary by bare token in the install source_dir, asserts the `<id>-psyche` perch appears). Net-independence already holds (spawn_live_host @brainproc.rs:230 is unconditional, before the heartbeat net-gate) — do NOT change that; the E2E locks it in.

THE FIX (4 gater refinements — follow exactly):

1. HARNESS-REACHABLE SIGNAL: Add an additive, N-1-safe field to the perch info.json (crates/spt-store/src/info.rs — the InfoJson struct) capturing the CURRENT psyche-host-failure state. Suggested shape: an `Option` field, e.g.
   `psyche_host_error: Option<PsycheHostError>` where `PsycheHostError { reason: String, ts: String, attempts: u32 }`
   (or whatever matches the struct's existing conventions — check how other optional fields like `status`/`resources` are done: `#[serde(default, skip_serializing_if = "Option::is_none")]`, so old readers/writers round-trip cleanly). Add a helper to set/clear it analogous to the existing `set_status`/`read_info`/`write_info` in info.rs. For `ts`, use the SAME timestamp mechanism the codebase already uses for info.json stamps (grep info.rs / nearby for how `now`/last_active timestamps are produced — reuse it; do NOT invent a new clock).

2. WRITE ON FAILURE, CLEAR ON SUCCESS, never touch status: In crates/spt-daemon/src/livehost.rs `host_one` (~:216-239):
   - On `spawn_psyche(...)` Err: keep the existing `eprintln!("LIVEHOST_PSYCHE_FAIL:...")`, AND read the live perch's info.json, set/overwrite the psyche_host_error field (reason = the error string, ts = now, attempts = previous attempts + 1 — read the prior value to increment; if absent, 1), write it back. Resolve the perch path the same way the file already does (`perch::resolve_perch_path(id, ParentHint::Infer)`). Do NOT modify `status`.
   - On `spawn_psyche(...)` Ok (the LIVEHOST_PSYCHE success path): CLEAR the psyche_host_error field (set to None and write) if it was present — so a recovered host shows clean. Keep status untouched.
   This is a CURRENT-STATE stamp (overwrite each retry, cleared on success), NOT an append log. host_one runs once per host attempt; on failure it returns without joining the hosted set so it retries every 5s (LIVE_RECONCILE_INTERVAL_MS) — each retry overwrites reason+ts and increments attempts; on eventual success the field clears.

3. SURFACE IT: In the `spt endpoint list` / `spt whoami` render (grep for where endpoint liveness/status + the SELF pin + `resources`/description are rendered — likely crates/spt/src/cli.rs around endpoint listing, and/or spt-store), when an endpoint's info.json carries psyche_host_error, show it inline after the liveness state, e.g. `psyche-host: FAILED (<reason>; <attempts> attempts; <ts>)`. Keep it concise; match the surrounding render style. status=online still renders authoritatively — this is an ADDITIONAL line/annotation, not a replacement.

4. TESTS + TRACEABILITY:
   - Extend crates/spt/tests/livehost_bootrace_e2e.rs: keep the POSITIVE case (net-less real-daemon → `<id>-psyche` perch appears = hosted; assert psyche_host_error is absent/cleared on the live perch). ADD/strengthen the NEGATIVE case: with the psyche binary MISSING from the install dir, after a reconcile tick (~6s) assert the live perch's info.json now carries psyche_host_error with a non-empty reason (and attempts>=1) — i.e. the failure is now harness-diagnosable on the perch, NOT just on stderr. Use bounded deadlines (~25s) so a failure is a clean assert, never a hang. Reap spawned PIDs scoped to the test's own PIDs only (NEVER machine-wide `Get-Process spt | Stop-Process` — a live broker shares this box).
   - Add a small UNIT test (in info.rs or spt-store) for the info.json round-trip of the new field + the set/clear/increment helper.
   - Activate REQ-HAZARD-LIVEHOST-BOOT-RACE in traceable-reqs.toml: set `required_stages = ["impl", "unit", "int"]`. Tag evidence in the SAME commit: `// [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]` on the host_one write/clear + the info field + the render; `// [unit->REQ-HAZARD-LIVEHOST-BOOT-RACE]` on the round-trip unit test; `// [int->REQ-HAZARD-LIVEHOST-BOOT-RACE]` on livehost_bootrace_e2e.rs (it's currently tagged [int->REQ-DAEMON-1] — ADD the hazard int tag; keep DAEMON-1 if still apt). Tag on/above the real evidence, never at file top.

5. DOCS: The new perch field is observable behavior. Add a concise CONTEXT.md note (internal doc — milestone refs OK there) describing the psyche-host-health field + that status stays authoritative. If the `spt endpoint list`/whoami `--help` or any CLI render text changes, run `cargo run -p xtask -- gen` to regen docs-site/src/cli/reference.md (docs-drift gate). Tag a `<!-- [doc->REQ-HAZARD-LIVEHOST-BOOT-RACE] -->` on the CONTEXT note if you add doc as a required stage (only if you set doc stage — default is impl/unit/int, so doc tag is optional/not required; do NOT add a doc required_stage).

PREFLIGHT (run these EXACT commands, targeted only — report each result verbatim):
- `cargo clippy --workspace --all-targets -- -D warnings`  (the EXACT CI invocation — NOT -p)
- `traceable-reqs check`  (must EXIT 0)
- `cargo test -p spt --test livehost_bootrace_e2e -- --nocapture`  (the E2E, positive+negative)
- the new unit test alone (e.g. `cargo test -p spt-store <unit_test_name>`)
- `cargo run -p xtask -- check`  (docs-drift; if it fails, run `cargo run -p xtask -- gen` and re-check)

THEN COMMIT on the branch (do NOT push) with message describing the fix + the 4 refinements, ending with the trailer line exactly:
Co-authored by: todlando

REPORT BACK: the commit sha, a concise file-by-file diff summary, and each preflight command's verbatim result (pass/fail). If anything fails preflight, fix it before committing. Ground everything in real output. Do NOT push — todlando reviews + gates + pings doyle.


<owl_working_perch id="todlando-w161">
You are owl working perch todlando-w161. Your parent agent is todlando.
To message your parent: $OWL send todlando todlando-w161
</owl_working_perch>


- autoplan: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk
and runs them sequentially with auto-decisions using 6 decision principles. Surfaces
taste decisions (close approaches, borderline scope, codex disagreements) at a final
approval gate. One command, fully reviewed plan out.
Use when asked to "auto review", "autoplan", "run all reviews", "review this plan
automatically", or "make the decisions for me".
Proactively suggest when the user has a plan file and wants to run the full review
gauntlet without answering 15-30 intermediate questions. (gstack)
- benchmark: Performance regression detection using the browse daemon. Establishes
baselines for page load times, Core Web Vitals, and resource sizes.
Compares before/after on every PR. Tracks performance trends over time.
Use when: "performance", "benchmark", "page speed", "lighthouse", "web vitals",
"bundle size", "load time". (gstack)
- browse: Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with
elements, verify page state, diff before/after actions, take annotated screenshots, check
responsive layouts, test forms and uploads, handle dialogs, and assert element states.
~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a
user flow, or file a bug with evidence. Use when asked to "open in browser", "test the
site", "take a screenshot", or "dogfood this". (gstack)
- canary: Post-deploy canary monitoring. Watches the live app for console errors,
performance regressions, and page failures using the browse daemon. Takes
periodic screenshots, compares against pre-deploy baselines, and alerts
on anomalies. Use when: "monitor deploy", "canary", "post-deploy check",
"watch production", "verify deploy". (gstack)
- careful: Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE,
force-push, git reset --hard, kubectl delete, and similar destructive operations.
User can override each warning. Use when touching prod, debugging live systems,
or working in a shared environment. Use when asked to "be careful", "safety mode",
"prod mode", or "careful mode". (gstack)
- caveman: Ultra-compressed communication mode. Cuts token usage ~75% by dropping filler, articles, and pleasantries while keeping full technical accuracy. Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", "be brief", or invokes /caveman.
- ccs-delegation: Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, enhances prompts with context, executes via `ccs {profile} -p "task"` or `ccs {profile}:continue`, and reports results. Triggers on "use ccs [task]" patterns, typo/test/refactor keywords. Excludes complex architecture, security-critical code, performance optimization, breaking changes.
- checkpoint: Save and resume working state checkpoints. Captures git state, decisions made,
and remaining work so you can pick up exactly where you left off — even across
Conductor workspace handoffs between branches.
Use when asked to "checkpoint", "save progress", "where was I", "resume",
"what was I working on", or "pick up where I left off".
Proactively suggest when a session is ending, the user is switching context,
or before a long break. (gstack)
- codex: OpenAI Codex CLI wrapper — three modes. Code review: independent diff review via
codex review with pass/fail gate. Challenge: adversarial mode that tries to break
your code. Consult: ask codex anything with session continuity for follow-ups.
The "200 IQ autistic developer" second opinion. Use when asked to "codex review",
"codex challenge", "ask codex", "second opinion", or "consult codex". (gstack)
- connect-chrome: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.
Opens a visible browser window where you can watch every action in real time.
The sidebar shows a live activity feed and chat. Anti-bot stealth built in.
Use when asked to "open gstack browser", "launch browser", "connect chrome",
"open chrome", "real browser", "launch chrome", "side panel", or "control my browser".
- cso: Chief Security Officer mode. Infrastructure-first security audit: secrets archaeology,
dependency supply chain, CI/CD pipeline security, LLM/AI security, skill supply chain
scanning, plus OWASP Top 10, STRIDE threat modeling, and active verification.
Two modes: daily (zero-noise, 8/10 confidence gate) and comprehensive (monthly deep
scan, 2/10 bar). Trend tracking across audit runs.
Use when: "security audit", "threat model", "pentest review", "OWASP", "CSO review". (gstack)
- design-consultation: Design consultation: understands your product, researches the landscape, proposes a
complete design system (aesthetic, typography, color, layout, spacing, motion), and
generates font+color preview pages. Creates DESIGN.md as your project's design source
of truth. For existing sites, use /plan-design-review to infer the system instead.
Use when asked to "design system", "brand guidelines", or "create DESIGN.md".
Proactively suggest when starting a new project's UI with no existing
design system or DESIGN.md. (gstack)
- design-html: Design finalization: generates production-quality Pretext-native HTML/CSS.
Works with approved mockups from /design-shotgun, CEO plans from /plan-ceo-review,
design review context from /plan-design-review, or from scratch with a user
description. Text actually reflows, heights are computed, layouts are dynamic.
30KB overhead, zero deps. Smart API routing: picks the right Pretext patterns
for each design type. Use when: "finalize this design", "turn this into HTML",
"build me a page", "implement this design", or after any planning skill.
Proactively suggest when user has approved a design or has a plan ready. (gstack)
- design-review: Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems,
AI slop patterns, and slow interactions — then fixes them. Iteratively fixes issues
in source code, committing each fix atomically and re-verifying with before/after
screenshots. For plan-mode design review (before implementation), use /plan-design-review.
Use when asked to "audit the design", "visual QA", "check if it looks good", or "design polish".
Proactively suggest when the user mentions visual inconsistencies or
wants to polish the look of a live site. (gstack)
- design-shotgun: Design shotgun: generate multiple AI design variants, open a comparison board,
collect structured feedback, and iterate. Standalone design exploration you can
run anytime. Use when: "explore designs", "show me options", "design variants",
"visual brainstorm", or "I don't like how this looks".
Proactively suggest when the user describes a UI feature but hasn't seen
what it could look like. (gstack)
- devex-review: Live developer experience audit. Uses the browse tool to actually TEST the
developer experience: navigates docs, tries the getting started flow, times
TTHW, screenshots error messages, evaluates CLI help text. Produces a DX
scorecard with evidence. Compares against /plan-devex-review scores if they
exist (the boomerang: plan said 3 minutes, reality says 8). Use when asked to
"test the DX", "DX audit", "developer experience test", or "try the
onboarding". Proactively suggest after shipping a developer-facing feature. (gstack)
- diagnose: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says "diagnose this" / "debug this", reports a bug, says something is broken/throwing/failing, or describes a performance regression.
- document-release: Post-ship documentation update. Reads all project docs, cross-references the
diff, updates README/ARCHITECTURE/CONTRIBUTING/CLAUDE.md to match what shipped,
polishes CHANGELOG voice, cleans up TODOS, and optionally bumps VERSION. Use when
asked to "update the docs", "sync documentation", or "post-ship docs".
Proactively suggest after a PR is merged or code is shipped. (gstack)
- find-skills: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
- freeze: Restrict file edits to a specific directory for the session. Blocks Edit and
Write outside the allowed path. Use when debugging to prevent accidentally
"fixing" unrelated code, or when you want to scope changes to one module.
Use when asked to "freeze", "restrict edits", "only edit this folder",
or "lock down edits". (gstack)
- grill-me: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
- grill-with-docs: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
- gsd-add-tests: Generate tests for a completed phase based on UAT criteria and implementation
- gsd-ai-integration-phase: Generate an AI-SPEC.md design contract for phases that involve building AI systems.
- gsd-audit-fix: Autonomous audit-to-fix pipeline — find issues, classify, fix, test, commit
- gsd-audit-milestone: Audit milestone completion against original intent before archiving
- gsd-audit-uat: Cross-phase audit of all outstanding UAT and verification items
- gsd-autonomous: Run all remaining phases autonomously — discuss→plan→execute per phase
- gsd-capture: Capture ideas, tasks, notes, and seeds to their destination
- gsd-cleanup: Archive accumulated phase directories from completed milestones
- gsd-code-review: Review source files changed during a phase for bugs, security issues, and code quality problems
- gsd-complete-milestone: Archive completed milestone and prepare for next version
- gsd-config: Configure GSD settings — workflow toggles, advanced knobs, integrations, and model profile
- gsd-debug: Systematic debugging with persistent state across context resets
- gsd-discuss-phase: Gather phase context through adaptive questioning before planning.
- gsd-docs-update: Generate or update project documentation verified against the codebase
- gsd-eval-review: Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan.
- gsd-execute-phase: Execute all plans in a phase with wave-based parallelization
- gsd-explore: Socratic ideation and idea routing — think through ideas before committing to plans
- gsd-extract-learnings: Extract decisions, lessons, patterns, and surprises from completed phase artifacts
- gsd-fast: Execute a trivial task inline — no subagents, no planning overhead
- gsd-forensics: Post-mortem investigation for failed GSD workflows — diagnoses what went wrong.
- gsd-graphify: Build, query, and inspect the project knowledge graph in .planning/graphs/
- gsd-health: Diagnose planning directory health and optionally repair issues
- gsd-help: Show available GSD commands and usage guide
- gsd-import: Ingest external plans with conflict detection against project decisions before writing anything.
- gsd-inbox: Triage and review open GitHub issues and PRs against project templates and contribution guidelines.
- gsd-ingest-docs: Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo.
- gsd-manager: Interactive command center for managing multiple phases from one terminal
- gsd-map-codebase: Analyze codebase with parallel mapper agents to produce .planning/codebase/ documents
- gsd-milestone-summary: Generate a comprehensive project summary from milestone artifacts for team onboarding and review
- gsd-mvp-phase: Plan a phase as a vertical MVP slice — user story, SPIDR splitting, then plan-phase
- gsd-new-milestone: Start a new milestone cycle — update PROJECT.md and route to requirements
- gsd-new-project: Initialize a new project with deep context gathering and PROJECT.md
- gsd-ns-context: codebase intelligence | map graphify docs learnings
- gsd-ns-ideate: exploration capture | explore sketch spike spec capture
- gsd-ns-manage: config workspace | workstreams thread update ship inbox
- gsd-ns-project: project lifecycle | milestones audits summary
- gsd-ns-review: quality gates | code review debug audit security eval ui
- gsd-ns-workflow: workflow | discuss plan execute verify phase progress
- gsd-pause-work: Create context handoff when pausing work mid-phase
- gsd-phase: CRUD for phases in ROADMAP.md — add, insert, remove, or edit phases
- gsd-plan-phase: Create detailed phase plan (PLAN.md) with verification loop
- gsd-plan-review-convergence: Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain.
- gsd-pr-branch: Create a clean PR branch by filtering out .planning/ commits — ready for code review
- gsd-profile-user: Generate developer behavioral profile and create Claude-discoverable artifacts
- gsd-progress: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command
- gsd-quick: Execute a quick task with GSD guarantees (atomic commits, state tracking) but skip optional agents
- gsd-resume-work: Resume work from previous session with full context restoration
- gsd-review: Request cross-AI peer review of phase plans from external AI CLIs
- gsd-review-backlog: Review and promote backlog items to active milestone
- gsd-secure-phase: Retroactively verify threat mitigations for a completed phase
- gsd-settings: Configure GSD workflow toggles and model profile
- gsd-ship: Create PR, run review, and prepare for merge after verification passes
- gsd-sketch: Sketch UI/design ideas with throwaway HTML mockups, or propose what to sketch next (frontier mode)
- gsd-spec-phase: Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase.
- gsd-spike: Spike an idea through experiential exploration, or propose what to spike next (frontier mode)
- gsd-stats: Display project statistics — phases, plans, requirements, git metrics, and timeline
- gsd-surface: Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall
- gsd-thread: Manage persistent context threads for cross-session work
- gsd-ui-phase: Generate UI design contract (UI-SPEC.md) for frontend phases
- gsd-ui-review: Retroactive 6-pillar visual audit of implemented frontend code
- gsd-ultraplan-phase: [BETA] Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back.
- gsd-undo: Safe git revert. Roll back phase or plan commits using the phase manifest with dependency checks.
- gsd-update: Update GSD to latest version with changelog display
- gsd-validate-phase: Retroactively audit and fill Nyquist validation gaps for a completed phase
- gsd-verify-work: Validate built features through conversational UAT
- gsd-workspace: Manage GSD workspaces — create, list, or remove isolated workspace environments
- gsd-workstreams: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume
- gstack: Fast headless browser for QA testing and site dogfooding. Navigate pages, interact with
elements, verify state, diff before/after, take annotated screenshots, test responsive
layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or
test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
- gstack-upgrade: Upgrade gstack to the latest version. Detects global vs vendored install,
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
"update gstack", or "get latest version".
- guard: Full safety mode: destructive command warnings + directory-scoped edits.
Combines /careful (warns before rm -rf, DROP TABLE, force-push, etc.) with
/freeze (blocks edits outside a specified directory). Use for maximum safety
when touching prod or debugging live systems. Use when asked to "guard mode",
"full safety", "lock it down", or "maximum safety". (gstack)
- handoff: Compact the current conversation into a handoff document for another agent to pick up.
- health: Code quality dashboard. Wraps existing project tools (type checker, linter,
test runner, dead code detector, shell linter), computes a weighted composite
0-10 score, and tracks trends over time. Use when: "health check",
"code quality", "how healthy is the codebase", "run all checks",
"quality score". (gstack)
- improve-codebase-architecture: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
- investigate: Systematic debugging with root cause investigation. Four phases: investigate,
analyze, hypothesize, implement. Iron Law: no fixes without root cause.
Use when asked to "debug this", "fix this bug", "why is this broken",
"investigate this error", or "root cause analysis".
Proactively invoke this skill (do NOT debug directly) when the user reports
errors, 500 errors, stack traces, unexpected behavior, "it was working
yesterday", or is troubleshooting why something stopped working. (gstack)
- land-and-deploy: Land and deploy workflow. Merges the PR, waits for CI and deploy,
verifies production health via canary checks. Takes over after /ship
creates the PR. Use when: "merge", "land", "deploy", "merge and verify",
"land it", "ship it to production". (gstack)
- learn: Manage project learnings. Review, search, prune, and export what gstack
has learned across sessions. Use when asked to "what have we learned",
"show learnings", "prune stale learnings", or "export learnings".
Proactively suggest when the user asks about past patterns or wonders
"didn't we fix this before?"
- mux-send: Send keystrokes to a psmux terminal session. Use when the user says "send to",
"type in", "send keys to", "mux send", or wants to send input to another terminal
session by name.
- office-hours: YC Office Hours — two modes. Startup mode: six forcing questions that expose
demand reality, status quo, desperate specificity, narrowest wedge, observation,
and future-fit. Builder mode: design thinking brainstorming for side projects,
hackathons, learning, and open source. Saves a design doc.
Use when asked to "brainstorm this", "I have an idea", "help me think through
this", "office hours", or "is this worth building".
Proactively invoke this skill (do NOT answer directly) when the user describes
a new product idea, asks whether something is worth building, wants to think
through design decisions for something that doesn't exist yet, or is exploring
a concept before any code is written.
Use before /plan-ceo-review or /plan-eng-review. (gstack)
- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.
Opens a visible browser window where you can watch every action in real time.
The sidebar shows a live activity feed and chat. Anti-bot stealth built in.
Use when asked to "open gstack browser", "launch browser", "connect chrome",
"open chrome", "real browser", "launch chrome", "side panel", or "control my browser".
- pair-agent: Pair a remote AI agent with your browser. One command generates a setup key and
prints instructions the other agent can follow to connect. Works with OpenClaw,
Hermes, Codex, Cursor, or any agent that can make HTTP requests. The remote agent
gets its own tab with scoped access (read+write by default, admin on request).
Use when asked to "pair agent", "connect agent", "share browser", "remote browser",
"let another agent use my browser", or "give browser access". (gstack)
- plan-ceo-review: CEO/founder-mode plan review. Rethink the problem, find the 10-star product,
challenge premises, expand scope when it creates a better product. Four modes:
SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick
expansions), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials).
Use when asked to "think bigger", "expand scope", "strategy review", "rethink this",
or "is this ambitious enough".
Proactively suggest when the user is questioning scope or ambition of a plan,
or when the plan feels like it could be thinking bigger. (gstack)
- plan-design-review: Designer's eye plan review — interactive, like CEO and Eng review.
Rates each design dimension 0-10, explains what would make it a 10,
then fixes the plan to get there. Works in plan mode. For live site
visual audits, use /design-review. Use when asked to "review the design plan"
or "design critique".
Proactively suggest when the user has a plan with UI/UX components that
should be reviewed before implementation. (gstack)
- plan-devex-review: Interactive developer experience plan review. Explores developer personas,
benchmarks against competitors, designs magical moments, and traces friction
points before scoring. Three modes: DX EXPANSION (competitive advantage),
DX POLISH (bulletproof every touchpoint), DX TRIAGE (critical gaps only).
Use when asked to "DX review", "developer experience audit", "devex review",
or "API design review".
Proactively suggest when the user has a plan for developer-facing products
(APIs, CLIs, SDKs, libraries, platforms, docs). (gstack)
- plan-eng-review: Eng manager-mode plan review. Lock in the execution plan — architecture,
data flow, diagrams, edge cases, test coverage, performance. Walks through
issues interactively with opinionated recommendations. Use when asked to
"review the architecture", "engineering review", or "lock in the plan".
Proactively suggest when the user has a plan or design doc and is about to
start coding — to catch architecture issues before implementation. (gstack)
- propose-issue: Challenge an issue statement before fixing it. Probes scope, root cause, and assumptions
through interactive questioning grounded in the codebase. Use when the user says "propose issue",
"challenge this bug", "question this fix", "is this the right problem", or wants to pressure-test
a problem statement before jumping to implementation.
- qa: Systematically QA test a web application and fix bugs found. Runs QA testing,
then iteratively fixes bugs in source code, committing each fix atomically and
re-verifying. Use when asked to "qa", "QA", "test this site", "find bugs",
"test and fix", or "fix what's broken".
Proactively suggest when the user says a feature is ready for testing
or asks "does this work?". Three tiers: Quick (critical/high only),
Standard (+ medium), Exhaustive (+ cosmetic). Produces before/after health scores,
fix evidence, and a ship-readiness summary. For report-only mode, use /qa-only. (gstack)
- qa-only: Report-only QA testing. Systematically tests a web application and produces a
structured report with health score, screenshots, and repro steps — but never
fixes anything. Use when asked to "just report bugs", "qa report only", or
"test but don't fix". For the full test-fix-verify loop, use /qa instead.
Proactively suggest when the user wants a bug report without any code changes. (gstack)
- retro: Weekly engineering retrospective. Analyzes commit history, work patterns,
and code quality metrics with persistent history and trend tracking.
Team-aware: breaks down per-person contributions with praise and growth areas.
Use when asked to "weekly retro", "what did we ship", or "engineering retrospective".
Proactively suggest at the end of a work week or sprint. (gstack)
- review: Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust
boundary violations, conditional side effects, and other structural issues. Use when
asked to "review this PR", "code review", "pre-landing review", or "check my diff".
Proactively suggest when the user is about to merge or land code changes. (gstack)
- screenshot-capture: Capture, process, and analyze screenshots of any running application on Windows 10/11. Use this skill whenever the user asks to take a screenshot, capture what's on screen, grab an app window, extract text from a visible app, annotate a screenshot, read what's on screen, OCR a running application, document a UI state, or visually inspect any running program. Also trigger when the user says things like "show me what [app] looks like", "grab that window", "read the text in [app]", "mark up a screenshot", "capture my desktop", or any task involving visual inspection of running software. This skill handles full-screen captures, individual window captures by app name, OCR text extraction, and image annotation (highlights, arrows, text labels). Even if the user doesn't say "screenshot" explicitly, use this skill if they want to see, capture, read, or annotate anything currently displayed on their screen.
- self-clear: Clear the calling agent's own Claude Code context with /clear, then re-seed it with a user note so it wakes and resumes work after the wipe. Use when the user says "self-clear", "/self-clear", "clear myself", "clear and continue", or "reset my context and keep going".
- setup-browser-cookies: Import cookies from your real Chromium browser into the headless browse session.
Opens an interactive picker UI where you select which cookie domains to import.
Use before QA testing authenticated pages. Use when asked to "import cookies",
"login to the site", or "authenticate the browser". (gstack)
- setup-deploy: Configure deployment settings for /land-and-deploy. Detects your deploy
platform (Fly.io, Render, Vercel, Netlify, Heroku, GitHub Actions, custom),
production URL, health check endpoints, and deploy status commands. Writes
the configuration to CLAUDE.md so all future deploys are automatic.
Use when: "setup deploy", "configure deployment", "set up land-and-deploy",
"how do I deploy with gstack", "add deploy config".
- ship: Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION,
update CHANGELOG, commit, push, create PR. Use when asked to "ship", "deploy",
"push to main", "create a PR", "merge and push", or "get it deployed".
Proactively invoke this skill (do NOT push/PR directly) when the user says code
is ready, asks about deploying, wants to push code up, or asks to create a PR. (gstack)
- to-prd: Turn the current conversation context into a PRD and publish it to the project issue tracker. Use when user wants to create a PRD from the current context.
- trace: Find old conversations in Claude Code session history. Use when the user says
"find the conversation where", "when did we discuss", "trace back to", or wants
to locate a past session by topic, command, commit, or keyword.
- traceable-reqs: traceable-reqs
- unfreeze: Clear the freeze boundary set by /freeze, allowing edits to all directories
again. Use when you want to widen edit scope without ending the session.
Use when asked to "unfreeze", "unlock edits", "remove freeze", or
"allow all edits". (gstack)
- win-marketplace-add: Add a plugin marketplace on Windows, bypassing the EBUSY rename bug. Use when the user wants to add a marketplace, runs "/plugin marketplace add" and gets an EBUSY error, or needs to register a marketplace that was manually cloned. Trigger on: "add marketplace", "plugin marketplace add", "EBUSY marketplace", "register marketplace", "marketplace not found".
- write-a-skill: Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
- ccs: Delegate task with intelligent profile selection [AUTO ENHANCE]
- ccs:continue: Continue last CCS delegation session [AUTO ENHANCE]
- deep-research: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. - When the user wants a deep, multi-source, fact-checked research report on any topic. BEFORE invoking, check if the question is specific enough to research directly — if underspecified (e.g., "what car to buy" without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.
- frontend-design:frontend-design: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
- spt:clear-psyche: Clear stored Psyche context. Use when the user says "clear psyche",
"reset psyche context", or wants to wipe a Psyche's saved state.
- spt:commune: Send a communal update to your Psyche. Use when the user says "commune",
"update psyche", or wants to sync context with their Psyche companion.
- spt:force-stop: Force-stop an SPT agent -- ready agent or live agent (with its
Psyche). Use when the user says "stop listening", "stop owl", "kill live
agent", "force stop", "tear down perch", or "immediately terminate".
Session-aware: a live session (or a target with `live:true` in info.json)
routes through `$LIVE stop` (3-step kill including Psyche teardown); a
ready agent routes through `$OWL stop`.
- spt:list-agents: Unified listing for SPT agents -- ready agents, live agents, and Psyche
processes. Use when the user says "list agents", "who's live", "who's
listening", "show psyches", "show active listeners", or wants a roster of
active perches. Session-aware: defaults to live agents in a live session,
ready agents otherwise. Pass `--psyches` to list Psyche processes
instead.
- spt:live: Run a live agent session. For past sessions, restores a summarized context.

EXPLICIT START phrases (route to /spt:live <id>):
- "live as"
- "start live"
- "go live"
- "start a live agent"

AUTO-RESUME phrases (route to /spt:live --auto, resumes most-recently-active live agent):
- continue live work
- resume live work
- continue live agent
- resume live agent
- live agent continue
- live agent resume
- live work continue
- live work resume

Does NOT route here (too ambiguous — require BOTH "live" AND ("agent" or "work")):
- "keep going"
- "resume work"
- "continue" (bare)
- spt:new-alarm: Schedule a one-shot timed alarm delivered as TIMED PULSE. Use when the user
says "remind me", "check back in", "set an alarm", "wake me when", or wants a
delayed notification.
- spt:psyche-sync-setup: Enable cross-machine Psyche context sync via a private GitHub repo
(spt-agent-storage). Drives install + auth checks for the `gh` CLI,
creates the repo via `gh repo create --private`, wires it as the
`origin` remote for every existing agent/project worktree, and
seeds the remote with `git push --all`. Idempotent — running again
when already enabled reports current status.

Use when the user says "enable cross-machine sync", "set up psyche
sync", "back up my agent context to GitHub", or runs the skill
directly via /spt:psyche-sync-setup.
- spt:ready: Enable inter-agent messaging for the current session.
Use when the user says "listen as", "ready as", or wants to receive inter-agent messages.
- spt:revive: Restart a live agent with new generation. Use when the user says "revive",
"restart live agent", or wants to refresh a live agent's session.
- spt:send: Send a message to another SPT agent. Use when the user says "send to",
"message", "tell <agent>", or wants to communicate with another agent.
- spt:signoff: Graceful live agent shutdown with final context summary. Use when the user
says "sign off", "graceful stop", or wants to cleanly end a live session.
- spt:whoami: Check which owl agent belongs to the current session. Use when the user asks
"which agent am I?", "what's my owl name?", or when you need to discover your
own perch identity.
- caveman:caveman: Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, wenyan-lite, wenyan-full, wenyan-ultra. Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
- caveman:caveman-commit: Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why" isn't obvious. Use when user says "write a commit", "commit message", "generate commit", "/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
- caveman:caveman-help: Quick-reference card for all caveman modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /caveman-help, "caveman help", "what caveman commands", "how do I use caveman".
- caveman:caveman-review: Ultra-compressed code review comments. Cuts noise from PR feedback while preserving the actionable signal. Each comment is one line: location, problem, fix. Use when user says "review this PR", "code review", "review the diff", "/review", or invokes /caveman-review. Auto-triggers when reviewing pull requests.
- caveman:compress: Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format to save input tokens. Preserves all technical substance, code, URLs, and structure. Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. Trigger: /caveman:compress <filepath> or "compress memory file"
- everything:search: Instantly search files and folders on Windows via voidtools Everything 1.5a HTTP JSON API. Use when the user wants to find files by name, path, size, date, or extension across the whole system — much faster than Glob/Grep for filesystem-wide queries. Triggers on "find file", "where is", "locate", "search my drive", "find all .ext files", "everything search".
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by running the app and observing behavior. Use when asked to verify a PR, confirm a fix works, test a change manually, check that a feature works, or validate local changes before pushing.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch

autoplan benchmark browse canary careful caveman ccs-delegation checkpoint codex connect-chrome cso design-consultation design-html design-review design-shotgun devex-review diagnose document-release find-skills freeze grill-me grill-with-docs gsd-add-tests gsd-ai-integration-phase gsd-audit-fix gsd-audit-milestone gsd-audit-uat gsd-autonomous gsd-capture gsd-cleanup gsd-code-review gsd-complete-milestone gsd-config gsd-debug gsd-discuss-phase gsd-docs-update gsd-eval-review gsd-execute-phase gsd-explore gsd-extract-learnings gsd-fast gsd-forensics gsd-graphify gsd-health gsd-help gsd-import gsd-inbox gsd-ingest-docs gsd-manager gsd-map-codebase gsd-milestone-summary gsd-mvp-phase gsd-new-milestone gsd-new-project gsd-ns-context gsd-ns-ideate gsd-ns-manage gsd-ns-project gsd-ns-review gsd-ns-workflow gsd-pause-work gsd-phase gsd-plan-phase gsd-plan-review-convergence gsd-pr-branch gsd-profile-user gsd-progress gsd-quick gsd-resume-work gsd-review gsd-review-backlog gsd-secure-phase gsd-settings gsd-ship gsd-sketch gsd-spec-phase gsd-spike gsd-stats gsd-surface gsd-thread gsd-ui-phase gsd-ui-review gsd-ultraplan-phase gsd-undo gsd-update gsd-validate-phase gsd-verify-work gsd-workspace gsd-workstreams gstack gstack-upgrade guard handoff health improve-codebase-architecture investigate land-and-deploy learn mux-send office-hours open-gstack-browser pair-agent plan-ceo-review plan-design-review plan-devex-review plan-eng-review propose-issue qa qa-only retro review screenshot-capture self-clear setup-browser-cookies setup-deploy ship to-prd trace traceable-reqs unfreeze win-marketplace-add write-a-skill ccs ccs:continue deep-research frontend-design:frontend-design spt:clear-psyche spt:commune spt:force-stop spt:list-agents spt:live spt:new-alarm spt:psyche-sync-setup spt:ready spt:revive spt:send spt:signoff spt:whoami caveman:caveman caveman:caveman-commit caveman:caveman-help caveman:caveman-review caveman:compress everything:search update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


I'll start by investigating the relevant files to understand the existing conventions before implementing.


1	//! `info.json` — the per-perch identity/status record at `<perch_path>/info.json`.
2	//!
3	//! Adapted from sister `types.rs` (`InfoJson` / `PidValue`) per ADR-0001:
4	//! - [`PidValue`] (untagged numeric-or-`"BUSY"`) is copied verbatim — the
5	//!   `"BUSY"` sentinel is load-bearing (a busy poll-listener writes it; numeric
6	//!   readers must treat it as "no usable pid").
7	//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)
8	//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch
9	//!   kinds the spt-core taxonomy drops — see T4).
10	//!
11	//! ## Torn-read tolerance (KNOWN-HAZARDS 1.2, REQ-HAZARD-INFO-JSON-TORN-READ)
12	//! A reader may hit the file mid-write. Two layers guarantee it never observes a
13	//! truncated record:
14	//! 1. **Writers go through [`write_info`]** → [`crate::atomic::atomic_write_string`]
15	//!    (tmp + rename): a same-volume rename is atomic, so a concurrent reader
16	//!    sees the *complete* old file or the *complete* new file, never a torn one.
17	//! 2. **Readers tolerate failure**: [`read_info`] / [`read_pid`] parse via a
18	//!    bounded retry and return `None` on persistent failure — never panic.
19
20	use serde::{Deserialize, Serialize};
21	use std::path::Path;
22
23	use crate::atomic::atomic_write_string;
24
25	/// Pid field that is either a real numeric pid or the `"BUSY"` sentinel a
26	/// listener writes while it holds the perch. Untagged: serializes as a bare
27	/// integer or a bare string. Copied verbatim (sister `PidValue`).
28	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29	#[serde(untagged)]
30	pub enum PidValue {
31	    Numeric(u32),
32	    Busy(String),
33	}
34
35	/// On-disk shape of a perch's `info.json`. Field order is locked by declaration
36	/// (serde_json `preserve_order`). Optional fields are omitted when `None`.
37	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38	pub struct InfoJson {
39	    /// The perch / agent id.
40	    pub id: String,
41	    /// Creation timestamp (opaque string; producer's format).
42	    pub started: String,
43	    /// Owning process pid, or the `"BUSY"` sentinel.
44	    pub pid: PidValue,
45	    /// Harness session id bound to this perch.
46	    pub session_id: String,
47	    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).
48	    pub state: String,
49	    #[serde(skip_serializing_if = "Option::is_none")]
50	    pub parent_pid: Option<u32>,
51	    /// Working directory the perch was created in (forward-slash normalized).
52	    #[serde(skip_serializing_if = "Option::is_none")]
53	    pub cwd: Option<String>,
54	    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**
55	    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's
56	    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`
57	    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is
58	    /// the per-pid probe. The single liveness resolver
59	    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.
60	    #[serde(skip_serializing_if = "Option::is_none")]
61	    pub status: Option<String>,
62	    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this
63	    /// endpoint's activity (pulse tick / input / bind). The recency signal the
64	    /// consent gate resolves "most-recently-active live session" against
65	    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never
66	    /// stamped (an interim perch, or one with no activity yet); a resolver treats
67	    /// that as least-recent.
68	    #[serde(skip_serializing_if = "Option::is_none")]
69	    pub last_active_ms: Option<u64>,
70	    /// The node currently remote-driving this endpoint's session (hex node id),
71	    /// stamped by the daemon when it serves a net-attached surface and cleared
72	    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an
73	    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`
74	    /// so a pre-D5c record (no field) still parses.
75	    #[serde(default, skip_serializing_if = "Option::is_none")]
76	    pub driven_by: Option<String>,
77	    /// The number of read-only viewers currently attached to this endpoint's
78	    /// session under the W2.5 controller/viewer model (REQ-RCVIEW-1). The BROKER
79	    /// is the single writer (it owns the viewers map), stamped on every viewer
80	    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N
81	    /// viewing)`. Absent / `0` ⇒ no viewers (or a pre-W2.5 record). Same
82	    /// best-effort/self-heal caveat as `driven_by`. `default` so older records
83	    /// still parse.
84	    #[serde(default, skip_serializing_if = "Option::is_none")]
85	    pub viewer_count: Option<u32>,
86	    /// The daemon-owned resting state (D9-2, REQ-INST-3): `"active"` /
87	    /// `"dormant"` / `"suspended"` — the explicit instance state machine's
88	    /// durable record (`spt_daemon::resting` owns the transitions; this field
89	    /// is only ever written through [`set_rest_state`]). Distinct from
90	    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);
91	    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a
92	    /// pre-D9-2 / interim record (readers fall back to liveness-derived
93	    /// status). `default` so older records still parse.
94	    #[serde(default, skip_serializing_if = "Option::is_none")]
95	    pub rest_state: Option<String>,
96	    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in
97	    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from
98	    /// dormancy onset, never from last activity). Present iff `rest_state`
99	    /// is `"dormant"`; cleared on every other transition.
100	    #[serde(default, skip_serializing_if = "Option::is_none")]
101	    pub dormant_since_ms: Option<u64>,
102	    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
103	    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
104	    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
105	    /// even when the node enables it.
106	    #[serde(default, skip_serializing_if = "Option::is_none")]
107	    pub auto_suspend_after_ms: Option<u64>,
108	    /// The endpoint-authored resource-advertisement blurb (D9-4,
109	    /// REQ-INST-14): the agent's own "what I can serve" line, written via
110	    /// `spt resources set` ([`set_resources`]) and advertised into every
111	    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
112	    /// `daemon.json` seed (if any) advertises instead — config seeds the
113	    /// default, the agent refines its own at runtime (CONTEXT §resource
114	    /// advertisement, both-authored).
115	    #[serde(default, skip_serializing_if = "Option::is_none")]
116	    pub resources: Option<String>,
117	    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —
118	    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by
119	    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never
120	    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.
121	    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a
122	    /// pre-D9-5 record. Creation seams must **carry this forward** on a
123	    /// re-bind (a revive rewrites `info.json`; home survives by contract).
124	    #[serde(default, skip_serializing_if = "Option::is_none")]
125	    pub home_subnet: Option<String>,
126	    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the
127	    /// adapter-at-creation rule): recorded when a creation/bind path knows
128	    /// it, changed **only** via launch/resume-under-new (the bind paths are
129	    /// the sole writers; no standalone setter). Absent ⇒ created by an
130	    /// adapterless surface (`spt ready`) or a pre-D9-5 record.
131	    #[serde(default, skip_serializing_if = "Option::is_none")]
132	    pub adapter: Option<String>,
133	}
134
135	impl InfoJson {
136	    /// Construct a numeric-pid record.
137	    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
138	        InfoJson {
139	            id: id.to_string(),
140	            started: started.to_string(),
141	            pid: PidValue::Numeric(pid),
142	            session_id: session_id.to_string(),
143	            state: state.to_string(),
144	            parent_pid: None,
145	            cwd: None,
146	            status: None,
147	            last_active_ms: None,
148	            driven_by: None,
149	            viewer_count: None,
150	            rest_state: None,
151	            dormant_since_ms: None,
152	            auto_suspend_after_ms: None,
153	            resources: None,
154	            home_subnet: None,
155	            adapter: None,
156	        }
157	    }
158	}
159
160	/// The serialized read-modify-write path for `info.json` (M11-W0.4,
161	/// REQ-DAEMON-1): EVERY mutating setter funnels through here so concurrent
162	/// writers — cross-process by design (the brain's pulse stamps recency/rest, the
163	/// spt process's signoff marks offline, the broker stamps viewers) — cannot lose
164	/// an update. The lost-update this closes: a pulse `set_last_active` read→write
165	/// interleaving a `mark_offline` could clobber `status=offline` back to online,
166	/// so the un-host never fired and a signed-off agent kept its Psyche.
167	///
168	/// **The lock is taken on a STABLE per-perch sentinel `<perch>/.info.lock`
169	/// (created once, NEVER renamed) — NOT on `info.json`.** This is load-bearing:
170	/// `info.json` is replaced by a tmp+rename on every write (a NEW inode each
171	/// time), and advisory locks (`flock`/`LockFileEx`) lock the INODE — so a lock
172	/// on `info.json`'s fd does NOT serialize across the rename (a second writer
173	/// could open+lock the old, now-unlinked inode, read a stale record, and clobber
174	/// the first writer's rename — a *fake* lock). Locking the constant sentinel
175	/// inode is what actually serializes the RMW. The OS advisory lock auto-releases
176	/// on fd drop / process exit, so a crashed holder strands nothing (no stale-lock
177	/// reaping needed).
178	///
179	/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a
180	/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS
181	/// only. Errors if the record is absent/unreadable — the daemon only mutates
182	/// perches it bound.
183	// [impl->REQ-DAEMON-1]
184	fn mutate_info(perch_path: &Path, mutate: impl FnOnce(&mut InfoJson)) -> std::io::Result<()> {
185	    use fs2::FileExt;
186	    // The stable sentinel — created once, never renamed/removed (teardown removes
187	    // the whole perch dir). Locking it serializes the RMW across processes.
188	    let lock = std::fs::OpenOptions::new()
189	        .create(true)
190	        .write(true)
191	        .truncate(false)
192	        .open(perch_path.join(".info.lock"))?;
193	    lock.lock_exclusive()?; // RAII: released when `lock` drops (incl. on `?` early-return) or process exit
194	    let mut rec = read_info(perch_path).ok_or_else(|| {
195	        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
196	    })?;
197	    mutate(&mut rec);
198	    write_info(perch_path, &rec)
199	}
200
201	/// Set the daemon-managed `status` (the 2.5 daemon-authoritative liveness
202	/// marker). Serialized via [`mutate_info`] so a concurrent recency/rest stamp
203	/// never clobbers it. Errors if the record is absent — the daemon only marks
204	/// perches it bound.
205	// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
206	pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
207	    mutate_info(perch_path, |rec| rec.status = Some(status.to_string()))
208	}
209
210	/// Stamp the `last_active_ms` recency marker (REQ-UPD-4). Serialized via
211	/// [`mutate_info`].
212	// [impl->REQ-UPD-4]
213	pub fn set_last_active(perch_path: &Path, now_ms: u64) -> std::io::Result<()> {
214	    mutate_info(perch_path, |rec| rec.last_active_ms = Some(now_ms))
215	}
216
217	/// Stamp the daemon-owned resting state (D9-2, REQ-INST-3). `dormant_since_ms`
218	/// (the auto-suspend anchor) moves with it atomically — `Some` exactly when
219	/// `state` is dormant, so a reader never sees a dormant record without its anchor.
220	/// Serialized via [`mutate_info`].
221	// [impl->REQ-INST-3]
222	pub fn set_rest_state(
223	    perch_path: &Path,
224	    state: &str,
225	    dormant_since_ms: Option<u64>,
226	) -> std::io::Result<()> {
227	    mutate_info(perch_path, |rec| {
228	        rec.rest_state = Some(state.to_string());
229	        rec.dormant_since_ms = dormant_since_ms;
230	    })
231	}
232
233	/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
234	/// REQ-INST-14) — the `spt resources set` write. Serialized via [`mutate_info`].
235	// [impl->REQ-INST-14]
236	pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
237	    mutate_info(perch_path, |rec| rec.resources = blurb.map(str::to_string))
238	}
239
240	/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
241	/// which node's operator is driving this endpoint's session. Serialized via
242	/// [`mutate_info`].
243	// [impl->REQ-REACH-1]
244	pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
245	    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
246	}
247
248	/// Stamp the read-only `viewer_count` (REQ-RCVIEW-1). The broker is the single
249	/// logical writer (it owns the viewers map); `0` clears the field. Serialized via
250	/// [`mutate_info`].
251	// [impl->REQ-RCVIEW-1]
252	pub fn set_viewer_count(perch_path: &Path, count: u32) -> std::io::Result<()> {
253	    mutate_info(perch_path, |rec| {
254	        rec.viewer_count = if count == 0 { None } else { Some(count) }
255	    })
256	}
257
258	/// Number of read attempts before giving up (torn-read retry).
259	const READ_ATTEMPTS: u32 = 5;
260
261	/// Atomically write `info` to `<perch_path>/info.json` (tmp + rename, EBUSY
262	/// retry via the atomic layer). The perch directory must already exist.
263	// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]
264	pub fn write_info(perch_path: &Path, info: &InfoJson) -> std::io::Result<()> {
265	    let json = serde_json::to_string(info)
266	        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
267	    atomic_write_string(&perch_path.join("info.json"), &json)
268	}
269
270	/// Read and parse `<perch_path>/info.json`. Tolerant: a transient read/parse
271	/// failure (e.g. catching the file mid-replace on a filesystem where that is
272	/// observable) is retried up to [`READ_ATTEMPTS`] times; returns `None` on
273	/// persistent absence/corruption — never panics.
274	// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]
275	pub fn read_info(perch_path: &Path) -> Option<InfoJson> {
276	    let path = perch_path.join("info.json");
277	    for _ in 0..READ_ATTEMPTS {
278	        if let Ok(content) = std::fs::read_to_string(&path) {
279	            if let Ok(info) = serde_json::from_str::<InfoJson>(&content) {
280	                return Some(info);
281	            }
282	        }
283	    }
284	    None
285	}
286
287	/// Read just the numeric pid, tolerantly. Routed through `serde_json::Value` so
288	/// a schema-minimal `{"pid": N}` (as wrapper-owned listeners write) parses
289	/// without the full `InfoJson` shape. Returns `None` for a missing file, the
290	/// `"BUSY"` sentinel, or any non-numeric pid. Copied semantics from sister
291	/// `get_pid_from_info`.
292	// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]
293	pub fn read_pid(perch_path: &Path) -> Option<u32> {
294	    let path = perch_path.join("info.json");
295	    for _ in 0..READ_ATTEMPTS {
296	        if let Ok(content) = std::fs::read_to_string(&path) {
297	            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
298	                // `as_u64` is None for the "BUSY" string sentinel → None pid.
299	                return val.get("pid").and_then(|v| v.as_u64()).map(|n| n as u32);
300	            }
301	        }
302	    }
303	    None
304	}
305
306	#[cfg(test)]
307	mod tests {
308	    use super::*;
309	    use std::sync::atomic::{AtomicBool, Ordering};
310	    use std::sync::Arc;
311
312	    fn perch() -> tempfile::TempDir {
313	        tempfile::tempdir().unwrap()
314	    }
315
316	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] write → read round-trip; optional
317	    // fields omitted when None.
318	    #[test]
319	    fn write_read_round_trip() {
320	        let d = perch();
321	        let mut info = InfoJson::new("doyle", "2026-05-31T00:00:00Z", 4242, "sid-1", "live_agent");
322	        info.cwd = Some("C:/repo/spt-core".to_string());
323	        write_info(d.path(), &info).unwrap();
324
325	        let got = read_info(d.path()).expect("reads back");
326	        assert_eq!(got, info);
327
328	        // parent_pid omitted (None) → absent from JSON.
329	        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
330	        assert!(
331	            !raw.contains("parent_pid"),
332	            "None field must be omitted: {raw}"
333	        );
334	        assert!(raw.contains("\"state\":\"live_agent\""));
335	    }
336
337	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] BUSY sentinel: full parse keeps it;
338	    // read_pid yields None (no usable pid).
339	    #[test]
340	    fn busy_sentinel_round_trips_and_read_pid_is_none() {
341	        let d = perch();
342	        let info = InfoJson {
343	            id: "doyle".to_string(),
344	            started: "t".to_string(),
345	            pid: PidValue::Busy("BUSY".to_string()),
346	            session_id: "sid".to_string(),
347	            state: "ready_agent".to_string(),
348	            parent_pid: None,
349	            cwd: None,
350	            status: None,
351	            last_active_ms: None,
352	            driven_by: None,
353	            viewer_count: None,
354	            rest_state: None,
355	            dormant_since_ms: None,
356	            auto_suspend_after_ms: None,
357	            resources: None,
358	            home_subnet: None,
359	            adapter: None,
360	        };
361	        write_info(d.path(), &info).unwrap();
362	        assert_eq!(
363	            read_info(d.path()).unwrap().pid,
364	            PidValue::Busy("BUSY".to_string())
365	        );
366	        assert_eq!(read_pid(d.path()), None, "BUSY must yield no numeric pid");
367	    }
368
369	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] set_status stamps the daemon
370	    // liveness marker onto an existing record; absent by default (interim
371	    // perches carry no status); set_status on an absent record errors.
372	    #[test]
373	    fn set_status_marks_existing_record() {
374	        let d = perch();
375	        write_info(
376	            d.path(),
377	            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
378	        )
379	        .unwrap();
380	        // Interim record: no status field serialized.
381	        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
382	        assert!(
383	            !raw.contains("status"),
384	            "interim perch carries no status: {raw}"
385	        );
386
387	        set_status(d.path(), "online").unwrap();
388	        assert_eq!(
389	            read_info(d.path()).unwrap().status.as_deref(),
390	            Some("online")
391	        );
392	        // Re-mark offline.
393	        set_status(d.path(), "offline").unwrap();
394	        assert_eq!(
395	            read_info(d.path()).unwrap().status.as_deref(),
396	            Some("offline")
397	        );
398
399	        // No record to mark → error, never a panic / silent write.
400	        let empty = perch();
401	        assert!(set_status(empty.path(), "online").is_err());
402	    }
403
404	    // [unit->REQ-DAEMON-1] the lost-update lock under contention: many threads spam
405	    // the transient setters (last_active/rest/driven_by/viewer_count — each a
406	    // full-record read-modify-write, the brain-pulse churn) while one thread marks
407	    // the perch OFFLINE mid-storm. mutate_info's per-perch sentinel lock serializes
408	    // every RMW, so the offline write is NEVER clobbered back to online by a racing
409	    // whole-record rewrite — i.e. un-host can reliably fire. Unlocked, a spam RMW
410	    // that read status=online before the mark and wrote after it would resurrect
411	    // online (a signed-off agent keeping its Psyche). The sentinel lock makes the
412	    // post-offline spam read offline and preserve it.
413	    #[test]
414	    fn mutate_info_serializes_writers_so_offline_is_never_clobbered() {
415	        use std::sync::atomic::{AtomicBool, Ordering};
416	        use std::sync::Arc;
417	        let d = perch();
418	        let path = d.path().to_path_buf();
419	        write_info(&path, &InfoJson::new("gw", "t", 4242, "sid", "live_agent")).unwrap();
420	        set_status(&path, "online").unwrap();
421
422	        let stop = Arc::new(AtomicBool::new(false));
423	        let mut spammers = Vec::new();
424	        for t in 0..6u64 {
425	            let path = path.clone();
426	            let stop = Arc::clone(&stop);
427	            spammers.push(std::thread::spawn(move || {
428	                let mut n = t;
429	                while !stop.load(Ordering::Relaxed) {
430	                    let _ = set_last_active(&path, n);
431	                    let _ = set_rest_state(&path, "active", None);
432	                    let _ = set_driven_by(&path, Some("nodehex"));
433	                    let _ = set_viewer_count(&path, (n % 3) as u32);
434	                    n += 1;
435	                }
436	            }));
437	        }
438	        // Run the storm, mark OFFLINE mid-storm (the write that must survive), then
439	        // keep spamming so any unlocked clobber would fire after the mark.
440	        std::thread::sleep(std::time::Duration::from_millis(50));
441	        set_status(&path, "offline").unwrap();
442	        std::thread::sleep(std::time::Duration::from_millis(50));
443	        stop.store(true, Ordering::Relaxed);
444	        for s in spammers {
445	            s.join().unwrap();
446	        }
447	        // Status survived the contention. (Transient fields racing EACH OTHER is
448	        // cosmetic — only status correctness gates un-host.)
449	        assert_eq!(
450	            read_info(&path).unwrap().status.as_deref(),
451	            Some("offline"),
452	            "the sentinel-locked RMW must not let a transient stamp clobber status=offline"
453	        );
454	    }
455
456	    // [unit->REQ-UPD-4] set_last_active stamps the recency marker on an existing
457	    // record (absent by default), and errors on a missing record.
458	    #[test]
459	    fn set_last_active_stamps_recency() {
460	        let d = perch();
461	        write_info(
462	            d.path(),
463	            &InfoJson::new("doyle", "t", 4242, "sid", "live_agent"),
464	        )
465	        .unwrap();
466	        assert_eq!(
467	            read_info(d.path()).unwrap().last_active_ms,
468	            None,
469	            "absent by default"
470	        );
471
472	        set_last_active(d.path(), 1234).unwrap();
473	        assert_eq!(read_info(d.path()).unwrap().last_active_ms, Some(1234));
474	        // A later stamp overwrites.
475	        set_last_active(d.path(), 5678).unwrap();
476	        assert_eq!(read_info(d.path()).unwrap().last_active_ms, Some(5678));
477
478	        // No record → error, never a silent write.
479	        let empty = perch();
480	        assert!(set_last_active(empty.path(), 1).is_err());
481	    }
482
483	    // [unit->REQ-INST-3] set_rest_state stamps the resting record (state +
484	    // dormancy anchor as one atomic pair) onto an existing perch: absent by
485	    // default (pre-D9-2 records parse with rest_state == None), the anchor
486	    // rides only the dormant state, and a missing record errors.
487	    #[test]
488	    fn set_rest_state_stamps_state_and_anchor_as_a_pair() {
489	        let d = perch();
490	        write_info(
491	            d.path(),
492	            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
493	        )
494	        .unwrap();
495	        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
496	        assert!(
497	            !raw.contains("rest_state"),
498	            "interim perch carries no resting record: {raw}"
499	        );
500
501	        // Dormant carries its auto-suspend anchor.
502	        set_rest_state(d.path(), "dormant", Some(1_000)).unwrap();
503	        let rec = read_info(d.path()).unwrap();
504	        assert_eq!(rec.rest_state.as_deref(), Some("dormant"));
505	        assert_eq!(rec.dormant_since_ms, Some(1_000));
506
507	        // Waking clears the anchor in the SAME write — no stale anchor on a
508	        // non-dormant record.
509	        set_rest_state(d.path(), "active", None).unwrap();
510	        let rec = read_info(d.path()).unwrap();
511	        assert_eq!(rec.rest_state.as_deref(), Some("active"));
512	        assert_eq!(
513	            rec.dormant_since_ms, None,
514	            "anchor cleared on leaving dormancy"
515	        );
516
517	        // No record → error, never a silent write.
518	        let empty = perch();
519	        assert!(set_rest_state(empty.path(), "dormant", Some(1)).is_err());
520
521	        // Forward tolerance: a pre-D9-2 record (no fields) parses clean.
522	        let old = perch();
523	        atomic_write_string(
524	            &old.path().join("info.json"),
525	            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
526	        )
527	        .unwrap();
528	        let rec = read_info(old.path()).unwrap();
529	        assert_eq!(rec.rest_state, None);
530	        assert_eq!(rec.auto_suspend_after_ms, None);
531	    }
532
533	    // [unit->REQ-INST-14] set_resources writes the endpoint-authored blurb
534	    // (absent by default — config's node seed advertises instead), clears it
535	    // with None, and errors on a missing record.
536	    #[test]
537	    fn set_resources_writes_and_clears_the_blurb() {
538	        let d = perch();
539	        write_info(
540	            d.path(),
541	            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
542	        )
543	        .unwrap();
544	        assert_eq!(
545	            read_info(d.path()).unwrap().resources,
546	            None,
547	            "absent by default"
548	        );
549
550	        set_resources(d.path(), Some("code review; rust builds")).unwrap();
551	        assert_eq!(
552	            read_info(d.path()).unwrap().resources.as_deref(),
553	            Some("code review; rust builds")
554	        );
555	        // The agent refines its own at runtime — a later set overwrites.
556	        set_resources(d.path(), Some("rust builds only")).unwrap();
557	        assert_eq!(
558	            read_info(d.path()).unwrap().resources.as_deref(),
559	            Some("rust builds only")
560	        );
561
562	        set_resources(d.path(), None).unwrap();
563	        assert_eq!(read_info(d.path()).unwrap().resources, None, "cleared");
564
565	        let empty = perch();
566	        assert!(
567	            set_resources(empty.path(), Some("x")).is_err(),
568	            "no record, no write"
569	        );
570	    }
571
572	    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an
573	    // existing record (absent by default = locally driven), clears it with
574	    // None, errors on a missing record — and a pre-D5c record (no field at
575	    // all) still parses with driven_by == None.
576	    #[test]
577	    fn set_driven_by_stamps_and_clears() {
578	        let d = perch();
579	        write_info(
580	            d.path(),
581	            &InfoJson::new("ling", "t", 4242, "sid", "live_agent"),
582	        )
583	        .unwrap();
584	        let raw = std::fs::read_to_string(d.path().join("info.json")).unwrap();
585	        assert!(
586	            !raw.contains("driven_by"),
587	            "locally driven perch carries no marker: {raw}"
588	        );
589
590	        set_driven_by(d.path(), Some("cafe1234")).unwrap();
591	        assert_eq!(
592	            read_info(d.path()).unwrap().driven_by.as_deref(),
593	            Some("cafe1234")
594	        );
595
596	        set_driven_by(d.path(), None).unwrap();
597	        assert_eq!(
598	            read_info(d.path()).unwrap().driven_by,
599	            None,
600	            "viewport over → cleared"
601	        );
602
603	        // No record → error, never a silent write.
604	        let empty = perch();
605	        assert!(set_driven_by(empty.path(), Some("cafe1234")).is_err());
606
607	        // Forward tolerance: a pre-D5c record without the field parses.
608	        let old = perch();
609	        atomic_write_string(
610	            &old.path().join("info.json"),
611	            r#"{"id":"ling","started":"t","pid":1,"session_id":"sid","state":"live_agent"}"#,
612	        )
613	        .unwrap();
614	        assert_eq!(read_info(old.path()).unwrap().driven_by, None);
615	    }
616
617	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] read_pid tolerates a schema-minimal
618	    // {"pid": N} record (wrapper-owned listener shape).
619	    #[test]
620	    fn read_pid_tolerates_schema_minimal() {
621	        let d = perch();
622	        atomic_write_string(&d.path().join("info.json"), r#"{"pid": 1234}"#).unwrap();
623	        assert_eq!(read_pid(d.path()), Some(1234));
624	        // But the full InfoJson parse fails on the minimal shape → None, no panic.
625	        assert_eq!(read_info(d.path()), None);
626	    }
627
628	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] corrupt / truncated content is
629	    // tolerated: None, never a panic.
630	    #[test]
631	    fn corrupt_content_returns_none() {
632	        let d = perch();
633	        std::fs::write(d.path().join("info.json"), "{ \"id\": \"doyle\", \"pi").unwrap();
634	        assert_eq!(read_info(d.path()), None);
635	        assert_eq!(read_pid(d.path()), None);
636	        // Missing file too.
637	        let empty = perch();
638	        assert_eq!(read_info(empty.path()), None);
639	    }
640
641	    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] under a concurrent writer
642	    // continuously rewriting info.json, every successful read is a COMPLETE,
643	    // valid record — never a torn parse. Atomic rename guarantees old-or-new.
644	    #[test]
645	    fn concurrent_read_during_writes_never_torn() {
646	        let d = perch();
647	        let path = Arc::new(d.path().to_path_buf());
648	        // Seed a valid file so early readers have something.
649	        write_info(&path, &InfoJson::new("doyle", "t", 1, "sid", "live_agent")).unwrap();
650
651	        let stop = Arc::new(AtomicBool::new(false));
652	        let writer_path = Arc::clone(&path);
653	        let writer_stop = Arc::clone(&stop);
654	        let writer = std::thread::spawn(move || {
655	            let mut n = 0u32;
656	            while !writer_stop.load(Ordering::Relaxed) {
657	                n = n.wrapping_add(1);
658	                // Vary length (long cwd) so a torn read would be detectable.
659	                let mut info = InfoJson::new("doyle", "t", n, "sid", "live_agent");
660	                info.cwd = Some("x".repeat((n % 500) as usize));
661	                let _ = write_info(&writer_path, &info);
662	            }
663	        });
664
665	        // Hammer reads; each must be None or a valid record with id == "doyle".
666	        let mut valid_reads = 0;
667	        for _ in 0..3000 {
668	            if let Some(info) = read_info(&path) {
669	                assert_eq!(info.id, "doyle", "a read must never be torn/garbage");
670	                valid_reads += 1;
671	            }
672	        }
673	        stop.store(true, Ordering::Relaxed);
674	        writer.join().unwrap();
675	        assert!(valid_reads > 0, "should have observed some complete reads");
676	    }
677	}
678