Do a gater-required RE-SPIN on the spt-core Rust workspace (cwd C:\Users\decid\Documents\projects\spt-core, branch v0.8.2-templating-residency, already checked out, HEAD 3a5364f). Two changes (the gater approved everything else). Do NOT run the full test suite (hangs in preflight) — run ONLY the targeted tests named below, bounded. Ground in the existing code (read the cited files). Commit on the branch (do NOT push); report back.

BACKGROUND: v0.8.2 added F-010 Psyche residency-confirm in crates/spt-daemon/src/livehost.rs: a `classify_residency(elapsed, grace, alive) -> Resident/Pending/Failed`; on Failed it stamps the PARENT perch `psyche_host_error` (via spt_store::info::set_psyche_host_error), de-onlines the phantom nested `{id}-psyche` perch, un-hosts, and arms a cooldown. There's a residency grace constant (find it — likely `PSYCHE_RESIDENCY_GRACE` ~5s). The clear-on-success was moved to the Resident arm.

=== CHANGE 1 (Note 1 — reason reword, gater-ratified) ===
The F-010 host-failure reason string is currently something like "host exited within <n>s…". The exit code is unobtainable across the detached spawn seam, so the gater ratified this exact reason wording instead:
    host not resident within <n>s (psyche perch missing/dead pid)
where <n> is the grace in seconds (use the actual grace constant value; if the code builds the string with the number interpolated, keep that).
Apply it in THREE places so they all match:
1. crates/spt-daemon/src/livehost.rs — the reason string passed to set_psyche_host_error on the Failed path.
2. traceable-reqs.toml — REQ-HAZARD-LIVEHOST-NONRESIDENT title's reason clause (change `host exited <code> within <n>s` → `host not resident within <n>s (psyche perch missing/dead pid)`).
3. crates/spt/tests/livehost_nonresident_e2e.rs — the assertion that checks the reason text (update the substring it greps for, e.g. it now must contain "not resident" / "within").

=== CHANGE 2 (Note 2 — REQUIRED: give the positive E2E a LONG-LIVED resident psyche) ===
Problem: crates/spt/tests/livehost_bootrace_e2e.rs positive E2E uses a FAST-EXIT psyche fixture (psychebin --version, exits ~immediately). Post-F-010 that is a FAILED host by definition; the test only passes by killing the brain before the 5s grace fires — a hollow positive + flake-prone on the slow shared kitsubito CI runner, AND it leaves the RESIDENT path with ZERO E2E coverage (nothing proves residency-confirm CONFIRMS a healthy psyche, clears the stamp, and does NOT false-fail a slow-but-alive one).

FIX: give the positive E2E a LONG-LIVED resident psyche fixture that stays alive well past the residency grace (~20s), then WAIT past grace + one reconcile tick (~7-8s) and assert BOTH:
  - the `{id}-psyche` perch's pid is STILL ALIVE (use the same liveness check the impl uses — spt_store::proc::is_process_alive on the pid in the nested perch info.json) → proves Resident, and
  - the PARENT perch `psyche_host_error` is None (a resident host shows clean).
This kills the timing race AND gives the Resident-path proof.

HOW TO MAKE A LONG-LIVED PSYCHE FIXTURE (study the existing fixture setup in livehost_bootrace_e2e.rs + livehost_nonresident_e2e.rs first — how `psychebin` is created/copied into the install dir and how `[session.psyche_init].command` invokes it by BARE token for REQ-INSTALL-11 resolution): the fixture must STILL be a real binary resolved from the adapter install dir (keep REQ-INSTALL-11 fidelity), but invoked so it BLOCKS ~20s. Investigate the most robust option and pick one:
  - psychebin = a copy of the `spt` binary invoked with a BLOCKING subcommand (e.g. `spt ready <throwaway-id>` blocks as a message listener until killed — verify it blocks and needs only SPT_HOME which the test has; pick a throwaway id that won't collide), OR
  - a tiny cross-platform sleeper binary placed in the install dir (e.g. reuse an existing built helper/example in the repo, or a platform sleep), still referenced by bare token.
Pick whatever is robust + cross-platform (Windows hfenduleam + Linux kitsubito both run CI). Justify the choice in a comment.
REAP the resident psyche pid at teardown too (scoped to the pid you spawned — read it from the nested perch info.json; NEVER a machine-wide `Get-Process spt | Stop-Process`, a live broker shares this box). Keep bounded deadlines (~30s). Keep the existing `[int->...]` tags.
(Bonus, NOT required — skip unless trivial: induce a transient psyche_host_error first, then prove the resident host CLEARS it.)

=== GATE (run EXACTLY these, targeted only; report verbatim) ===
- `cargo clippy --workspace --all-targets -- -D warnings`   (exact CI invocation)
- `traceable-reqs check`   (EXIT 0)
- `cargo test -p spt --test livehost_bootrace_e2e -- --nocapture`   (the REWORKED resident-positive — must now assert Resident pid-alive + phe None)
- `cargo test -p spt --test livehost_nonresident_e2e -- --nocapture`   (negative — reason text updated)
- `cargo test -p spt --test livehost_psyche_fail_e2e -- --nocapture`   (v0.8.1 missing-binary regression — must still pass)
- `cargo test -p spt-daemon --lib livehost::`   (classify + livehost units)
- `cargo run -p xtask -- check`   (docs-drift)

THEN COMMIT on the branch (do NOT push). Message: "v0.8.2 re-spin: resident-positive E2E + residency reason reword (gater notes 1+2)". End with the trailer line exactly:
Co-authored by: todlando

REPORT BACK: commit sha, file-by-file diff summary, the long-lived-fixture mechanism you chose + why, and each gate command's verbatim result. If any gate fails, fix before committing. Do NOT push — todlando reviews + runs the final gate + pings the gater.


<owl_working_perch id="todlando-w163">
You are owl working perch todlando-w163. Your parent agent is todlando.
To message your parent: $OWL send todlando todlando-w163
</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 grounding in the cited files to understand the current state.


1	//! The brain's live-agent Psyche host (M11-W0.1, REQ-DAEMON-1) — re-host the
2	//! daemon-hosted LiveAgent lifecycle from disk and reconcile the host-set.
3	//!
4	//! This is the production caller `run_pulse_loop` lacked: the brain instantiates
5	//! a [`BrainLifecycle`] per hosted **live** endpoint and drives its config-paced
6	//! pulse loop on a supervised thread. It is the **harness-endpoint mirror** of
7	//! [`crate::shellwake::reconcile_once`] (which re-hosts offline shells from
8	//! disk): same shape — enumerate Self perches, resolve each manifest from its
9	//! `info.json` `adapter` option, hold a stop flag per supervised thread, and run
10	//! one bounded reconcile sweep at boot and every tick. Where the shell waker
11	//! hosts **offline** shells, this hosts **online** live endpoints — `status` is
12	//! daemon-authoritative liveness (CONTEXT:194 / KH 2.5), so re-hosting the
13	//! online ones re-hosts exactly what was live before a brain (re)start, and
14	//! never resurrects a gracefully-departed (offline) agent.
15	//!
16	//! **Online-only is staged behind the interim.** The interim `spt api listen`
17	//! process still first-hosts its live agents today; those perches are
18	//! `status`-ABSENT (not "online"), so this scan ignores them — no double-host in
19	//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the
20	//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this
21	//! reconcile reacts within one tick, the same way the shell waker reacts to a
22	//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is
23	//! proven through the real brain process by the W0.4 production int.
24	// [impl->REQ-DAEMON-1]
25
26	use std::collections::HashMap;
27	use std::path::Path;
28	use std::sync::atomic::{AtomicBool, Ordering};
29	use std::sync::{Arc, Mutex};
30	use std::thread::JoinHandle;
31	use std::time::{Duration, Instant};
32
33	use spt_runtime::registry::AdapterRecord;
34	use spt_runtime::Manifest;
35	use spt_store::liveness::STATUS_ONLINE;
36	use spt_store::perch::{self, ParentHint};
37
38	use crate::brainproc::StartReason;
39	use crate::config::DaemonConfig;
40	use crate::lifecycle::BrainLifecycle;
41
42	/// The `InfoJson::state` tag of a live agent — only these get a daemon-hosted
43	/// Psyche; a `ready_agent` (messaging-only) never does.
44	const LIVE_AGENT_STATE: &str = "live_agent";
45
46	/// The reconcile cadence (ms): a lifecycle flip lands in a CLI process, so the
47	/// daemon loop is the invariant holder — a flip is at most one tick stale
48	/// (mirrors [`crate::shellwake::RECONCILE_INTERVAL_MS`]).
49	pub const LIVE_RECONCILE_INTERVAL_MS: u64 = 5_000;
50
51	/// Residency grace after a host attempt: a hosted child's `{id}-psyche` perch
52	/// must show a LIVE pid within this window, else the host is declared failed
53	/// (REQ-HAZARD-LIVEHOST-NONRESIDENT). Chosen as ~one reconcile interval (5s): a
54	/// child that exited fast (the F-009 bad-argv case, ~1s) is well past it on the
55	/// next tick, while a slow-but-healthy psyche binary that is still coming up
56	/// inside the first interval is NOT prematurely torn down. The check runs on a
57	/// LATER tick (never a synchronous wait inside the sweep), so the effective
58	/// detection latency is `grace + up to one reconcile interval`.
59	pub const PSYCHE_RESIDENCY_GRACE: Duration = Duration::from_secs(5);
60
61	/// The residency verdict for a hosted endpoint's `{id}-psyche` child, decided
62	/// purely from observed facts (no IO) so it is unit-testable in isolation.
63	// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
64	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65	pub enum Residency {
66	    /// The psyche child is alive (a live recorded pid) — host confirmed.
67	    Resident,
68	    /// Still inside the grace window — too early to judge; check again next tick.
69	    Pending,
70	    /// Past grace with a missing/dead `{id}-psyche` pid — a host failure.
71	    Failed,
72	}
73
74	/// Decide residency from observed facts: how long since the host attempt, the
75	/// grace window, and whether the `{id}-psyche` perch currently shows a LIVE pid
76	/// (`None` ⇒ the perch is missing or carries no/dead numeric pid). Pure.
77	// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
78	pub fn classify_residency(elapsed: Duration, grace: Duration, psyche_alive: bool) -> Residency {
79	    if psyche_alive {
80	        Residency::Resident
81	    } else if elapsed < grace {
82	        Residency::Pending
83	    } else {
84	        Residency::Failed
85	    }
86	}
87
88	/// One brain-hosted live lifecycle: its config-paced pulse-driver thread, the
89	/// held stop flag (tripped to un-host on an offline-transition), and the host
90	/// start instant (the anchor the residency grace counts from).
91	struct HostedLife {
92	    stop: Arc<AtomicBool>,
93	    thread: JoinHandle<()>,
94	    /// When [`host_one`] spawned this endpoint's Psyche — the residency-grace
95	    /// anchor (REQ-HAZARD-LIVEHOST-NONRESIDENT).
96	    hosted_at: Instant,
97	}
98
99	/// After a residency failure, hold off re-hosting the doomed endpoint for this
100	/// long so the system goes QUIESCENT in the failed state — the parent's
101	/// `psyche_host_error` stamp stays visible and the phantom nested perch stays
102	/// cleared, instead of a spawn-storm of a fast-dying psyche every reconcile tick
103	/// (and a flickering stamp). One reconcile interval: the next host attempt is the
104	/// tick AFTER the cooldown, so a transiently-broken psyche still gets retried, a
105	/// permanently-broken one is stamped + quiet (REQ-HAZARD-LIVEHOST-NONRESIDENT).
106	pub const RESIDENCY_RETRY_COOLDOWN: Duration = Duration::from_millis(LIVE_RECONCILE_INTERVAL_MS);
107
108	/// The brain's hosted live-endpoint table, keyed by endpoint id. Shared between
109	/// the reconcile loop and anything that must stop a host. Mirror of
110	/// [`crate::shellwake::WakeSet`].
111	#[derive(Default)]
112	pub struct LiveSet {
113	    inner: Mutex<HashMap<String, HostedLife>>,
114	    /// Per-endpoint cooldown deadline after a residency failure: the start-side
115	    /// skips re-hosting until now passes this instant, so a doomed psyche is not
116	    /// re-spawned every tick (REQ-HAZARD-LIVEHOST-NONRESIDENT).
117	    nonresident_until: Mutex<HashMap<String, Instant>>,
118	}
119
120	impl LiveSet {
121	    pub fn new() -> Self {
122	        Self::default()
123	    }
124
125	    /// How many lifecycles are hosted (finished driver threads pruned first).
126	    pub fn len(&self) -> usize {
127	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
128	        map.retain(|_, h| !h.thread.is_finished());
129	        map.len()
130	    }
131
132	    pub fn is_empty(&self) -> bool {
133	        self.len() == 0
134	    }
135
136	    /// Is `id` currently hosted (driver thread alive)? Prunes a finished entry so
137	    /// a crashed driver is re-hosted next sweep.
138	    fn contains(&self, id: &str) -> bool {
139	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
140	        match map.get(id) {
141	            Some(h) if !h.thread.is_finished() => true,
142	            Some(_) => {
143	                map.remove(id);
144	                false
145	            }
146	            None => false,
147	        }
148	    }
149
150	    fn insert(&self, id: &str, handle: HostedLife) {
151	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
152	        map.insert(id.to_string(), handle);
153	    }
154
155	    /// The currently-tabled ids (finished pruned) — the stop-side reconcile input.
156	    fn ids(&self) -> Vec<String> {
157	        let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
158	        map.retain(|_, h| !h.thread.is_finished());
159	        map.keys().cloned().collect()
160	    }
161
162	    /// How long since `id` was hosted (the residency-grace anchor), or `None` if
163	    /// it is not tabled. [`host_one`] stamps `hosted_at` at spawn time.
164	    // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
165	    fn hosted_elapsed(&self, id: &str) -> Option<Duration> {
166	        let map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
167	        map.get(id).map(|h| h.hosted_at.elapsed())
168	    }
169
170	    /// Arm the residency-failure cooldown for `id` (the start-side then skips
171	    /// re-hosting until it expires). // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
172	    fn arm_nonresident_cooldown(&self, id: &str) {
173	        let mut map = self
174	            .nonresident_until
175	            .lock()
176	            .unwrap_or_else(|p| p.into_inner());
177	        map.insert(id.to_string(), Instant::now() + RESIDENCY_RETRY_COOLDOWN);
178	    }
179
180	    /// Is `id` still inside its residency-failure cooldown (skip re-host)? Expired
181	    /// entries are pruned. // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
182	    fn in_nonresident_cooldown(&self, id: &str) -> bool {
183	        let mut map = self
184	            .nonresident_until
185	            .lock()
186	            .unwrap_or_else(|p| p.into_inner());
187	        match map.get(id) {
188	            Some(until) if Instant::now() < *until => true,
189	            Some(_) => {
190	                map.remove(id);
191	                false
192	            }
193	            None => false,
194	        }
195	    }
196
197	    /// Un-host one lifecycle: trip its stop flag, join the driver, de-table. The
198	    /// pulse loop checks the flag each sleep-slice, so the join is prompt. We do
199	    /// NOT re-stamp `offline` — the perch already left the online set (graceful
200	    /// signoff cleared it, or the perch is gone); a brain *crash* deliberately
201	    /// leaves `status=online` so the next brain rehydrates it (CONTEXT:194).
202	    fn stop_host(&self, id: &str) {
203	        let handle = {
204	            let mut map = self.inner.lock().unwrap_or_else(|p| p.into_inner());
205	            map.remove(id)
206	        };
207	        if let Some(h) = handle {
208	            h.stop.store(true, Ordering::SeqCst);
209	            let _ = h.thread.join();
210	        }
211	    }
212	}
213
214	/// One reconcile sweep — the brain's live host-set invariant holder, run at boot
215	/// and every [`LIVE_RECONCILE_INTERVAL_MS`]. The complete set-diff against the
216	/// on-disk **online** live endpoints (mirror of [`crate::shellwake::reconcile_once`]):
217	///
218	/// - **un-host** (tabled ∧ ¬online) — stop the driver + de-table (exit guard: a
219	///   signed-off / removed endpoint must not keep pulsing — thread + dead-agent leak).
220	/// - **host** (online ∧ ¬tabled) — spawn the Psyche + start the driver.
221	/// - **leave-alone** (online ∧ tabled).
222	///
223	/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are
224	/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.
225	// [impl->REQ-DAEMON-1]
226	pub fn reconcile_once(
227	    owlery: &Path,
228	    registered: &[(AdapterRecord, Manifest)],
229	    adapters_dir: &Path,
230	    set: &Arc<LiveSet>,
231	    cfg: &DaemonConfig,
232	    reason: StartReason,
233	) {
234	    // Stop side first: hosted endpoints that DEFINITIVELY left the online set —
235	    // trip the flag, join, de-table. Un-host on a definitive departure ONLY, never
236	    // on a TRANSIENT read failure (M11-W0.4 (ii)): a momentarily-unreadable
237	    // info.json must not stop+respawn the driver (Psyche churn on a disk blip). The
238	    // discriminator is the perch DIR:
239	    //   - dir gone        → torn down (teardown removed the perch) → un-host;
240	    //   - dir present, read_info Some(status≠online) → signed-off/offline → un-host;
241	    //   - dir present, read_info None (transient/torn read) → LEAVE the driver, retry.
242	    // [impl->REQ-DAEMON-1]
243	    for id in set.ids() {
244	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
245	        let unhost = if !perch.exists() {
246	            true // the perch dir is gone — torn down
247	        } else {
248	            match spt_store::info::read_info(&perch) {
249	                Some(info) => info.status.as_deref() != Some(STATUS_ONLINE),
250	                None => false, // dir present but unreadable: transient → keep hosting
251	            }
252	        };
253	        if unhost {
254	            set.stop_host(&id);
255	            continue;
256	        }
257	        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] RESIDENCY confirm: the parent is
258	        // still online, but if its `{id}-psyche` child spawned then DIED within the
259	        // grace window (the F-009 bad-argv exit-2 case — spawn() returned Ok so the
260	        // v0.8.1 spawn-Err stamp never fired), stamp the parent psyche_host_error,
261	        // un-host, and clear the phantom online nested perch. Runs on a LATER tick,
262	        // never a synchronous wait — the sweep is not blocked.
263	        confirm_residency_or_unhost(set, &id);
264	    }
265
266	    // Start side: every online live endpoint not yet hosted.
267	    for id in perch::list_self_perch_ids(owlery) {
268	        if set.contains(&id) {
269	            continue; // already hosted (leave-alone)
270	        }
271	        // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT] a just-failed residency endpoint is
272	        // held off until its cooldown expires — do NOT re-spawn the doomed psyche this
273	        // same sweep (that would clear+recreate the phantom + churn). One quiescent
274	        // window, then a single retry the tick after the cooldown.
275	        if set.in_nonresident_cooldown(&id) {
276	            continue;
277	        }
278	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
279	        let Some(info) = spt_store::info::read_info(&perch) else {
280	            continue;
281	        };
282	        if info.state != LIVE_AGENT_STATE {
283	            continue; // not a live agent (ready_agent etc.)
284	        }
285	        if info.status.as_deref() != Some(STATUS_ONLINE) {
286	            continue; // interim (status-absent) or signed-off (offline)
287	        }
288	        let Some(adapter) = info.adapter.as_deref() else {
289	            continue; // adapterless endpoint — never live-capable
290	        };
291	        let Ok(manifest) = spt_runtime::registry::resolve_option_in(registered, adapters_dir, adapter)
292	        else {
293	            continue; // deregistered adapter: nothing to host
294	        };
295	        // [impl->REQ-INSTALL-11] resolve the Psyche role program against the
296	        // adapter install dir — the registry record's precise `source_dir`, the
297	        // same dir Feature E's api seam uses (mod.rs::resolve_ctx_manifest). The
298	        // daemon-host site is the heir to the retired in-process spawn's
299	        // install-dir resolution: without it the daemon-hosted Psyche resolves a
300	        // copy-mode psyche binary only via PATH, not its install dir.
301	        let parent = adapter.split(':').next().unwrap_or(adapter);
302	        let install_dir = registered
303	            .iter()
304	            .find(|(rec, _)| rec.name == parent)
305	            .map(|(rec, _)| std::path::PathBuf::from(&rec.source_dir));
306	        let Some(lifecycle) =
307	            BrainLifecycle::with_config_in(&manifest, &id, cfg.clone(), install_dir.as_deref())
308	        else {
309	            continue; // manifest declares no [session.psyche_init]
310	        };
311	        host_one(set, &id, lifecycle, info.session_id, reason);
312	    }
313	}
314
315	/// Host one live endpoint: sweep a stale signoff (3.2), spawn the Psyche (the
316	/// daemon online-stamp makes liveness daemon-authoritative), then start the
317	/// config-paced pulse driver on its own supervised thread with a held stop flag.
318	/// A Psyche-spawn failure tables nothing — the next sweep retries.
319	// [impl->REQ-DAEMON-1]
320	fn host_one(
321	    set: &Arc<LiveSet>,
322	    id: &str,
323	    lifecycle: BrainLifecycle,
324	    session_id: String,
325	    reason: StartReason,
326	) {
327	    lifecycle.sweep_on_start();
328	    // Resolve the live perch once — the same path the start-scan resolved it by
329	    // (resolve_perch_path/Infer), so the harness-reachable stamp lands on the
330	    // exact info.json `spt endpoint list` / `whoami` reads.
331	    let perch = perch::resolve_perch_path(id, ParentHint::Infer);
332	    match lifecycle.spawn_psyche(Some(&session_id), None) {
333	        Ok(h) => {
334	            eprintln!("LIVEHOST_PSYCHE:{id} pid={}", h.pid);
335	            // NOTE: the prior psyche-host-failure stamp is NOT cleared here. Post
336	            // F-010 a successful detached spawn() is NOT a successful host — the
337	            // child may die within moments (the bad-argv exit-2 case). The clear
338	            // moved to confirm_residency_or_unhost's Resident arm, where a LIVE
339	            // resident psyche past the grace window proves the host truly took.
340	            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
341	        }
342	        Err(e) => {
343	            // Keep the (invisible) brain-stderr line AND stamp the failure onto the
344	            // perch's info.json so it is HARNESS-REACHABLE — a harness/human reads
345	            // perch state, never the brain's stderr. Current-state stamp: overwrite
346	            // reason+ts, increment attempts (the helper reads the prior count inside
347	            // its locked RMW). `status` is NOT touched — liveness stays authoritative.
348	            // host_one returns without tabling, so the 5s reconcile retries and each
349	            // retry re-stamps; a later CONFIRMED-RESIDENT host clears it (in
350	            // confirm_residency_or_unhost, not on bare spawn-Ok — see F-010 note above).
351	            // [impl->REQ-HAZARD-LIVEHOST-BOOT-RACE]
352	            eprintln!("LIVEHOST_PSYCHE_FAIL:{id}: {e}");
353	            let _ = spt_store::info::set_psyche_host_error(&perch, Some(&e.to_string()));
354	            return; // no Psyche → no driver; retried next reconcile
355	        }
356	    }
357	    let stop = Arc::new(AtomicBool::new(false));
358	    let handle = {
359	        let stop = Arc::clone(&stop);
360	        std::thread::spawn(move || {
361	            lifecycle.run_pulse_loop(Some(&session_id), &stop, reason, |_report| {});
362	        })
363	    };
364	    // Stamp the host instant — the residency-grace anchor a later tick measures
365	    // the `{id}-psyche` perch's liveness against (REQ-HAZARD-LIVEHOST-NONRESIDENT).
366	    set.insert(
367	        id,
368	        HostedLife {
369	            stop,
370	            thread: handle,
371	            hosted_at: Instant::now(),
372	        },
373	    );
374	}
375
376	/// Confirm RESIDENCY of a hosted endpoint's `{id}-psyche` child past its grace
377	/// window — the F-010 fix (REQ-HAZARD-LIVEHOST-NONRESIDENT). `spawn_psyche`
378	/// detaches (the summarizer handle is dropped), so a child that spawned Ok but
379	/// DIED within moments (a bad-argv exit-2 child — the F-009 case) leaves the
380	/// nested `{id}-psyche` info.json written `status=online` with a real-but-DEAD
381	/// pid while the PARENT carries no failure. The reconcile-tick guard:
382	///
383	/// - Read the `{id}-psyche` perch's RECORDED pid and pid-probe it DIRECTLY
384	///   ([`spt_store::proc::is_process_alive`]) — NOT [`is_perch_alive`], which
385	///   short-circuits on the `status=online` stamp and would mask the dead pid.
386	/// - [`classify_residency`] then decides: alive ⇒ resident (leave alone);
387	///   inside grace ⇒ pending (retry next tick); past grace + dead/missing ⇒ a
388	///   host failure → stamp the PARENT perch `psyche_host_error{reason: host
389	///   exited within <n>s}`, un-host the driver, and clear the phantom online
390	///   nested perch so it is not falsely reported live.
391	///
392	/// Returns `true` when it un-hosted (so the caller knows the endpoint left the
393	/// table). Best-effort: every store call ignores transient errors.
394	// [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
395	fn confirm_residency_or_unhost(set: &Arc<LiveSet>, id: &str) -> bool {
396	    let Some(elapsed) = set.hosted_elapsed(id) else {
397	        return false; // not tabled (or just un-hosted) — nothing to confirm
398	    };
399	    let psyche_perch = perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
400	    // DIRECT pid probe — the psyche perch is stamped status=online by spawn_psyche,
401	    // so is_perch_alive() would read it alive even with a dead pid (that is the
402	    // exact masking this closes). read_pid → dead/missing ⇒ not alive.
403	    let psyche_alive = spt_store::info::read_pid(&psyche_perch)
404	        .map(spt_store::proc::is_process_alive)
405	        .unwrap_or(false);
406	    let parent_perch = perch::resolve_perch_path(id, ParentHint::Infer);
407	    match classify_residency(elapsed, PSYCHE_RESIDENCY_GRACE, psyche_alive) {
408	        Residency::Resident => {
409	            // RESIDENT past observation: the host truly succeeded. CLEAR any prior
410	            // psyche-host-failure stamp so a now-confirmed live agent shows clean.
411	            // (The clear was deliberately MOVED here from host_one's spawn-Ok leg:
412	            // post-F-010 a successful spawn() is NOT a successful host — only a
413	            // live, resident psyche is. Best-effort; status untouched.)
414	            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
415	            let _ = spt_store::info::set_psyche_host_error(&parent_perch, None);
416	            false
417	        }
418	        Residency::Pending => false, // inside grace — judge next tick
419	        Residency::Failed => {
420	            let reason = format!(
421	                "host exited within {}s (psyche perch missing/dead pid)",
422	                PSYCHE_RESIDENCY_GRACE.as_secs()
423	            );
424	            // Stamp the PARENT (harness-reachable signal) — status is NOT touched
425	            // (liveness stays daemon-authoritative; the SIGNAL is psyche_host_error,
426	            // sibling of the v0.8.1 spawn-Err stamp).
427	            let _ = spt_store::info::set_psyche_host_error(&parent_perch, Some(&reason));
428	            // Clear the phantom online nested perch so it is not falsely reported
429	            // live: mark it offline (best-effort; the perch dir may already be gone).
430	            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);
431	            // Un-host the driver + de-table, then ARM a cooldown so the start-side
432	            // does NOT re-spawn the doomed psyche this same sweep (which would clear
433	            // the phantom then re-create it + churn). The system goes quiescent in
434	            // the failed state until the cooldown expires (then one retry).
435	            eprintln!("LIVEHOST_PSYCHE_NONRESIDENT:{id}: {reason}");
436	            set.stop_host(id);
437	            set.arm_nonresident_cooldown(id);
438	            true
439	        }
440	    }
441	}
442
443	/// Spawn the brain's live host: one thread sweeping [`reconcile_once`] at boot
444	/// and every [`LIVE_RECONCILE_INTERVAL_MS`] until `stop`. The registered set +
445	/// config are re-read each sweep (adapter / pulse-period changes land between
446	/// ticks). Mirror of [`crate::shellwake::spawn_wake_host`]; the stop flag is held
447	/// for symmetry — the threads die with the brain process and the next brain
448	/// rehydrates from disk.
449	// [impl->REQ-DAEMON-1]
450	pub fn spawn_live_host(stop: Arc<AtomicBool>, reason: StartReason) -> JoinHandle<()> {
451	    std::thread::spawn(move || {
452	        let set = Arc::new(LiveSet::new());
453	        while !stop.load(Ordering::SeqCst) {
454	            let owlery = perch::owlery_dir();
455	            let adapters_dir = perch::adapters_dir();
456	            let registered = spt_runtime::registry::registered(&adapters_dir);
457	            let cfg = DaemonConfig::load();
458	            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);
459	            // Sleep in slices so a stop lands promptly.
460	            let mut left = LIVE_RECONCILE_INTERVAL_MS;
461	            while left > 0 && !stop.load(Ordering::SeqCst) {
462	                let step = left.min(100);
463	                std::thread::sleep(Duration::from_millis(step));
464	                left -= step;
465	            }
466	        }
467	    })
468	}
469
470	#[cfg(test)]
471	mod tests {
472	    use super::*;
473	    use crate::test_home::with_home;
474	    use std::time::{Duration, Instant};
475
476	    // [unit->REQ-HAZARD-LIVEHOST-NONRESIDENT] the pure residency decision: a live
477	    // psyche pid is RESIDENT regardless of elapsed; a dead/missing pid is PENDING
478	    // inside the grace (too early to judge — a slow-but-healthy psyche may still be
479	    // coming up) and FAILED only once past the grace (the F-009 fast-exit case).
480	    #[test]
481	    fn residency_classification() {
482	        let grace = Duration::from_secs(5);
483	        // alive ⇒ resident (even at t=0, even past grace).
484	        assert_eq!(
485	            classify_residency(Duration::from_millis(0), grace, true),
486	            Residency::Resident
487	        );
488	        assert_eq!(
489	            classify_residency(Duration::from_secs(60), grace, true),
490	            Residency::Resident
491	        );
492	        // dead/missing inside grace ⇒ pending (retry next tick, no premature stamp).
493	        assert_eq!(
494	            classify_residency(Duration::from_secs(1), grace, false),
495	            Residency::Pending
496	        );
497	        assert_eq!(
498	            classify_residency(Duration::from_millis(4999), grace, false),
499	            Residency::Pending
500	        );
501	        // dead/missing AT/past grace ⇒ failed (a host that exited within the window).
502	        assert_eq!(
503	            classify_residency(grace, grace, false),
504	            Residency::Failed
505	        );
506	        assert_eq!(
507	            classify_residency(Duration::from_secs(10), grace, false),
508	            Residency::Failed
509	        );
510	    }
511
512	    // A no-op Psyche summarizer command: spawns + exits 0 (we never depend on its
513	    // output — the daemon online-stamp is what hosts it, not the pid).
514	    #[cfg(windows)]
515	    const NOOP_CMD: &str = "cmd /C rem";
516	    #[cfg(unix)]
517	    const NOOP_CMD: &str = "true";
518
519	    /// Register a live-capable adapter `name` (declares `[session.psyche_init]` +
520	    /// a watched commune/signoff dir) so `resolve_option_in` finds it from disk.
521	    fn register_live_adapter(name: &str, drops: &Path) {
522	        let dir = drops.to_string_lossy().replace('\\', "/");
523	        let toml = format!(
524	            "[adapter]\nname=\"{name}\"\nkind=\"harness\"\nversion=\"1\"\n\
525	             min_spt_core_version=\"0\"\n\n\
526	             [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
527	             [session.psyche_init]\ncommand='{NOOP_CMD}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
528	        );
529	        let src = perch::spt_home().join("srcs").join(name);
530	        std::fs::create_dir_all(&src).unwrap();
531	        std::fs::write(src.join("manifest.toml"), toml).unwrap();
532	        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
533	    }
534
535	    /// Seed a Self **live** perch on disk: `state=live_agent`, the given status,
536	    /// and the adapter option that resolves the manifest.
537	    fn seed_live_perch(id: &str, adapter: &str, status: &str) {
538	        let perch = perch::resolve_perch_path(id, ParentHint::Infer);
539	        std::fs::create_dir_all(&perch).unwrap();
540	        let mut rec = spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
541	        rec.adapter = Some(adapter.to_string());
542	        spt_store::info::write_info(&perch, &rec).unwrap();
543	        spt_store::info::set_status(&perch, status).unwrap();
544	    }
545
546	    /// A fast-pulse config so the driver loop ticks quickly in tests.
547	    fn fast_cfg() -> DaemonConfig {
548	        DaemonConfig {
549	            pulse_period: Duration::from_millis(15),
550	            ..DaemonConfig::default()
551	        }
552	    }
553
554	    fn registered_now() -> Vec<(AdapterRecord, Manifest)> {
555	        spt_runtime::registry::registered(&perch::adapters_dir())
556	    }
557
558	    /// Poll a predicate up to `bound`, sleeping 20ms between checks.
559	    fn wait_until(bound: Duration, mut pred: impl FnMut() -> bool) -> bool {
560	        let deadline = Instant::now() + bound;
561	        while Instant::now() < deadline {
562	            if pred() {
563	                return true;
564	            }
565	            std::thread::sleep(Duration::from_millis(20));
566	        }
567	        pred()
568	    }
569
570	    // [unit->REQ-DAEMON-1] (a) FIRST-HOST scan: an online live endpoint on disk is
571	    // hosted by the reconcile — the Psyche is spawned AND the config-paced pulse
572	    // driver actually runs (it stamps last_active_ms on the Self perch each tick).
573	    #[test]
574	    fn reconcile_hosts_an_online_live_endpoint_and_drives_its_pulse() {
575	        with_home(|_| {
576	            let drops = perch::spt_home().join("drops");
577	            std::fs::create_dir_all(&drops).unwrap();
578	            register_live_adapter("mock", &drops);
579	            seed_live_perch("doyle", "mock", STATUS_ONLINE);
580
581	            let set = Arc::new(LiveSet::new());
582	            let cfg = fast_cfg();
583	            reconcile_once(
584	                &perch::owlery_dir(),
585	                &registered_now(),
586	                &perch::adapters_dir(),
587	                &set,
588	                &cfg,
589	                StartReason::Cold,
590	            );
591	            assert_eq!(set.len(), 1, "the online live endpoint is hosted");
592
593	            // The driver pulse runs: an Active perch stamps last_active_ms each tick.
594	            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
595	            let ran = wait_until(Duration::from_secs(3), || {
596	                spt_store::info::read_info(&self_perch)
597	                    .and_then(|i| i.last_active_ms)
598	                    .is_some()
599	            });
600	            assert!(ran, "the pulse driver loop ticks (stamps last_active_ms)");
601
602	            // Teardown so the test process doesn't leak the driver thread.
603	            set.stop_host("doyle");
604	        });
605	    }
606
607	    // [unit->REQ-DAEMON-1] (b) ENTRY GUARD: a never-hosted OFFLINE live endpoint is
608	    // skipped — online-only. (Also covers the interim status-absent case shape.)
609	    #[test]
610	    fn reconcile_skips_an_offline_live_endpoint() {
611	        with_home(|_| {
612	            let drops = perch::spt_home().join("drops");
613	            std::fs::create_dir_all(&drops).unwrap();
614	            register_live_adapter("mock", &drops);
615	            seed_live_perch("doyle", "mock", spt_store::liveness::STATUS_OFFLINE);
616
617	            let set = Arc::new(LiveSet::new());
618	            reconcile_once(
619	                &perch::owlery_dir(),
620	                &registered_now(),
621	                &perch::adapters_dir(),
622	                &set,
623	                &fast_cfg(),
624	                StartReason::Cold,
625	            );
626	            assert!(set.is_empty(), "an offline live endpoint is never hosted");
627	        });
628	    }
629
630	    // [unit->REQ-DAEMON-1] (c) NO DOUBLE-HOST: a second reconcile over an
631	    // already-tabled endpoint leaves exactly one host (the contains-guard).
632	    #[test]
633	    fn reconcile_does_not_double_host() {
634	        with_home(|_| {
635	            let drops = perch::spt_home().join("drops");
636	            std::fs::create_dir_all(&drops).unwrap();
637	            register_live_adapter("mock", &drops);
638	            seed_live_perch("doyle", "mock", STATUS_ONLINE);
639
640	            let set = Arc::new(LiveSet::new());
641	            let cfg = fast_cfg();
642	            let run = || {
643	                reconcile_once(
644	                    &perch::owlery_dir(),
645	                    &registered_now(),
646	                    &perch::adapters_dir(),
647	                    &set,
648	                    &cfg,
649	                    StartReason::Cold,
650	                )
651	            };
652	            run();
653	            run();
654	            assert_eq!(set.len(), 1, "the same endpoint is hosted exactly once");
655	            set.stop_host("doyle");
656	        });
657	    }
658
659	    // [unit->REQ-DAEMON-1] (d) REHYDRATE: a FRESH LiveSet (a restarted brain) over a
660	    // perch left `status=online` by a prior brain re-hosts it — distinct from (a)
661	    // in that the table starts empty against an already-online perch (the
662	    // brain-restart path), not a freshly-flipped one.
663	    #[test]
664	    fn fresh_brain_rehydrates_an_online_live_endpoint() {
665	        with_home(|_| {
666	            let drops = perch::spt_home().join("drops");
667	            std::fs::create_dir_all(&drops).unwrap();
668	            register_live_adapter("mock", &drops);
669	            seed_live_perch("doyle", "mock", STATUS_ONLINE);
670
671	            // First brain hosts it.
672	            let first = Arc::new(LiveSet::new());
673	            let cfg = fast_cfg();
674	            reconcile_once(
675	                &perch::owlery_dir(),
676	                &registered_now(),
677	                &perch::adapters_dir(),
678	                &first,
679	                &cfg,
680	                StartReason::Cold,
681	            );
682	            assert_eq!(first.len(), 1);
683	            // Brain "restart": drop the table (threads die with the process), the
684	            // perch stays online on disk.
685	            first.stop_host("doyle");
686	            drop(first);
687
688	            // Fresh brain rehydrates from the still-online perch.
689	            let second = Arc::new(LiveSet::new());
690	            reconcile_once(
691	                &perch::owlery_dir(),
692	                &registered_now(),
693	                &perch::adapters_dir(),
694	                &second,
695	                &cfg,
696	                StartReason::Crash,
697	            );
698	            assert_eq!(second.len(), 1, "a fresh brain re-hosts the online endpoint");
699	            set_then_teardown(&second);
700	        });
701	    }
702
703	    fn set_then_teardown(set: &Arc<LiveSet>) {
704	        set.stop_host("doyle");
705	    }
706
707	    // [unit->REQ-DAEMON-1] (e) EXIT GUARD / UN-HOST: a hosted endpoint that flips to
708	    // status=offline (graceful signoff) is un-hosted by the next reconcile — the
709	    // driver thread is stopped + de-tabled (no forever-pulsing dead agent).
710	    #[test]
711	    fn reconcile_unhosts_on_offline_transition() {
712	        with_home(|_| {
713	            let drops = perch::spt_home().join("drops");
714	            std::fs::create_dir_all(&drops).unwrap();
715	            register_live_adapter("mock", &drops);
716	            seed_live_perch("doyle", "mock", STATUS_ONLINE);
717
718	            let set = Arc::new(LiveSet::new());
719	            // FAST pulse: the driver HAMMERS set_last_active every tick while the
720	            // test marks the perch offline — the un-host-UNDER-CONTENTION proof. The
721	            // W0.4 mutate_info sentinel lock makes status reliably stay offline, so
722	            // un-host reliably fires; the earlier long-pulse+settle workaround is
723	            // removed (the lock, not timing, is what makes this deterministic).
724	            let cfg = fast_cfg();
725	            let run = |reason| {
726	                reconcile_once(
727	                    &perch::owlery_dir(),
728	                    &registered_now(),
729	                    &perch::adapters_dir(),
730	                    &set,
731	                    &cfg,
732	                    reason,
733	                )
734	            };
735	            run(StartReason::Cold);
736	            assert_eq!(set.len(), 1, "hosted while online");
737
738	            // Graceful signoff flips the Self perch offline WHILE the driver pulses.
739	            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
740	            spt_store::info::set_status(&self_perch, spt_store::liveness::STATUS_OFFLINE).unwrap();
741
742	            run(StartReason::Cold);
743	            assert!(
744	                set.is_empty(),
745	                "an offline-transitioned endpoint is un-hosted (driver stopped + de-tabled)"
746	            );
747	        });
748	    }
749
750	    // [unit->REQ-DAEMON-1] (f) READ-ERROR discrimination (M11-W0.4 (ii)): a hosted
751	    // endpoint whose info.json becomes momentarily UNREADABLE while its perch DIR
752	    // remains is NOT un-hosted (transient → keep the driver, no Psyche stop/respawn
753	    // churn on a disk blip); only a GONE perch dir (torn down) un-hosts.
754	    #[test]
755	    fn reconcile_keeps_driver_on_transient_read_fail_unhosts_on_dir_gone() {
756	        with_home(|_| {
757	            let drops = perch::spt_home().join("drops");
758	            std::fs::create_dir_all(&drops).unwrap();
759	            register_live_adapter("mock", &drops);
760	            seed_live_perch("doyle", "mock", STATUS_ONLINE);
761
762	            let set = Arc::new(LiveSet::new());
763	            let cfg = fast_cfg();
764	            let run = || {
765	                reconcile_once(
766	                    &perch::owlery_dir(),
767	                    &registered_now(),
768	                    &perch::adapters_dir(),
769	                    &set,
770	                    &cfg,
771	                    StartReason::Cold,
772	                )
773	            };
774	            run();
775	            assert_eq!(set.len(), 1, "hosted while online");
776
777	            // TRANSIENT read failure: info.json gone, but the perch DIR remains.
778	            let self_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
779	            std::fs::remove_file(self_perch.join("info.json")).unwrap();
780	            run();
781	            assert_eq!(
782	                set.len(),
783	                1,
784	                "a transient read failure (dir present) must NOT un-host — driver kept"
785	            );
786
787	            // DEFINITIVE: the perch dir is gone (torn down) → un-host. The fast-pulse
788	            // driver holds perch files open between pulses; on Windows that
789	            // transiently blocks dir removal (os err 32) — retry past the µs window
790	            // (production teardown stops the host first, so this race is test-only).
791	            let mut removed = false;
792	            for _ in 0..100 {
793	                if std::fs::remove_dir_all(&self_perch).is_ok() {
794	                    removed = true;
795	                    break;
796	                }
797	                std::thread::sleep(std::time::Duration::from_millis(20));
798	            }
799	            assert!(removed, "could not remove the perch dir for the torn-down case");
800	            run();
801	            assert!(set.is_empty(), "a gone perch dir un-hosts the driver");
802	        });
803	    }
804	}
805