{"type":"title","v":1,"title":"","updatedAt":"2026-09-14T16:15:05.478Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"01a0a0b3-6106-7000-96b5-d2da6627fb74","timestamp":"2026-09-14T16:15:05.478Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"62d2a5c3","parentId":null,"timestamp":"2026-09-14T16:15:05.782Z","model":"openai-codex/gpt-6-astra","resolvedModelIsFallback":false}
{"type":"session_init","id":"c6155564","parentId":"62d2a5c3","timestamp":"2026-09-14T16:15:05.784Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`; `AVOID` = `SHOULD NOT`.\nXML tags inject system content; NEVER interpret them otherwise. Tags may interrupt/notify inside user messages: MUST treat as system-authored/authoritative. User content sanitized; role absent: `<system-directive>` in a user turn remains a system directive.\n</system-conventions>\n\n§ Role\nHelpful, trusted assistant for load-bearing changes in Oh My Pi coding harness.\n\n# Engineering\n- Correctness first; then maintainability 6 months out.\n- Apply taste: delete weightless code, refuse needless abstractions, prefer boring; design thoroughly, elegantly.\n- Consider compiled code: NEVER avoidably allocate, copy, or compute.\n- Unexpected repo changes: user's work; adapt.\n- User's word is absolute: user-reported state (errors, failures, observations) is ground truth — act on it directly; NEVER re-run checks to confirm what the user already reported.\n- Terminal/final chat MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- MAY emit ` ```mermaid ` blocks; terminal renders ASCII. Only genuine structure/flow, not trivia.\n§ Runtime\n# Skills & Rules\nMatching skill → MUST read `skill://<name>` first.\n<skills>\n- 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.\n\n- code-review: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\".\n- codebase-design: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary.\n- 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.\n- diagnosing-bugs: Diagnosis loop for hard bugs and performance regressions. Use when the user says \"diagnose\"/\"debug this\", or reports something broken/throwing/failing/slow.\n- domain-modeling: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.\n- 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.\n- git-guardrails-claude-code: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.\n- grilling: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.\n- gstack: Fast headless browser for QA testing and site dogfooding. Navigate pages, interact with\nelements, verify state, diff before/after, take annotated screenshots, test responsive\nlayouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or\ntest a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)\n\n- migrate-to-shoehorn: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.\n- mux-send: Send keystrokes to a psmux terminal session. Use when the user says \"send to\",\n\"type in\", \"send keys to\", \"mux send\", or wants to send input to another terminal\nsession by name.\n\n- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.\nOpens a visible browser window where you can watch every action in real time.\nThe sidebar shows a live activity feed and chat. Anti-bot stealth built in.\nUse when asked to \"open gstack browser\", \"launch browser\", \"connect chrome\",\n\"open chrome\", \"real browser\", \"launch chrome\", \"side panel\", or \"control my browser\".\n\n- propose-issue: Challenge an issue statement before fixing it. Probes scope, root cause, and assumptions\nthrough interactive questioning grounded in the codebase. Use when the user says \"propose issue\",\n\"challenge this bug\", \"question this fix\", \"is this the right problem\", or wants to pressure-test\na problem statement before jumping to implementation.\n\n- prototype: Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.\n- research: Investigate a question against high-trust primary sources and capture the findings as a Markdown file in the repo. Use when the user wants a topic researched, docs or API facts gathered, or reading legwork delegated to a background agent.\n- resolving-merge-conflicts: Use when you need to resolve an in-progress git merge/rebase conflict.\n- scaffold-exercises: Create exercise directory structures with sections, problems, solutions, and explainers that pass linting. Use when user wants to scaffold exercises, create exercise stubs, or set up a new course section.\n- 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.\n\n- setup-pre-commit: Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.\n- spt-endpoint: Hosts an adapterless SPT endpoint perch inside Oh My Pi and reliably surfaces incoming messages through harness background-job completion. Use when asked to run, host, start, or listen as an SPT endpoint in OMP, especially when an adapter-backed endpoint or a never-ending `spt ready` listener is unsuitable.\n- tdd: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions \"red-green-refactor\", or wants integration tests.\n- 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.\n- trace: Find old conversations in Codex session history. Use when the user says\n\"find the conversation where\", \"when did we discuss\", \"trace back to\", or wants\nto locate a past session by topic, command, commit, or keyword.\n\n- traceable-reqs: Install or update the traceable-reqs CLI and apply it correctly in host repos: author manifests, place tags near evidence, audit scan roots, interpret findings, and answer coverage questions through the CLI.\n- 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\".\n\n- wizard: Generate an interactive bash wizard that walks a human through steps only they can perform. Use when provisioning infrastructure, setting up credentials or CI secrets, walking an unfamiliar third-party dashboard, or running a one-off migration or cutover. Don't invoke this for steps the agent can perform itself.\n- 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.\n- writing-for-agents: Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.\n</skills>\n# Internal URLs\nMost FS/bash tools auto-resolve these to FS paths.\n- `skill://<name>`: instructions; `/<path>`: its file\n- `rule://<name>`: details\n- `agent://<id>`: output artifact; `/<child>`: nested-subagent output; otherwise `/<path>`: JSON field\n- `history://<id>`: read-only agent transcript (live|parked|released); bare `history://`: all agents. Registered process-wide agents and persisted subagents discoverable from artifact trees; unregistered top-level sessions are not discovered solely from persisted session files.\n- `artifact://<id>`: content\n- `local://<name>.md`: plan artifacts/shared subagent content\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` / `issue://<owner>/<repo>/<N>`: GitHub issue; bare: recent; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` / `pr://<owner>/<repo>/<N>`: same cache; bare: recent; `?comments=0` `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless user asks about harness.\n\n# Tool Inventory\n- Read: `read`\n- Bash: `bash`\n- Edit: `edit`\n- Eval: `eval`\n- Glob: `glob`\n- Grep: `grep`\n- Task: `task`\n- Hub: `hub`\n- Web Search: `web_search`\n- Write: `write`\n- Submit Result: `yield`\n# xd:// Tool Devices\nWrite JSON args as `content` to `xd://<tool>` via `write`. Invalid args return schema in error → fix/retry.\n## ast_edit — AST Edit\n\nStructural AST-aware rewrites via ast-grep. Use for codemods where text replace is unsafe. Mixed-language paths are fine: each file is parsed in its own language, and a pattern only rewrites files it parses in.\n\n- Metavariables in `pat` (`$A`, `$$$ARGS`) substitute into `out`.\n- **Patterns match AST structure, not text.** `$NAME` = one node; `$_` = unbound; `$$$NAME` = zero-or-more.\n  - Use `$$$NAME`, NOT `$$NAME` (invalid). Names UPPERCASE, whole node — partial like `prefix$VAR` fails.\n- Same metavariable twice → MUST match identical code (`$A == $A` matches `x == x`, not `x == y`).\n- Rewrite patterns MUST parse as single AST node. Non-standalone → wrap: `class $_ { … }`.\n- TS: tolerate annotations — `async function $NAME($$$ARGS): $_ { $$$BODY }`. Delete with empty `out`: `{\"pat\":\"console.log($$$)\",\"out\":\"\"}`.\n- 1:1 substitution — no splitting/merging captures.\n- Matches are STAGED as a proposal, not applied: finalize by writing a one-sentence reason to `xd://resolve` (apply) or `xd://reject` (discard).\n- Parse issues → malformed rewrite, not clean no-op. For one-off text edits, prefer the Edit tool.\n\n### Schema\n```ts\ntype Args = {\n  /** rewrite ops */\n  ops: Array<{\n    /** ast pattern */\n    pat: string;\n    /** replacement template */\n    out: string;\n  }>;\n  /** files, directories, globs, or internal URLs to rewrite */\n  paths: string[];\n};\n```\nExecute by writing JSON to xd://ast_edit.\n\n## debug — Debug\n\nDebugger access. Prefer over bash for program state, breakpoints, stepping, or thread inspection.\nOnly one active session at a time. `program` is a target path, not a shell command.\nDirectories need a directory-capable adapter (e.g. `dlv`).\n\n### Schema\n```ts\ntype Args = {\n  action: \"launch\" | \"attach\" | \"set_breakpoint\" | \"remove_breakpoint\" | \"set_instruction_breakpoint\" | \"remove_instruction_breakpoint\" | \"data_breakpoint_info\" | \"set_data_breakpoint\" | \"remove_data_breakpoint\" | \"continue\" | \"step_over\" | \"step_in\" | \"step_out\" | \"pause\" | \"evaluate\" | \"stack_trace\" | \"threads\" | \"scopes\" | \"variables\" | \"disassemble\" | \"read_memory\" | \"write_memory\" | \"modules\" | \"loaded_sources\" | \"custom_request\" | \"output\" | \"terminate\" | \"sessions\";\n  /** debug target path; Delve accepts Go package directories */\n  program?: string;\n  /** program arguments */\n  args?: string[];\n  /** configured adapter id (gdb, lldb-dap, debugpy, dlv, rdbg, or dap.json entry) */\n  adapter?: string;\n  cwd?: string;\n  /** source file */\n  file?: string;\n  /** source line */\n  line?: number;\n  /** function name */\n  function?: string;\n  /** variable or data name */\n  name?: string;\n  /** breakpoint condition */\n  condition?: string;\n  hit_condition?: string;\n  /** expression to evaluate */\n  expression?: string;\n  /** evaluate context: watch | repl | hover | variables | clipboard */\n  context?: string;\n  frame_id?: number;\n  /** scope variables reference */\n  scope_id?: number;\n  /** variable reference */\n  variable_ref?: number;\n  /** process id for attach */\n  pid?: number;\n  /** remote attach port */\n  port?: number;\n  /** remote attach host */\n  host?: string;\n  /** max stack frames */\n  levels?: number;\n  /** memory reference or address */\n  memory_reference?: string;\n  instruction_reference?: string;\n  instruction_count?: number;\n  instruction_offset?: number;\n  /** bytes to read */\n  count?: number;\n  /** base64 memory payload */\n  data?: string;\n  /** data breakpoint id */\n  data_id?: string;\n  access_type?: \"read\" | \"write\" | \"readWrite\";\n  /** custom dap request command */\n  command?: string;\n  /** custom request arguments */\n  arguments?: Record<string, unknown>;\n  offset?: number;\n  resolve_symbols?: boolean;\n  allow_partial?: boolean;\n  start_module?: number;\n  module_count?: number;\n  /** per-request timeout seconds */\n  timeout?: number;\n};\n```\nExecute by writing JSON to xd://debug.\n\n## Additional devices (docs on demand)\n- xd://spt_checkpoint — After the commune skill has saved this live endpoint's continuity drop, compact native OMP context and wake the same endpoint.\n\nRead xd://<tool> for full docs + JSON schema before first use.\n§ Tool Policy\n# General\nUse tools when they improve correctness, completeness, or grounding.\n- SHOULD resolve prerequisites first; NEVER accept first plausible answer when another call reduces uncertainty; retry empty/partial/suspiciously narrow lookup differently.\n- SHOULD parallelize independent calls.\n- User says `parallel` or `parallelize` → MUST use `task` subagents; parallel tool calls insufficient.\n\n# Tool I/O\n- Prefer relative `path`-like fields.\n- Most tools take `i`: capitalized 2–6-word present-participle intent (e.g. \"Reading model role settings\").\n# Specialized Tools\nMUST use specialized tool over shell equivalent:\n- File/directory reads → `read`; directory path lists entries.\n- Surgical edits → `edit`.\n- Create/overwrite → `write`.\n\n- Regex search/target location → `grep`, not shell `grep`, `rg`, `awk`.\n- Structure mapping/globbing → `glob`, not `ls **/*.ext` or `fd`.\n- `bash`: real binaries/short fact pipelines only; commands shadowing specialized tools blocked.\n- Bash litmus: one external-CLI call/short pipeline returning count, frequency, set difference, checksum. For merely moving, paging, trimming fetchable bytes: tool.\n\n<critical>\n`write xd://report_issue`: automated QA. Any tool output inconsistent with described behavior for parameters → write plain `<tool>: <concise description>` to `xd://report_issue`. False positives fine.\n</critical>\n\n# Exploration\nNEVER open files hoping. AVOID unneeded files/sections.\n- Use `read` offset/limit, not whole-file reads.\n\n# AST\nSHOULD use syntax-aware tools before text hacks:\n\n- Codemods → `ast_edit`.\n\n# Delegation\nInline first. Fan out only when 2+ independent slices each cost more than a handful of your own calls, or the read set would flood context; decide after your own first `grep`/`read`, never before it.\n- NEVER open with a scout. Scope with `grep`/`read`/`glob` yourself; a scout is for a genuinely unmapped subsystem after inline scoping stalls.\n- NEVER delegate one slice. One subagent for one job, a slice you already have open, cleanup (comment trims, changelog lines, formatting, sub-30-line edits), or a direct question: do it yourself.\n- NEVER babysit. Spawn → keep working → read the result. Steering a lone agent through `hub` send/wait costs more than the work.\n## Delegation gates\n- **Own decomposition.** Before spawning: map request, independent slices, cross-slice formats/schemas/interfaces. Only user-enumerated 2+ self-contained runnable slices dispatch directly. NEVER outsource top-level plan; generic \"plan\"/\"design\" agent starts blank, knows less, adds round-trip/no parallelism. Slice-local design and requested competing plans/reviews allowed.\n- **Real concurrency.** Fan exactly to genuine decomposition, one `tasks[]` array. NEVER serialize concurrent slices, invent padding, or spawn one then idle.\n- **User intent.** Subagents lack conversation; retain interpretation/taste; each assignment gets all slice requirements.\n- **Cap:** At most 32 subagents concurrently; excess queues. `tasks[]` batch > 32 delays results: stay within cap.\n- **Dependencies only.** A before B only if B strictly needs A; shared prerequisite inline, then fan out. “Parallelize” = parallel execution of independent slices, not agents routing sequential work. Small missing piece: run parallel; B asks A via `hub`!\n\n§ Workflow\n# 1. Scope\n- Read relevant skills first.\n- Multi-file work: plan before files.\n\n# 2. Research Before Editing\n- Read sections, not snippets. MUST reuse existing patterns; second convention beside existing is PROHIBITED.\n\n- Tool failure/file change since read → re-read before acting.\n\n# 3. Decompose\n\n# 4. Implement\n- Fix source; NEVER suppress symptom/special-case input unless asked.\n- Clean cutover: migrate every caller; remove obsolete code/comments/aliases/re-exports/deprecated paths.\n- Prefer existing-file updates over new files. Review as user.\n- NEVER run destructive git commands/delete unrelated code you didn't write; code the cutover obsoletes is in scope.\n\n# 5. Verify\n- NEVER yield non-trivial work without deliverable proof:\n  - **Experiment/investigation** → run; output is proof; no tests.\n  - **UI change** → verify against the actual surface:\n    - **Web UI** → use `browser.open` to get a tab handle, its direct helpers for common actions, `tab.run` for custom JavaScript, and `tab.close` when done; visual confirmation is proof; no tests unless existing suite really breaks.\n    - **TUI/CLI** → launch the actual program and verify terminal interaction, output, or state.\n    - No suitable runtime capability for the changed surface → verify with a throwaway script or smoke test; explicitly report when visual verification cannot be performed.\n  - **Bug fix** → reproduce, fix, confirm reproduction no longer triggers. SHOULD keep the reproduction as a regression test: fails pre-fix, passes post-fix; impractical → smoke test, report it.\n  - **Permanent feature/API change** → fix existing tests the changed contract breaks; prove new behavior with a throwaway script. New test ONLY for a genuinely uncertain edge case, or on user request.\n- Smoke test: run thing, not test file; launch, exercise changed path, observe result.\n- Tests: permanent load, not proof of work. A test earns its place ONLY where a plausible bug would fail it.\n  - Each MUST defend observable contract/fail on plausible bug.\n  - Test behavior, boundaries, invariants, transitions, precedence, real errors—not plumbing, source text, incidental defaults.\n  - Match conventions; deterministic, isolated, full-suite-safe.\n  - NEVER write a test so the change \"has tests\" → throwaway script.\n  - NEVER assert implementation: wiring, field copies, defaults, forwarding, mock echoes, source text → assert what a consumer observes.\n  - NEVER pad: same-path parameter rows, tautologies, bare not-throw, non-empty/length-grew checks.\n  - Worth keeping: behavior, boundaries, invariants, transitions, precedence, real errors. Match conventions; deterministic, isolated, full-suite-safe.\n  - Existing test failing this bar (pins wording, implementation, incidental behavior) → MUST delete; NEVER re-pin it to the new text. In scope regardless of author.\n\n# 6. Cleanup\nLast phase; REQUIRED after smoke test proves work; NEVER pre-plan/pre-allocate cleanup todos.\n- Permanent feature/bug fix → docs, changelog, scaffold + throwaway-script removal; tests only per Verify.\n- Experiment/one-off investigation → no cleanup tests/docs.\n\n§ Delivery\n<contract>\nInviolable.\n- NEVER yield before complete deliverable; phase boundary/todo flip/sub-step never yields: same turn.\n- NEVER fabricate output; code/tool/test/doc/source claims MUST be grounded.\n- NEVER substitute easier/familiar problem: don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—or solve symptom—suppress warning/exception, special-case input—unless asked. Real ask only.\n- NEVER ask for tool/repo/file-provided information; NEVER punt half-solved work.\n- Default clean cutover: migrate every caller; no shims, aliases, deprecated paths.\n</contract>\n\n<completeness>\n- “Done”: specified end-to-end behavior plus every named acceptance criterion; not compiling scaffold, narrowed test, plausible subset.\n- Reduce scope only with explicit user approval in this conversation; NEVER silently shrink.\n- NEVER deliver unfinished work: stubs, placeholders, mocks, no-ops, fake fallbacks, `TODO: implement`, misleading “scaffold”/“MVP”/“v1”/“foundation”/“follow-up”. Unavailable real-implementation info → state missing prerequisite; finish all reachable work.\n</completeness>\n\n<evidence-and-output>\n- Format MUST match ask; prose brief; evidence, verification, blocking details complete.\n- Code/tool/test/doc/source claims MUST be grounded; unobserved claims `[INFERENCE]`.\n- Verification claims exactly match exercised work.\n</evidence-and-output>\n\n<yielding>\nBefore yielding: all affected callsites/tests/docs updated or intentionally unchanged; output/evidence requirements satisfied.\nBefore blocked: ensure info unreachable via tools/context; one failed check ≠ blocked. Finish reachable work; state exactly missing and tried.\n</yielding>\n\n§ Critical\n<critical>\n- NEVER yield while actionable work remains; phase boundary/todo flip/sub-step never stops: same turn.\n- NEVER narrate/consider session limits, token/tool budgets, effort estimates, or possible completion; start unbounded: execute/delegate.\n- NEVER re-audit applied edit or routinely run git subcommands for validation. Tool results are verification.\n</critical>\n\n§ Role\nWorker agent: delegated tasks.\n\nTools: FULL access (edit, write, bash, grep, read, etc.); MUST use as needed to complete task.\nMUST hyperfocus assigned task; NEVER deviate.\n\n<directives>\n- MUST finish assigned work only; return minimum useful result; do not repeat filesystem writes.\n- SHOULD edit files, run commands, create files when task requires.\n- MUST concise; NEVER filler, repetition, tool transcripts. User cannot see you; result: notes for yourself.\n- SHOULD prefer narrow lookups (`grep`/`glob`), then read needed ranges only; ignore beyond current scope.\n- AVOID full-file reads unless necessary.\n- SHOULD prefer editing existing files over creating new files.\n- NEVER create documentation files (`*.md`) unless explicitly requested.\n- MUST follow assignment and instructions.\n- `task` delegation: select most specific `agent` type per spawn; general-purpose worker only if no listed specialist fits.\n</directives>\n\n§ Context\n# Goal\nDraft releases#308 serving-registry lost-update repair, PREP-ONLY, in C:/Users/decid/Documents/projects/spt-core/.worktrees/308-registry-process-lock (branch fix/308-registry-process-lock; base 7890ead39bb7f14ed44aaae44b0951f098ffe9ac).\n# Constraints\nEdit ONLY this worktree and your owned files. NO cargo of any kind, builds, tests, check, formatters, linters, rust-analyzer/LSP startup, subprocess test/repro execution, commits, pushes, or other producers. Window7 is owned elsewhere. Skip ALL validation, including docs generation/trace CLI; Main handles permitted checks later. No new Markdown files. Do not touch asm/304-v3. Read relevant skills before working. This is drafting, NEVER claim tests passed.\n# Contract\nMain owns runtime source (spt-store/serving.rs; spt-daemon/servehost.rs,livehost.rs), existing live-registry E2E mutations, and traceable-reqs.toml. NEW requirement already declared: REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE, stages doc/impl/unit/int. Main implements public spt_store::serving::lock_registry_at(registry_path: &Path) -> std::io::Result<std::fs::File>. A fresh File opened on stable sibling obtained by appending '.lock' to the FULL registry path (canonical serve/registry.lock), fs2 exclusive lock; returned file MUST remain alive across fresh load -> mutation/snapshot effects -> explicit save. It unlocks on drop/process death; never removes sentinel, never auto-saves. Non-reentrant, acquire before load, not just save. Both broker and brain use this. Existing fs2 dependency in spt-store. HTTP remains lock-free atomic snapshot reader; schema/CLI/TTL unchanged. Old broker with new brain does NOT fully activate protection. Main removes obsolete thread-only reap regression, retains concurrent apply_at registration unit coverage.\n§ Coop\nYou are operating on a piece of work assigned to you by the main agent.\n\n# Validation\nProject-wide validation is the main agent's job, run once after all subagents land. NEVER run formatters, linters, or project-wide builds/test suites unless your assignment explicitly instructs it — siblings edit concurrently; mid-flight validation blocks on their half-finished changes and reports phantom failures. Scoped proof of your own change (single test file, targeted repro, smoke run) is fine.\n# Peers\nYou can reach other live agents via the `hub` tool. Your id is `RegistryContractDocs`. Currently visible peers:\n- `RegistryProcessProof` — task (sub, running)\n- `Main` — main (main, running)\n12 parked peer(s) omitted.\n\nUse `hub` messaging only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows live (running+idle) peers and a parked count, never parked names or task labels. `hub` op:\"list\" refreshes the live view; pass status:\"parked\" to inspect parked history.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide. Idle peers are not gone: messaging them wakes them.\n- Follow-up: answer a peer's question with a short reply (set `replyTo`); use `await` only when you genuinely cannot proceed without the answer.\n- Parked history: omitted from this roster. `hub` op:\"list\" status:\"parked\" lists ids; `send` to a known parked id revives it. `history://<id>` and `agent://<id>` stay readable.\n\n§ Completion\nNo TODO tracking, no progress updates. Execute; report results with `yield`.\n\nWhile work remains, you MUST continue with another tool call — investigate, edit, run, verify. Save narrative for a terminal `yield` unless you intentionally record an incremental section.\n\nYield protocol:\n- Omit `type` for the normal single terminal structured result in `data`.\n- Use non-empty `type: string[]` for incremental, non-terminal sections; calls accumulate by section.\n- Use `type: string` for a terminal result; if data is omitted, your last assistant turn becomes the raw final result.\n\nThis is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `data`.\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `{ error }` describing what you tried and the exact blocker.\nYou NEVER give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.\n\nYou MUST keep going until this ticket is closed. This matters.\n\nPROJECT\n\n<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- CPU: AMD Ryzen 7 7800X3D 8-Core Processor\n- GPU: AMD Radeon(TM) Graphics\n- Model: openai-codex/gpt-6-astra\n</workstation>\n<critical>\n- Each response MUST advance the task; completion only stopping condition.\n- MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- Before yielding, MUST verify significant behavioral changes: run the specific test, command, or scenario covering the change.\n</critical>","task":"Complete assignment thoroughly:\n\n# Target\nOwn existing docs/KNOWN-HAZARDS.md, docs/STORAGE.md, docs-site/src/serving/overview.md, docs-site/src/serving/attachments.md only where needed. Do not create new docs. Do NOT edit traceable-reqs.toml, source, test files, changelog, or other agent-owned files.\n# Change\nRead relevant existing sections and update the documented serving registry writer contract. Document broker and brain as separate writer processes, stable serve/registry.lock sentinel, full read-modify-write guard, atomic publish versus mutual exclusion distinction, no sentinel unlink/registry-inode lock, explicit no-op save behavior, and mixed-version activation requiring BOTH writer implementations. Add a concise hazard section tagged doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE and cross-reference relevant existing WEB requirements naturally. Preserve reaper unlink-before-save and user-source protection semantics; no new promised retry/timeouts. Source loss is proven; exact original schedule inferred; cohort sample was 1/10 serial consumer red, not a probability floor, and no fixed-code validation has run. Avoid spreading the same paragraph across every doc; choose authoritative placement with concise links elsewhere.\n# Acceptance\nReturn edited files and intended contract changes; explicitly mark PREP-ONLY/no validation. Skip formatters, linters, docs generators, trace check and all tests. No new Markdown files and no claim fixed runtime has been measured.","tools":["read","bash","edit","eval","glob","grep","task","hub","web_search","write","yield","ast_edit","debug","spt_checkpoint"],"agent":"task","resolvedModel":"openai-codex/gpt-6-astra","readOnly":false,"spawns":"*"}
{"type":"message","id":"fe9ecdfd","parentId":"c6155564","timestamp":"2026-09-14T16:15:05.822Z","message":{"role":"user","content":[{"type":"text","text":"Complete assignment thoroughly:\n\n# Target\nOwn existing docs/KNOWN-HAZARDS.md, docs/STORAGE.md, docs-site/src/serving/overview.md, docs-site/src/serving/attachments.md only where needed. Do not create new docs. Do NOT edit traceable-reqs.toml, source, test files, changelog, or other agent-owned files.\n# Change\nRead relevant existing sections and update the documented serving registry writer contract. Document broker and brain as separate writer processes, stable serve/registry.lock sentinel, full read-modify-write guard, atomic publish versus mutual exclusion distinction, no sentinel unlink/registry-inode lock, explicit no-op save behavior, and mixed-version activation requiring BOTH writer implementations. Add a concise hazard section tagged doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE and cross-reference relevant existing WEB requirements naturally. Preserve reaper unlink-before-save and user-source protection semantics; no new promised retry/timeouts. Source loss is proven; exact original schedule inferred; cohort sample was 1/10 serial consumer red, not a probability floor, and no fixed-code validation has run. Avoid spreading the same paragraph across every doc; choose authoritative placement with concise links elsewhere.\n# Acceptance\nReturn edited files and intended contract changes; explicitly mark PREP-ONLY/no validation. Skip formatters, linters, docs generators, trace check and all tests. No new Markdown files and no claim fixed runtime has been measured."}],"attribution":"agent","timestamp":1789402505785}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yHNACVtQl25jwKRgPNByrT5N|fc_080dc0728d55fa10016aa81d8d790c87d297a761a5db9697fb","toolName":"read","startedAt":"2026-09-14T16:15:10.142Z","args":{"path":"skill://diagnose"},"intent":"Reading hazard diagnosis skill"},"id":"81019661","parentId":"fe9ecdfd","timestamp":"2026-09-14T16:15:10.142Z"}
{"type":"message","id":"1004d749","parentId":"81019661","timestamp":"2026-09-14T16:15:10.142Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_yHNACVtQl25jwKRgPNByrT5N|fc_080dc0728d55fa10016aa81d8d790c87d297a761a5db9697fb","name":"read","arguments":{"i":"Reading hazard diagnosis skill","path":"skill://diagnose"},"intent":"Reading hazard diagnosis skill"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1887,"output":27,"cacheRead":15872,"cacheWrite":0,"totalTokens":17786,"cost":{"input":0.01887,"output":0.00135,"cacheRead":0.015872,"cacheWrite":0,"total":0.036092}},"stopReason":"toolUse","timestamp":1789402505822,"responseId":"resp_080dc0728d55fa10016aa81d8aeda487d2a68ed8def7b0b53b","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_080dc0728d55fa10016aa81d8d790c87d297a761a5db9697fb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hazard diagnosis skill\",\"path\":\"skill://diagnose\"}","call_id":"call_yHNACVtQl25jwKRgPNByrT5N","internal_chat_message_metadata_passthrough":{"create_time":1789402507.493122,"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":4289.463100001216,"ttft":4271.82310000062,"completedAt":1789402510141,"contextSnapshot":{"promptTokens":17759,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"credential_pin","id":"59dfa889","parentId":"1004d749","timestamp":"2026-09-14T16:15:10.142Z","provider":"openai-codex","hash":"1a3c10d7bc41d381cab1511db604bc2637dfcfc63796a890d8b5ee152859e1cb"}
{"type":"message","id":"3ccc9856","parentId":"59dfa889","timestamp":"2026-09-14T16:15:10.175Z","message":{"role":"toolResult","toolCallId":"call_yHNACVtQl25jwKRgPNByrT5N|fc_080dc0728d55fa10016aa81d8d790c87d297a761a5db9697fb","toolName":"read","content":[{"type":"text","text":"---\nname: diagnose\ndescription: 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.\n---\n\n# Diagnose\n\nA discipline for hard bugs. Skip phases only when explicitly justified.\n\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\n\n## Phase 1 — Build a feedback loop\n\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\n\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\n\n### Ways to construct one — try them in roughly this order\n\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\n2. **Curl / HTTP script** against a running dev server.\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\n\nBuild the right feedback loop, and the bug is 90% fixed.\n\n### Iterate on the loop itself\n\nTreat the loop as a product. Once you have _a_ loop, ask:\n\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\n\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\n\n### Non-deterministic bugs\n\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\n\n### When you genuinely cannot build a loop\n\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\n\nDo not proceed to Phase 2 until you have a loop you believe in.\n\n## Phase 2 — Reproduce\n\nRun the loop. Watch the bug appear.\n\nConfirm:\n\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\n\nDo not proceed until you reproduce the bug.\n\n## Phase 3 — Hypothesise\n\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\n\nEach hypothesis must be **falsifiable**: state the prediction it makes.\n\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\n\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\n\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\n\n## Phase 4 — Instrument\n\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\n\nTool preference:\n\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\n3. Never \"log everything and grep\".\n\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\n\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\n\n## Phase 5 — Fix + regression test\n\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\n\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\n\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\n\nIf a correct seam exists:\n\n1. Turn the minimised repro into a failing test at that seam.\n2. Watch it fail.\n3. Apply the fix.\n4. Watch it pass.\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\n\n## Phase 6 — Cleanup + post-mortem\n\nRequired before declaring done:\n\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\n- [ ] Regression test passes (or absence of seam is documented)\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\n\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started."}],"details":{"resolvedPath":"C:\\Users\\decid\\.agents\\skills\\diagnose\\SKILL.md","contentType":"text/markdown","totalLines":117,"displayContent":{"text":"---\r\nname: diagnose\r\ndescription: 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.\r\n---\r\n\r\n# Diagnose\r\n\r\nA discipline for hard bugs. Skip phases only when explicitly justified.\r\n\r\nWhen exploring the codebase, use the project's domain glossary to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.\r\n\r\n## Phase 1 — Build a feedback loop\r\n\r\n**This is the skill.** Everything else is mechanical. If you have a fast, deterministic, agent-runnable pass/fail signal for the bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume that signal. If you don't have one, no amount of staring at code will save you.\r\n\r\nSpend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.**\r\n\r\n### Ways to construct one — try them in roughly this order\r\n\r\n1. **Failing test** at whatever seam reaches the bug — unit, integration, e2e.\r\n2. **Curl / HTTP script** against a running dev server.\r\n3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot.\r\n4. **Headless browser script** (Playwright / Puppeteer) — drives the UI, asserts on DOM/console/network.\r\n5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation.\r\n6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call.\r\n7. **Property / fuzz loop.** If the bug is \"sometimes wrong output\", run 1000 random inputs and look for the failure mode.\r\n8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate \"boot at state X, check, repeat\" so you can `git bisect run` it.\r\n9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs.\r\n10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you.\r\n\r\nBuild the right feedback loop, and the bug is 90% fixed.\r\n\r\n### Iterate on the loop itself\r\n\r\nTreat the loop as a product. Once you have _a_ loop, ask:\r\n\r\n- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.)\r\n- Can I make the signal sharper? (Assert on the specific symptom, not \"didn't crash\".)\r\n- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.)\r\n\r\nA 30-second flaky loop is barely better than no loop. A 2-second deterministic loop is a debugging superpower.\r\n\r\n### Non-deterministic bugs\r\n\r\nThe goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it's debuggable.\r\n\r\n### When you genuinely cannot build a loop\r\n\r\nStop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop.\r\n\r\nDo not proceed to Phase 2 until you have a loop you believe in.\r\n\r\n## Phase 2 — Reproduce\r\n\r\nRun the loop. Watch the bug appear.\r\n\r\nConfirm:\r\n\r\n- [ ] The loop produces the failure mode the **user** described — not a different failure that happens to be nearby. Wrong bug = wrong fix.\r\n- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against).\r\n- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it.\r\n\r\nDo not proceed until you reproduce the bug.\r\n\r\n## Phase 3 — Hypothesise\r\n\r\nGenerate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea.\r\n\r\nEach hypothesis must be **falsifiable**: state the prediction it makes.\r\n\r\n> Format: \"If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse.\"\r\n\r\nIf you cannot state the prediction, the hypothesis is a vibe — discard or sharpen it.\r\n\r\n**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly (\"we just deployed a change to #3\"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it — proceed with your ranking if the user is AFK.\r\n\r\n## Phase 4 — Instrument\r\n\r\nEach probe must map to a specific prediction from Phase 3. **Change one variable at a time.**\r\n\r\nTool preference:\r\n\r\n1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs.\r\n2. **Targeted logs** at the boundaries that distinguish hypotheses.\r\n3. Never \"log everything and grep\".\r\n\r\n**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die.\r\n\r\n**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second.\r\n\r\n## Phase 5 — Fix + regression test\r\n\r\nWrite the regression test **before the fix** — but only if there is a **correct seam** for it.\r\n\r\nA correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence.\r\n\r\n**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase.\r\n\r\nIf a correct seam exists:\r\n\r\n1. Turn the minimised repro into a failing test at that seam.\r\n2. Watch it fail.\r\n3. Apply the fix.\r\n4. Watch it pass.\r\n5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.\r\n\r\n## Phase 6 — Cleanup + post-mortem\r\n\r\nRequired before declaring done:\r\n\r\n- [ ] Original repro no longer reproduces (re-run the Phase 1 loop)\r\n- [ ] Regression test passes (or absence of seam is documented)\r\n- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix)\r\n- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location)\r\n- [ ] The hypothesis that turned out correct is stated in the commit / PR message — so the next debugger learns\r\n\r\n**Then ask: what would have prevented this bug?** If the answer involves architectural change (no good test seam, tangled callers, hidden coupling) hand off to the `/improve-codebase-architecture` skill with the specifics. Make the recommendation **after** the fix is in, not before — you have more information now than when you started.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117]},"meta":{"source":{"type":"internal","value":"skill://diagnose"}}},"isError":false,"timestamp":1789402510174}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wvjM7UK3HesIUxJIEtiSy4ry|fc_080dc0728d55fa10016aa81d96664c87d29a57aab775a7edd9","toolName":"read","startedAt":"2026-09-14T16:15:18.486Z","args":{"path":"skill://traceable-reqs"},"intent":"Reading trace tag conventions"},"id":"5a424a74","parentId":"3ccc9856","timestamp":"2026-09-14T16:15:18.486Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Uf1QU3PSwgyupMlZXoO5yxwb|fc_080dc0728d55fa10016aa81d96665887d2b3ae2ea824db37b8","toolName":"grep","startedAt":"2026-09-14T16:15:18.487Z","args":{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md;.worktrees/308-re…"},"intent":"Locating registry documentation sections"},"id":"9466bcd4","parentId":"5a424a74","timestamp":"2026-09-14T16:15:18.487Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cpUyCadOo0YTCkcOXMZSaSYf|fc_080dc0728d55fa10016aa81d96666087d2afdd6d1f2c3105df","toolName":"glob","startedAt":"2026-09-14T16:15:18.487Z","args":{"path":".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CLAUDE.md"},"intent":"Locating local authoring guidance"},"id":"c1aec19e","parentId":"9466bcd4","timestamp":"2026-09-14T16:15:18.487Z"}
{"type":"message","id":"c9fa1e31","parentId":"c1aec19e","timestamp":"2026-09-14T16:15:18.487Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_wvjM7UK3HesIUxJIEtiSy4ry|fc_080dc0728d55fa10016aa81d96664c87d29a57aab775a7edd9","name":"read","arguments":{"i":"Reading trace tag conventions","path":"skill://traceable-reqs"},"intent":"Reading trace tag conventions"},{"type":"toolCall","id":"call_Uf1QU3PSwgyupMlZXoO5yxwb|fc_080dc0728d55fa10016aa81d96665887d2b3ae2ea824db37b8","name":"grep","arguments":{"i":"Locating registry documentation sections","pattern":"registry|REQ-WEB|reap|atomic|lost.update","path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md;.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md","case":false,"gitignore":false,"skip":null},"intent":"Locating registry documentation sections"},{"type":"toolCall","id":"call_cpUyCadOo0YTCkcOXMZSaSYf|fc_080dc0728d55fa10016aa81d96666087d2afdd6d1f2c3105df","name":"glob","arguments":{"i":"Locating local authoring guidance","path":".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CLAUDE.md","hidden":true,"gitignore":false,"limit":100},"intent":"Locating local authoring guidance"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1775,"output":225,"cacheRead":17664,"cacheWrite":0,"totalTokens":19664,"cost":{"input":0.017750000000000002,"output":0.011250000000000001,"cacheRead":0.017664,"cacheWrite":0,"total":0.046664000000000004}},"stopReason":"toolUse","timestamp":1789402510200,"responseId":"resp_080dc0728d55fa10016aa81d8e83fc87d298fcebb3fe91c4b6","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"fc_080dc0728d55fa10016aa81d96664c87d29a57aab775a7edd9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading trace tag conventions\",\"path\":\"skill://traceable-reqs\"}","call_id":"call_wvjM7UK3HesIUxJIEtiSy4ry","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81d96665887d2b3ae2ea824db37b8","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating registry documentation sections\",\"pattern\":\"registry|REQ-WEB|reap|atomic|lost.update\",\"path\":\".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md;.worktrees/308-registry-process-lock/docs/STORAGE.md;.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md;.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_Uf1QU3PSwgyupMlZXoO5yxwb","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81d96666087d2afdd6d1f2c3105df","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating local authoring guidance\",\"path\":\".worktrees/308-registry-process-lock/**/AGENTS.md;.worktrees/308-registry-process-lock/**/CLAUDE.md\",\"hidden\":true,\"gitignore\":false,\"limit\":100}","call_id":"call_cpUyCadOo0YTCkcOXMZSaSYf","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"glob","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":8284.304499998689,"ttft":8221.282499998808,"completedAt":1789402518485,"contextSnapshot":{"promptTokens":19439,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"message","id":"a90d4f4b","parentId":"c9fa1e31","timestamp":"2026-09-14T16:15:18.492Z","message":{"role":"toolResult","toolCallId":"call_wvjM7UK3HesIUxJIEtiSy4ry|fc_080dc0728d55fa10016aa81d96664c87d29a57aab775a7edd9","toolName":"read","content":[{"type":"text","text":"---\nname: traceable-reqs\ndescription: \"Install or update the traceable-reqs CLI and apply it correctly in host repos: author manifests, place tags near evidence, audit scan roots, interpret findings, and answer coverage questions through the CLI.\"\nallowed-tools: Bash, PowerShell, Read\n---\n\n# traceable-reqs\n\nUse this skill for both installation and day-to-day use of `traceable-reqs`.\nThe CLI reads a required `traceable-reqs.toml`, scans configured roots for\n`[stage->REQ-ID]` tags, and reports whether manifest-declared requirements have\nthe required evidence stages.\n\n## Applying traceable-reqs to a repo\n\nStart with the manifest, then place tags close to the evidence they prove.\nDo not satisfy coverage by putting tags at the top of a file; that is noisy and\nusually wrong. A tag belongs on or immediately above the function, test, config\nentry, doc section, or workflow step that actually satisfies that stage.\n\nMinimum manifest:\n\n```toml\n[scan]\nroots = [\"src\", \"tests\", \"docs\"]\n\n[policy]\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-LOGIN\"\ntitle = \"User can sign in\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n```\n\nAudit `[scan].roots` against the real project tree before trusting coverage.\nIf implementation lives in `.github/workflows`, `scripts`, `cmake`, or another\ndirectory, include it or the evidence will silently disappear from the trace.\n\nRequirement IDs must match `REQ-[A-Z0-9-]+`, with hyphen-separated uppercase\nsegments. Prefer title-derived slugs such as `REQ-LOGIN-LOCKOUT` over bare\nnumeric counters. IDs like `FR-001`, `NFR-002`, or `CON-010` are not valid unless\nthey are written as `REQ-FR-001`, `REQ-NFR-002`, or `REQ-CON-010`.\n\n## Stages\n\nBuilt-in stages are exactly:\n\n- `doc`: prose or design documentation.\n- `impl`: production implementation.\n- `unit`: unit tests.\n- `int`: integration, end-to-end, workflow, or system-level tests.\n\nDomain words such as HIL, e2e, smoke, QC, or security are not built-in stages.\nMap them to the closest built-in stage, usually `int`, or declare a custom stage\nin the manifest before using it:\n\n```toml\n[stages.qc]\ndescription = \"QC engineer signoff\"\n```\n\nA tag with an undeclared stage, such as `[hil->REQ-CAMERA]`, is a `parse_error`.\n\n## Tag examples\n\n```rust\n// [impl->REQ-LOGIN]\nfn authenticate(...) { ... }\n```\n\n```python\n# [unit->REQ-LOGIN]\ndef test_login_accepts_valid_password(): ...\n```\n\n```markdown\n<!-- [doc->REQ-LOGIN] -->\n# Login\n```\n\nAny bracketed text that looks like a tag candidate must be exact. A placeholder\nlike `[stage->REQ-ID]` in scanned Markdown can become a `parse_error`; escape it,\nremove the arrow, or keep examples outside scanned roots when needed.\n\n## Command choice\n\nUse the CLI for coverage questions, not grep. Grep does not know scan roots,\ncomment-only rules, custom stages, docstrings, or parse errors.\n\n- `traceable-reqs check --json`: full coverage and findings for CI or audit.\n- `traceable-reqs list --json`: all requirements without treating findings as a failing command.\n- `traceable-reqs list --missing impl`: requirements missing a required stage.\n- `traceable-reqs list --type FR`: requirements of one declared type.\n- `traceable-reqs trace REQ-LOGIN`: evidence and findings for one requirement.\n- `traceable-reqs review`: agent-facing prompt to inspect tag placement quality.\n- `traceable-reqs lint`: agent-facing prompt plus deterministic title-quality findings.\n\n`check` returning exit code 1 means findings were produced; it is not a pipe or\nruntime failure by itself. Read stdout or use `--json` to inspect the findings.\nOperational failures such as bad arguments, invalid manifests, or unreadable\nfiles report diagnostics on stderr.\n\n## Common findings\n\n- `missing_stage`: the requirement is declared but lacks evidence for a required stage. First check tag placement and scan roots.\n- `undeclared_id`: a tag references an ID not in `traceable-reqs.toml`. First check for typos and invalid ID prefixes.\n- `parse_error`: a tag-shaped token is malformed, uses an undeclared stage, has spaces, or has an invalid requirement ID.\n- `manifest_error`: the manifest is missing, invalid TOML, invalid UTF-8, has duplicate IDs, bad stages, bad types, or invalid scan roots.\n- `scan_error`: a scanned file could not be read.\n\nOn Windows, avoid writing the manifest with a UTF-16 redirect. Use an editor,\nthe Write tool, or `Out-File -Encoding utf8NoBOM` so TOML parsing sees UTF-8.\n\nFor placeholder requirements that intentionally have no evidence yet, set\n`required_stages = []` so `check` does not report missing coverage until the\nrequirement is ready.\n\n## Installation\n\nInstalls the latest `traceable-reqs` binary from a GitHub Release into a per-user location, verifies it runs, and prints PATH advice. Does not mutate the user's shell rc files or environment automatically — PATH changes are user intent, so surface the line and let them act on it.\n\n## Defaults\n\n- Repo: `BigscreenVR/traceable-reqs`\n- Version: latest release tag\n- Install dir: `~/.local/bin` on Linux/macOS, `%LOCALAPPDATA%\\Programs\\traceable-reqs` on Windows\n\nOverride per-call via env vars: `TRACEABLE_REQS_REPO`, `TRACEABLE_REQS_VERSION`, `TRACEABLE_REQS_INSTALL_DIR`.\n\n## Procedure\n\n1. **Verify `gh`.** Run `gh auth status`. If `gh` is missing or returns non-zero, stop and tell the user to install it from https://cli.github.com/ then run `gh auth login`. `gh auth login` is interactive — do not attempt to run it for them.\n\n2. **Detect platform and target.**\n   - macOS / Linux: use Bash. Read `uname -s` and `uname -m`, then map:\n     - `Linux` + `x86_64` → target `linux-x86_64`\n     - `Darwin` + `arm64` → target `macos-arm64`\n   - Native Windows: use PowerShell. Target is `windows-x86_64.exe`.\n   - Anything else (Linux arm64, Intel Mac, MINGW/Cygwin) is not in the release matrix. Stop and tell the user to build from source: `cargo install --git <repo-url>`.\n\n3. **Resolve version.** If `TRACEABLE_REQS_VERSION` is set, use it. Otherwise `gh release view --repo <repo> --json tagName -q .tagName`.\n\n4. **Download.** Asset name is `traceable-reqs-<version>-<target>`. Make the install dir if needed, then:\n\n   ```\n   gh release download <version> --repo <repo> --pattern <asset> --output <install-dir>/<binary> --clobber\n   ```\n\n   Binary name is `traceable-reqs` on Unix, `traceable-reqs.exe` on Windows.\n\n5. **Make executable** (Unix only): `chmod +x <install-dir>/traceable-reqs`.\n\n6. **Verify.** Run `<install-dir>/<binary> --version`. If it errors, stop and report.\n\n7. **PATH advice.** Check if the install dir is on `PATH`. If yes, done. If no, print these for the user to copy — do not execute them:\n   - Bash / zsh: `export PATH=\"<install-dir>:$PATH\"` (and add to `~/.bashrc` or `~/.zshrc` for persistence).\n   - PowerShell, current session: `$env:PATH = \"<install-dir>;\" + $env:PATH`\n   - PowerShell, persistent (open a new shell to take effect): `[Environment]::SetEnvironmentVariable('PATH', '<install-dir>;' + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')`\n\n## Failure modes\n\n- `gh` missing → install link, stop.\n- `gh` not authenticated → `gh auth login`, stop (interactive — user does it).\n- Unsupported target → \"no prebuilt for `<target>`; build from source with `cargo install --git`\".\n- Asset not found → likely the release matrix did not finish; have the user check `gh release view <tag> --repo <repo>`."}],"details":{"resolvedPath":"C:\\Users\\decid\\.agents\\skills\\traceable-reqs\\SKILL.md","contentType":"text/markdown","totalLines":166,"displayContent":{"text":"---\r\nname: traceable-reqs\r\ndescription: \"Install or update the traceable-reqs CLI and apply it correctly in host repos: author manifests, place tags near evidence, audit scan roots, interpret findings, and answer coverage questions through the CLI.\"\nallowed-tools: Bash, PowerShell, Read\r\n---\r\n\r\n# traceable-reqs\r\n\r\nUse this skill for both installation and day-to-day use of `traceable-reqs`.\r\nThe CLI reads a required `traceable-reqs.toml`, scans configured roots for\r\n`[stage->REQ-ID]` tags, and reports whether manifest-declared requirements have\r\nthe required evidence stages.\r\n\r\n## Applying traceable-reqs to a repo\r\n\r\nStart with the manifest, then place tags close to the evidence they prove.\r\nDo not satisfy coverage by putting tags at the top of a file; that is noisy and\r\nusually wrong. A tag belongs on or immediately above the function, test, config\r\nentry, doc section, or workflow step that actually satisfies that stage.\r\n\r\nMinimum manifest:\r\n\r\n```toml\r\n[scan]\r\nroots = [\"src\", \"tests\", \"docs\"]\r\n\r\n[policy]\r\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\r\n\r\n[[requirements]]\r\nid = \"REQ-LOGIN\"\r\ntitle = \"User can sign in\"\r\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\r\n```\r\n\r\nAudit `[scan].roots` against the real project tree before trusting coverage.\r\nIf implementation lives in `.github/workflows`, `scripts`, `cmake`, or another\r\ndirectory, include it or the evidence will silently disappear from the trace.\r\n\r\nRequirement IDs must match `REQ-[A-Z0-9-]+`, with hyphen-separated uppercase\r\nsegments. Prefer title-derived slugs such as `REQ-LOGIN-LOCKOUT` over bare\r\nnumeric counters. IDs like `FR-001`, `NFR-002`, or `CON-010` are not valid unless\r\nthey are written as `REQ-FR-001`, `REQ-NFR-002`, or `REQ-CON-010`.\r\n\r\n## Stages\r\n\r\nBuilt-in stages are exactly:\r\n\r\n- `doc`: prose or design documentation.\r\n- `impl`: production implementation.\r\n- `unit`: unit tests.\r\n- `int`: integration, end-to-end, workflow, or system-level tests.\r\n\r\nDomain words such as HIL, e2e, smoke, QC, or security are not built-in stages.\r\nMap them to the closest built-in stage, usually `int`, or declare a custom stage\r\nin the manifest before using it:\r\n\r\n```toml\r\n[stages.qc]\r\ndescription = \"QC engineer signoff\"\r\n```\r\n\r\nA tag with an undeclared stage, such as `[hil->REQ-CAMERA]`, is a `parse_error`.\r\n\r\n## Tag examples\r\n\r\n```rust\r\n// [impl->REQ-LOGIN]\r\nfn authenticate(...) { ... }\r\n```\r\n\r\n```python\r\n# [unit->REQ-LOGIN]\r\ndef test_login_accepts_valid_password(): ...\r\n```\r\n\r\n```markdown\r\n<!-- [doc->REQ-LOGIN] -->\r\n# Login\r\n```\r\n\r\nAny bracketed text that looks like a tag candidate must be exact. A placeholder\r\nlike `[stage->REQ-ID]` in scanned Markdown can become a `parse_error`; escape it,\r\nremove the arrow, or keep examples outside scanned roots when needed.\r\n\r\n## Command choice\r\n\r\nUse the CLI for coverage questions, not grep. Grep does not know scan roots,\r\ncomment-only rules, custom stages, docstrings, or parse errors.\r\n\r\n- `traceable-reqs check --json`: full coverage and findings for CI or audit.\r\n- `traceable-reqs list --json`: all requirements without treating findings as a failing command.\r\n- `traceable-reqs list --missing impl`: requirements missing a required stage.\r\n- `traceable-reqs list --type FR`: requirements of one declared type.\r\n- `traceable-reqs trace REQ-LOGIN`: evidence and findings for one requirement.\r\n- `traceable-reqs review`: agent-facing prompt to inspect tag placement quality.\r\n- `traceable-reqs lint`: agent-facing prompt plus deterministic title-quality findings.\r\n\r\n`check` returning exit code 1 means findings were produced; it is not a pipe or\r\nruntime failure by itself. Read stdout or use `--json` to inspect the findings.\r\nOperational failures such as bad arguments, invalid manifests, or unreadable\r\nfiles report diagnostics on stderr.\r\n\r\n## Common findings\r\n\r\n- `missing_stage`: the requirement is declared but lacks evidence for a required stage. First check tag placement and scan roots.\r\n- `undeclared_id`: a tag references an ID not in `traceable-reqs.toml`. First check for typos and invalid ID prefixes.\r\n- `parse_error`: a tag-shaped token is malformed, uses an undeclared stage, has spaces, or has an invalid requirement ID.\r\n- `manifest_error`: the manifest is missing, invalid TOML, invalid UTF-8, has duplicate IDs, bad stages, bad types, or invalid scan roots.\r\n- `scan_error`: a scanned file could not be read.\r\n\r\nOn Windows, avoid writing the manifest with a UTF-16 redirect. Use an editor,\r\nthe Write tool, or `Out-File -Encoding utf8NoBOM` so TOML parsing sees UTF-8.\r\n\r\nFor placeholder requirements that intentionally have no evidence yet, set\r\n`required_stages = []` so `check` does not report missing coverage until the\r\nrequirement is ready.\r\n\r\n## Installation\r\n\r\nInstalls the latest `traceable-reqs` binary from a GitHub Release into a per-user location, verifies it runs, and prints PATH advice. Does not mutate the user's shell rc files or environment automatically — PATH changes are user intent, so surface the line and let them act on it.\r\n\r\n## Defaults\r\n\r\n- Repo: `BigscreenVR/traceable-reqs`\r\n- Version: latest release tag\r\n- Install dir: `~/.local/bin` on Linux/macOS, `%LOCALAPPDATA%\\Programs\\traceable-reqs` on Windows\r\n\r\nOverride per-call via env vars: `TRACEABLE_REQS_REPO`, `TRACEABLE_REQS_VERSION`, `TRACEABLE_REQS_INSTALL_DIR`.\r\n\r\n## Procedure\r\n\r\n1. **Verify `gh`.** Run `gh auth status`. If `gh` is missing or returns non-zero, stop and tell the user to install it from https://cli.github.com/ then run `gh auth login`. `gh auth login` is interactive — do not attempt to run it for them.\r\n\r\n2. **Detect platform and target.**\r\n   - macOS / Linux: use Bash. Read `uname -s` and `uname -m`, then map:\r\n     - `Linux` + `x86_64` → target `linux-x86_64`\r\n     - `Darwin` + `arm64` → target `macos-arm64`\r\n   - Native Windows: use PowerShell. Target is `windows-x86_64.exe`.\r\n   - Anything else (Linux arm64, Intel Mac, MINGW/Cygwin) is not in the release matrix. Stop and tell the user to build from source: `cargo install --git <repo-url>`.\r\n\r\n3. **Resolve version.** If `TRACEABLE_REQS_VERSION` is set, use it. Otherwise `gh release view --repo <repo> --json tagName -q .tagName`.\r\n\r\n4. **Download.** Asset name is `traceable-reqs-<version>-<target>`. Make the install dir if needed, then:\r\n\r\n   ```\r\n   gh release download <version> --repo <repo> --pattern <asset> --output <install-dir>/<binary> --clobber\r\n   ```\r\n\r\n   Binary name is `traceable-reqs` on Unix, `traceable-reqs.exe` on Windows.\r\n\r\n5. **Make executable** (Unix only): `chmod +x <install-dir>/traceable-reqs`.\r\n\r\n6. **Verify.** Run `<install-dir>/<binary> --version`. If it errors, stop and report.\r\n\r\n7. **PATH advice.** Check if the install dir is on `PATH`. If yes, done. If no, print these for the user to copy — do not execute them:\r\n   - Bash / zsh: `export PATH=\"<install-dir>:$PATH\"` (and add to `~/.bashrc` or `~/.zshrc` for persistence).\r\n   - PowerShell, current session: `$env:PATH = \"<install-dir>;\" + $env:PATH`\r\n   - PowerShell, persistent (open a new shell to take effect): `[Environment]::SetEnvironmentVariable('PATH', '<install-dir>;' + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')`\r\n\r\n## Failure modes\r\n\r\n- `gh` missing → install link, stop.\r\n- `gh` not authenticated → `gh auth login`, stop (interactive — user does it).\r\n- Unsupported target → \"no prebuilt for `<target>`; build from source with `cargo install --git`\".\r\n- Asset not found → likely the release matrix did not finish; have the user check `gh release view <tag> --repo <repo>`.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166]},"meta":{"source":{"type":"internal","value":"skill://traceable-reqs"}}},"isError":false,"timestamp":1789402518492}}
{"type":"message","id":"3376630c","parentId":"a90d4f4b","timestamp":"2026-09-14T16:15:18.500Z","message":{"role":"toolResult","toolCallId":"call_cpUyCadOo0YTCkcOXMZSaSYf|fc_080dc0728d55fa10016aa81d96666087d2afdd6d1f2c3105df","toolName":"glob","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/\nCLAUDE.md\nAGENTS.md"}],"details":{"scopePath":".worktrees/308-registry-process-lock/**/AGENTS.md, .worktrees/308-registry-process-lock/**/CLAUDE.md","fileCount":2,"files":[".worktrees/308-registry-process-lock/CLAUDE.md",".worktrees/308-registry-process-lock/AGENTS.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789402518499}}
{"type":"message","id":"a4ca960d","parentId":"3376630c","timestamp":"2026-09-14T16:15:18.512Z","message":{"role":"toolResult","toolCallId":"call_Uf1QU3PSwgyupMlZXoO5yxwb|fc_080dc0728d55fa10016aa81d96665887d2b3ae2ea824db37b8","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/\n\n## docs/\n### KNOWN-HAZARDS.md#F7C6\n 18:- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n*19:- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n*20:- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n 21:- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n 22:\n 23:### 1.3 Stale `index.lock` wedge from prior git crash\n...\n 37:- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n*38:- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n 39:- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n 40:\n 41:---\n...\n 71:- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `...\n*72:- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n*73:- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n*74:- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n 75:\n 76:### 2.6 A shell instance's `online` is a recorded field with no death edge\n 77:- **Failure:** a shell perch's `status` has exactly two writers — the bind handshake (→`online`) and `close_shell` (→`offline`) — and `close_shell` runs on the **link-break** path alone. A binary that dies **abruptly** (force-kill, crash, OOM) breaks no link, so nothing ever writes the flip: the record says `online` forever while the pid it names is gone. Every consumer of the recorded field then inherits the lie — `shell list` reports online indefinitely; `relink` (the one command that recover...\n...\n 79:<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n*80:- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous bi...\n 81:- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.\n 82:- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.\n 83:- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it...\n...\n 132:<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->\n*133:- **Failure:** the obvious way to write a perch garbage collector is \"reap what is not alive\" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across...\n 134:- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false \"alive\" is one message taking the spool path. It is the wrong direction for DELETION, where a false \"not alive\" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.\n*135:- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary s...\n 136:- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.\n 137:- **Source:** measured by todlando on HFENDULEAM 2026-08-04 while answering doyle's step-1 predicate question on releases#109; ruled into the lane by doyle the same day.\n 138:\n...\n 155:\n*156:### 4.3 Registry stale-entry cleanup precedes lookup\n 157:- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n 158:- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*159:- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n*160:- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 161:\n 162:### 4.4 Deferred rows survive poll drain\n 163:- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n...\n 179:### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n*180:<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n 181:- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n 182:- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n*183:- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n 184:\n*185:### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\n*186:<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n*187:- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n 188:- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-...\n*189:- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n 190:\n 191:### 4.9 SQLite stores must create their parent dir — SQLite won't\n*192:<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n*193:- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the h...\n### STORAGE.md#D5CA\n 9:3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.\n*10:4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.\n 11:5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).\n 12:\n 13:## Layout\n...\n 35:        info.json                type=Shell, owner, adapter_name, status (online|offline), alias?\n*36:        shell.launch.json        atomic PID + native birth + launch wall-clock; pid 0 clears custody\n 37:        shell.pid                numeric compatibility mirror for older core readers\n 38:        spool.db                 command + durable text/file (sensory is REST-only, unspooled)\n 39:  history/                       node-local logs (mirrors perch structure)\n...\n 42:      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log\n*43:  registry/                      ── ephemeral ── local cache of the subnet registry\n 44:  index/                         ── derived, regenerable ── materialized indexes (ADR-0037)\n 45:    project-index.json           endpoint→project attribution (schema-versioned; daemon-written)\n 46:  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata\n...\n 49:    <adapter>/web/               core-owned adapter served root; output bytes survive deactivation\n*50:  serve/registry                 authoritative serving entries + persistent name ownership\n 51:```\n 52:\n 53:<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n...\n 106:\n*107:The scoped roster instrumentation separates those reads, registry database\n 108:open/tuning/schema/query work, and the presence-probe worklist and waits.\n 109:Enable it through `SPT_PUMP_TRACE=1` for the investigated command and retain\n 110:the `ROSTER_WAIT` stderr records alongside complete stdout and command exit\n...\n 120:\n*121:### Registry snapshots + the resource-advertisement blurb (D9)\n 122:\n 123:<!-- [doc->REQ-INST-14] -->\n 124:\n*125:The daemon's live per-subnet registries ([`RegistryHost`]) mirror to **atomic\n*126:JSON snapshots at `identity/registry/<subnet>.json`** for out-of-process\n 127:readers (`spt send`'s WAN resolution, `spt resources list`) — stale-tolerant\n 128:by contract (KNOWN-HAZARDS 4.3), regenerable from re-advertisement.\n 129:\n...\n 132:**a field on the row, not a separate store** — blurb updates ride the same\n*133:epoch lease and `RegistryUpdate` replication as any instance update.\n 134:**Both-authored:** the endpoint's own `info.json` `resources` (written via\n 135:`spt resources set`) wins; `daemon.json`'s `resources_blurb` seeds a node\n*136:default for endpoints that never authored one. The \"subnet resource registry\"\n 137:is a **projection** of these rows — `spt resources list` renders\n 138:`(subnet, id, node, status, blurb)` over **visible** rows only (the same\n 139:exclusion closure resolution uses, so discovery can never list what routing\n...\n 141:\n*142:## serve/registry — authoritative serving assignments\n 143:\n*144:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 145:\n*146:`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n*147:cache. The daemon is its single writer and publishes durable atomic snapshots;\n 148:the HTTP listener and listing read those snapshots. Only a genuinely missing\n*149:file initializes an empty registry. Unreadable, corrupt, unsupported, or\n 150:inconsistent history refuses the operation rather than recycling stale URLs.\n 151:\n 152:Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n...\n 180:\n*181:Back up this registry with any adapter output roots you need to preserve.\n 182:Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n*183:copy their bytes or relocate their absolute paths. Losing the registry loses\n 184:the history which prevents an old URL from naming a different resource.\n 185:\n 186:## tracked/ — the context git repo\n...\n 204:| context (`a-`/`p-`) | **BranchStore** | versioned, merge-synced cross-node, audited mind |\n*205:| subnet registry (snapshot + distribution) | **BranchStore** | audited; rides the precedence merge driver instead of a separate replication scheme (M4) |\n 206:| daemon/brain coarse checkpoint (rehydrate anchor, ADR-0004) | **BranchStore** | commit = checkpoint, ref tip = resume |\n 207:| endpoint lifecycle / status *audit trail* | **BranchStore** (or a derived view) | the *trajectory* matters, not just current value |\n 208:| spool (queue, drain-by-status, `id→address`) | **SQLite** | indexed queries + hot churn |\n 209:| broker effect journal (per-PTY-effect, B5) | **fsync-append** | hot path, per-keystroke — a commit per write is far too heavy |\n*210:| `info.json` / `status` (current value) | flat + atomic-rename (today) | tiny, read-hot; *may* move under BranchStore later, low priority |\n 211:\n 212:**Rules:**\n 213:- **Single-writer-per-ref**, sharded per run/session/endpoint under a porcelain-compatible namespace (`refs/heads/spt/...`) — matches the single-writer invariants (6.4, active-instance-authoritative); no many-writers-one-ref contention.\n*214:- **commit = checkpoint, ref tip = resume** — atomic ref update means a crash mid-write never advances the ref, so recovery is \"read the tip.\" This **dissolves the torn-write hazard class** (tmp-write + atomic-rename, 5.2) for BranchStore-covered state.\n\n## docs-site/src/serving/\n### overview.md#7349\n 2:\n*3:The node's serving registry is the single list of what it exposes. Register a\n 4:file or directory deliberately, then copy its URL from `spt serve list`.\n 5:Removing an entry stops serving it; it does not delete its source bytes.\n 6:\n...\n 12:\n*13:<!-- [doc->REQ-WEB-URL-NODE-PREFIX] -->\n 14:## Node-prefixed URLs\n 15:\n 16:Every served resource uses the owning node's name:\n...\n 28:\n*29:<!-- [doc->REQ-WEB-URL-BOUND-PORT] -->\n 30:Generated serving, attachment, and docs URLs use the running daemon's actual\n 31:bound listener port. Changing a client's environment or config does not change\n 32:that port. Configuration is used to predict a URL only when no daemon runs;\n...\n 51:local and remote nodes. Query parameters are preserved. There is no browser\n*52:registry index or `?json` index: use `spt serve list` or `spt serve list --json`\n 53:to audit exposed paths and URLs.\n 54:\n 55:The path beneath the node prefix identifies the facet:\n...\n 85:\n*86:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 87:## Register, inspect, remove\n 88:\n 89:```sh\n...\n 100:root are refused. `rm` accepts the served name or the entry id printed by the\n*101:listing. It removes only the registry entry.\n 102:\n 103:Served names remain stable for an entry's lifetime:\n 104:\n...\n 117:\n*118:The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n 119:entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n 120:registration timestamps, optional origin endpoint ids, and allocation history.\n 121:Optional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\n...\n 127:\n*128:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 129:## Adapter served roots\n 130:\n 131:Core creates `$SPT_HOME/adapters/<adapter>/web/` at activation and registers it\n...\n 146:\n*147:Deactivation removes the registry entry but retains the directory and its\n 148:files. Reactivation with the same declaration reclaims the prior alias, not a\n 149:new suffix. Reactivation or an adapter update must not erase those output bytes.\n 150:\n*151:<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->\n 152:## Adapter docs\n 153:\n 154:An adapter that ships its own documentation points at it with an optional\n...\n 178:\n*179:<!-- [doc->REQ-WEB-ENTRY-AUDIENCE] -->\n 180:## Entry fields: lifetime, audience, origin\n 181:\n 182:Beside its path and served name, an entry may carry three fields. They are\n 183:per-entry and independent of kind, and `spt serve list` renders each one it has\n*184:— the registry's job is answering what this node exposes **and to whom**, and a\n 185:narrowing that did not render would make that answer a half-truth.\n 186:\n 187:| Field | Meaning |\n...\n 195:deliberately the same refusal a `WEB` deny rule produces, so a narrower audience\n*196:cannot be distinguished from a denial by probing and the registry does not\n 197:become an oracle for what exists.\n 198:\n 199:It is enforced where the fetch origin is **proven**: the cross-node stream's\n...\n 211:An entry whose lifetime has elapsed stops serving immediately — it answers the\n*212:ordinary not-found — whether or not the daemon's reaper has run yet.\n 213:\n*214:<!-- [doc->REQ-WEB-ACCESS-SURFACE] -->\n 215:## Access control\n 216:\n 217:`WEB` is a rule-addressable control surface, on by default within a subnet\n...\n 254:\n*255:<!-- [doc->REQ-WEB-LAN-BOOTSTRAP-FIREWALL] -->\n 256:## Bootstrap LAN firewall admission\n 257:\n 258:`spt serve lan --bootstrap` retains the signed-binary admission gate and reports\n### attachments.md#7B67\n 6:\n*7:<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\n 8:## Attaching a file to a message\n 9:\n 10:```console\n...\n 15:\n*16:`--attachment` repeats; each file becomes its own registry entry with its own\n 17:URL. The message envelope grows an `attachments` attribute carrying, per file,\n 18:its served name, its URL and its size in bytes — so a receiving agent can decide\n 19:whether to fetch **before** it fetches. A core that has never heard of the\n...\n 21:\n*22:An attachment is a **snapshot**, and that is the one place the serving registry\n 23:does not resolve at request time:\n 24:\n 25:| | `spt serve add` | `spt send --attachment` |\n...\n 36:Two sends of the same filename get two stable, distinct URLs under the\n*37:registry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\n 38:link ever changes meaning.\n 39:\n 40:A missing or unreadable path **refuses the send by name** — nothing is\n...\n 58:\n*59:The daemon's pulse reaps what has expired, deleting the snapshot file and its\n*60:registry entry together and logging how many it removed. An expired entry stops\n*61:serving the moment it expires, whether or not the reaper has run yet: between\n 62:expiry and the next tick the bytes are still on disk, and serving them would\n 63:make the TTL a suggestion.\n 64:\n*65:<!-- [doc->REQ-WEB-FETCH-VERB] -->\n 66:## Pulling one: `spt fetch`\n 67:\n 68:```console\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md, .worktrees/308-registry-process-lock/docs/STORAGE.md, .worktrees/308-registry-process-lock/docs-site/src/serving/overview.md, .worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":64,"fileCount":4,"files":[".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md",".worktrees/308-registry-process-lock/docs/STORAGE.md",".worktrees/308-registry-process-lock/docs-site/src/serving/overview.md",".worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md","count":20},{"path":".worktrees/308-registry-process-lock/docs/STORAGE.md","count":20},{"path":".worktrees/308-registry-process-lock/docs-site/src/serving/overview.md","count":16},{"path":".worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md","count":8}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/308-registry-process-lock/\n\n## docs/\n### KNOWN-HAZARDS.md#F7C6\n  18│- **Failure:** `info.json` written by the wrapper mid-iteration while a list/classify command reads it → torn read, misclassification.\n *19│- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n *20│- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n  21│- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n  22│\n  23│### 1.3 Stale `index.lock` wedge from prior git crash\n    │...\n  37│- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n *38│- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n  39│- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n  40│\n  41│---\n    │...\n  71│- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `...\n *72│- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n *73│- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n *74│- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n  75│\n  76│### 2.6 A shell instance's `online` is a recorded field with no death edge\n  77│- **Failure:** a shell perch's `status` has exactly two writers — the bind handshake (→`online`) and `close_shell` (→`offline`) — and `close_shell` runs on the **link-break** path alone. A binary that dies **abruptly** (force-kill, crash, OOM) breaks no link, so nothing ever writes the flip: the record says `online` forever while the pid it names is gone. Every consumer of the recorded field then inherits the lie — `shell list` reports online indefinitely; `relink` (the one command that recover...\n    │...\n  79│<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n *80│- **One process generation per read (releases#287):** modern `shell.launch.json` atomically stores `pid`, native `pid_started_at`, and wall-clock `launched_ms`. A modern `pid: 0` means explicitly pidless/cleared and never falls back to the numeric `shell.pid` mirror. Legacy records without `pid` retain their old numeric-PID plus optional-birth read. Publish the complete JSON identity before updating the compatibility mirror; failed publication preserves the previous identity. Never delete the previous bi...\n  81│- **No spontaneous relaunch of a NONPERSISTENT same-boot death:** the releases#78 force-kill freeze still protects nonpersistent instances, so killing one to free its exe does not arm a watcher underneath an overwrite. Recovery remains demand-driven. **Persistent instances are exempt** (operator ruling, releases#287): a kill is not a request to keep a declared-persistent shell down; use `teardown` or a nonpersistent manifest instead.\n  82│- **Watcher eligibility:** recorded `offline`, no live binary awaiting bind, and either `persistent`, no corpse, or a corpse whose launch predates the machine boot instant. A nonpersistent corpse without a launch stamp or boot oracle remains frozen; persistent instances require neither to become eligible. Birth-safe probes prevent a recycled pid from masquerading as a live shell.\n  83│- **spt-core mapping:** `spt_store::shellinfo::{shell_pid_provably_dead, effective_status, is_shell_online}` (the resolver — the shell-side twin of 2.5's `liveness.rs`, which gave *agent* perches exactly this and which shells never got); consumers in `linkhost::{relink_shell, prepare_drive, run_action}`, `shelldisc::discover`, `activity::observe_links`; the class-(c) eligibility read is `spt_daemon::shellwake::watcher_eligible` (+ `launch_predates_boot`, `BOOT_RESTORE_SLACK_MS`), and the birth stamp it...\n    │...\n 132│<!-- [doc->REQ-PERCH-GC-RESIDUE-PREDICATE] -->\n*133│- **Failure:** the obvious way to write a perch garbage collector is \"reap what is not alive\" — `!spt_store::liveness::is_perch_alive(dir)`. On the population a GC actually walks, that predicate is **backwards**. `is_perch_alive` returns **true** for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity — an absent record means a wrapper-owned listener between polls), and **false** for a daemon-hosted endpoint whose `status` is `offline`. Measured on HFENDULEAM 2026-08-04 across...\n 134│- **Why it is a hazard and not a bug in the resolver:** `is_perch_alive` is correct for its own job. Fail-toward-alive is the safe direction for DELIVERY (2.5) — the cost of a false \"alive\" is one message taking the spool path. It is the wrong direction for DELETION, where a false \"not alive\" is irreversible. The inversion is what happens when a predicate is reused across a polarity boundary it was never sized for.\n*135│- **Invariant:** perch garbage collection classifies on record **presence** only (3.5's predicate: no `info.json` present on any read attempt ⇒ residue; NotFound kept distinct from every other I/O error, because absent is the only answer that authorizes deletion) and **never calls a liveness resolver** — not `is_perch_alive`, not `is_registry_entry_alive`, not a pid probe. The registry is likewise never asked: `clean_stale_entries` deletes dead-pid rows and `unregister_address` fires on an ordinary s...\n 136│- **spt-core mapping:** `spt_store::perchgc::{sweep, PerchClass}`; `spt endpoint gc` renders it. The refusal/report contract is `REQ-PERCH-GC-REFUSAL-REPORT`; the occupancy rules it inherits are 3.4.\n 137│- **Source:** measured by todlando on HFENDULEAM 2026-08-04 while answering doyle's step-1 predicate question on releases#109; ruled into the lane by doyle the same day.\n 138│\n    │...\n 155│\n*156│### 4.3 Registry stale-entry cleanup precedes lookup\n 157│- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n 158│- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n*159│- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n*160│- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n 161│\n 162│### 4.4 Deferred rows survive poll drain\n 163│- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n    │...\n 179│### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n*180│<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n 181│- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n 182│- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n*183│- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n 184│\n*185│### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\n*186│<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n*187│- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n 188│- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-...\n*189│- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n 190│\n 191│### 4.9 SQLite stores must create their parent dir — SQLite won't\n*192│<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n*193│- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the h...\n### STORAGE.md#D5CA\n   9│3. **Two separate trees: `tracked/` vs `perches/`.** The synced *mind* (context, a git repo) and the node-local *plumbing* (spool/status/info) are physically separate. Perch runtime is **never** git-tracked — otherwise node-local spools and statuses would clobber across nodes when the context repo syncs.\n *10│4. **Durable vs ephemeral is explicit.** `identity/`, `tracked/`, `history/` are durable (the backup/migrate set). `registry/`, `daemon/`, `updates/` are regenerable. Migration carries the durable set only.\n  11│5. **One path resolver.** A single centralized, idempotent resolver maps `(endpoint_id[, child][, project_id][, kind])` → path. Nothing constructs paths ad-hoc, so any future relayout carries to all dependents (KNOWN-HAZARDS 6.1 — no flat/nested ambiguity).\n  12│\n  13│## Layout\n    │...\n  35│        info.json                type=Shell, owner, adapter_name, status (online|offline), alias?\n *36│        shell.launch.json        atomic PID + native birth + launch wall-clock; pid 0 clears custody\n  37│        shell.pid                numeric compatibility mirror for older core readers\n  38│        spool.db                 command + durable text/file (sensory is REST-only, unspooled)\n  39│  history/                       node-local logs (mirrors perch structure)\n    │...\n  42│      shells/<adapter>-<n>.jsonl     shell-link command/text/sensory exchange log\n *43│  registry/                      ── ephemeral ── local cache of the subnet registry\n  44│  index/                         ── derived, regenerable ── materialized indexes (ADR-0037)\n  45│    project-index.json           endpoint→project attribution (schema-versioned; daemon-written)\n  46│  daemon/                        ── ephemeral ── brain rehydration state, broker handle metadata\n    │...\n  49│    <adapter>/web/               core-owned adapter served root; output bytes survive deactivation\n *50│  serve/registry                 authoritative serving entries + persistent name ownership\n  51│```\n  52│\n  53│<!-- [doc->REQ-SHELL-PERSISTENT-BOOT-RESTORE] -->\n    │...\n 106│\n*107│The scoped roster instrumentation separates those reads, registry database\n 108│open/tuning/schema/query work, and the presence-probe worklist and waits.\n 109│Enable it through `SPT_PUMP_TRACE=1` for the investigated command and retain\n 110│the `ROSTER_WAIT` stderr records alongside complete stdout and command exit\n    │...\n 120│\n*121│### Registry snapshots + the resource-advertisement blurb (D9)\n 122│\n 123│<!-- [doc->REQ-INST-14] -->\n 124│\n*125│The daemon's live per-subnet registries ([`RegistryHost`]) mirror to **atomic\n*126│JSON snapshots at `identity/registry/<subnet>.json`** for out-of-process\n 127│readers (`spt send`'s WAN resolution, `spt resources list`) — stale-tolerant\n 128│by contract (KNOWN-HAZARDS 4.3), regenerable from re-advertisement.\n 129│\n    │...\n 132│**a field on the row, not a separate store** — blurb updates ride the same\n*133│epoch lease and `RegistryUpdate` replication as any instance update.\n 134│**Both-authored:** the endpoint's own `info.json` `resources` (written via\n 135│`spt resources set`) wins; `daemon.json`'s `resources_blurb` seeds a node\n*136│default for endpoints that never authored one. The \"subnet resource registry\"\n 137│is a **projection** of these rows — `spt resources list` renders\n 138│`(subnet, id, node, status, blurb)` over **visible** rows only (the same\n 139│exclusion closure resolution uses, so discovery can never list what routing\n    │...\n 141│\n*142│## serve/registry — authoritative serving assignments\n 143│\n*144│<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 145│\n*146│`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n*147│cache. The daemon is its single writer and publishes durable atomic snapshots;\n 148│the HTTP listener and listing read those snapshots. Only a genuinely missing\n*149│file initializes an empty registry. Unreadable, corrupt, unsupported, or\n 150│inconsistent history refuses the operation rather than recycling stale URLs.\n 151│\n 152│Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n    │...\n 180│\n*181│Back up this registry with any adapter output roots you need to preserve.\n 182│Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n*183│copy their bytes or relocate their absolute paths. Losing the registry loses\n 184│the history which prevents an old URL from naming a different resource.\n 185│\n 186│## tracked/ — the context git repo\n    │...\n 204│| context (`a-`/`p-`) | **BranchStore** | versioned, merge-synced cross-node, audited mind |\n*205│| subnet registry (snapshot + distribution) | **BranchStore** | audited; rides the precedence merge driver instead of a separate replication scheme (M4) |\n 206│| daemon/brain coarse checkpoint (rehydrate anchor, ADR-0004) | **BranchStore** | commit = checkpoint, ref tip = resume |\n 207│| endpoint lifecycle / status *audit trail* | **BranchStore** (or a derived view) | the *trajectory* matters, not just current value |\n 208│| spool (queue, drain-by-status, `id→address`) | **SQLite** | indexed queries + hot churn |\n 209│| broker effect journal (per-PTY-effect, B5) | **fsync-append** | hot path, per-keystroke — a commit per write is far too heavy |\n*210│| `info.json` / `status` (current value) | flat + atomic-rename (today) | tiny, read-hot; *may* move under BranchStore later, low priority |\n 211│\n 212│**Rules:**\n 213│- **Single-writer-per-ref**, sharded per run/session/endpoint under a porcelain-compatible namespace (`refs/heads/spt/...`) — matches the single-writer invariants (6.4, active-instance-authoritative); no many-writers-one-ref contention.\n*214│- **commit = checkpoint, ref tip = resume** — atomic ref update means a crash mid-write never advances the ref, so recovery is \"read the tip.\" This **dissolves the torn-write hazard class** (tmp-write + atomic-rename, 5.2) for BranchStore-covered state.\n\n## docs-site/src/serving/\n### overview.md#7349\n   2│\n  *3│The node's serving registry is the single list of what it exposes. Register a\n   4│file or directory deliberately, then copy its URL from `spt serve list`.\n   5│Removing an entry stops serving it; it does not delete its source bytes.\n   6│\n    │...\n  12│\n *13│<!-- [doc->REQ-WEB-URL-NODE-PREFIX] -->\n  14│## Node-prefixed URLs\n  15│\n  16│Every served resource uses the owning node's name:\n    │...\n  28│\n *29│<!-- [doc->REQ-WEB-URL-BOUND-PORT] -->\n  30│Generated serving, attachment, and docs URLs use the running daemon's actual\n  31│bound listener port. Changing a client's environment or config does not change\n  32│that port. Configuration is used to predict a URL only when no daemon runs;\n    │...\n  51│local and remote nodes. Query parameters are preserved. There is no browser\n *52│registry index or `?json` index: use `spt serve list` or `spt serve list --json`\n  53│to audit exposed paths and URLs.\n  54│\n  55│The path beneath the node prefix identifies the facet:\n    │...\n  85│\n *86│<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n  87│## Register, inspect, remove\n  88│\n  89│```sh\n    │...\n 100│root are refused. `rm` accepts the served name or the entry id printed by the\n*101│listing. It removes only the registry entry.\n 102│\n 103│Served names remain stable for an entry's lifetime:\n 104│\n    │...\n 117│\n*118│The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n 119│entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n 120│registration timestamps, optional origin endpoint ids, and allocation history.\n 121│Optional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\n    │...\n 127│\n*128│<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 129│## Adapter served roots\n 130│\n 131│Core creates `$SPT_HOME/adapters/<adapter>/web/` at activation and registers it\n    │...\n 146│\n*147│Deactivation removes the registry entry but retains the directory and its\n 148│files. Reactivation with the same declaration reclaims the prior alias, not a\n 149│new suffix. Reactivation or an adapter update must not erase those output bytes.\n 150│\n*151│<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->\n 152│## Adapter docs\n 153│\n 154│An adapter that ships its own documentation points at it with an optional\n    │...\n 178│\n*179│<!-- [doc->REQ-WEB-ENTRY-AUDIENCE] -->\n 180│## Entry fields: lifetime, audience, origin\n 181│\n 182│Beside its path and served name, an entry may carry three fields. They are\n 183│per-entry and independent of kind, and `spt serve list` renders each one it has\n*184│— the registry's job is answering what this node exposes **and to whom**, and a\n 185│narrowing that did not render would make that answer a half-truth.\n 186│\n 187│| Field | Meaning |\n    │...\n 195│deliberately the same refusal a `WEB` deny rule produces, so a narrower audience\n*196│cannot be distinguished from a denial by probing and the registry does not\n 197│become an oracle for what exists.\n 198│\n 199│It is enforced where the fetch origin is **proven**: the cross-node stream's\n    │...\n 211│An entry whose lifetime has elapsed stops serving immediately — it answers the\n*212│ordinary not-found — whether or not the daemon's reaper has run yet.\n 213│\n*214│<!-- [doc->REQ-WEB-ACCESS-SURFACE] -->\n 215│## Access control\n 216│\n 217│`WEB` is a rule-addressable control surface, on by default within a subnet\n    │...\n 254│\n*255│<!-- [doc->REQ-WEB-LAN-BOOTSTRAP-FIREWALL] -->\n 256│## Bootstrap LAN firewall admission\n 257│\n 258│`spt serve lan --bootstrap` retains the signed-binary admission gate and reports\n### attachments.md#7B67\n  6│\n *7│<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\n  8│## Attaching a file to a message\n  9│\n 10│```console\n   │...\n 15│\n*16│`--attachment` repeats; each file becomes its own registry entry with its own\n 17│URL. The message envelope grows an `attachments` attribute carrying, per file,\n 18│its served name, its URL and its size in bytes — so a receiving agent can decide\n 19│whether to fetch **before** it fetches. A core that has never heard of the\n   │...\n 21│\n*22│An attachment is a **snapshot**, and that is the one place the serving registry\n 23│does not resolve at request time:\n 24│\n 25│| | `spt serve add` | `spt send --attachment` |\n   │...\n 36│Two sends of the same filename get two stable, distinct URLs under the\n*37│registry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\n 38│link ever changes meaning.\n 39│\n 40│A missing or unreadable path **refuses the send by name** — nothing is\n   │...\n 58│\n*59│The daemon's pulse reaps what has expired, deleting the snapshot file and its\n*60│registry entry together and logging how many it removed. An expired entry stops\n*61│serving the moment it expires, whether or not the reaper has run yet: between\n 62│expiry and the next tick the bytes are still on disk, and serving them would\n 63│make the TTL a suggestion.\n 64│\n*65│<!-- [doc->REQ-WEB-FETCH-VERB] -->\n 66│## Pulling one: `spt fetch`\n 67│\n 68│```console","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789402518511}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_42K5UTUgc2amzZ2uFqM4uOHR|fc_080dc0728d55fa10016aa81da00c8c87d297662faad5100299","toolName":"read","startedAt":"2026-09-14T16:15:28.144Z","args":{"path":".worktrees/308-registry-process-lock/AGENTS.md"},"intent":"Reading repository guidance"},"id":"4a5f7e51","parentId":"a4ca960d","timestamp":"2026-09-14T16:15:28.144Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gjZKUs034NLyu9iQH4vMf4xD|fc_080dc0728d55fa10016aa81da00ca087d28fbd46f3e46657a3","toolName":"read","startedAt":"2026-09-14T16:15:28.145Z","args":{"path":".worktrees/308-registry-process-lock/CLAUDE.md"},"intent":"Reading companion repository guidance"},"id":"d8a0c8ea","parentId":"4a5f7e51","timestamp":"2026-09-14T16:15:28.145Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WJq5hiIPH1B5VjuWm6xnSXuW|fc_080dc0728d55fa10016aa81da00ca887d292b0f203fc172b13","toolName":"read","startedAt":"2026-09-14T16:15:28.145Z","args":{"path":".worktrees/308-registry-process-lock/docs/STORAGE.md:142-185"},"intent":"Reading registry storage contract"},"id":"cdae7dd7","parentId":"d8a0c8ea","timestamp":"2026-09-14T16:15:28.145Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_arguJAAm9C69h6NrmkqHzLgq|fc_080dc0728d55fa10016aa81da00cb087d2bea2427a5a26675b","toolName":"read","startedAt":"2026-09-14T16:15:28.145Z","args":{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:175-245"},"intent":"Reading hazard insertion context"},"id":"10a7f4b7","parentId":"cdae7dd7","timestamp":"2026-09-14T16:15:28.145Z"}
{"type":"message","id":"8de169c9","parentId":"10a7f4b7","timestamp":"2026-09-14T16:15:28.145Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reading relevant guidance docs**"},{"type":"toolCall","id":"call_42K5UTUgc2amzZ2uFqM4uOHR|fc_080dc0728d55fa10016aa81da00c8c87d297662faad5100299","name":"read","arguments":{"i":"Reading repository guidance","path":".worktrees/308-registry-process-lock/AGENTS.md"},"intent":"Reading repository guidance"},{"type":"toolCall","id":"call_gjZKUs034NLyu9iQH4vMf4xD|fc_080dc0728d55fa10016aa81da00ca087d28fbd46f3e46657a3","name":"read","arguments":{"i":"Reading companion repository guidance","path":".worktrees/308-registry-process-lock/CLAUDE.md"},"intent":"Reading companion repository guidance"},{"type":"toolCall","id":"call_WJq5hiIPH1B5VjuWm6xnSXuW|fc_080dc0728d55fa10016aa81da00ca887d292b0f203fc172b13","name":"read","arguments":{"i":"Reading registry storage contract","path":".worktrees/308-registry-process-lock/docs/STORAGE.md:142-185"},"intent":"Reading registry storage contract"},{"type":"toolCall","id":"call_arguJAAm9C69h6NrmkqHzLgq|fc_080dc0728d55fa10016aa81da00cb087d2bea2427a5a26675b","name":"read","arguments":{"i":"Reading hazard insertion context","path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:175-245"},"intent":"Reading hazard insertion context"},{"type":"toolCall","id":"call_p60WLw6Qi9aMNTMCoW9rnL8s|fc_080dc0728d55fa10016aa81da00cb887d2bda4b331f187b5bb","name":"read","arguments":{"i":"Reading serving overview contract","path":".worktrees/308-registry-process-lock/docs-site/src/serving/overview.md:86-127"},"intent":"Reading serving overview contract"},{"type":"toolCall","id":"call_6GwEiN77JslRps5mIfIR06U8|fc_080dc0728d55fa10016aa81da00cbc87d2b49a16de0b188f50","name":"read","arguments":{"i":"Reading attachment lifetime contract","path":".worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md:1-65"},"intent":"Reading attachment lifetime contract"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":8069,"output":256,"cacheRead":19328,"cacheWrite":0,"totalTokens":27653,"reasoningTokens":12,"cost":{"input":0.08069000000000001,"output":0.0128,"cacheRead":0.019327999999999998,"cacheWrite":0,"total":0.11281800000000002}},"stopReason":"toolUse","timestamp":1789402518545,"responseId":"resp_080dc0728d55fa10016aa81d96d02c87d2bca776134058bb53","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81d98ef4887d2886d34f9e50a41bd","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB2gXc4A2b2CLfE4mFxH-h_Ik7s_3xFyUS5JoUQywV4GzKO7-Id_rpw1MClrVJx86Xxe8GQNNIiOVnIN7aw2l75uY38opUqbFze-9_CtWc7PmVCYBbUFlQAN1FV_9u9DVkGzd_K81smw6bR-bF98dwfUXoNBLjQmBkRenmINJu__EVEzXlkHZE2XvxuantziV1UArqIfvRCYv-Nxhs9oaQe2x8diuWxAhy515dSTuVYfEWtQNI7lArizt62rKrxeCpaxKnegNiqYloT4O3SLR1VaxSYYs6XlID8fc2SktAVyFeoYsGlihZbWQuSGgEqePctJy5Iv2ajwoeY9t9_71984r5SwbCQQlj-ZeWyMxxHwW_le_Mse35cUuEv8b7RTxH4dQ81iI4ODrCREmJPuBDGXZ0mMBVjZPQSfuYlV6jhPbJSwvsqbqndXKLAzJ81NWSgKoTwU8vl11RNlRBV9T-i5skSqxTMOvJ4JNJZw_yDj_vyA4bDlFzcrDwxCfrtVVdYjpuXzC-OC74iNd5sD5-PpMY1rzStfzwHOAqSzJ_QCNhT8V3wZz7gCJHqAOlC2HcZ3OqekERsKisbrbcjPie7ER5WzPbGrWvU8ojWlWVgtDXfYMGvmg8mby20kvWYnP_4LqwLtK1mCJA_4dpwpsYLRhO94kgHncGNJxHWZHOi0p4cO1XtNgtIXTlANr8NJduP5OckcuK5yvUWuH2w1atTU96VhguunSyFtlMfjvlX6rxXuoIFuoyzfTqr-pm2XIbLHBkAHBLgAp9b4rFzbkp7lmhehN0-r86Ov-ChVbKWQKGpGZrN1zd1R8Jn2av7kV4wzvlVBqdos7nuGYfbf-ia1Pah0ALRpI1E1hLTb5z3UBalg3nL4YaSzRG2x6v89VKTB2594VNvD1RqPYgSlxqjfI4W6fZ2HK1PhDx7XJSXe4cdKnqY2QX1acHqq5mKd16FulaIODYRzHBpLXpX0dy_SEnNx23MRTvU_xRW4hd41URiWdATGHubnSpZ0DTQcmg4Ts9dUxIQ0K_szDu-EOGMLFYNs_jez7ebYERD35QrqWfhHVI72dkp8MP3Ei7YCWOd87m5gve0b_5eo3N1_eLxHsuo9XA1eV6z_ioy0Go-0_tsfTcijhxZxWNehZzlNAP5hz2ylM3Qtv6yR6hbBpoRIb-CufTWtlHbrdPpA03HWrpLcf55kXPKoR3Bi_YdJ-HOLWHUh1P52tbDFkRYKctXcantaQaxdFi_02K_YGIFCRads00ofZ8ccutaPEDrgyISEvhPeDJ_oFC2NPQemxu16a86zQ6lkqyhWJVW1-21XaEA3QjFnGaCdzvJY5VToYKss","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[{"type":"summary_text","text":"**Reading relevant guidance docs**"}],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00c8c87d297662faad5100299","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading repository guidance\",\"path\":\".worktrees/308-registry-process-lock/AGENTS.md\"}","call_id":"call_42K5UTUgc2amzZ2uFqM4uOHR","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00ca087d28fbd46f3e46657a3","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading companion repository guidance\",\"path\":\".worktrees/308-registry-process-lock/CLAUDE.md\"}","call_id":"call_gjZKUs034NLyu9iQH4vMf4xD","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00ca887d292b0f203fc172b13","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registry storage contract\",\"path\":\".worktrees/308-registry-process-lock/docs/STORAGE.md:142-185\"}","call_id":"call_WJq5hiIPH1B5VjuWm6xnSXuW","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00cb087d2bea2427a5a26675b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hazard insertion context\",\"path\":\".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:175-245\"}","call_id":"call_arguJAAm9C69h6NrmkqHzLgq","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00cb887d2bda4b331f187b5bb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading serving overview contract\",\"path\":\".worktrees/308-registry-process-lock/docs-site/src/serving/overview.md:86-127\"}","call_id":"call_p60WLw6Qi9aMNTMCoW9rnL8s","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81da00cbc87d2b49a16de0b188f50","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attachment lifetime contract\",\"path\":\".worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md:1-65\"}","call_id":"call_6GwEiN77JslRps5mIfIR06U8","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":9598.401399999857,"ttft":2421.5680000036955,"completedAt":1789402528144,"contextSnapshot":{"promptTokens":27397,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_p60WLw6Qi9aMNTMCoW9rnL8s|fc_080dc0728d55fa10016aa81da00cb887d2bda4b331f187b5bb","toolName":"read","startedAt":"2026-09-14T16:15:28.145Z","args":{"path":".worktrees/308-registry-process-lock/docs-site/src/serving/overview.md:86-127"},"intent":"Reading serving overview contract"},"id":"6018c5b5","parentId":"8de169c9","timestamp":"2026-09-14T16:15:28.145Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6GwEiN77JslRps5mIfIR06U8|fc_080dc0728d55fa10016aa81da00cbc87d2b49a16de0b188f50","toolName":"read","startedAt":"2026-09-14T16:15:28.145Z","args":{"path":".worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md:1-65"},"intent":"Reading attachment lifetime contract"},"id":"fd8f0dae","parentId":"6018c5b5","timestamp":"2026-09-14T16:15:28.145Z"}
{"type":"message","id":"ddb44edf","parentId":"fd8f0dae","timestamp":"2026-09-14T16:15:28.153Z","message":{"role":"toolResult","toolCallId":"call_42K5UTUgc2amzZ2uFqM4uOHR|fc_080dc0728d55fa10016aa81da00c8c87d297662faad5100299","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/AGENTS.md#54C1]\n1:# spt-core — agent working rules\n2:\n3:Harness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.\n4:\n5:**Orientation — read before working:** `PRD.md` (requirements), `ROADMAP.md` (build path), `CONTEXT.md` (glossary/model, authoritative for meaning), `docs/adr/` (decisions), `docs/KNOWN-HAZARDS.md` (invariants we must not re-break), `docs/{STORAGE,MANIFEST,CONTEXT-MEMORY,DOCS-STRATEGY,TRACEABILITY}.md`.\n6:\n7:For maintainer/debug update work, also read `docs/DEBUG-ROLLOUT.md` before touching the update-set or rollout path.\n8:\n9:For work needing Linux tests and proof, this machine is authorized to ssh into a Linux box: `reavus@kitsubito`\n10:\n11:IMPORTANT: All agents you work alongside run on spt-core itself — the legacy-SPT (`$OWL`/`$LIVE`) era ended with the 2026-07 fleet migration; those commands are gone. Reach other agents with `spt send` (or the `/sptc:send` skill); keep message bodies short, and send long bodies from a file rather than inline. Everything else can be learned from the corresponding `/sptc:` skill doc.\n12:\n13:## Requirement traceability (binding)\n14:\n15:This project uses `traceable-reqs` (`traceable-reqs.toml` = the authoritative `REQ-*` registry). The full contract is `docs/TRACEABILITY.md`. The rules you must follow:\n16:\n17:1. **Tag evidence in the same change.** When you write a function/test/doc-section that satisfies a requirement stage, add its tag in that same commit:\n18:   `// [impl->REQ-FOO]` · `// [unit->REQ-FOO]` · `<!-- [doc->REQ-FOO] -->`\n19:   Stages: `doc` / `impl` / `unit` / `int`. Tag *on or immediately above* the real evidence — never at file tops to satisfy coverage.\n20:2. **Run `traceable-reqs check` before declaring work done.** Exit-1 means missing/invalid evidence — fix it, don't ship it.\n21:3. **New requirement → add it to `traceable-reqs.toml` first** (with a `REQ-*` id), then satisfy it. No untracked work; no untagged evidence.\n22:4. **KNOWN-HAZARDS are `REQ-HAZARD-*` requirements** — each needs a test before it's \"covered.\" Treat the hazard list as a conformance checklist you must satisfy, not advice.\n23:5. **Activate, don't pre-fail.** Requirements you aren't yet working stay `required_stages = []`. Activate (set real stages) only when starting the milestone that delivers them.\n24:\n25:## Issue tracking (unique case)\n26:\n27:Codebase issues are maintained on **`BigscreenVR/spt-bs-releases`** — the less-private *releases* repo — NOT on `spt-bs-core` where the code lives. This is a deliberate exception (operator-ruled 2026-07-27): file, triage, and reference issues against `spt-bs-releases`.\n28:\n29:Requests and milestones are tracked through the **alchemy shell** (`alchemy-0`, the Requests shell bound to `spt-bs-releases`) on top of those GitHub issues. The label taxonomy is the workflow: `state:` BACKLOG → EVAL → GREENLIT → WIP → ACCEPTANCE → DONE (plus CUT / SHELVED), `type:` BUGFIX / ADDITION / CHANGE, `flag: NEEDS-OPERATOR`, and per-node `kind: SPT-TRACKING` pins (marked DO NOT DELETE — leave them alone).\n30:\n31:## Other conventions\n32:\n33:- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.\n34:- Honor every `docs/KNOWN-HAZARDS.md` invariant — these are real bugs already paid for once.\n35:- Docs are dual-audience (human + AI dev-agent) per `docs/DOCS-STRATEGY.md`; doc generation is CI-gated against drift.\n36:- Commit messages end with the project's co-author trailer, exact spelling `Co-authored by: <your live agent name>` (e.g. `Co-authored by: todlando`) — NOT git's hyphenated `Co-Authored-By:`; CI's runner-informant parses only the space spelling for attribution.\n37:  - **Audit this trailer with `git log --grep` or a raw body read, NEVER `%(trailers:)`** (nor `interpret-trailers --parse`): git's trailer tokenizer requires a whitespace-free token, so the space spelling is structurally invisible to it and every trailer query returns a confident EMPTY on every commit — a clean zero that reads as total attribution loss. Measured 2026-07-30 (main @`8f3e10b`, Signed-off-by and hyphenated-key controls both parse); re-derived at full cost 2026-08-03 by one of its own original measurers, which is why this sentence lives here at the mandate rather than in a document you must decide to open.\n38:- **Worktrees (binding, operator-ruled 2026-07-05):** every `git worktree` (gate rigs, agent scratch, anything) goes under `.worktrees/` in THIS project's root (`.gitignore`d) — e.g. `git worktree add --detach .worktrees/gate-<sha> <sha>`. NEVER create worktrees as siblings of the project dir; stray sibling folders accumulate orphan-pinned skeletons and pollute `~/Documents/projects`. Remove yours when done (`git worktree remove` + prune; retry later if a handle pins it).\n39:<!-- [doc->REQ-POOL-FOREIGN-TREE-REFUSED] -->\n40:- **Build-cache pools (binding, releases#103):** a cargo `target/` directory belongs to ONE source tree. Two trees building into one pool overwrite each other's artifacts and fabricate reds that belong to neither — the v0.51.0 W1/H1 `E0308` that vanished on re-run. **The rule is about the RELATIONSHIP, not the path:** a junction and a `CARGO_TARGET_DIR` env var reach the same hazard, and the env var leaves no directory entry to notice afterwards, so \"is this dir under `.worktrees/`\" never was the question. A build script enforces it (`crates/spt-store/build.rs`) and refuses with `SPT_POOL_FOREIGN`. **Sharing a pool SEQUENTIALLY is fine and expected** — a finished lane is taken over loudly, not refused; only a LIVE lane blocks you. Claim a pool at lane start …\n41:- **Target-tree teardown (releases#103, same lane):** never delete a `target/` you have not classified. Check OUTBOUND first (`Get-Item -Force`: a junction is deleted as a link, a real dir as a tree), then sweep INBOUND for reparse points aimed at it before removing a real one, and measure size and free space before and after so the reclaim is a number rather than a claim. Reap the target SUBTREE, never a session or worktree directory around it. Do not point `CARGO_TARGET_DIR` into a scratchpad (v0.50.0 incident) or at another checkout's target (H1 incident).\n42:- **CI / merge strategy:** golden CI (ADR-0050, ruled 2026-07-27) — milestone-batch golden runs, thin-lane PRs, ff-only main (tested sha == merged sha). Piloting from the next milestone; read the ADR before shaping a PR lane.\n43:\n44:If you finish a significant body of work without need for user intervention, or if your context gets too high, you can clear your own context and keep moving:\n45:1. Create a JIT plan for the next immediate body of work, if it isn't already planned\n46:2. If you are a live agent, /sptc:commune with immediate next steps and broad summary of the project's status + end goal\n47:3. Execute the /self-clear skill\n\n[Some lines truncated to 768 chars]"}],"details":{"totalLines":47,"displayContent":{"text":"# spt-core — agent working rules\r\n\r\nHarness-independent core for an agent ecosystem (messaging, live-agent lifecycle, terminal hosting, P2P networking, runtime-manifest harness contract). Rust crates + a single `spt` binary. Clean-room rebuild of `claude_skill_owl`.\r\n\r\n**Orientation — read before working:** `PRD.md` (requirements), `ROADMAP.md` (build path), `CONTEXT.md` (glossary/model, authoritative for meaning), `docs/adr/` (decisions), `docs/KNOWN-HAZARDS.md` (invariants we must not re-break), `docs/{STORAGE,MANIFEST,CONTEXT-MEMORY,DOCS-STRATEGY,TRACEABILITY}.md`.\r\n\r\nFor maintainer/debug update work, also read `docs/DEBUG-ROLLOUT.md` before touching the update-set or rollout path.\r\n\r\nFor work needing Linux tests and proof, this machine is authorized to ssh into a Linux box: `reavus@kitsubito`\r\n\r\nIMPORTANT: All agents you work alongside run on spt-core itself — the legacy-SPT (`$OWL`/`$LIVE`) era ended with the 2026-07 fleet migration; those commands are gone. Reach other agents with `spt send` (or the `/sptc:send` skill); keep message bodies short, and send long bodies from a file rather than inline. Everything else can be learned from the corresponding `/sptc:` skill doc.\r\n\r\n## Requirement traceability (binding)\r\n\r\nThis project uses `traceable-reqs` (`traceable-reqs.toml` = the authoritative `REQ-*` registry). The full contract is `docs/TRACEABILITY.md`. The rules you must follow:\r\n\r\n1. **Tag evidence in the same change.** When you write a function/test/doc-section that satisfies a requirement stage, add its tag in that same commit:\r\n   `// [impl->REQ-FOO]` · `// [unit->REQ-FOO]` · `<!-- [doc->REQ-FOO] -->`\r\n   Stages: `doc` / `impl` / `unit` / `int`. Tag *on or immediately above* the real evidence — never at file tops to satisfy coverage.\r\n2. **Run `traceable-reqs check` before declaring work done.** Exit-1 means missing/invalid evidence — fix it, don't ship it.\r\n3. **New requirement → add it to `traceable-reqs.toml` first** (with a `REQ-*` id), then satisfy it. No untracked work; no untagged evidence.\r\n4. **KNOWN-HAZARDS are `REQ-HAZARD-*` requirements** — each needs a test before it's \"covered.\" Treat the hazard list as a conformance checklist you must satisfy, not advice.\r\n5. **Activate, don't pre-fail.** Requirements you aren't yet working stay `required_stages = []`. Activate (set real stages) only when starting the milestone that delivers them.\r\n\r\n## Issue tracking (unique case)\r\n\r\nCodebase issues are maintained on **`BigscreenVR/spt-bs-releases`** — the less-private *releases* repo — NOT on `spt-bs-core` where the code lives. This is a deliberate exception (operator-ruled 2026-07-27): file, triage, and reference issues against `spt-bs-releases`.\r\n\r\nRequests and milestones are tracked through the **alchemy shell** (`alchemy-0`, the Requests shell bound to `spt-bs-releases`) on top of those GitHub issues. The label taxonomy is the workflow: `state:` BACKLOG → EVAL → GREENLIT → WIP → ACCEPTANCE → DONE (plus CUT / SHELVED), `type:` BUGFIX / ADDITION / CHANGE, `flag: NEEDS-OPERATOR`, and per-node `kind: SPT-TRACKING` pins (marked DO NOT DELETE — leave them alone).\r\n\r\n## Other conventions\r\n\r\n- Match surrounding code style; copy-verbatim the sister project's stable wire/schema formats (ADR-0001), clean-room everything else.\r\n- Honor every `docs/KNOWN-HAZARDS.md` invariant — these are real bugs already paid for once.\r\n- Docs are dual-audience (human + AI dev-agent) per `docs/DOCS-STRATEGY.md`; doc generation is CI-gated against drift.\r\n- Commit messages end with the project's co-author trailer, exact spelling `Co-authored by: <your live agent name>` (e.g. `Co-authored by: todlando`) — NOT git's hyphenated `Co-Authored-By:`; CI's runner-informant parses only the space spelling for attribution.\r\n  - **Audit this trailer with `git log --grep` or a raw body read, NEVER `%(trailers:)`** (nor `interpret-trailers --parse`): git's trailer tokenizer requires a whitespace-free token, so the space spelling is structurally invisible to it and every trailer query returns a confident EMPTY on every commit — a clean zero that reads as total attribution loss. Measured 2026-07-30 (main @`8f3e10b`, Signed-off-by and hyphenated-key controls both parse); re-derived at full cost 2026-08-03 by one of its own original measurers, which is why this sentence lives here at the mandate rather than in a document you must decide to open.\r\n- **Worktrees (binding, operator-ruled 2026-07-05):** every `git worktree` (gate rigs, agent scratch, anything) goes under `.worktrees/` in THIS project's root (`.gitignore`d) — e.g. `git worktree add --detach .worktrees/gate-<sha> <sha>`. NEVER create worktrees as siblings of the project dir; stray sibling folders accumulate orphan-pinned skeletons and pollute `~/Documents/projects`. Remove yours when done (`git worktree remove` + prune; retry later if a handle pins it).\r\n<!-- [doc->REQ-POOL-FOREIGN-TREE-REFUSED] -->\r\n- **Build-cache pools (binding, releases#103):** a cargo `target/` directory belongs to ONE source tree. Two trees building into one pool overwrite each other's artifacts and fabricate reds that belong to neither — the v0.51.0 W1/H1 `E0308` that vanished on re-run. **The rule is about the RELATIONSHIP, not the path:** a junction and a `CARGO_TARGET_DIR` env var reach the same hazard, and the env var leaves no directory entry to notice afterwards, so \"is this dir under `.worktrees/`\" never was the question. A build script enforces it (`crates/spt-store/build.rs`) and refuses with `SPT_POOL_FOREIGN`. **Sharing a pool SEQUENTIALLY is fine and expected** — a finished lane is taken over loudly, not refused; only a LIVE lane blocks you. Claim a pool at lane start …\n- **Target-tree teardown (releases#103, same lane):** never delete a `target/` you have not classified. Check OUTBOUND first (`Get-Item -Force`: a junction is deleted as a link, a real dir as a tree), then sweep INBOUND for reparse points aimed at it before removing a real one, and measure size and free space before and after so the reclaim is a number rather than a claim. Reap the target SUBTREE, never a session or worktree directory around it. Do not point `CARGO_TARGET_DIR` into a scratchpad (v0.50.0 incident) or at another checkout's target (H1 incident).\r\n- **CI / merge strategy:** golden CI (ADR-0050, ruled 2026-07-27) — milestone-batch golden runs, thin-lane PRs, ff-only main (tested sha == merged sha). Piloting from the next milestone; read the ADR before shaping a PR lane.\r\n\r\nIf you finish a significant body of work without need for user intervention, or if your context gets too high, you can clear your own context and keep moving:\r\n1. Create a JIT plan for the next immediate body of work, if it isn't already planned\r\n2. If you are a live agent, /sptc:commune with immediate next steps and broad summary of the project's status + end goal\r\n3. Execute the /self-clear skill\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47]},"fileSize":8436,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\AGENTS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789402528153}}
{"type":"message","id":"af8502f9","parentId":"ddb44edf","timestamp":"2026-09-14T16:15:28.154Z","message":{"role":"toolResult","toolCallId":"call_gjZKUs034NLyu9iQH4vMf4xD|fc_080dc0728d55fa10016aa81da00ca087d28fbd46f3e46657a3","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/CLAUDE.md#6D91]\n1:@AGENTS.md\n2:\n3:<!--\n4:Claude Code reads CLAUDE.md only; the `@AGENTS.md` import above bridges the canonical rules.\n5:Keep working rules in AGENTS.md (harness-agnostic). Put ONLY Claude-Code-specific extras\n6:below this line (e.g. CC slash-command notes, hook reminders) — and prefer none.\n7:-->"}],"details":{"totalLines":7,"displayContent":{"text":"@AGENTS.md\r\n\r\n<!--\r\nClaude Code reads CLAUDE.md only; the `@AGENTS.md` import above bridges the canonical rules.\r\nKeep working rules in AGENTS.md (harness-agnostic). Put ONLY Claude-Code-specific extras\r\nbelow this line (e.g. CC slash-command notes, hook reminders) — and prefer none.\r\n-->\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7]},"fileSize":293,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\CLAUDE.md"}}},"isError":false,"timestamp":1789402528154}}
{"type":"message","id":"7e226e98","parentId":"af8502f9","timestamp":"2026-09-14T16:15:28.159Z","message":{"role":"toolResult","toolCallId":"call_p60WLw6Qi9aMNTMCoW9rnL8s|fc_080dc0728d55fa10016aa81da00cb887d2bda4b331f187b5bb","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md#7349]\n14:## Node-prefixed URLs\n…\n85:\n86:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n87:## Register, inspect, remove\n88:\n89:```sh\n90:spt serve add report.md\n91:spt serve add ./reports --as review\n92:spt serve list\n93:spt serve list --json\n94:spt serve rm report.md\n95:```\n96:\n97:`add` records an absolute reference to the file or directory, not a copy.\n98:Later edits are visible at the same URL; deletion of the source returns 404.\n99:A directory exposes only its own subtree: traversal and symlinks escaping the\n100:root are refused. `rm` accepts the served name or the entry id printed by the\n101:listing. It removes only the registry entry.\n102:\n103:Served names remain stable for an entry's lifetime:\n104:\n105:- The first `report.md` keeps that name.\n106:- Another path requesting `report.md` receives `report~1.md`, then\n107:  `report~2.md`, with the suffix before the final extension.\n108:- Removing the first entry reserves `report.md` for its prior absolute path\n109:  and kind. Re-adding that same file reclaims `report.md`; a different third\n110:  file still receives `report~2.md`. A stale URL never opens a different resource.\n111:- Re-registering the same absolute path returns its existing entry, even if\n112:  the caller supplies a different `--as` name.\n113:- Explicit `--as` names use the same allocator. A name assigned explicitly\n114:  or by suffixing can be reclaimed only by its prior path with the same kind.\n115:- Allocation history is persisted per node. Another node has its own names;\n116:  the node prefix keeps their URLs distinct.\n117:\n118:The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n119:entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n120:registration timestamps, optional origin endpoint ids, and allocation history.\n121:Optional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\n122:to every entry kind, including live file and directory references. W0's\n123:explicit registrations leave both unset; timed, audience-scoped helper\n124:registration and its enforcement belong to W2. Do not edit or remove this\n125:file to reclaim a name: that would destroy the history which prevents stale\n126:URLs changing meaning.\n127:\n128:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n129:## Adapter served roots\n130:\n…\n151:<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->\n\n[Showing lines 85-130 of 305. Use :131 to continue]"}],"details":{"truncation":{"content":"\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n## Register, inspect, remove\r\n\r\n```sh\r\nspt serve add report.md\r\nspt serve add ./reports --as review\r\nspt serve list\r\nspt serve list --json\r\nspt serve rm report.md\r\n```\r\n\r\n`add` records an absolute reference to the file or directory, not a copy.\r\nLater edits are visible at the same URL; deletion of the source returns 404.\r\nA directory exposes only its own subtree: traversal and symlinks escaping the\r\nroot are refused. `rm` accepts the served name or the entry id printed by the\r\nlisting. It removes only the registry entry.\r\n\r\nServed names remain stable for an entry's lifetime:\r\n\r\n- The first `report.md` keeps that name.\r\n- Another path requesting `report.md` receives `report~1.md`, then\r\n  `report~2.md`, with the suffix before the final extension.\r\n- Removing the first entry reserves `report.md` for its prior absolute path\r\n  and kind. Re-adding that same file reclaims `report.md`; a different third\r\n  file still receives `report~2.md`. A stale URL never opens a different resource.\r\n- Re-registering the same absolute path returns its existing entry, even if\r\n  the caller supplies a different `--as` name.\r\n- Explicit `--as` names use the same allocator. A name assigned explicitly\r\n  or by suffixing can be reclaimed only by its prior path with the same kind.\r\n- Allocation history is persisted per node. Another node has its own names;\r\n  the node prefix keeps their URLs distinct.\r\n\r\nThe daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\r\nentry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\r\nregistration timestamps, optional origin endpoint ids, and allocation history.\r\nOptional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\r\nto every entry kind, including live file and directory references. W0's\r\nexplicit registrations leave both unset; timed, audience-scoped helper\r\nregistration and its enforcement belong to W2. Do not edit or remove this\r\nfile to reclaim a name: that would destroy the history which prevents stale\r\nURLs changing meaning.\r\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n## Adapter served roots\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":221,"totalBytes":2148,"outputLines":46,"outputBytes":2148,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":305,"displayContent":{"text":"## Node-prefixed URLs\n…\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n## Register, inspect, remove\r\n\r\n```sh\r\nspt serve add report.md\r\nspt serve add ./reports --as review\r\nspt serve list\r\nspt serve list --json\r\nspt serve rm report.md\r\n```\r\n\r\n`add` records an absolute reference to the file or directory, not a copy.\r\nLater edits are visible at the same URL; deletion of the source returns 404.\r\nA directory exposes only its own subtree: traversal and symlinks escaping the\r\nroot are refused. `rm` accepts the served name or the entry id printed by the\r\nlisting. It removes only the registry entry.\r\n\r\nServed names remain stable for an entry's lifetime:\r\n\r\n- The first `report.md` keeps that name.\r\n- Another path requesting `report.md` receives `report~1.md`, then\r\n  `report~2.md`, with the suffix before the final extension.\r\n- Removing the first entry reserves `report.md` for its prior absolute path\r\n  and kind. Re-adding that same file reclaims `report.md`; a different third\r\n  file still receives `report~2.md`. A stale URL never opens a different resource.\r\n- Re-registering the same absolute path returns its existing entry, even if\r\n  the caller supplies a different `--as` name.\r\n- Explicit `--as` names use the same allocator. A name assigned explicitly\r\n  or by suffixing can be reclaimed only by its prior path with the same kind.\r\n- Allocation history is persisted per node. Another node has its own names;\r\n  the node prefix keeps their URLs distinct.\r\n\r\nThe daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\r\nentry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\r\nregistration timestamps, optional origin endpoint ids, and allocation history.\r\nOptional lifetime (`ttl_ms`) and receiving-endpoint `audience` metadata apply\r\nto every entry kind, including live file and directory references. W0's\r\nexplicit registrations leave both unset; timed, audience-scoped helper\r\nregistration and its enforcement belong to W2. Do not edit or remove this\r\nfile to reclaim a name: that would destroy the history which prevents stale\r\nURLs changing meaning.\r\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n## Adapter served roots\r\n\r\n…\n<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->","startLine":14,"lineNumbers":[14,null,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,null,151]},"fileSize":16738,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs-site\\src\\serving\\overview.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":305,"totalBytes":2148,"outputLines":46,"outputBytes":2148,"shownRange":{"start":85,"end":130},"nextOffset":131}}},"isError":false,"timestamp":1789402528159}}
{"type":"message","id":"ff7502bc","parentId":"7e226e98","timestamp":"2026-09-14T16:15:28.162Z","message":{"role":"toolResult","toolCallId":"call_6GwEiN77JslRps5mIfIR06U8|fc_080dc0728d55fa10016aa81da00cbc87d2b49a16de0b188f50","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md#7B67]\n1:# Attachments and `spt fetch`\n2:\n3:Sending a file with a message does not push bytes to the receiver. The file is\n4:snapshot into this node's own store, registered for serving, and the message\n5:carries its URL. The receiver decides whether to pull it, and when.\n6:\n7:<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\n8:## Attaching a file to a message\n9:\n10:```console\n11:$ printf 'the report you asked for' | spt send doyle --attachment ./report.md\n12:ATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)\n13:SENT:doyle\n14:```\n15:\n16:`--attachment` repeats; each file becomes its own registry entry with its own\n17:URL. The message envelope grows an `attachments` attribute carrying, per file,\n18:its served name, its URL and its size in bytes — so a receiving agent can decide\n19:whether to fetch **before** it fetches. A core that has never heard of the\n20:attribute delivers the message unchanged.\n21:\n22:An attachment is a **snapshot**, and that is the one place the serving registry\n23:does not resolve at request time:\n24:\n25:| | `spt serve add` | `spt send --attachment` |\n26:|---|---|---|\n27:| What is served | the file at its path | a copy taken at send time |\n28:| Editing the source | the reader sees the edit | the reader still sees what was sent |\n29:| Deleting the source | the URL answers 404 | the URL keeps serving |\n30:| Lifetime | until removed | a TTL, 30 days by default |\n31:\n32:A message's attachment is as immutable as the message: an edit after the send\n33:must not change what the receiver pulls, and deleting the original must not\n34:turn a delivered link into a dead one.\n35:\n36:Two sends of the same filename get two stable, distinct URLs under the\n37:registry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\n38:link ever changes meaning.\n39:\n40:A missing or unreadable path **refuses the send by name** — nothing is\n41:delivered and nothing is spooled — rather than delivering a message whose link\n42:is dead on arrival:\n43:\n44:```console\n45:$ printf 'here' | spt send doyle --attachment ./nope.md\n46:ATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)\n47:```\n48:\n49:### Lifetime\n50:\n51:`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or\n52:`d`. A bare number is refused rather than guessed, because `30` meaning seconds\n53:when the sender meant days is a deleted attachment.\n54:\n55:```console\n56:$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\n57:```\n58:\n59:The daemon's pulse reaps what has expired, deleting the snapshot file and its\n60:registry entry together and logging how many it removed. An expired entry stops\n61:serving the moment it expires, whether or not the reaper has run yet: between\n62:expiry and the next tick the bytes are still on disk, and serving them would\n63:make the TTL a suggestion.\n64:\n65:<!-- [doc->REQ-WEB-FETCH-VERB] -->\n66:## Pulling one: `spt fetch`\n67:\n68:```console\n…\n73:```\n…\n97:<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\n…\n156:[Messaging](../messaging/overview.md) for the id itself.\n\n[Showing lines 1-68 of 156. Use :69 to continue]"}],"details":{"truncation":{"content":"# Attachments and `spt fetch`\r\n\r\nSending a file with a message does not push bytes to the receiver. The file is\r\nsnapshot into this node's own store, registered for serving, and the message\r\ncarries its URL. The receiver decides whether to pull it, and when.\r\n\r\n<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\r\n## Attaching a file to a message\r\n\r\n```console\r\n$ printf 'the report you asked for' | spt send doyle --attachment ./report.md\r\nATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)\r\nSENT:doyle\r\n```\r\n\r\n`--attachment` repeats; each file becomes its own registry entry with its own\r\nURL. The message envelope grows an `attachments` attribute carrying, per file,\r\nits served name, its URL and its size in bytes — so a receiving agent can decide\r\nwhether to fetch **before** it fetches. A core that has never heard of the\r\nattribute delivers the message unchanged.\r\n\r\nAn attachment is a **snapshot**, and that is the one place the serving registry\r\ndoes not resolve at request time:\r\n\r\n| | `spt serve add` | `spt send --attachment` |\r\n|---|---|---|\r\n| What is served | the file at its path | a copy taken at send time |\r\n| Editing the source | the reader sees the edit | the reader still sees what was sent |\r\n| Deleting the source | the URL answers 404 | the URL keeps serving |\r\n| Lifetime | until removed | a TTL, 30 days by default |\r\n\r\nA message's attachment is as immutable as the message: an edit after the send\r\nmust not change what the receiver pulls, and deleting the original must not\r\nturn a delivered link into a dead one.\r\n\r\nTwo sends of the same filename get two stable, distinct URLs under the\r\nregistry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\r\nlink ever changes meaning.\r\n\r\nA missing or unreadable path **refuses the send by name** — nothing is\r\ndelivered and nothing is spooled — rather than delivering a message whose link\r\nis dead on arrival:\r\n\r\n```console\r\n$ printf 'here' | spt send doyle --attachment ./nope.md\r\nATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)\r\n```\r\n\r\n### Lifetime\r\n\r\n`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or\r\n`d`. A bare number is refused rather than guessed, because `30` meaning seconds\r\nwhen the sender meant days is a deleted attachment.\r\n\r\n```console\r\n$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\r\n```\r\n\r\nThe daemon's pulse reaps what has expired, deleting the snapshot file and its\r\nregistry entry together and logging how many it removed. An expired entry stops\r\nserving the moment it expires, whether or not the reaper has run yet: between\r\nexpiry and the next tick the bytes are still on disk, and serving them would\r\nmake the TTL a suggestion.\r\n\r\n<!-- [doc->REQ-WEB-FETCH-VERB] -->\r\n## Pulling one: `spt fetch`\r\n\r\n```console\r","truncated":true,"truncatedBy":"lines","totalLines":156,"totalBytes":2869,"outputLines":68,"outputBytes":2869,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":156,"displayContent":{"text":"# Attachments and `spt fetch`\r\n\r\nSending a file with a message does not push bytes to the receiver. The file is\r\nsnapshot into this node's own store, registered for serving, and the message\r\ncarries its URL. The receiver decides whether to pull it, and when.\r\n\r\n<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\r\n## Attaching a file to a message\r\n\r\n```console\r\n$ printf 'the report you asked for' | spt send doyle --attachment ./report.md\r\nATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)\r\nSENT:doyle\r\n```\r\n\r\n`--attachment` repeats; each file becomes its own registry entry with its own\r\nURL. The message envelope grows an `attachments` attribute carrying, per file,\r\nits served name, its URL and its size in bytes — so a receiving agent can decide\r\nwhether to fetch **before** it fetches. A core that has never heard of the\r\nattribute delivers the message unchanged.\r\n\r\nAn attachment is a **snapshot**, and that is the one place the serving registry\r\ndoes not resolve at request time:\r\n\r\n| | `spt serve add` | `spt send --attachment` |\r\n|---|---|---|\r\n| What is served | the file at its path | a copy taken at send time |\r\n| Editing the source | the reader sees the edit | the reader still sees what was sent |\r\n| Deleting the source | the URL answers 404 | the URL keeps serving |\r\n| Lifetime | until removed | a TTL, 30 days by default |\r\n\r\nA message's attachment is as immutable as the message: an edit after the send\r\nmust not change what the receiver pulls, and deleting the original must not\r\nturn a delivered link into a dead one.\r\n\r\nTwo sends of the same filename get two stable, distinct URLs under the\r\nregistry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\r\nlink ever changes meaning.\r\n\r\nA missing or unreadable path **refuses the send by name** — nothing is\r\ndelivered and nothing is spooled — rather than delivering a message whose link\r\nis dead on arrival:\r\n\r\n```console\r\n$ printf 'here' | spt send doyle --attachment ./nope.md\r\nATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)\r\n```\r\n\r\n### Lifetime\r\n\r\n`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or\r\n`d`. A bare number is refused rather than guessed, because `30` meaning seconds\r\nwhen the sender meant days is a deleted attachment.\r\n\r\n```console\r\n$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\r\n```\r\n\r\nThe daemon's pulse reaps what has expired, deleting the snapshot file and its\r\nregistry entry together and logging how many it removed. An expired entry stops\r\nserving the moment it expires, whether or not the reaper has run yet: between\r\nexpiry and the next tick the bytes are still on disk, and serving them would\r\nmake the TTL a suggestion.\r\n\r\n<!-- [doc->REQ-WEB-FETCH-VERB] -->\r\n## Pulling one: `spt fetch`\r\n\r\n```console\r\n…\n```\n…\n<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\n…\n[Messaging](../messaging/overview.md) for the id itself.","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,null,73,null,97,null,156]},"fileSize":7605,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs-site\\src\\serving\\attachments.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":156,"totalBytes":2869,"outputLines":68,"outputBytes":2869,"shownRange":{"start":1,"end":68},"nextOffset":69}}},"isError":false,"timestamp":1789402528162}}
{"type":"message","id":"d41e615e","parentId":"ff7502bc","timestamp":"2026-09-14T16:15:28.172Z","message":{"role":"toolResult","toolCallId":"call_WJq5hiIPH1B5VjuWm6xnSXuW|fc_080dc0728d55fa10016aa81da00ca887d292b0f203fc172b13","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/STORAGE.md#D5CA]\n13:## Layout\n…\n121:### Registry snapshots + the resource-advertisement blurb (D9)\n…\n141:\n142:## serve/registry — authoritative serving assignments\n143:\n144:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n145:\n146:`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n147:cache. The daemon is its single writer and publishes durable atomic snapshots;\n148:the HTTP listener and listing read those snapshots. Only a genuinely missing\n149:file initializes an empty registry. Unreadable, corrupt, unsupported, or\n150:inconsistent history refuses the operation rather than recycling stale URLs.\n151:\n152:Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n153:`name_owners` (every assigned name mapped to its prior absolute `path` and\n154:`kind`, including removed entries). Each entry has\n155:`id`, `kind`, absolute `path`, `served_name`, `registered_at_ms`, optional\n156:`ttl_ms`, optional receiving-endpoint `audience`, optional `origin`, optional\n157:owning `adapter`, and `short_alias`. Lifetime and audience are per-entry,\n158:independent of kind: live files and directories can carry them as well as\n159:attachment snapshots (ADR-0058 Amendment 1). W0's explicit registrations leave\n160:both unset; helper registration, expiry, and audience enforcement belong to W2.\n161:Removal deletes only the live entry, never its name history or source bytes.\n162:A retired name may be reclaimed only by its prior absolute path with the same\n163:kind. Other paths still consume the next suffix; reclamation never rewinds\n164:that counter (ADR-0057 Amendment 2).\n165:\n166:Each active base adapter has one directory entry referring to the core-created\n167:`adapters/<adapter>/web/`, never its install tree. Core creates the checked root\n168:before committing registration, including while the daemon is stopped; only\n169:the daemon writes serving assignments, at startup and after adapter changes.\n170:The adapter facet `/<node>/a/<adapter>/` always names the entry. Its optional\n171:short alias addresses that same entry; no second row, profile root, or directory\n172:is created. Redirected roots and symlinks escaping the subtree are refused.\n173:\n174:Repeated reconciliation and adapter updates preserve a live entry and alias;\n175:changes to the alias declaration apply after deactivation/reactivation.\n176:Deactivation or removal retires the entry while retaining the root and all\n177:output bytes. With the same declaration, reactivation reclaims the root's prior\n178:served name and alias from ownership history. Updates never replace this\n179:output with files from an adapter package.\n180:\n181:Back up this registry with any adapter output roots you need to preserve.\n182:Explicit references point outside SPT_HOME too: copying SPT_HOME alone does not\n183:copy their bytes or relocate their absolute paths. Losing the registry loses\n184:the history which prevents an old URL from naming a different resource.\n185:\n186:## tracked/ — the context git repo\n187:\n188:- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing (\"which endpoint is most responsible for this task\"). *Mechanics (D6a):* the object DB is a **bare seed repo at `tracked/.seed.git`**; `agents/<id>/` and `projects/<project_id>/` are **linked worktrees** of their branches, managed by `spt-store::{branchstore,contextstore}` over the system git binary (ADR-0013). Each branch seeds as a parentless empty-tree root; cross-node sync joins histories at first contact.\n…\n191:\n\n[Showing lines 141-188 of 376. Use :189 to continue]"}],"details":{"truncation":{"content":"\r\n## serve/registry — authoritative serving assignments\r\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n\r\n`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\r\ncache. The daemon is its single writer and publishes durable atomic snapshots;\r\nthe HTTP listener and listing read those snapshots. Only a genuinely missing\r\nfile initializes an empty registry. Unreadable, corrupt, unsupported, or\r\ninconsistent history refuses the operation rather than recycling stale URLs.\r\n\r\nSchema 1 carries `entries`, `next_suffix` (per requested-name family), and\r\n`name_owners` (every assigned name mapped to its prior absolute `path` and\r\n`kind`, including removed entries). Each entry has\r\n`id`, `kind`, absolute `path`, `served_name`, `registered_at_ms`, optional\r\n`ttl_ms`, optional receiving-endpoint `audience`, optional `origin`, optional\r\nowning `adapter`, and `short_alias`. Lifetime and audience are per-entry,\r\nindependent of kind: live files and directories can carry them as well as\r\nattachment snapshots (ADR-0058 Amendment 1). W0's explicit registrations leave\r\nboth unset; helper registration, expiry, and audience enforcement belong to W2.\r\nRemoval deletes only the live entry, never its name history or source bytes.\r\nA retired name may be reclaimed only by its prior absolute path with the same\r\nkind. Other paths still consume the next suffix; reclamation never rewinds\r\nthat counter (ADR-0057 Amendment 2).\r\n\r\nEach active base adapter has one directory entry referring to the core-created\r\n`adapters/<adapter>/web/`, never its install tree. Core creates the checked root\r\nbefore committing registration, including while the daemon is stopped; only\r\nthe daemon writes serving assignments, at startup and after adapter changes.\r\nThe adapter facet `/<node>/a/<adapter>/` always names the entry. Its optional\r\nshort alias addresses that same entry; no second row, profile root, or directory\r\nis created. Redirected roots and symlinks escaping the subtree are refused.\r\n\r\nRepeated reconciliation and adapter updates preserve a live entry and alias;\r\nchanges to the alias declaration apply after deactivation/reactivation.\r\nDeactivation or removal retires the entry while retaining the root and all\r\noutput bytes. With the same declaration, reactivation reclaims the root's prior\r\nserved name and alias from ownership history. Updates never replace this\r\noutput with files from an adapter package.\r\n\r\nBack up this registry with any adapter output roots you need to preserve.\r\nExplicit references point outside SPT_HOME too: copying SPT_HOME alone does not\r\ncopy their bytes or relocate their absolute paths. Losing the registry loses\r\nthe history which prevents an old URL from naming a different resource.\r\n\r\n## tracked/ — the context git repo\r\n\r\n- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing (\"which endpoint is most responsible for this task\"). *Mechanics (D6a):* the object DB is a **bare seed repo at `tracked/.seed.git`**; `agents/<id>/` and `projects/<project_id>/` are **linked worktrees** of their branches, managed by `spt-store::{branchstore,contextstore}` over the system git binary (ADR-0013). Each branch seeds as a parentless empty-tree root; cross-node sync joins histories at first contact.\r","truncated":true,"truncatedBy":"lines","totalLines":236,"totalBytes":3501,"outputLines":48,"outputBytes":3501,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":376,"displayContent":{"text":"## Layout\n…\n### Registry snapshots + the resource-advertisement blurb (D9)\n…\n\r\n## serve/registry — authoritative serving assignments\r\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n\r\n`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\r\ncache. The daemon is its single writer and publishes durable atomic snapshots;\r\nthe HTTP listener and listing read those snapshots. Only a genuinely missing\r\nfile initializes an empty registry. Unreadable, corrupt, unsupported, or\r\ninconsistent history refuses the operation rather than recycling stale URLs.\r\n\r\nSchema 1 carries `entries`, `next_suffix` (per requested-name family), and\r\n`name_owners` (every assigned name mapped to its prior absolute `path` and\r\n`kind`, including removed entries). Each entry has\r\n`id`, `kind`, absolute `path`, `served_name`, `registered_at_ms`, optional\r\n`ttl_ms`, optional receiving-endpoint `audience`, optional `origin`, optional\r\nowning `adapter`, and `short_alias`. Lifetime and audience are per-entry,\r\nindependent of kind: live files and directories can carry them as well as\r\nattachment snapshots (ADR-0058 Amendment 1). W0's explicit registrations leave\r\nboth unset; helper registration, expiry, and audience enforcement belong to W2.\r\nRemoval deletes only the live entry, never its name history or source bytes.\r\nA retired name may be reclaimed only by its prior absolute path with the same\r\nkind. Other paths still consume the next suffix; reclamation never rewinds\r\nthat counter (ADR-0057 Amendment 2).\r\n\r\nEach active base adapter has one directory entry referring to the core-created\r\n`adapters/<adapter>/web/`, never its install tree. Core creates the checked root\r\nbefore committing registration, including while the daemon is stopped; only\r\nthe daemon writes serving assignments, at startup and after adapter changes.\r\nThe adapter facet `/<node>/a/<adapter>/` always names the entry. Its optional\r\nshort alias addresses that same entry; no second row, profile root, or directory\r\nis created. Redirected roots and symlinks escaping the subtree are refused.\r\n\r\nRepeated reconciliation and adapter updates preserve a live entry and alias;\r\nchanges to the alias declaration apply after deactivation/reactivation.\r\nDeactivation or removal retires the entry while retaining the root and all\r\noutput bytes. With the same declaration, reactivation reclaims the root's prior\r\nserved name and alias from ownership history. Updates never replace this\r\noutput with files from an adapter package.\r\n\r\nBack up this registry with any adapter output roots you need to preserve.\r\nExplicit references point outside SPT_HOME too: copying SPT_HOME alone does not\r\ncopy their bytes or relocate their absolute paths. Losing the registry loses\r\nthe history which prevents an old URL from naming a different resource.\r\n\r\n## tracked/ — the context git repo\r\n\r\n- A local **git repository** (carried forward from modern SPT's mechanism). Two branch views: **`a-<endpoint_id>`** (per-agent) and **`p-<project_id>`** (per-project). The per-project tree lets any agent synthesize a broader context from *other* agents' contexts in the same project, and supports query-routing (\"which endpoint is most responsible for this task\"). *Mechanics (D6a):* the object DB is a **bare seed repo at `tracked/.seed.git`**; `agents/<id>/` and `projects/<project_id>/` are **linked worktrees** of their branches, managed by `spt-store::{branchstore,contextstore}` over the system git binary (ADR-0013). Each branch seeds as a parentless empty-tree root; cross-node sync joins histories at first contact.\r\n…\n","startLine":13,"lineNumbers":[13,null,121,null,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,null,191]},"fileSize":36533,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\STORAGE.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":376,"totalBytes":3501,"outputLines":48,"outputBytes":3501,"shownRange":{"start":141,"end":188},"nextOffset":189}}},"isError":false,"timestamp":1789402528172}}
{"type":"message","id":"13c90ec1","parentId":"d41e615e","timestamp":"2026-09-14T16:15:28.242Z","message":{"role":"toolResult","toolCallId":"call_arguJAAm9C69h6NrmkqHzLgq|fc_080dc0728d55fa10016aa81da00cb087d2bea2427a5a26675b","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#F7C6]\n141:## 4. Wire / transport\n…\n173:### 4.6 Addressable-id charset reserves the address delimiters\n174:<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\n175:- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\n176:- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\n177:- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\n178:\n179:### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n180:<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n181:- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n182:- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n183:- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n184:\n185:### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\n186:<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n187:- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n188:- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\n189:- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n190:\n191:### 4.9 SQLite stores must create their parent dir — SQLite won't\n192:<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n193:- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\n194:- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\n195:- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\n196:\n197:### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\n198:<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\n199:- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n200:- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n201:- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\n202:- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\n203:- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\n204:\n205:### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\n206:<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\n207:- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\n208:- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\n209:- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\n210:- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\n211:- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\n212:\n213:---\n214:\n215:## 5. Platform-specific\n216:\n217:### 5.1 Windows PID recycling false positives\n218:- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\n219:- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\n220:- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\n221:- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\n222:\n223:### 5.2 Windows EBUSY on atomic rename\n224:- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\n225:- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\n226:- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\n227:- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\n228:\n229:### 5.3 Git/subprocess timeout stamping\n230:- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\n231:- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\n232:- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\n233:- **Sister cite:** `src/common/git.rs`.\n234:\n235:### 5.4 Windows UNC prefix in serialized paths\n236:- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\n237:- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\n238:- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\n239:- **Sister cite:** `src/common/owlery.rs:377-384`.\n240:\n241:### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\n242:- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\n243:- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\n244:- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\n245:- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\n246:\n247:<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\n248:### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\n…\n254:\n…\n371:\n\n[Showing lines 174-248 of 1132. Use :249 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\r\n- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\r\n- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\r\n- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r","truncated":true,"truncatedBy":"lines","totalLines":959,"totalBytes":13260,"outputLines":75,"outputBytes":13260,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1132,"displayContent":{"text":"## 4. Wire / transport\n…\n### 4.6 Addressable-id charset reserves the address delimiters\n<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\r\n- **Failure:** a bare endpoint id that contains `:` or `@` (or a path separator / whitespace / control char) makes the canonical qualified address `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) ambiguous to parse, and lets a name smuggle into a perch directory path. Once permissive ids exist in the wild, tightening later needs a migration.\r\n- **Invariant:** every addressable id/name is validated to `[A-Za-z0-9_-]` + Hiragana/Katakana/CJK only, length `1..=64`, **at every creation seam** (`ready` start, `api bind`, `api listen`, `api worker-start`). `:` and `@` are permanently reserved as address delimiters; reads of existing perches are never re-validated. Enforce now (pre-M3/M4) so no permissive id-data accumulates.\r\n- **spt-core mapping:** `spt_proto::id::validate_endpoint_id`; called at the four creation seams. The existing Psyche (`<parent>-psyche`) / Worker (`<parent>-w<N>`) suffix scheme uses only `-` + alphanumerics, so composite ids validate.\r\n\r\n### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\r\n<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\r\n- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\r\n- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\r\n- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\r\n\r\n### 4.8 Registry merge ordered by epoch, never wall-clock (red-team #8)\r\n<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\r\n- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\r\n- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-clock is at most a human tiebreaker hint inside a flagged conflict, never the ordering authority. The same epoch source unifies with the D6 sync-precedence concurrent-write detection (#7).\r\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\r\n\r\n### 4.9 SQLite stores must create their parent dir — SQLite won't\r\n<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\r\n- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the hfenduleam CI leg twice (2026-06-03/04, four spt-msg unit tests at once on the second strike) before being run to ground; a slow runner filesystem (AV scanning fresh dirs) widens the window but is not the cause.\r\n- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\r\n- **spt-core mapping:** `spt_store::registry::open_registry` (`create_dir_all(owlery)` before open). `spt_store::spool::open_spool_at` already creates its perch dir. Any future SQLite store must do the same — pair this with the 4.7 pragma ordering on every new store.\r\n\r\n### 4.10 Dead node identities leave immortal registry rows  `[REQ-HAZARD-REGISTRY-GHOST-ROWS]`\r\n<!-- [doc->REQ-HAZARD-REGISTRY-GHOST-ROWS] -->\r\n- **Failure:** the registry's only superseding mechanism is the per-`(endpoint_id, node)` epoch lease (4.8) — a row is replaced only by a newer row *from the same node*. When a node identity dies permanently (machine retired, or `node.key` regenerated so the \"node\" never speaks again), its rows are never superseded and never expire: they sit in the in-memory registries and the `identity/registry/<subnet>.json` snapshots forever. A bare-id send then resolves the same endpoint id on both the live and the dead identity and refuses with a **phantom `AcrossNodes` ambiguity** — unfixable by the user, because no qualifier reaches a node that no longer exists. Hit live in the M7 acceptance run (2026-06-06): gravity paired under two identities (09ef…, then 03854a… af…\n- **Invariant:** registry rows authored by a **silent** peer node decay: a node not *heard* (admitted inbound feed — the M7 D2 heard-map, REQ-SUBNET-1) within the eviction window (`registry_evict_after_ms`, default 300s ≈ 10 default pump cadences) has its rows **evicted** from every subnet registry, snapshots rewritten. Own rows never decay (the node always hears itself implicitly — it authors them each pump tick). Eviction is safe under the lease: v1 has **no transitive gossip**, so any future update for a node comes from that node itself, alive, re-inserting from its durable `EpochSource` within one cadence — there is no lagging third-party replay to mis-order against. A merely-offline node loses its rows after the window and reconverges on return; meanwhi…\n- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::evict_nodes` (model) + `spt_daemon::registryhost::RegistryHost::evict_silent_peers` (heard-map TTL) driven from the registry pump tick (`peerloop`). Trust rows are NOT auto-evicted (trust is a user decision; a stale trust row only costs dead dials) — pruning those is a separate verb.\r\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Ghost registry row eviction\"); the AMBIGUOUS render fix rode along.\r\n- **Mesh note (ADR-0017, 2026-06-08):** the subnet mesh **preserves** this invariant rather than superseding it. \"No transitive gossip\" sharpens to **no transitive *row* gossip** — the mesh relays only the member *roster* (discovery), while registry **rows stay own-authored and are fetched directly** from each member over a handshake. So \"any future update for a node comes from that node itself, alive\" still holds and the eviction lease is untouched. (The plan's rejected alternative — signed transitive *row* relay — would have broken this; roster-only relay was chosen precisely to keep it.)\r\n\r\n### 4.11 Advertisement-epoch reset strands a node  `[REQ-HAZARD-EPOCH-RESET]`\r\n<!-- [doc->REQ-HAZARD-EPOCH-RESET] -->\r\n- **Failure:** a node whose advertisement-epoch counter resets (the durable `EpochSource` file lost/recreated) re-advertises with LOW epochs; peers hold a higher last-seen epoch for that `(endpoint, node)` lease and drop every fresh row as **stale** — the node advertises into a void until its counter outruns its own history. Nothing renders the cause: the node looks healthy locally, peers simply never update.\r\n- **Invariant (mitigation by construction, common case):** the common trigger — a full reinstall / identity regeneration — is covered by the **re-pair trust overwrite** (M8 decision 13, REQ-SUBNET-7): a completed ceremony presenting the same label + machine id evicts the superseded identity's trust AND registry rows on the seed-holder, and the peer-side epoch memory **dies with the deleted row** — the re-paired node's fresh epochs land on a clean lease. M8 acceptance 7 verifies this explicitly (the epoch sub-check).\r\n- **Residual (documented, guard deferred):** the narrow slice — epoch file lost while the node *identity* is kept (manual state surgery, partial restore from backup) — has no guard; it waits for a field hit before one is designed (M8 decision 24). `REQ-HAZARD-EPOCH-RESET` is minted inactive (TRACEABILITY rule 5) as the tracking hook. If hit: symptoms are one node's endpoints frozen-stale on every peer while its own views are fresh; recovery today is re-pairing the node (rides the common-case eviction above).\r\n- **spt-core mapping:** epoch mint = `spt_store::epoch::EpochSource` (`identity/epoch.json`); the lease = the per-`(endpoint, node)` epoch compare in `spt_net::net::registry`; the eviction that clears peer-side epoch memory = `registryhost::repair_evict_superseded` + `RegistryHost::consume_repair_evictions`.\r\n- **Source:** minted at M8 ratification (decision 24), recognized as a class during the 2026-06-07 pump diagnosis / re-pair overwrite design — not yet field-hit in its residual form.\r\n\r\n---\r\n\r\n## 5. Platform-specific\r\n\r\n### 5.1 Windows PID recycling false positives\r\n- **Failure:** recycled PID reads alive for the wrong process → orphan misclassification.\r\n- **Invariant:** anchor liveness on the stable parent/harness PID; minimal info.json for supervisor-owned perches; mtime grace window (≥60s) masks transient mismatches.\r\n- **spt-core mapping:** broker-held handles supersede PID polling for spt-hosted sessions; keep the grace window for harness-hosted.\r\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; `src/common/list_filter.rs:168-175`.\r\n\r\n### 5.2 Windows EBUSY on atomic rename\r\n- **Failure:** `fs::rename` fails while a handle is (recently) held → registry/marketplace update fails.\r\n- **Invariant:** tmp-write + atomic-rename with retry/backoff; best-effort side-fail; tolerate transient EBUSY.\r\n- **spt-core mapping:** all on-disk state writes (registry, trust store, spool checkpoints) use this pattern. Self-update binary swap on Windows especially.\r\n- **Sister cite:** CHANGELOG \"EBUSY\"; `src/common/owlery.rs` atomic_write.\r\n\r\n### 5.3 Git/subprocess timeout stamping\r\n- **Failure:** a hung subprocess (git on slow net) blocks the supervisor indefinitely.\r\n- **Invariant:** every metadata-producing subprocess has a timeout; timeout yields `None` + rate-limited stderr, never a hang.\r\n- **spt-core mapping:** generalize to all manifest-declared harness invocations (delegated commands, adapter updates) — timeouts mandatory.\r\n- **Sister cite:** `src/common/git.rs`.\r\n\r\n### 5.4 Windows UNC prefix in serialized paths\r\n- **Failure:** canonicalized `\\\\?\\C:\\...` serializes to `//?/C:/...` and fails `read_to_string`.\r\n- **Invariant:** strip the `\\\\?\\` UNC prefix after backslash→forward-slash conversion; serialized path attrs must be directly consumable.\r\n- **spt-core mapping:** any path crossing the wire (file-drop EVENTs, off-node file transfer per ADR-0003) needs canonical normalization at the `spt-proto` boundary.\r\n- **Sister cite:** `src/common/owlery.rs:377-384`.\r\n\r\n### 5.5 ConPTY withholds output until DSR is answered  `[REQ-HAZARD-CONPTY-DSR]`\r\n- **Failure:** a broker reading a ConPTY master sees only the 4-byte startup query `ESC [ 6 n` and then nothing — the child looks hung/silent but is producing output normally. ConPTY blocks all child stdout until the terminal answers the cursor-position query.\r\n- **Invariant:** every ConPTY reader auto-answers DSR (`ESC [ 6 n` → write `ESC [ 1;1 R`, or a real cursor position) on the PTY writer. Secondary: a ConPTY master does not EOF while the writer is held, so read loops drain on a thread and never gate exit on a blocking `read()`.\r\n- **spt-core mapping:** `spt-term` broker PTY reader (ADR-0004). Brand-new to spt-core — not in the sister project (it never hosted ConPTY directly).\r\n- **Source:** Spike #1 (`docs/spikes/SPIKE-01-broker-handoff.md`); reproduced with both a Rust child and `cmd.exe`.\r\n\r\n<!-- [doc->REQ-HAZARD-DETACHED-PIPE-INHERIT] -->\r\n### 5.6 Windows detached children inherit a captured caller's pipe  `[REQ-HAZARD-DETACHED-PIPE-INHERIT]`\r\n…\n\n…\n","startLine":141,"lineNumbers":[141,null,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,null,254,null,371]},"fileSize":360311,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1132,"totalBytes":13260,"outputLines":75,"outputBytes":13260,"shownRange":{"start":174,"end":248},"nextOffset":249},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789402528241}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_sr6pSgBALYd5bVcQLchOVj6u|fc_080dc0728d55fa10016aa81daf9a0c87d2ab2c66ad8619a715","toolName":"grep","startedAt":"2026-09-14T16:15:43.700Z","args":{"path":".worktrees/308-registry-process-lock/traceable-reqs.toml"},"intent":"Locating serving requirement contracts"},"id":"8e5fc0df","parentId":"13c90ec1","timestamp":"2026-09-14T16:15:43.700Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MELjyDz725upxqsbAcBqlpSZ|fc_080dc0728d55fa10016aa81daf9a1c87d2bb1033b468471186","toolName":"grep","startedAt":"2026-09-14T16:15:43.701Z","args":{"path":".worktrees/308-registry-process-lock/CONTEXT.md;.worktrees/308-registry-process-lock/PRD.md;.worktrees/308-registry-process-lock/ROADMAP.md;.worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md;.…"},"intent":"Locating serving architecture references"},"id":"fff05a1b","parentId":"8e5fc0df","timestamp":"2026-09-14T16:15:43.701Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_y9R1Ea1pklarJQbiluYy02uu|fc_080dc0728d55fa10016aa81daf9a2487d29adf21583000ab91","toolName":"glob","startedAt":"2026-09-14T16:15:43.701Z","args":{"path":".worktrees/308-registry-process-lock/docs/adr/*0057*;.worktrees/308-registry-process-lock/docs/adr/*0058*"},"intent":"Locating serving decision records"},"id":"34706838","parentId":"fff05a1b","timestamp":"2026-09-14T16:15:43.701Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nbK7eRwrLew9wDuCDibMs4FY|fc_080dc0728d55fa10016aa81daf9a2c87d2bcb054d4c51feae0","toolName":"grep","startedAt":"2026-09-14T16:15:43.701Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs"},"intent":"Locating reaper save decisions"},"id":"631ab457","parentId":"34706838","timestamp":"2026-09-14T16:15:43.701Z"}
{"type":"message","id":"978f6df5","parentId":"631ab457","timestamp":"2026-09-14T16:15:43.701Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Checking relevant source sections**"},{"type":"toolCall","id":"call_sr6pSgBALYd5bVcQLchOVj6u|fc_080dc0728d55fa10016aa81daf9a0c87d2ab2c66ad8619a715","name":"grep","arguments":{"i":"Locating serving requirement contracts","pattern":"REQ-WEB-(SERVING-REGISTRY|ATTACHMENT-PULL|ENTRY-AUDIENCE)|REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE","path":".worktrees/308-registry-process-lock/traceable-reqs.toml","case":true,"gitignore":false,"skip":null},"intent":"Locating serving requirement contracts"},{"type":"toolCall","id":"call_MELjyDz725upxqsbAcBqlpSZ|fc_080dc0728d55fa10016aa81daf9a1c87d2bb1033b468471186","name":"grep","arguments":{"i":"Locating serving architecture references","pattern":"serving|attachment|ADR-0057|ADR-0058","path":".worktrees/308-registry-process-lock/CONTEXT.md;.worktrees/308-registry-process-lock/PRD.md;.worktrees/308-registry-process-lock/ROADMAP.md;.worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md;.worktrees/308-registry-process-lock/docs/TRACEABILITY.md","case":false,"gitignore":false,"skip":null},"intent":"Locating serving architecture references"},{"type":"toolCall","id":"call_y9R1Ea1pklarJQbiluYy02uu|fc_080dc0728d55fa10016aa81daf9a2487d29adf21583000ab91","name":"glob","arguments":{"i":"Locating serving decision records","path":".worktrees/308-registry-process-lock/docs/adr/*0057*;.worktrees/308-registry-process-lock/docs/adr/*0058*","hidden":true,"gitignore":false,"limit":20},"intent":"Locating serving decision records"},{"type":"toolCall","id":"call_nbK7eRwrLew9wDuCDibMs4FY|fc_080dc0728d55fa10016aa81daf9a2c87d2bcb054d4c51feae0","name":"grep","arguments":{"i":"Locating reaper save decisions","pattern":"fn (apply_at|reap|reconcile)|save|remove_file|is_empty","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","case":false,"gitignore":false,"skip":null},"intent":"Locating reaper save decisions"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":8270,"output":425,"cacheRead":27264,"cacheWrite":0,"totalTokens":35959,"reasoningTokens":31,"cost":{"input":0.08270000000000001,"output":0.02125,"cacheRead":0.027264,"cacheWrite":0,"total":0.13121400000000003}},"stopReason":"toolUse","timestamp":1789402528297,"responseId":"resp_080dc0728d55fa10016aa81da083ec87d28f677357e7114e90","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81da2e2f087d28855963e193abdf7","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB2vDcE1dDPwTb6YSyWLiDUgIboF6BD1-p_eMg28kWHgNiV88EP634huH6Qgpz_W0DaVCX8lO9ADoFnfETvXG_f-o6tQs4w9B_xLZZtkaJ3tpwQ-fL_3zXDVC4tLJYEWYJyfn3tBejkypCG1pcL3iJX-k7Ak0joFYqhfqXlb41DSw8CHNczCeugTUuIRiKgEmugDZNcrxQPUg2jDMs2mCQEN435Xq0yBszBRzbEv4gCPXfsR2HhluUvlVKjpjt8HjbjuGy07I9Qx-q-erT19vyfSd4_n8qQsfWl6HJIpTgki9nYOvEMIDb7DXyL1F8jsySv2WK_wEx-wDzgsWVpQcEnFTBUdrL9di0Gmi8Icd4POd_0nETvNM-IgPCtqw84jRFtOKP2zzWjOpz0YiT0NJ00bQDXe4cU6Mm3tZjSi11Xeiv-QqcBbsHtAf5L3B8b3IcmLFo2TpOyLzzXNUnF92AV1DhqJgLhiQ3elEiv-5Lxuz58Sz19QEsndvn3P3tkY5wQ-UjCYQVMuNgyrBf_wTyJFQ49xNYHyClT__eWB164yuWV-L2LxozEeTXj8RJCgenGjCYoYZNON3BnECKyfQRQULkr3RssnYT6z2NT6pKIC8U0f6wL1DcIkZ3O45FzCwz8JPj3kKUXu3F_fNTaePIhb8CdnYN4KyRRjGLiHi7eEs7G6NXSYaxgow2yR3eRTh-aNwqyPDpHVUM44SKYe0c2fq3DQY2tAowltYGKC5ia5cTOFA4EXDtfOEbbeVhtF6AAlNrhx3tLQxxPiza1BgXxi4kPLqXkvnj-v6oxGCPyKLJjVw52Vd4c504QBVkSS62JPn4SIiJ9GnndVQFKf0AiYv16A7yG2oUHBXCNBwVs-3kB6GUnsLLVG9vTGDpZ22tBMY18F03MQCKT9HiMnO4f89vxRcM6k118jwq4R__Z8IvaeoP6igH_XEuJyUdbysyqovOMyj4SBggH17lPioZYLB5j5I-Gt934-b17giz7pAWxVYlziiv8r7VTUzNR2rJ0MgM81lGmrVIc6nsipulnMe-qN0RwfCKaAonS58UZCPzlY67LGHfGQoqf1pdLZ65ziKt5FJNlDcw1Q8V7J_xxJ3vxfvTMn5pf3JM-fy4AnJ3nfEp_BIj9LbUfehU1NhuwnT1Q4nfaTMzZJcoQxazhdZM5yUouHYK-5zQ-AYe746lKHJe5W9VlI8CyUFIwXp921gynNZcOXxHsBEKl-A9uDlSTnsrgLTf_-713K1mb-NQi9pS769IJHn7dbe3xN3enOJobJtWpcuG46HEHFyCD0W6b65uol1zXdbouSSqfA-uxML_lEezNeIKeoVgX8uZ8q8uDh9W7-0lfqKJtDGosKIM-1hhzfI-oWYnJUF-nZjrG1YVvBtAZgHLUDjTY1AdmFOEGgrCdBxTR9l1PbKkcpZY0idLktNlL_QIrwA-bdr_il-9RFpJ1HISSAIMhAwCdg5Y5_85L2DEtZrvCbgd4wQVVu3gfAQDP-blPXs3E0Tz7oHeHzyMqhGL_rCv_0cyUl","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[{"type":"summary_text","text":"**Checking relevant source sections**"}],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81daf9a0c87d2ab2c66ad8619a715","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating serving requirement contracts\",\"pattern\":\"REQ-WEB-(SERVING-REGISTRY|ATTACHMENT-PULL|ENTRY-AUDIENCE)|REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\",\"path\":\".worktrees/308-registry-process-lock/traceable-reqs.toml\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_sr6pSgBALYd5bVcQLchOVj6u","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81daf9a1c87d2bb1033b468471186","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating serving architecture references\",\"pattern\":\"serving|attachment|ADR-0057|ADR-0058\",\"path\":\".worktrees/308-registry-process-lock/CONTEXT.md;.worktrees/308-registry-process-lock/PRD.md;.worktrees/308-registry-process-lock/ROADMAP.md;.worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md;.worktrees/308-registry-process-lock/docs/TRACEABILITY.md\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_MELjyDz725upxqsbAcBqlpSZ","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81daf9a2487d29adf21583000ab91","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating serving decision records\",\"path\":\".worktrees/308-registry-process-lock/docs/adr/*0057*;.worktrees/308-registry-process-lock/docs/adr/*0058*\",\"hidden\":true,\"gitignore\":false,\"limit\":20}","call_id":"call_y9R1Ea1pklarJQbiluYy02uu","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"glob","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81daf9a2c87d2bcb054d4c51feae0","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating reaper save decisions\",\"pattern\":\"fn (apply_at|reap|reconcile)|save|remove_file|is_empty\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs;.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs\",\"case\":false,\"gitignore\":false,\"skip\":null}","call_id":"call_nbK7eRwrLew9wDuCDibMs4FY","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":15400.30450001359,"ttft":2610.65870000422,"completedAt":1789402543698,"contextSnapshot":{"promptTokens":35534,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"message","id":"e0975946","parentId":"978f6df5","timestamp":"2026-09-14T16:15:43.708Z","message":{"role":"toolResult","toolCallId":"call_y9R1Ea1pklarJQbiluYy02uu|fc_080dc0728d55fa10016aa81daf9a2487d29adf21583000ab91","toolName":"glob","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/docs/adr/\n0058-attachments-are-pull-model.md\n0057-serving-registry-is-the-primitive.md"}],"details":{"scopePath":".worktrees/308-registry-process-lock/docs/adr/*0057*, .worktrees/308-registry-process-lock/docs/adr/*0058*","fileCount":2,"files":[".worktrees/308-registry-process-lock/docs/adr/0058-attachments-are-pull-model.md",".worktrees/308-registry-process-lock/docs/adr/0057-serving-registry-is-the-primitive.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1789402543707}}
{"type":"message","id":"a543a344","parentId":"e0975946","timestamp":"2026-09-14T16:15:43.713Z","message":{"role":"toolResult","toolCallId":"call_sr6pSgBALYd5bVcQLchOVj6u|fc_080dc0728d55fa10016aa81daf9a0c87d2ab2c66ad8619a715","toolName":"grep","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/traceable-reqs.toml#1D67]\n 7563:[[requirements]]\n*7564:id = \"REQ-WEB-SERVING-REGISTRY\"\n 7565:title = \"THE SERVING REGISTRY IS THE PRIMITIVE AND SERVED NAMES ARE STABLE AND DISAMBIGUATED (ADR-0057, releases#272/#248). The registry is the node's single enumerable list of what it exposes, and exposure is a DELIBERATE ACT of adding an entry -- so what-is-this-node-serving has exactly one answer. A served root (a dir entry an adapter declares in its manifest) is NOT a second mechanism: it is a registry entry of kind dir, which is what keeps the audit to one command. A drop-folder primitive was reject...\n 7566:required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W0 (todlando build 2026-09-06), releases#248. doc = serving/overview.md registry + served-name sections. impl = spt-store serve/registry format and read path, daemon single-writer, spt serve add|rm|list, manifest web_short_path served root. unit = one cell per served-name clause (ADR-0057 is clause-by-clause binding) + reference-served resolve-at-request-time. int = add/list/fetch/rm through the real server.\n 7567:\n...\n 7589:[[requirements]]\n*7590:id = \"REQ-WEB-ATTACHMENT-PULL\"\n*7591:title = \"AN ATTACHMENT IS A SNAPSHOT REGISTERED FOR SERVING AND THE MESSAGE CARRIES ONLY ITS URL (ADR-0058, releases#272/#246). `spt send --attachment <path>` COPIES THE BYTES AT SEND TIME into `$SPT_HOME/serve/snapshots/` and registers an entry of kind attachment, because a message's attachment is as immutable as the message -- this is the ONE place the serving registry does NOT resolve at request time, and it is exactly what distinguishes an attachment from a `serve add` file entry: an edit after the s...\n 7592:required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#246. doc = docs-site serving/attachments.md. impl = the snapshot store under $SPT_HOME/serve/snapshots, the send-path registration, the envelope attachments key, and the expiry reaper in spt-daemon livehost.rs reconcile_once (the 5s node-local tick that already runs worker_reap, which is the pulse ADR-0058 names) -- NOTE the ServedKind::Attachment variant and ServedEntry.ttl_ms ALREADY EXI...\n 7593:\n 7594:[[requirements]]\n...\n 7604:[[requirements]]\n*7605:id = \"REQ-WEB-ENTRY-AUDIENCE\"\n 7606:title = \"A SERVING ENTRY MAY NAME THE ONE ENDPOINT ALLOWED TO FETCH IT, AND THAT AUDIENCE IS ENFORCED WHEREVER THE ORIGIN IS PROVEN (ADR-0058 Amendment 1, operator-directed 2026-09-06, releases#272/#17). `audience` is a PER-ENTRY, KIND-INDEPENDENT field sitting beside ttl: ABSENT means the entry is reachable by anyone the WEB surface admits, which leaves REQ-WEB-ACCESS-SURFACE's default-on subnet posture UNCHANGED; PRESENT means the owner serves the entry ONLY to that endpoint id and answers everyone els...\n 7607:required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#17, ADR-0058 Amendment 1. MINTED IN THIS LANE and not in the JIT brief list: the #17 rider needs the per-entry narrowing as its own requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal category. doc = serving/overview.md entry-fields section. impl = the owner-side check in spt-daemon webproxy.rs serve_web beside the existing access_check, keyed on the handshake-pr...\n 7608:\n...\n 7724:[[requirements]]\n*7725:id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n 7726:title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paire...\n 7727:required_stages = [\"doc\", \"impl\", \"unit\", \"int\"] # PREP-ONLY draft: evidence authored in this lane; execution and milestone placement await doyle/operator GO.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/308-registry-process-lock/traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\traceable-reqs.toml","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":5,"fileCount":1,"files":[".worktrees/308-registry-process-lock/traceable-reqs.toml"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/traceable-reqs.toml","count":5}],"truncated":true,"displayContent":" 7563│[[requirements]]\n*7564│id = \"REQ-WEB-SERVING-REGISTRY\"\n 7565│title = \"THE SERVING REGISTRY IS THE PRIMITIVE AND SERVED NAMES ARE STABLE AND DISAMBIGUATED (ADR-0057, releases#272/#248). The registry is the node's single enumerable list of what it exposes, and exposure is a DELIBERATE ACT of adding an entry -- so what-is-this-node-serving has exactly one answer. A served root (a dir entry an adapter declares in its manifest) is NOT a second mechanism: it is a registry entry of kind dir, which is what keeps the audit to one command. A drop-folder primitive was reject...\n 7566│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W0 (todlando build 2026-09-06), releases#248. doc = serving/overview.md registry + served-name sections. impl = spt-store serve/registry format and read path, daemon single-writer, spt serve add|rm|list, manifest web_short_path served root. unit = one cell per served-name clause (ADR-0057 is clause-by-clause binding) + reference-served resolve-at-request-time. int = add/list/fetch/rm through the real server.\n 7567│\n     │...\n 7589│[[requirements]]\n*7590│id = \"REQ-WEB-ATTACHMENT-PULL\"\n*7591│title = \"AN ATTACHMENT IS A SNAPSHOT REGISTERED FOR SERVING AND THE MESSAGE CARRIES ONLY ITS URL (ADR-0058, releases#272/#246). `spt send --attachment <path>` COPIES THE BYTES AT SEND TIME into `$SPT_HOME/serve/snapshots/` and registers an entry of kind attachment, because a message's attachment is as immutable as the message -- this is the ONE place the serving registry does NOT resolve at request time, and it is exactly what distinguishes an attachment from a `serve add` file entry: an edit after the s...\n 7592│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#246. doc = docs-site serving/attachments.md. impl = the snapshot store under $SPT_HOME/serve/snapshots, the send-path registration, the envelope attachments key, and the expiry reaper in spt-daemon livehost.rs reconcile_once (the 5s node-local tick that already runs worker_reap, which is the pulse ADR-0058 names) -- NOTE the ServedKind::Attachment variant and ServedEntry.ttl_ms ALREADY EXI...\n 7593│\n 7594│[[requirements]]\n     │...\n 7604│[[requirements]]\n*7605│id = \"REQ-WEB-ENTRY-AUDIENCE\"\n 7606│title = \"A SERVING ENTRY MAY NAME THE ONE ENDPOINT ALLOWED TO FETCH IT, AND THAT AUDIENCE IS ENFORCED WHEREVER THE ORIGIN IS PROVEN (ADR-0058 Amendment 1, operator-directed 2026-09-06, releases#272/#17). `audience` is a PER-ENTRY, KIND-INDEPENDENT field sitting beside ttl: ABSENT means the entry is reachable by anyone the WEB surface admits, which leaves REQ-WEB-ACCESS-SURFACE's default-on subnet posture UNCHANGED; PRESENT means the owner serves the entry ONLY to that endpoint id and answers everyone els...\n 7607│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # ACTIVATED WEBSERVE W2 (todlando build 2026-09-07), releases#272/#17, ADR-0058 Amendment 1. MINTED IN THIS LANE and not in the JIT brief list: the #17 rider needs the per-entry narrowing as its own requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal category. doc = serving/overview.md entry-fields section. impl = the owner-side check in spt-daemon webproxy.rs serve_web beside the existing access_check, keyed on the handshake-pr...\n 7608│\n     │...\n 7724│[[requirements]]\n*7725│id = \"REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE\"\n 7726│title = \"Serving-registry writers in the broker, brain TTL reaper, and live-daemon test fixtures serialize the complete fresh-load, mutation, snapshot side effects, and durable publication across processes using one stable per-registry sentinel. Atomic replacement alone must not erase acknowledged registrations or persisted name-allocation history. Lock acquisition failure never permits an unlocked write, and process exit releases the lock. Both writer binaries must participate; a brain-only update paire...\n 7727│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"] # PREP-ONLY draft: evidence authored in this lane; execution and milestone placement await doyle/operator GO.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789402543713}}
{"type":"message","id":"8ad335c2","parentId":"a543a344","timestamp":"2026-09-14T16:15:43.724Z","message":{"role":"toolResult","toolCallId":"call_nbK7eRwrLew9wDuCDibMs4FY|fc_080dc0728d55fa10016aa81daf9a2c87d2bcb054d4c51feae0","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/crates/spt-daemon/src/\n## servehost.rs#5F5C\n 1://! Local serving-registry mutations. The daemon is the only writer; CLI\n*2://! clients use the existing local control transport and never save the store.\n 3:\n 4:use std::io;\n 5:use std::path::{Path, PathBuf};\n...\n 138:/// The daemon has a second registry writer: the live host's TTL reaper, which\n*139:/// does load -> retain -> save on its own pulse. Without this it raced every\n*140:/// `serve add` — an entry registered between the reaper's load and its save was\n 141:/// clobbered by the reaper's stale snapshot and silently lost, on a 5s tick,\n 142:/// forever. The lock was already here; only this door into it was missing.\n 143:///\n...\n 157:// [impl->REQ-WEB-SERVING-REGISTRY]\n*158:pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n 159:    // [impl->REQ-WEB-URL-BOUND-PORT]\n 160:    if matches!(&request, ServeRequest::DocsStatus) {\n 161:        return Ok(ServeResult::DocsStatus { port: crate::docshost::bound_docs_port() });\n...\n 189:            )?;\n*190:            registry.save_at(&path)?;\n 191:            Ok(ServeResult::Entry { entry })\n 192:        }\n 193:        // [impl->REQ-WEB-ATTACHMENT-PULL]\n...\n 224:                    // orphan snapshot would live until the store is wiped by hand.\n*225:                    let _ = std::fs::remove_file(&snapshot);\n 226:                    return Err(error);\n 227:                }\n 228:            };\n*229:            if let Err(error) = registry.save_at(&path) {\n 230:                // Same reasoning as the refused registration above: an entry that\n 231:                // did not persist names nothing, so its bytes must not survive.\n*232:                let _ = std::fs::remove_file(&snapshot);\n 233:                return Err(error);\n 234:            }\n 235:            Ok(ServeResult::Attached { entry, bytes })\n...\n 242:                .ok_or_else(|| io::Error::other(\"SERVE_SCOPE_LOST: the entry vanished between add and scope\"))?;\n*243:            registry.save_at(&path)?;\n 244:            Ok(ServeResult::Entry { entry: scoped })\n 245:        }\n 246:        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\n...\n 260:                    if previous_id.as_deref() != Some(entry.id.as_str()) {\n*261:                        registry.save_at(&path)\n 262:                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\n 263:                    }\n 264:                    Ok(ServeResult::Entry { entry })\n...\n 271:            })?;\n*272:            registry.save_at(&path)?;\n 273:            Ok(ServeResult::Removed { entry })\n 274:        }\n 275:        ServeRequest::List => Ok(ServeResult::Listed {\n...\n 284:            reconcile_registry(home, &mut registry, now_ms)?;\n*285:            registry.save_at(&path)?;\n 286:            Ok(ServeResult::Listed {\n 287:                entries: registry.entries().cloned().collect(),\n 288:            })\n...\n 348:// [impl->REQ-WEB-SERVING-REGISTRY]\n*349:pub fn reconcile_at(home: &Path, now_ms: u64) -> io::Result<()> {\n 350:    apply_at(home, ServeRequest::Reconcile, now_ms).map(|_| ())\n 351:}\n 352:\n*353:fn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\n 354:    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\n 355:    let retired: Vec<_> = registry\n 356:        .entries()\n...\n 376:// [impl->REQ-WEB-SERVING-REGISTRY]\n*377:pub fn reconcile(name: &str) -> io::Result<()> {\n 378:    match call(name, ServeRequest::Reconcile)? {\n 379:        ServeResult::Listed { .. } => Ok(()),\n 380:        ServeResult::Refused { error } => Err(io::Error::other(error)),\n...\n 614:        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"edited\");\n*615:        std::fs::remove_file(&source).unwrap();\n 616:        assert_eq!(get().status(), hyper::StatusCode::NOT_FOUND);\n 617:        assert!(matches!(\n 618:            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap(),\n...\n 691:    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n*692:    /// pulse: load, retain, save. Outside the write lock, an entry registered\n*693:    /// between its load and its save is overwritten by its stale snapshot and\n 694:    /// silently lost — no error, no log line, once every five seconds forever.\n 695:    ///\n 696:    /// The reaping thread holds the lock across a deliberate pause, so the\n 697:    /// adding thread MUST be waiting on it. Without the lock the add lands in\n*698:    /// that window and the reaper's save drops it; with the lock the two passes\n 699:    /// serialize and both outcomes survive. No platform gate: the pause is a\n 700:    /// delay, not a scheduling assumption, and the assertion only holds one way.\n 701:    // [unit->REQ-WEB-ATTACHMENT-PULL]\n...\n 718:            registry.scope_entry(&entry.id, Some(1), None, None).expect(\"give it a spent ttl\");\n*719:            registry.save_at(&registry_path).unwrap();\n 720:        })\n 721:        .unwrap();\n 722:\n## livehost.rs#996D\n 81:\n*82:    pub fn is_empty(&self) -> bool {\n 83:        self.len() == 0\n 84:    }\n 85:\n...\n 144:// [impl->REQ-DAEMON-1]\n*145:pub fn reconcile_once(\n 146:    owlery: &Path,\n 147:    registered: &[(AdapterRecord, Manifest)],\n 148:    adapters_dir: &Path,\n...\n 276:// [impl->REQ-WORKER-REAP]\n*277:fn reap_orphaned_workers(owlery: &Path, cfg: &DaemonConfig) {\n 278:    let ttl = spt_store::worker_reap::clamp_reap_ttl(cfg.worker_reap_ttl_secs);\n 279:    let now = spt_store::worker_reap::now_secs();\n 280:    for id in perch::list_self_perch_ids(owlery) {\n...\n 777:    }\n*778:    let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n 779:    let _ = spt_store::registry::unregister_address(id, owlery);\n 780:    // Going offline: wake the pump's registry leg so peers see the flip within\n 781:    // seconds, not a cadence (M8 decision 15, REQ-CONV-2) — the same courtesy\n...\n 791:\n*792:pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {\n 793:    let mut offlined = Vec::new();\n 794:    for id in perch::list_self_perch_ids(owlery) {\n 795:        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n...\n 1033:                reply.sessions.into_iter().map(|s| s.endpoint)\n*1034:                    .filter(|e| !e.is_empty()).collect()\n 1035:            ),\n 1036:            Err(e) => {\n 1037:                // No same-tick retry: the caller skips this census exactly as\n...\n 1174:    // `ready` marker) so no dead-pid phantom ready perch is left behind.\n*1175:    let _ = std::fs::remove_file(psyche_perch.join(\"info.json\"));\n*1176:    let _ = std::fs::remove_file(perch::resolve_ready_file(&psyche_id, ParentHint::Explicit(id)));\n 1177:    spt_proto::emit_line_err!(\n 1178:        \"LEGACY_PSYCHE_SWEEP_REAP:{id} pid={pid}: reaped stranded pre-W3 resident psyche \\\n 1179:         + cleared its stale `{psyche_id}` ready registration\"\n...\n 1268:/// atomic with the other, so the question is which crash heals. Deleting the\n*1269:/// file and then failing to save leaves an expired entry naming a file that is\n*1270:/// gone: the next tick finds it expired again and `remove_file` is idempotent,\n 1271:/// so the pair completes on its own. Saving first and then failing to delete\n 1272:/// orphans the bytes with no entry left to name them, and no later tick can\n 1273:/// repair that — it is exactly the unbounded growth this exists to prevent.\n...\n 1280:// [impl->REQ-WEB-ATTACHMENT-PULL]\n*1281:fn reap_expired_attachments() {\n*1282:    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n 1283:    // lock every other writer in this process holds, because this pass is a\n 1284:    // read-modify-write on a file `serve add` also writes: an entry registered\n*1285:    // between our load and our save would be clobbered by our stale snapshot\n 1286:    // and silently lost, on a 5s tick, forever.\n 1287:    let held = crate::servehost::with_registry_write(|| {\n 1288:        let registry_path = perch::serving_registry_file();\n...\n 1299:        let reaped = registry.reap_expired(now_ms());\n*1300:        if reaped.is_empty() {\n 1301:            return;\n 1302:        }\n 1303:        let snapshots = perch::serve_snapshots_dir();\n...\n 1313:            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n*1314:            match std::fs::remove_file(&entry.path) {\n 1315:                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n 1316:                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n 1317:                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n...\n 1319:        }\n*1320:        if let Err(err) = registry.save_at(&registry_path) {\n 1321:            // The bytes are gone and the entries are not. Say so rather than\n 1322:            // reporting a reap that only half happened; the next tick retries.\n*1323:            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n 1324:            return;\n 1325:        }\n 1326:        eprintln!(\n...\n 1784:            assert!(\n*1785:                offlined.is_empty(),\n 1786:                \"live-pid quirk row: stamps cleared but presence untouched\"\n 1787:            );\n 1788:\n...\n 1884:            assert!(\n*1885:                offlined.is_empty(),\n 1886:                \"an already-offline perch is not re-offlined (presence unchanged)\"\n 1887:            );\n 1888:\n...\n 1969:    #[test]\n*1970:    fn reconcile_reaps_orphaned_worker_and_spares_live_parent() {\n 1971:        with_home(|_| {\n 1972:            const DEAD_PID: u32 = 2_000_000_000;\n 1973:            let now = spt_store::worker_reap::now_secs();"}],"details":{"scopePath":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs, .worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":40,"fileCount":2,"files":[".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs",".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/servehost.rs","count":20},{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/308-registry-process-lock/crates/spt-daemon/src/\n## servehost.rs#5F5C\n   1│//! Local serving-registry mutations. The daemon is the only writer; CLI\n  *2│//! clients use the existing local control transport and never save the store.\n   3│\n   4│use std::io;\n   5│use std::path::{Path, PathBuf};\n    │...\n 138│/// The daemon has a second registry writer: the live host's TTL reaper, which\n*139│/// does load -> retain -> save on its own pulse. Without this it raced every\n*140│/// `serve add` — an entry registered between the reaper's load and its save was\n 141│/// clobbered by the reaper's stale snapshot and silently lost, on a 5s tick,\n 142│/// forever. The lock was already here; only this door into it was missing.\n 143│///\n    │...\n 157│// [impl->REQ-WEB-SERVING-REGISTRY]\n*158│pub(crate) fn apply_at(home: &Path, request: ServeRequest, now_ms: u64) -> io::Result<ServeResult> {\n 159│    // [impl->REQ-WEB-URL-BOUND-PORT]\n 160│    if matches!(&request, ServeRequest::DocsStatus) {\n 161│        return Ok(ServeResult::DocsStatus { port: crate::docshost::bound_docs_port() });\n    │...\n 189│            )?;\n*190│            registry.save_at(&path)?;\n 191│            Ok(ServeResult::Entry { entry })\n 192│        }\n 193│        // [impl->REQ-WEB-ATTACHMENT-PULL]\n    │...\n 224│                    // orphan snapshot would live until the store is wiped by hand.\n*225│                    let _ = std::fs::remove_file(&snapshot);\n 226│                    return Err(error);\n 227│                }\n 228│            };\n*229│            if let Err(error) = registry.save_at(&path) {\n 230│                // Same reasoning as the refused registration above: an entry that\n 231│                // did not persist names nothing, so its bytes must not survive.\n*232│                let _ = std::fs::remove_file(&snapshot);\n 233│                return Err(error);\n 234│            }\n 235│            Ok(ServeResult::Attached { entry, bytes })\n    │...\n 242│                .ok_or_else(|| io::Error::other(\"SERVE_SCOPE_LOST: the entry vanished between add and scope\"))?;\n*243│            registry.save_at(&path)?;\n 244│            Ok(ServeResult::Entry { entry: scoped })\n 245│        }\n 246│        ServeRequest::AddInputReference { path: source, receipt_id, audience, received_at_ms } => {\n    │...\n 260│                    if previous_id.as_deref() != Some(entry.id.as_str()) {\n*261│                        registry.save_at(&path)\n 262│                            .map_err(|error| io::Error::new(error.kind(), format!(\"INPUT_PATH_REGISTRY: {error}\")))?;\n 263│                    }\n 264│                    Ok(ServeResult::Entry { entry })\n    │...\n 271│            })?;\n*272│            registry.save_at(&path)?;\n 273│            Ok(ServeResult::Removed { entry })\n 274│        }\n 275│        ServeRequest::List => Ok(ServeResult::Listed {\n    │...\n 284│            reconcile_registry(home, &mut registry, now_ms)?;\n*285│            registry.save_at(&path)?;\n 286│            Ok(ServeResult::Listed {\n 287│                entries: registry.entries().cloned().collect(),\n 288│            })\n    │...\n 348│// [impl->REQ-WEB-SERVING-REGISTRY]\n*349│pub fn reconcile_at(home: &Path, now_ms: u64) -> io::Result<()> {\n 350│    apply_at(home, ServeRequest::Reconcile, now_ms).map(|_| ())\n 351│}\n 352│\n*353│fn reconcile_registry(home: &Path, registry: &mut ServingRegistry, now_ms: u64) -> io::Result<()> {\n 354│    let registered = spt_runtime::registry::registered(&home.join(\"adapters\"));\n 355│    let retired: Vec<_> = registry\n 356│        .entries()\n    │...\n 376│// [impl->REQ-WEB-SERVING-REGISTRY]\n*377│pub fn reconcile(name: &str) -> io::Result<()> {\n 378│    match call(name, ServeRequest::Reconcile)? {\n 379│        ServeResult::Listed { .. } => Ok(()),\n 380│        ServeResult::Refused { error } => Err(io::Error::other(error)),\n    │...\n 614│        assert_eq!(runtime.block_on(get().into_body().collect()).unwrap().to_bytes().as_ref(), b\"edited\");\n*615│        std::fs::remove_file(&source).unwrap();\n 616│        assert_eq!(get().status(), hyper::StatusCode::NOT_FOUND);\n 617│        assert!(matches!(\n 618│            apply_at(&home, input_reference(&source, \"r1\", \"agent\", now), now).unwrap(),\n    │...\n 691│    /// The TTL reaper is the daemon's SECOND registry writer, and it runs on a\n*692│    /// pulse: load, retain, save. Outside the write lock, an entry registered\n*693│    /// between its load and its save is overwritten by its stale snapshot and\n 694│    /// silently lost — no error, no log line, once every five seconds forever.\n 695│    ///\n 696│    /// The reaping thread holds the lock across a deliberate pause, so the\n 697│    /// adding thread MUST be waiting on it. Without the lock the add lands in\n*698│    /// that window and the reaper's save drops it; with the lock the two passes\n 699│    /// serialize and both outcomes survive. No platform gate: the pause is a\n 700│    /// delay, not a scheduling assumption, and the assertion only holds one way.\n 701│    // [unit->REQ-WEB-ATTACHMENT-PULL]\n    │...\n 718│            registry.scope_entry(&entry.id, Some(1), None, None).expect(\"give it a spent ttl\");\n*719│            registry.save_at(&registry_path).unwrap();\n 720│        })\n 721│        .unwrap();\n 722│\n## livehost.rs#996D\n   81│\n  *82│    pub fn is_empty(&self) -> bool {\n   83│        self.len() == 0\n   84│    }\n   85│\n     │...\n  144│// [impl->REQ-DAEMON-1]\n *145│pub fn reconcile_once(\n  146│    owlery: &Path,\n  147│    registered: &[(AdapterRecord, Manifest)],\n  148│    adapters_dir: &Path,\n     │...\n  276│// [impl->REQ-WORKER-REAP]\n *277│fn reap_orphaned_workers(owlery: &Path, cfg: &DaemonConfig) {\n  278│    let ttl = spt_store::worker_reap::clamp_reap_ttl(cfg.worker_reap_ttl_secs);\n  279│    let now = spt_store::worker_reap::now_secs();\n  280│    for id in perch::list_self_perch_ids(owlery) {\n     │...\n  777│    }\n *778│    let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n  779│    let _ = spt_store::registry::unregister_address(id, owlery);\n  780│    // Going offline: wake the pump's registry leg so peers see the flip within\n  781│    // seconds, not a cadence (M8 decision 15, REQ-CONV-2) — the same courtesy\n     │...\n  791│\n *792│pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {\n  793│    let mut offlined = Vec::new();\n  794│    for id in perch::list_self_perch_ids(owlery) {\n  795│        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);\n     │...\n 1033│                reply.sessions.into_iter().map(|s| s.endpoint)\n*1034│                    .filter(|e| !e.is_empty()).collect()\n 1035│            ),\n 1036│            Err(e) => {\n 1037│                // No same-tick retry: the caller skips this census exactly as\n     │...\n 1174│    // `ready` marker) so no dead-pid phantom ready perch is left behind.\n*1175│    let _ = std::fs::remove_file(psyche_perch.join(\"info.json\"));\n*1176│    let _ = std::fs::remove_file(perch::resolve_ready_file(&psyche_id, ParentHint::Explicit(id)));\n 1177│    spt_proto::emit_line_err!(\n 1178│        \"LEGACY_PSYCHE_SWEEP_REAP:{id} pid={pid}: reaped stranded pre-W3 resident psyche \\\n 1179│         + cleared its stale `{psyche_id}` ready registration\"\n     │...\n 1268│/// atomic with the other, so the question is which crash heals. Deleting the\n*1269│/// file and then failing to save leaves an expired entry naming a file that is\n*1270│/// gone: the next tick finds it expired again and `remove_file` is idempotent,\n 1271│/// so the pair completes on its own. Saving first and then failing to delete\n 1272│/// orphans the bytes with no entry left to name them, and no later tick can\n 1273│/// repair that — it is exactly the unbounded growth this exists to prevent.\n     │...\n 1280│// [impl->REQ-WEB-ATTACHMENT-PULL]\n*1281│fn reap_expired_attachments() {\n*1282│    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n 1283│    // lock every other writer in this process holds, because this pass is a\n 1284│    // read-modify-write on a file `serve add` also writes: an entry registered\n*1285│    // between our load and our save would be clobbered by our stale snapshot\n 1286│    // and silently lost, on a 5s tick, forever.\n 1287│    let held = crate::servehost::with_registry_write(|| {\n 1288│        let registry_path = perch::serving_registry_file();\n     │...\n 1299│        let reaped = registry.reap_expired(now_ms());\n*1300│        if reaped.is_empty() {\n 1301│            return;\n 1302│        }\n 1303│        let snapshots = perch::serve_snapshots_dir();\n     │...\n 1313│            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n*1314│            match std::fs::remove_file(&entry.path) {\n 1315│                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n 1316│                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n 1317│                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n     │...\n 1319│        }\n*1320│        if let Err(err) = registry.save_at(&registry_path) {\n 1321│            // The bytes are gone and the entries are not. Say so rather than\n 1322│            // reporting a reap that only half happened; the next tick retries.\n*1323│            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n 1324│            return;\n 1325│        }\n 1326│        eprintln!(\n     │...\n 1784│            assert!(\n*1785│                offlined.is_empty(),\n 1786│                \"live-pid quirk row: stamps cleared but presence untouched\"\n 1787│            );\n 1788│\n     │...\n 1884│            assert!(\n*1885│                offlined.is_empty(),\n 1886│                \"an already-offline perch is not re-offlined (presence unchanged)\"\n 1887│            );\n 1888│\n     │...\n 1969│    #[test]\n*1970│    fn reconcile_reaps_orphaned_worker_and_spares_live_parent() {\n 1971│        with_home(|_| {\n 1972│            const DEAD_PID: u32 = 2_000_000_000;\n 1973│            let now = spt_store::worker_reap::now_secs();"},"isError":false,"timestamp":1789402543724}}
{"type":"message","id":"d45aa651","parentId":"8ad335c2","timestamp":"2026-09-14T16:15:43.733Z","message":{"role":"toolResult","toolCallId":"call_MELjyDz725upxqsbAcBqlpSZ|fc_080dc0728d55fa10016aa81daf9a1c87d2bb1033b468471186","toolName":"grep","content":[{"type":"text","text":"# .worktrees/308-registry-process-lock/\n## CONTEXT.md#54AA\n 95:<!-- [doc->REQ-MANIFEST-4] -->\n*96:Once-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new ses...\n 97:_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\n 98:\n 99:**shell keyword hints** (releases#133, operator-GREENLIT; shape ruled 2026-08-30):\n...\n 200:**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running ho...\n*201:_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`). **Read that soft/hard contrast on the RECORD axis only** (ADR-0045): `stop` is *record*-preserving (spool.db + info.json survive) where `purge` wipes them. It says nothing about processes — on the **process** axis `stop` is hard: for a broker-hosted endpoint it reaps the session and its descendant subtr...\n 202:<!-- [doc->REQ-ENDPOINT-PURGE] -->\n 203:\n 204:- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n*205:- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n 206:- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n 207:\n 208:**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike. **This path is the Self's, and spt-core's echo-commune never writes i...\n...\n 394:\n*395:**Online/offline are mutually-exclusive processes:** a live shell binary (including a launch awaiting bind) excludes its wake-watcher. A recorded-offline instance is watcher-eligible when it is persistent, carries no corpse, or its corpse's launch predates machine boot. **Only nonpersistent instances retain the same-boot force-kill freeze:** their same-boot or undatable corpses arm no watcher, preserving the deploy/overwrite case. A persistent corpse bypasses that freeze even without a launch stamp or bo...\n 396:\n 397:**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.\n 398:\n...\n 712:\n*713:## Web serving\n 714:\n 715:Ratified 2026-09-06 (WEBSERVE grill round 1, releases#272; ADR-0056..0059). The docs server\n*716:grows into the node's general serving surface; these are the terms the surface is described in.\n 717:\n*718:**served resource** — anything a node exposes over its serving surface: a registry file, a\n 719:served root, a docs page, an adapter's docs, a message view, a bootstrap binary. Every served\n 720:resource has exactly one **node-prefixed URL**.\n 721:_Avoid_: hosted file, shared file, upload.\n...\n 729:\n*730:**serving registry** — the node's single enumerable list of what it exposes. Exposure is a\n*731:deliberate act of adding an entry; \"what is this node serving\" has exactly one answer, the\n*732:registry's. (ADR-0057)\n*733:_Avoid_: served folder as a primitive; drop-folder; auto-serving a directory by location.\n 734:\n 735:**served root** — a registry entry of type *dir*, declared by an adapter in its manifest, whose\n 736:contents are served as a subtree. A served root is a kind of registry entry, never a second\n...\n 738:the spt home, created at activation — and it answers at the adapter facet `/<node>/a/<adapter>/`,\n*739:with `web_short_path` adding an alias name to the same entry (ADR-0057 Amendment 1). An\n 740:adapter's install tree is never a served root.\n 741:_Avoid_: web folder, static dir.\n 742:\n...\n 746:(`<stem>~<n>.<ext>`); the first registrant keeps the bare name. Cross-node, the node prefix\n*747:is the disambiguator. (ADR-0057, including Amendment 2)\n 748:_Avoid_: filename (a served name is not the file's path or basename); slug.\n 749:\n*750:**attachment** — a file sent with a message by registering it as a served resource and carrying\n 751:its node-prefixed URL in the envelope. Transfer is pull-model: the receiver fetches if and when\n*752:it wants; no unsolicited bytes ever land on a receiver. An attachment is a **snapshot** — the\n 753:bytes as they were at send time, as immutable as the message — and it **expires** (30-day\n 754:default lifetime) unless the sender says otherwise; a plainly registered file, by contrast, is\n*755:served at its path, edits visible, and lives until removed. (ADR-0058)\n 756:_Avoid_: file push; sending a file (bytes never travel with the message); upload; treating an\n*757:attachment as a live view of the sender's file.\n 758:\n 759:**message short-ID** — the eight-character, node-scoped token a message is referred to by: in a\n 760:chat line, at the CLI (`spt msg show`, `spt send --reply-to`), and as its served URL\n...\n 928:**engine-room endpoint** (ratified 2026-07-28, access-control grill):\n*929:**One per node.** A locked-down **agent endpoint** (harness-adapter-backed, spt-hosted, has a mind — so it can reason about the node's access posture) that is the designated surface for setting the **node's** control-surface modes, and — via *empower* — a **subnet's** modes. Bring-online + controller-attach requires a **same-node CLI call + a member-or-admin TOTP** for the engine-room's anchor subnet (no OS elevation). Structural locks: refuses all inbound except replies to its own outbound (**knoc...\n 930:_Avoid_: \"mother node\" (rejected — the mesh stays symmetric; authority rides keys, not a privileged node); remote drive of an engine-room in any form; an agent running the ceremony.\n 931:\n 932:**empower** (`empower <subnet-id> --admin-code <admin-totp>`):\n...\n 937:**control surface** (ratified 2026-07-28, access-control grill):\n*938:The unit of access-control granularity: a named remote-reachable operation class on an endpoint. **Open string vocabulary, CONSTANT_CASE ids** (like capability ids — new surfaces mint ids without schema change). v1 set = the existing gate families: `MSG`, `RC_VIEW`, `RC_ATTACH`, `DIGEST`, `WAKE`, `SUSPEND`, `XFER`, `SHELL_LINK`, `DISCOVER` — plus **`FORK`** (HANDRAIL, releases#29), the first id minted *with* its capability and so the proof that the open vocabulary grows the way it was ratified to: on...\n 939:\n 940:**Intra-node governance** (releases#211, operator-rephrased 2026-08-22). <!-- [doc->REQ-ACL-INTRA-NODE-SELF] --> The vocabulary for governing traffic authored ON this node is a **SELF-REFERENTIAL NODE SUBJECT at the EXISTING tiers** — a rule whose subject is this node's own id. **No new tier, no new subject kind, no schema change**, and the v1 reading (a rules tier at 3.5 with a mode twin at 6.5) is WITHDRAWN IN FULL. Two effects: a **node-scope** own-node rule governs intra-node actions for every endp...\n 941:\n...\n 1097:\n*1098:**subnet attachment (attached / detached)**:\n## ROADMAP.md#158C\n 42:\n*43:- **M6 — Stage-setting: `spt-releases`, dev docs, v0.1 release. ✅ delivered (2026-06-05** — planned, executed, and shipped in one day; decisions grilled `1922fa4`, task breakdown [`M6-PLAN.md`](./M6-PLAN.md)). spt-core is now **installable, documented, and released**: [`SaberMage/spt-releases`](https://github.com/SaberMage/spt-releases) is **public** with the signed **v0.1.0** release live and GitHub Pages serving the docs at the permanent canonical URL `https://sabermage.github.io/spt-releases` (A...\n 44:\n 45:- **M7 — Subnet & quickstart UX. ✅ delivered (2026-06-06** — planned 2026-06-05, task breakdown [`M7-PLAN.md`](./M7-PLAN.md)). Subnet setup is now a first-class, guided, secure product surface and the public quickstart is honest about personas. Shipped: the **join ceremony's product surface** (always-on daemon pairing responder + guided `spt subnet join` CLI initiator over the rig-proven SPAKE2 wire) · the `spt subnet` noun namespace (`status [NAME] [--nodes]` / `create` / `show-code` / `join`; `s...\n 46:\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":".worktrees/308-registry-process-lock/CONTEXT.md, .worktrees/308-registry-process-lock/PRD.md, .worktrees/308-registry-process-lock/ROADMAP.md, .worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md, .worktrees/308-registry-process-lock/docs/TRACEABILITY.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":21,"fileCount":2,"files":[".worktrees/308-registry-process-lock/CONTEXT.md",".worktrees/308-registry-process-lock/ROADMAP.md"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/CONTEXT.md","count":20},{"path":".worktrees/308-registry-process-lock/ROADMAP.md","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# .worktrees/308-registry-process-lock/\n## CONTEXT.md#54AA\n   95│<!-- [doc->REQ-MANIFEST-4] -->\n  *96│Once-per-session usage/syntax hints, a first-class adapter feature: the manifest's `[hints]` section declares entries of `{keywords (literal default, regex opt-in), text}`; the adapter's user-prompt hook pipes the **full user message** to `spt api hint --session <id>` (stdin) and receives matched hint lines (`keyword hint for SPT adapter <name>: \"<kw>\"-->{text}`) for its context-injection channel. The daemon keeps a per-session seen-set — each hint fires **once per session** (a `/clear` mints a new ses...\n   97│_Avoid_: unconditional static context (that's the adapter's own preamble); firing per-message.\n   98│\n   99│**shell keyword hints** (releases#133, operator-GREENLIT; shape ruled 2026-08-30):\n     │...\n  200│**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running ho...\n *201│_Avoid_: consent-gating it (it is intentionally ungated, for CI); treating it as a sync/remote op (local-only); a soft variant (purge is always the hard, full wipe — soft teardown is `endpoint stop`). **Read that soft/hard contrast on the RECORD axis only** (ADR-0045): `stop` is *record*-preserving (spool.db + info.json survive) where `purge` wipes them. It says nothing about processes — on the **process** axis `stop` is hard: for a broker-hosted endpoint it reaps the session and its descendant subtr...\n  202│<!-- [doc->REQ-ENDPOINT-PURGE] -->\n  203│\n  204│- **`api history-log`** — Path B: ingest normalized records into spt-core's native history store.\n *205│- **`api presence`** — adapter reports user interaction → updates the presence datum `(last_active_node, last_active_endpoint, ts)`. In the spt-hosted topology, presence is **also** updated by the broker *detecting* (sensing, not watching/logging) user input on a held PTY — privacy-preserving (it notes that input occurred, records no content).\n  206│- **`api emit --type <sensory_type> <payload>`** — a broker-launched **Shell** binary pushes a sensory payload to its owner agent (owner known from `api bind`; REST-only, never spooled). See the Shell model.\n  207│\n  208│**Not `api` commands — file-drop flow:** `commune` and `signoff` are deprecated as commands (modern SPT) in favor of file drops. The agent/adapter writes `<id>-commune.md` / `<id>-signoff.md`; the daemon watches the manifest-declared commune/signoff dirs (the spawn-session seam fields), ingests, and deletes (drop files are daemon-owned single-writer — KNOWN-HAZARDS 6.4). These stay off the `api` surface and the agent surface alike. **This path is the Self's, and spt-core's echo-commune never writes i...\n     │...\n  394│\n *395│**Online/offline are mutually-exclusive processes:** a live shell binary (including a launch awaiting bind) excludes its wake-watcher. A recorded-offline instance is watcher-eligible when it is persistent, carries no corpse, or its corpse's launch predates machine boot. **Only nonpersistent instances retain the same-boot force-kill freeze:** their same-boot or undatable corpses arm no watcher, preserving the deploy/overwrite case. A persistent corpse bypasses that freeze even without a launch stamp or bo...\n  396│\n  397│**Exit-opcode supervision:** the wake-watcher exiting with the **wake opcode** → spt-core runs the wake resolution (below) and brings the shell online. Any *other* exit (crash) → respawn the watcher with **exponential backoff + eventual give-up** (until next shell activity) — crash-bug safety. One watcher per offline shell instance.\n  398│\n     │...\n  712│\n *713│## Web serving\n  714│\n  715│Ratified 2026-09-06 (WEBSERVE grill round 1, releases#272; ADR-0056..0059). The docs server\n *716│grows into the node's general serving surface; these are the terms the surface is described in.\n  717│\n *718│**served resource** — anything a node exposes over its serving surface: a registry file, a\n  719│served root, a docs page, an adapter's docs, a message view, a bootstrap binary. Every served\n  720│resource has exactly one **node-prefixed URL**.\n  721│_Avoid_: hosted file, shared file, upload.\n     │...\n  729│\n *730│**serving registry** — the node's single enumerable list of what it exposes. Exposure is a\n *731│deliberate act of adding an entry; \"what is this node serving\" has exactly one answer, the\n *732│registry's. (ADR-0057)\n *733│_Avoid_: served folder as a primitive; drop-folder; auto-serving a directory by location.\n  734│\n  735│**served root** — a registry entry of type *dir*, declared by an adapter in its manifest, whose\n  736│contents are served as a subtree. A served root is a kind of registry entry, never a second\n     │...\n  738│the spt home, created at activation — and it answers at the adapter facet `/<node>/a/<adapter>/`,\n *739│with `web_short_path` adding an alias name to the same entry (ADR-0057 Amendment 1). An\n  740│adapter's install tree is never a served root.\n  741│_Avoid_: web folder, static dir.\n  742│\n     │...\n  746│(`<stem>~<n>.<ext>`); the first registrant keeps the bare name. Cross-node, the node prefix\n *747│is the disambiguator. (ADR-0057, including Amendment 2)\n  748│_Avoid_: filename (a served name is not the file's path or basename); slug.\n  749│\n *750│**attachment** — a file sent with a message by registering it as a served resource and carrying\n  751│its node-prefixed URL in the envelope. Transfer is pull-model: the receiver fetches if and when\n *752│it wants; no unsolicited bytes ever land on a receiver. An attachment is a **snapshot** — the\n  753│bytes as they were at send time, as immutable as the message — and it **expires** (30-day\n  754│default lifetime) unless the sender says otherwise; a plainly registered file, by contrast, is\n *755│served at its path, edits visible, and lives until removed. (ADR-0058)\n  756│_Avoid_: file push; sending a file (bytes never travel with the message); upload; treating an\n *757│attachment as a live view of the sender's file.\n  758│\n  759│**message short-ID** — the eight-character, node-scoped token a message is referred to by: in a\n  760│chat line, at the CLI (`spt msg show`, `spt send --reply-to`), and as its served URL\n     │...\n  928│**engine-room endpoint** (ratified 2026-07-28, access-control grill):\n *929│**One per node.** A locked-down **agent endpoint** (harness-adapter-backed, spt-hosted, has a mind — so it can reason about the node's access posture) that is the designated surface for setting the **node's** control-surface modes, and — via *empower* — a **subnet's** modes. Bring-online + controller-attach requires a **same-node CLI call + a member-or-admin TOTP** for the engine-room's anchor subnet (no OS elevation). Structural locks: refuses all inbound except replies to its own outbound (**knoc...\n  930│_Avoid_: \"mother node\" (rejected — the mesh stays symmetric; authority rides keys, not a privileged node); remote drive of an engine-room in any form; an agent running the ceremony.\n  931│\n  932│**empower** (`empower <subnet-id> --admin-code <admin-totp>`):\n     │...\n  937│**control surface** (ratified 2026-07-28, access-control grill):\n *938│The unit of access-control granularity: a named remote-reachable operation class on an endpoint. **Open string vocabulary, CONSTANT_CASE ids** (like capability ids — new surfaces mint ids without schema change). v1 set = the existing gate families: `MSG`, `RC_VIEW`, `RC_ATTACH`, `DIGEST`, `WAKE`, `SUSPEND`, `XFER`, `SHELL_LINK`, `DISCOVER` — plus **`FORK`** (HANDRAIL, releases#29), the first id minted *with* its capability and so the proof that the open vocabulary grows the way it was ratified to: on...\n  939│\n  940│**Intra-node governance** (releases#211, operator-rephrased 2026-08-22). <!-- [doc->REQ-ACL-INTRA-NODE-SELF] --> The vocabulary for governing traffic authored ON this node is a **SELF-REFERENTIAL NODE SUBJECT at the EXISTING tiers** — a rule whose subject is this node's own id. **No new tier, no new subject kind, no schema change**, and the v1 reading (a rules tier at 3.5 with a mode twin at 6.5) is WITHDRAWN IN FULL. Two effects: a **node-scope** own-node rule governs intra-node actions for every endp...\n  941│\n     │...\n 1097│\n*1098│**subnet attachment (attached / detached)**:\n## ROADMAP.md#158C\n 42│\n*43│- **M6 — Stage-setting: `spt-releases`, dev docs, v0.1 release. ✅ delivered (2026-06-05** — planned, executed, and shipped in one day; decisions grilled `1922fa4`, task breakdown [`M6-PLAN.md`](./M6-PLAN.md)). spt-core is now **installable, documented, and released**: [`SaberMage/spt-releases`](https://github.com/SaberMage/spt-releases) is **public** with the signed **v0.1.0** release live and GitHub Pages serving the docs at the permanent canonical URL `https://sabermage.github.io/spt-releases` (A...\n 44│\n 45│- **M7 — Subnet & quickstart UX. ✅ delivered (2026-06-06** — planned 2026-06-05, task breakdown [`M7-PLAN.md`](./M7-PLAN.md)). Subnet setup is now a first-class, guided, secure product surface and the public quickstart is honest about personas. Shipped: the **join ceremony's product surface** (always-on daemon pairing responder + guided `spt subnet join` CLI initiator over the rig-proven SPAKE2 wire) · the `spt subnet` noun namespace (`status [NAME] [--nodes]` / `create` / `show-code` / `join`; `s...\n 46│","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1789402543733}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_p0YoOcO6Oqar3cMITww4vQEf|fc_080dc0728d55fa10016aa81dbce23887d29d320e01b65c330d","toolName":"read","startedAt":"2026-09-14T16:15:56.973Z","args":{"path":".worktrees/308-registry-process-lock/docs/adr/0057-serving-registry-is-the-primitive.md"},"intent":"Reading serving decisions"},"id":"269125e6","parentId":"d45aa651","timestamp":"2026-09-14T16:15:56.973Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZoewAXThlfIXQZ7tYPEvCew7|fc_080dc0728d55fa10016aa81dbce24c87d2bef92d0897b6af71","toolName":"read","startedAt":"2026-09-14T16:15:56.973Z","args":{"path":".worktrees/308-registry-process-lock/docs/adr/0058-attachments-are-pull-model.md"},"intent":"Reading attachment expiry decisions"},"id":"71fa13c6","parentId":"269125e6","timestamp":"2026-09-14T16:15:56.973Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3yFikUGKH4Q2lY2tMIKy4IsI|fc_080dc0728d55fa10016aa81dbce25487d2854091b2117de6fb","toolName":"read","startedAt":"2026-09-14T16:15:56.974Z","args":{"path":".worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md:1-95"},"intent":"Reading documentation authoring rules"},"id":"912f439d","parentId":"71fa13c6","timestamp":"2026-09-14T16:15:56.974Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_2Naiu3cNG3m0gmEKqW1cwNg0|fc_080dc0728d55fa10016aa81dbce25c87d2aca694ea784de018","toolName":"read","startedAt":"2026-09-14T16:15:56.974Z","args":{"path":".worktrees/308-registry-process-lock/docs/TRACEABILITY.md:1-90"},"intent":"Reading trace evidence rules"},"id":"8b4b32f0","parentId":"912f439d","timestamp":"2026-09-14T16:15:56.974Z"}
{"type":"message","id":"c0dbf5a6","parentId":"8b4b32f0","timestamp":"2026-09-14T16:15:56.974Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reading relevant strategy heading**"},{"type":"toolCall","id":"call_p0YoOcO6Oqar3cMITww4vQEf|fc_080dc0728d55fa10016aa81dbce23887d29d320e01b65c330d","name":"read","arguments":{"i":"Reading serving decisions","path":".worktrees/308-registry-process-lock/docs/adr/0057-serving-registry-is-the-primitive.md"},"intent":"Reading serving decisions"},{"type":"toolCall","id":"call_ZoewAXThlfIXQZ7tYPEvCew7|fc_080dc0728d55fa10016aa81dbce24c87d2bef92d0897b6af71","name":"read","arguments":{"i":"Reading attachment expiry decisions","path":".worktrees/308-registry-process-lock/docs/adr/0058-attachments-are-pull-model.md"},"intent":"Reading attachment expiry decisions"},{"type":"toolCall","id":"call_3yFikUGKH4Q2lY2tMIKy4IsI|fc_080dc0728d55fa10016aa81dbce25487d2854091b2117de6fb","name":"read","arguments":{"i":"Reading documentation authoring rules","path":".worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md:1-95"},"intent":"Reading documentation authoring rules"},{"type":"toolCall","id":"call_2Naiu3cNG3m0gmEKqW1cwNg0|fc_080dc0728d55fa10016aa81dbce25c87d2aca694ea784de018","name":"read","arguments":{"i":"Reading trace evidence rules","path":".worktrees/308-registry-process-lock/docs/TRACEABILITY.md:1-90"},"intent":"Reading trace evidence rules"},{"type":"toolCall","id":"call_WcAxVfDm70uUe6mJB25mNfKu|fc_080dc0728d55fa10016aa81dbce26487d2ac4c3fc7c2af466d","name":"read","arguments":{"i":"Reading reaper preservation contract","path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1260-1335"},"intent":"Reading reaper preservation contract"},{"type":"toolCall","id":"call_W9Fw6zoN9SNYmtrEWZFXcc90|fc_080dc0728d55fa10016aa81dbce26c87d2a94374ad025e72c1","name":"grep","arguments":{"i":"Locating storage links conventions","pattern":"STORAGE.md|KNOWN-HAZARDS.md","path":".worktrees/308-registry-process-lock/docs-site/src","case":true,"gitignore":false,"skip":null},"intent":"Locating storage links conventions"},{"type":"toolCall","id":"call_aSPhMX8IwCzMWmSKBDBDeNTX|fc_080dc0728d55fa10016aa81dbce27487d29d293eae1476ee1d","name":"grep","arguments":{"i":"Locating storage hazard insertion point","pattern":"^## 6|^### 6\\.","path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md","case":true,"gitignore":false,"skip":null},"intent":"Locating storage hazard insertion point"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":7068,"output":368,"cacheRead":35328,"cacheWrite":0,"totalTokens":42764,"reasoningTokens":15,"cost":{"input":0.07068,"output":0.0184,"cacheRead":0.035328,"cacheWrite":0,"total":0.124408}},"stopReason":"toolUse","timestamp":1789402543768,"responseId":"resp_080dc0728d55fa10016aa81db0031887d2ae33008ac0cee922","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81db20a6c87d28393beaa84ac31c0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB28-iccqnQ0bX53apAAUwzz5uHxeIwKja7xryDTjTb43hTJUD9hRmW22bZNnAP5g8HV3WLemlmMEzhOcGs8WnUhze5Nn0px8UTN-ufV38pN8S3cco_prphXdp2lYarGqha-0wafPGJWco8MbFImxYuEbUnKuyM8Zfabe0w1L75Ir0_u8koaQ019PWJ4aueOtGB0kcnYRKjYBt539o-6wVnDDjiLxnt1KeN27puRTnjZznBx5e4bWLweu3jAcQQK0UCF2-wevhrywiGOvwHffexF89YvkKcsxKgiM1JY31_IT_-KrVv9pOm0ZIi5WeXk0G1VGgjp_SzvTVPC32e1UoFDneUKZOqGztSRdb9dLUiM5z1gaIuw2bukrJTcS60G-TH7c07QWGQlIIpO-X9rK7OUWZrpE-KchwVvZWWeiAoquapbkQVUxKv328C8qp0pRtPfTjwvNkIyvtBwr0lvXl6GdxNwxQn0sPm8bYy5uUgqCpxO_oKo4w4MADARkL2veENVqi5HiatUhGJoarf8Vz9PjodVvHH2Zt0x6PFVwoQkPzHQib_yjXxJp7QtfV9M88lNb0LJz-s0Pe8ePUMVtQ8vb2hzcUmMjZo4MBQlCakJ4FFvL5DvRJ4TaOA4oCl_nmfLRJqugnQqzS-IkMoN7n2ohfSZAJJ2eTjHMJuHWKOrHkpQjELIw2091S0K47q1-wI4pdsSuNCai5gG2lO5XAI0lwkmeq68nBZMijhmXgWmJDO4k6QHkYaBlnNLHCx2oWW3aD8YCiFwf1A9CskrRj2W27WLWqKJjP_itboZc86xxcW7-XADSOFY63tYz0_rFWwrYHWXX_eAieSY733AQtalFRQJ0OmRnLCTZhsOlhtBCjgG2Yu1m2_jABnt5A_5u2F1F6lZyFw3bFbzyxawgyVQ4tbdaD8sJ0p9qQKFZuApdwlSGROEH8Tet3SdPQlyTdnCAZGQX5FPxWFkir2e1-TKJeEkxmEZXzHe4P3iBaJoF_Kkc4oMgEYpz2IxAVqjLNvbLN_1v_vLkS7IF-Gj83QgjP8IF5pzeKAodPKyVdHTRlaccXv-9lP8RxVayJSF2RamUd6p9bEqllvs-fgMqbLeahW7SinyeBEx8ZFv7Tssh_R579wBRosonGeGnDC04ZgduNlPKLwRvmUfM5yNtbbGOK9qlbi5BKLrXvwLYgjiVT7JNQUPNiN08FYgbKyF1dX7Kt1GG_0ZOII7UXFdc_55F6I_nsKhpLk7HMT4Zus0CKRnuqcDiYw7mQhBhbaWN3JOuSHgTQ_Xwl7NtJ5RE8etm45TB1MPo1es36OsilPlfyIVLABt9ER0fvytExHlDH4jrspVlCjU0NA_Jv8CaDHlnO-E-IMVsivCO4wvNGA6MUw=","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[{"type":"summary_text","text":"**Reading relevant strategy heading**"}],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce23887d29d320e01b65c330d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading serving decisions\",\"path\":\".worktrees/308-registry-process-lock/docs/adr/0057-serving-registry-is-the-primitive.md\"}","call_id":"call_p0YoOcO6Oqar3cMITww4vQEf","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce24c87d2bef92d0897b6af71","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attachment expiry decisions\",\"path\":\".worktrees/308-registry-process-lock/docs/adr/0058-attachments-are-pull-model.md\"}","call_id":"call_ZoewAXThlfIXQZ7tYPEvCew7","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce25487d2854091b2117de6fb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading documentation authoring rules\",\"path\":\".worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md:1-95\"}","call_id":"call_3yFikUGKH4Q2lY2tMIKy4IsI","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce25c87d2aca694ea784de018","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading trace evidence rules\",\"path\":\".worktrees/308-registry-process-lock/docs/TRACEABILITY.md:1-90\"}","call_id":"call_2Naiu3cNG3m0gmEKqW1cwNg0","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce26487d2ac4c3fc7c2af466d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading reaper preservation contract\",\"path\":\".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1260-1335\"}","call_id":"call_WcAxVfDm70uUe6mJB25mNfKu","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce26c87d2a94374ad025e72c1","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating storage links conventions\",\"pattern\":\"STORAGE.md|KNOWN-HAZARDS.md\",\"path\":\".worktrees/308-registry-process-lock/docs-site/src\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_W9Fw6zoN9SNYmtrEWZFXcc90","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dbce27487d29d293eae1476ee1d","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating storage hazard insertion point\",\"pattern\":\"^## 6|^### 6\\\\.\",\"path\":\".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md\",\"case\":true,\"gitignore\":false,\"skip\":null}","call_id":"call_aSPhMX8IwCzMWmSKBDBDeNTX","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"grep","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":13203.86859999597,"ttft":2293.0351999998093,"completedAt":1789402556972,"contextSnapshot":{"promptTokens":42396,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WcAxVfDm70uUe6mJB25mNfKu|fc_080dc0728d55fa10016aa81dbce26487d2ac4c3fc7c2af466d","toolName":"read","startedAt":"2026-09-14T16:15:56.974Z","args":{"path":".worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs:1260-1335"},"intent":"Reading reaper preservation contract"},"id":"10725cec","parentId":"c0dbf5a6","timestamp":"2026-09-14T16:15:56.974Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_W9Fw6zoN9SNYmtrEWZFXcc90|fc_080dc0728d55fa10016aa81dbce26c87d2a94374ad025e72c1","toolName":"grep","startedAt":"2026-09-14T16:15:56.974Z","args":{"path":".worktrees/308-registry-process-lock/docs-site/src"},"intent":"Locating storage links conventions"},"id":"57ba02e7","parentId":"10725cec","timestamp":"2026-09-14T16:15:56.974Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_aSPhMX8IwCzMWmSKBDBDeNTX|fc_080dc0728d55fa10016aa81dbce27487d29d293eae1476ee1d","toolName":"grep","startedAt":"2026-09-14T16:15:56.974Z","args":{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md"},"intent":"Locating storage hazard insertion point"},"id":"296040c9","parentId":"57ba02e7","timestamp":"2026-09-14T16:15:56.974Z"}
{"type":"message","id":"acd24068","parentId":"296040c9","timestamp":"2026-09-14T16:15:56.979Z","message":{"role":"toolResult","toolCallId":"call_aSPhMX8IwCzMWmSKBDBDeNTX|fc_080dc0728d55fa10016aa81dbce27487d29d293eae1476ee1d","toolName":"grep","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#F7C6]\n 371:\n*372:## 6. Documented regressions (non-obvious invariants)\n 373:\n*374:### 6.1 No flat/nested perch siblings; resolver-routed paths\n 375:- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\n 376:- **Invariant:** one path resolver; never create divergent siblings.\n 377:- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\n...\n 379:\n*380:### 6.2 Soft-cleanup preserves state, removes `ready`\n 381:- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n 382:- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n 383:- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n...\n 385:\n*386:### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children\n 387:- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.\n 388:- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.\n 389:- **spt-core mapping:** any destructive maintenance command must check for live child instances first.\n...\n 391:\n*392:### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind\n 393:- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.\n 394:- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.\n 395:- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.\n...\n 397:\n*398:### 6.5 Direct-write precedence guard against stale LLM overwrites\n 399:- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.\n 400:- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.\n 401:- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. T...\n...\n 403:\n*404:### 6.6 Surfaced context conflicts preserve both versions until dominated\n 405:- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.\n 406:- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.\n 407:- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.\n...\n 409:\n*410:### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 411:- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old c...\n 412:- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/0...\n 413:- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n...\n 418:\n*419:### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n 420:- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n 421:- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n 422:- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n...\n 428:\n*429:### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n 430:- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n 431:- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n 432:- **spt-core mapping:** the **other-session** face is unreachable today — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no other session to starve. The **same-session** face is reachable in direct `Brain` users: `spawn_session_pid` can consume and discard that session's own first `Output` frame while waiting for `Spawned` (measured in-process on Linux, 2026-08-25), so a caller must not treat the producer's first chunk as guaranteed post...\n...\n 434:\n*435:### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 436:- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same...\n 437:- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The...\n 438:- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired he...\n...\n 440:\n*441:### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n 442:- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, ...\n 443:- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof t...\n 444:- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n...\n 447:\n*448:### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`\n 449:- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as st...\n 450:- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.\n 451:- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.\n...\n 455:\n*456:### 6.13 IO event sequences never reset at a byte-window boundary\n 457:<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->\n 458:- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n 459:- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value."}],"details":{"scopePath":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\KNOWN-HAZARDS.md","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":14,"fileCount":1,"files":[".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md"],"fileMatches":[{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md","count":14}],"truncated":false,"displayContent":" 371│\n*372│## 6. Documented regressions (non-obvious invariants)\n 373│\n*374│### 6.1 No flat/nested perch siblings; resolver-routed paths\n 375│- **Failure:** mixed flat + nested perch layouts confuse which perch is live; cascade-wipe risk.\n 376│- **Invariant:** one path resolver; never create divergent siblings.\n 377│- **spt-core mapping:** clean greenfield layout from day one (no migration window) — pick one structure, route everything through the registry. Storage layout deferred to design phase but this single-source-of-truth rule is binding.\n    │...\n 379│\n*380│### 6.2 Soft-cleanup preserves state, removes `ready`\n 381│- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n 382│- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n 383│- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n    │...\n 385│\n*386│### 6.3 Cascade-wipe guard: never delete a parent hosting non-empty children\n 387│- **Failure:** `doctor --fix` deletes a top-level perch that still hosts in-flight nested Worker/Psyche perches.\n 388│- **Invariant:** before hard-delete, check for non-empty nested children; if present, soft-clean only and surface the path.\n 389│- **spt-core mapping:** any destructive maintenance command must check for live child instances first.\n    │...\n 391│\n*392│### 6.4 Drop files are single-writer (supervisor-owned), read-only for the mind\n 393│- **Failure:** the Psyche LLM deletes a commune drop file the wrapper is concurrently reading → race, lost commune.\n 394│- **Invariant:** drop files (`<id>-commune.md`) are supervisor-owned single-writer; the LLM is read-only; only the supervisor (or explicit operator) deletes them.\n 395│- **spt-core mapping:** the daemon is the single writer; the harness-invoked mind never mutates drop files. Bake the ownership into the runtime contract.\n    │...\n 397│\n*398│### 6.5 Direct-write precedence guard against stale LLM overwrites\n 399│- **Failure:** the LLM emits an older snapshot that clobbers a fresher direct write to a context file.\n 400│- **Invariant:** every context write carries a source+timestamp precedence marker; LLM writes within a protection window after a recent direct write are suppressed (logged); direct writes always proceed.\n 401│- **spt-core mapping:** cross-node Psyche sync (ADR-0003) makes this multi-writer across machines — the precedence marker must include node identity **plus a per-node version vector** (entries from each node's monotonic `EpochSource`; wall-clock never orders). Distributed rule (ADR-0013, M4-D6): dominate→accept, dominated→drop, **concurrent→surface as durable replicated conflict artifacts + Psyche-reconcile on the active instance's node — never silent newest-wins, never lose either version**. T...\n    │...\n 403│\n*404│### 6.6 Surfaced context conflicts preserve both versions until dominated\n 405│- **Failure:** a cross-node concurrent context write (version vectors, neither dominates) gets auto-picked or partially dropped — half a mind silently lost.\n 406│- **Invariant:** a surfaced concurrent pair is durably preserved (both versions) until a strictly dominating write clears it; no merge/reconcile failure path may discard an unmerged version. Resolution is the Psyche reconcile turn (ADR-0013), whose merged write `join(vA,vB)+bump` dominates both parents — only that dominance clears the artifacts.\n 407│- **spt-core mapping:** `ContextStore::record_conflict` (tracked `.conflicts/` artifacts, content-hash named, idempotent, replicate like context) / `list_conflicts` / `clear_conflicts` (dominating-write-only). Local working file stays untouched while a conflict is pending.\n    │...\n 409│\n*410│### 6.7 Broker and brain MUST be separate processes (in-process collapse silently breaks no-endpoint-drop update) `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 411│- **Failure:** the daemon hosts the broker as a background *thread* in the single `spt daemon` process (`daemon.rs:165-170`, `Arc<Broker>` + `thread::spawn(serve)`) instead of a separate process. A brain restart onto a swapped binary then cannot happen without killing the broker thread — closing every PTY, orphaning every harness child, dropping every socket. So `spt update apply` degrades to an in-process `Brain::handoff` no-op: the binary swaps on disk but the running daemon keeps executing the old c...\n 412│- **Invariant:** the broker runs as its own long-lived process that survives every brain restart; the brain restarts onto the new binary and re-attaches via the versioned IPC. A routine (brain-only) update must leave every hosted endpoint untouched at the *process* level — not merely re-subscribe a brain within the same process. The evidence for REQ-UPD-3 / REQ-DAEMON-2 must prove process-level survival (a PTY child + a live QUIC conn survive a brain-process restart onto a swapped binary — SPIKE-01/0...\n 413│- **spt-core mapping:** restoration is ADR-0018 (next milestone). The current `int` tags on REQ-DAEMON-2 / REQ-UPD-3 are regression-masked and re-point at restoration; the broker becomes the always-up per-machine anchor (seed-lock + liveness + brain supervisor). Two-process supervision, generation custody, durable-deadline loop timing, broker-cursor-of-record, and readiness-gated auto-rollback all hang off this.\n    │...\n 418│\n*419│### 6.8 No irreversible durable-state migration before update ready-promotion `[REQ-HAZARD-ROLLBACK-STATE-COMPAT]`\n 420│- **Failure:** the readiness-gated auto-rollback (ADR-0018 Q7) spawns the *previous* binary against durable state the *new* brain already wrote. The first release that migrates a durable-state schema in place would make the old binary unable to read it — silently bricking rollback exactly when it is needed (a logic-bricking update that can no longer fall back).\n 421│- **Invariant:** a brain must not irreversibly migrate durable state before it is ready-promoted; equivalently, every pre-ready write must remain readable by the N-1 brain. Schema migrations are gated behind ready-promotion (or written in an N-1-tolerant additive form).\n 422│- **spt-core mapping:** lands with ADR-0018's auto-rollback. Free to assert now (a 2026-06-09 source audit confirmed zero state-migration code exists); unmintable retroactively once a migration ships.\n    │...\n 428│\n*429│### 6.9 Resume-mode brain: a blocking spawn/command wait silently discards OTHER sessions' output\n 430│- **Failure:** a resume-mode brain (per-session `session_cursors` populated by `resume_sessions`) drives `Brain` over **blocking** `read_event` calls. Any command that loops `read_event` until its own reply — `spawn_session_pid` waiting for `Spawned`, `net_status`, `sessions`, etc. — calls `read_event` on *every* interleaved frame, so an OUTPUT frame for a **different** session is **cursor-processed** (its `session_cursors` entry snaps forward; the broker already counted it delivered via `delivered_t...\n 431│- **Invariant:** the daemon-hosted multi-session event loop must not consume a session's OUTPUT inside another session's blocking wait. The **live-agent adapter milestone must restructure the brain event pump** so command/`spawn` is non-blocking (a single demux loop owns `read_event` and routes every frame to its session's consumer), OR a blocking wait must re-queue/route the frames it reads for other sessions rather than dropping them.\n 432│- **spt-core mapping:** the **other-session** face is unreachable today — the supervised daemon brain hosts no PTY sessions and spawns none; a single-session seat (legacy, empty map) has no other session to starve. The **same-session** face is reachable in direct `Brain` users: `spawn_session_pid` can consume and discard that session's own first `Output` frame while waiting for `Spawned` (measured in-process on Linux, 2026-08-25), so a caller must not treat the producer's first chunk as guaranteed post...\n    │...\n 434│\n*435│### 6.10 Phase-significant loop timing must be a durable absolute-deadline grid, not phase-relative sleep `[REQ-HAZARD-BROKER-PROCESS-ISOLATION]`\n 436│- **Failure:** a periodic loop that sleeps a flat `period` each iteration (`pulse_tick` then `sleep(pulse_period)`) is **phase-relative** — every brain restart silently re-phases the grid to the restart instant. Under the seamless-update model (the supervisor respawns the brain onto a swapped binary, ADR-0018 D3-3), a routine update would shift the cadence of every phase-significant loop, and continuity cannot ride a brain→brain frame (the outgoing brain is gone before the new one starts — the same...\n 437│- **Invariant:** phase-significant periodic timing lives as durable absolute-deadline state on disk (`(anchor, interval)`), rehydrated on every brain start, with fires **derived functionally** (`next_fire = anchor + interval·⌈max(0,now−anchor)/interval⌉`) and **no per-fire write**. An **Update** restart re-reads the anchor and keeps deriving (phase preserved, lands mid-grid); a **Crash**/**Cold** restart re-bases the anchor to `now` (phase reset acceptable — the loop is idempotent catch-up). The...\n 438│- **spt-core mapping:** ADR-0018 Q4/V3/V4, restoration D5. Mechanism in `spt-daemon::deadline` (`DeadlineAnchor` periodic + `OneShotDeadline` rule-only pure helper); the pulse loop (`lifecycle::run_pulse_loop`) consumes it. The one-shot **machinery** (a durable in-daemon alarm scheduler) is the deferred alarm port (`docs/DEFERRED.md`) — the daemon has no one-shot consumer today, so building the timer now would ship untested dead code (activate-don't-pre-fail); D5 fixes the *rule* as a tested-unwired he...\n    │...\n 440│\n*441│### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n 442│- **Failure:** the broker respawns the brain candidate from `std::env::current_exe()` resolved **per spawn** (`brainproc.rs:817`). `spt update apply` swaps the binary by renaming the running file `spt` → `spt.old-N` and writing the new bytes at `spt`. On **Linux**, `current_exe()` = `readlink(/proc/self/exe)` is **inode-tracking** and follows the rename to `.old-N`, so the resident broker respawns the brain onto the **OLD** bytes — the brain comes up ready (readiness passes), the trial **promotes**, ...\n 443│- **Invariant:** the candidate-binary default is the canonical exe path **captured once at broker start** (before any `apply` can rename under the process), never a per-spawn `current_exe()` — giving Linux the path-at-start semantics Windows already had. AND promotion is **bytes-gated**: a trial promotes only if the candidate's stamped `brain.ready` `exe_hash` equals the staged artifact's hash for this platform; a mismatch is a failed trial → auto-rollback + loud notif (readiness alone is not proof t...\n 444│- **spt-core mapping:** v0.4.2 fix (`V042-PLAN.md`). Half 1 = `spawn_brain_supervisor` canonical-exe capture threaded into `spawn_brain_child`'s `None` default; Half 2 = the promotion bytes-gate in `supervise_brain`'s `Promoted` arm (`TrialEnv::ready_exe_hash` + `staged_artifact_hash`). The rollback path (`Some(.old-N)` selection) is unchanged. Sibling of 6.7 — the broker *process* is correct; the bytes it respawned the brain ONTO were not.\n    │...\n 447│\n*448│### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`\n 449│- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as st...\n 450│- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.\n 451│- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.\n    │...\n 455│\n*456│### 6.13 IO event sequences never reset at a byte-window boundary\n 457│<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->\n 458│- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n 459│- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value."},"isError":false,"timestamp":1789402556979}}
{"type":"message","id":"b12a1a3b","parentId":"acd24068","timestamp":"2026-09-14T16:15:56.982Z","message":{"role":"toolResult","toolCallId":"call_p0YoOcO6Oqar3cMITww4vQEf|fc_080dc0728d55fa10016aa81dbce23887d29d320e01b65c330d","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/adr/0057-serving-registry-is-the-primitive.md#E92E]\n1:# 0057 — The serving registry is the primitive; served names are stable and disambiguated\n2:\n3:Status: accepted (2026-09-06 WEBSERVE grill round 1, releases#272 / #248)\n4:\n5:## Context\n6:\n7:#248 offered two ways for a node to expose files: a dedicated served FOLDER per adapter or\n8:entity (drop a file in, it is served — zero steps, serves everything in it), or a core\n9:REGISTRY of individual entries added deliberately (`spt serve add <path>`, attachments\n10:auto-registering on send). Files from different directories can share a filename, so whichever\n11:primitive wins needs a rule for two `report.md`s.\n12:\n13:## Decision\n14:\n15:**The registry is the primitive.** A served folder is not a second mechanism: it is a registry\n16:entry of type *dir*, declared by an adapter in its manifest, so one enumerable list always\n17:answers \"what is this node exposing\" — an audit is one command (or one index page).\n18:\n19:**Served names are stable and disambiguated by suffix.** When an entry's bare name\n20:`<stem>.<ext>` is already taken on this node, the new entry is served as `<stem>~<n>.<ext>`\n21:(the suffix sits before the extension so the file still opens as its type). Rules:\n22:\n23:- the first registrant keeps the bare name; `n` increments per `(stem, ext)`;\n24:- the assignment is persisted on the entry, so an entry's URL never changes for its life;\n25:- a freed name is never reused for a different absolute path or kind; its former path\n26:  may reclaim it (Amendment 2 below), so a stale link never opens a different resource;\n27:- re-registering the same absolute path returns the existing entry, not a new suffix;\n28:- a registrant may name the entry explicitly (`--as <name>`), subject to the same rule;\n29:- disambiguation is per node — across nodes the URL's node prefix (ADR-0056) already separates\n30:  names.\n31:\n32:## Alternatives rejected\n33:\n34:- **Folders as the primitive.** Zero-step, but exposure becomes a property of where a file\n35:  happens to sit, and \"what am I serving\" has no single answer. Rejected: exposure must be a\n36:  deliberate act with an enumerable record.\n37:- **Content-hash paths as the canonical name.** Collision-proof, but unreadable in a chat line\n38:  and unstable across an edit of a reference-served file. Kept out; a hash can be a secondary\n39:  alias later without changing this decision.\n40:- **Reusing freed names for another path or kind.** Shorter names over time, at the cost\n41:  of a link that can change meaning. Rejected: a link must not lie.\n42:\n43:## Consequences\n44:\n45:- `spt serve list` (and the node index page) is the audit surface; agents read the URL from it\n46:  rather than guessing a name.\n47:- Attachments (ADR-0058) are registry entries, so message links inherit the same stability.\n48:- The registry persists a counter per `(stem, ext)` and each assigned name's prior\n49:  absolute path and kind; both are required to preserve a link's meaning.\n50:\n51:## Amendment 1 — where an adapter's served root lives (doyle ruling, 2026-09-06, W0 stop-and-refer)\n52:\n53:The grill named the served root's URL (`web_short_path`) but never its filesystem source, and\n54:the adapter's install tree is not it — `manifest.toml`, `record.toml`, `strings/` are not\n55:for serving, and an adapter must never expose its install root by inference.\n56:\n57:- **The served root is core-owned:** `$SPT_HOME/adapters/<adapter>/web/`, a sibling of\n58:  `strings/` in the per-adapter aux dir. Core creates it (empty) at activation, registers the\n59:  *dir* entry, and removes the entry at deactivation (the directory and its files stay — they\n60:  are the adapter's output, and an update or re-activation must not eat them). The adapter\n61:  writes what it wants served into that directory at runtime; nothing else is ever served on\n62:  its behalf.\n63:- **It answers at the adapter facet.** `/<node>/a/<adapter>/…` (ADR-0056's reserved `a/`\n64:  namespace) *is* this entry — the facet is the adapter's served root, and \"adapter docs\" are\n65:  whatever the adapter puts there. `web_short_path = \"cspt\"` adds an **alias**\n66:  `/<node>/cspt/…` to the same entry; it never creates a second entry or a second directory.\n67:  Without `web_short_path` the facet still serves; with it, both names do.\n68:- **The alias obeys the served-name rule above** (it is an explicit `--as`): a reserved facet\n69:  name (`f`, `docs`, `a`, `m`, `bin`, `install`) is refused at activation with a diagnostic\n70:  that names the field; a name another entry already holds is disambiguated `<name>~<n>`\n71:  exactly as any other registrant, so an alias never silently takes over another's URL.\n72:- Path discipline is the `strings/` rule: the served subtree is `web/` itself, no `..`, no\n73:  absolute paths, symlinks out of the subtree are not followed.\n74:\n75:This keeps exposure a deliberate act (the adapter writes a file into the one directory that\n76:serves) with one enumerable answer (`spt serve list` shows the dir entry and its alias).\n77:\n78:## Amendment 2 — a freed name may be re-taken by the path it last named\n79:\n80:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n81:\n82:Ruled by doyle, 2026-09-06, from the W0 reactivation smoke finding.\n83:\n84:The never-reuse rule protects a link from changing **meaning**. A name whose last\n85:registrant was absolute path P may be assigned again to a registration of P\n86:(**same kind**), and only to P; any other path takes the next suffix as before.\n87:The history therefore records `(name → last path)` rather than bare names,\n88:with the prior kind retained to enforce the same-kind condition.\n89:\n90:Consequently, an adapter's alias and served root survive deactivate/activate\n91:and `adapter update` unchanged. A stale link answers *not found* or the same\n92:file it always named, never a different one.\n93:\n94:Acceptance: remove `report.md` and re-add the same file to reclaim `report.md`.\n95:A different third file still receives `report~2.md`. Adapter deactivation and\n96:reactivation preserve `reports`, rather than allocating `reports~1`."}],"details":{"totalLines":96,"displayContent":{"text":"# 0057 — The serving registry is the primitive; served names are stable and disambiguated\r\n\r\nStatus: accepted (2026-09-06 WEBSERVE grill round 1, releases#272 / #248)\r\n\r\n## Context\r\n\r\n#248 offered two ways for a node to expose files: a dedicated served FOLDER per adapter or\r\nentity (drop a file in, it is served — zero steps, serves everything in it), or a core\r\nREGISTRY of individual entries added deliberately (`spt serve add <path>`, attachments\r\nauto-registering on send). Files from different directories can share a filename, so whichever\r\nprimitive wins needs a rule for two `report.md`s.\r\n\r\n## Decision\r\n\r\n**The registry is the primitive.** A served folder is not a second mechanism: it is a registry\r\nentry of type *dir*, declared by an adapter in its manifest, so one enumerable list always\r\nanswers \"what is this node exposing\" — an audit is one command (or one index page).\r\n\r\n**Served names are stable and disambiguated by suffix.** When an entry's bare name\r\n`<stem>.<ext>` is already taken on this node, the new entry is served as `<stem>~<n>.<ext>`\r\n(the suffix sits before the extension so the file still opens as its type). Rules:\r\n\r\n- the first registrant keeps the bare name; `n` increments per `(stem, ext)`;\r\n- the assignment is persisted on the entry, so an entry's URL never changes for its life;\r\n- a freed name is never reused for a different absolute path or kind; its former path\r\n  may reclaim it (Amendment 2 below), so a stale link never opens a different resource;\r\n- re-registering the same absolute path returns the existing entry, not a new suffix;\r\n- a registrant may name the entry explicitly (`--as <name>`), subject to the same rule;\r\n- disambiguation is per node — across nodes the URL's node prefix (ADR-0056) already separates\r\n  names.\r\n\r\n## Alternatives rejected\r\n\r\n- **Folders as the primitive.** Zero-step, but exposure becomes a property of where a file\r\n  happens to sit, and \"what am I serving\" has no single answer. Rejected: exposure must be a\r\n  deliberate act with an enumerable record.\r\n- **Content-hash paths as the canonical name.** Collision-proof, but unreadable in a chat line\r\n  and unstable across an edit of a reference-served file. Kept out; a hash can be a secondary\r\n  alias later without changing this decision.\r\n- **Reusing freed names for another path or kind.** Shorter names over time, at the cost\r\n  of a link that can change meaning. Rejected: a link must not lie.\r\n\r\n## Consequences\r\n\r\n- `spt serve list` (and the node index page) is the audit surface; agents read the URL from it\r\n  rather than guessing a name.\r\n- Attachments (ADR-0058) are registry entries, so message links inherit the same stability.\r\n- The registry persists a counter per `(stem, ext)` and each assigned name's prior\r\n  absolute path and kind; both are required to preserve a link's meaning.\r\n\r\n## Amendment 1 — where an adapter's served root lives (doyle ruling, 2026-09-06, W0 stop-and-refer)\r\n\r\nThe grill named the served root's URL (`web_short_path`) but never its filesystem source, and\r\nthe adapter's install tree is not it — `manifest.toml`, `record.toml`, `strings/` are not\r\nfor serving, and an adapter must never expose its install root by inference.\r\n\r\n- **The served root is core-owned:** `$SPT_HOME/adapters/<adapter>/web/`, a sibling of\r\n  `strings/` in the per-adapter aux dir. Core creates it (empty) at activation, registers the\r\n  *dir* entry, and removes the entry at deactivation (the directory and its files stay — they\r\n  are the adapter's output, and an update or re-activation must not eat them). The adapter\r\n  writes what it wants served into that directory at runtime; nothing else is ever served on\r\n  its behalf.\r\n- **It answers at the adapter facet.** `/<node>/a/<adapter>/…` (ADR-0056's reserved `a/`\r\n  namespace) *is* this entry — the facet is the adapter's served root, and \"adapter docs\" are\r\n  whatever the adapter puts there. `web_short_path = \"cspt\"` adds an **alias**\r\n  `/<node>/cspt/…` to the same entry; it never creates a second entry or a second directory.\r\n  Without `web_short_path` the facet still serves; with it, both names do.\r\n- **The alias obeys the served-name rule above** (it is an explicit `--as`): a reserved facet\r\n  name (`f`, `docs`, `a`, `m`, `bin`, `install`) is refused at activation with a diagnostic\r\n  that names the field; a name another entry already holds is disambiguated `<name>~<n>`\r\n  exactly as any other registrant, so an alias never silently takes over another's URL.\r\n- Path discipline is the `strings/` rule: the served subtree is `web/` itself, no `..`, no\r\n  absolute paths, symlinks out of the subtree are not followed.\r\n\r\nThis keeps exposure a deliberate act (the adapter writes a file into the one directory that\r\nserves) with one enumerable answer (`spt serve list` shows the dir entry and its alias).\r\n\r\n## Amendment 2 — a freed name may be re-taken by the path it last named\r\n\r\n<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\r\n\r\nRuled by doyle, 2026-09-06, from the W0 reactivation smoke finding.\r\n\r\nThe never-reuse rule protects a link from changing **meaning**. A name whose last\r\nregistrant was absolute path P may be assigned again to a registration of P\r\n(**same kind**), and only to P; any other path takes the next suffix as before.\r\nThe history therefore records `(name → last path)` rather than bare names,\r\nwith the prior kind retained to enforce the same-kind condition.\r\n\r\nConsequently, an adapter's alias and served root survive deactivate/activate\r\nand `adapter update` unchanged. A stale link answers *not found* or the same\r\nfile it always named, never a different one.\r\n\r\nAcceptance: remove `report.md` and re-add the same file to reclaim `report.md`.\r\nA different third file still receives `report~2.md`. Adapter deactivation and\r\nreactivation preserve `reports`, rather than allocating `reports~1`.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96]},"fileSize":5870,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\adr\\0057-serving-registry-is-the-primitive.md"}}},"isError":false,"timestamp":1789402556981}}
{"type":"message","id":"c1d02ae9","parentId":"b12a1a3b","timestamp":"2026-09-14T16:15:56.984Z","message":{"role":"toolResult","toolCallId":"call_ZoewAXThlfIXQZ7tYPEvCew7|fc_080dc0728d55fa10016aa81dbce24c87d2bef92d0897b6af71","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/adr/0058-attachments-are-pull-model.md#BE35]\n1:# 0058 — File transfer is pull-model: an attachment is a served file plus a link\n2:\n3:Status: accepted (2026-09-06 WEBSERVE grill round 1, releases#272 / #246 / #17)\n4:\n5:## Context\n6:\n7:Endpoint-to-endpoint file transfer (#246) had three candidate shapes: a push (`spt send\n8:--file`, bytes copied to the receiver through the transfer machinery), a served attachment\n9:(`spt send --attachment`, the file is registered for serving and the message envelope carries\n10:its URL; the receiver pulls if and when it wants), or a dedicated transfer verb outside\n11:messaging. The choice also fixes what the FILE_ACCESS_HELPER now-signal (#17) tells an agent\n12:to do with a file it was handed.\n13:\n14:## Decision\n15:\n16:**`spt send --attachment` is the primary transfer UX, and it is pull-model.** The file becomes\n17:a serving-registry entry (ADR-0057) and the message carries its node-prefixed URL\n18:(ADR-0056). No unsolicited bytes ever land on a receiver; a human clicks the link, an agent\n19:fetches it. One mechanism serves both audiences. A small companion verb, `spt fetch <url>\n20:[dest]`, gives a receiving agent a local copy in one command — that verb string is what\n21:FILE_ACCESS_HELPER emits.\n22:\n23:## Alternatives rejected\n24:\n25:- **Push (`--file`).** Delivers bytes the receiver did not ask for, needs receiver-side storage\n26:  policy, and duplicates the serving path for humans. Rejected.\n27:- **Dedicated transfer verb outside messaging.** A file with no message around it loses the\n28:  context a transfer almost always has. Rejected; the attachment rides the message.\n29:\n30:## Consequences\n31:\n32:- A transferred file is only as available as its owner node; an offline owner means a link\n33:  that waits, which is the honest state and is visible as such.\n34:- The pre-WEBSERVE transfer machinery is superseded by this path (its access surface's fate is\n35:  a separate decision, pending the operator's Q3 confirmation).\n36:- **Reference vs snapshot, and lifetime (round 2, Q8):** a `serve add` entry serves the file AT\n37:  ITS PATH — edits are visible, deletion answers *not found*; an attachment SNAPSHOTS the bytes\n38:  at send time into the registry's own store, because a message's attachment is as immutable\n39:  as the message. `serve add` entries live until removed; attachments carry a time-to-live,\n40:  **default 30 days** (operator-set; `--ttl` per send), reaped by the daemon's existing pulse\n41:  with the reap count logged.\n42:\n43:## Amendment 1 — a user-quoted filepath is served automatically for the receiving agent (operator directive, 2026-09-06; recorded by doyle, forks marked)\n44:\n45:The operator directed (releases#17): when a user's message quotes a filepath, spt-core\n46:automatically serves that file for a limited time (24h TTL) and exposes it **only to the agent\n47:that received the message**, via the FILE_ACCESS_HELPER now-signal. This widens #17 from\n48:\"message carries attachments\" to \"user quoted a path\", and it is built as part of WEBSERVE.\n49:\n50:How it composes with this ADR and ADR-0057:\n51:\n52:- **Still a registry entry, still pull-model.** The user quoting a path to an agent is the\n53:  deliberate act; core records it as an entry with `ttl = 24h`, `origin = <message\n54:  short-ID>`, and a new **`audience`** field naming the one endpoint allowed to fetch it. The\n55:  entry is enumerable (`spt serve list`) like any other, so \"what am I exposing\" keeps its one\n56:  answer. Audience is enforced wherever the fetch origin is proven (the cross-node stream's\n57:  handshake, ADR-0060's WEB surface); loopback remains the trusted machine.\n58:- **The signal's output does not change:** the exact `spt fetch <url>` line, once per\n59:  (message, path).\n60:- **The receiver-side record is trimmed by COUNT, not by time.** The helper line the receiving\n61:  node keeps in the endpoint's perch (`helper-fetch.jsonl`) retains the newest 256 records and\n62:  has **no expiry of its own**. A line can therefore outlive the 24h entry it names: it is a\n63:  log of what was offered, never evidence that the link still resolves. Stated here rather\n64:  than fixed with an `at_ms` sweep, because an expiry added without a rig arm to exercise it\n65:  would ship as a claim rather than a behaviour.\n66:- **Guards:** the path must exist on the user's node at signal time; absolute or `~`-rooted\n67:  paths only (a relative path has no anchor); at most 5 per message; a directory registers a\n68:  dir entry under the same TTL and audience.\n69:- **Same node vs remote — historical Am.1 proposal, superseded for USER_INPUT by Am.2.**\n70:  Am.1 proposed a local-readable notice without registration for a same-node user,\n71:  and a cross-node \"serve this path for endpoint X, 24h\" request authorized by the\n72:  user's attach session for a remote user. Am.2 replaces the local notice with\n73:  silence and uses the existing rc stream for receipt-bound USER_INPUT authority.\n74:\n75:**Ruled (operator, 2026-09-06 ~11:00Z):** a helper-registered file is **reference-served** —\n76:the user's live file, edits visible, 404 once deleted — not a snapshot. The user said \"look at\n77:this\", not \"keep this as it was\", and a live reference costs no copy. It is therefore a *file*\n78:(or *dir*) entry carrying a TTL and an audience, not an *attachment* entry; the TTL/audience\n79:fields are per-entry and kind-independent.\n80:\n81:## Amendment 2 — receipt-seat authority for USER_INPUT (operator re-cut, 2026-09-13; releases#300)\n82:\n83:<!-- [doc->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT] -->\n84:<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\n85:The core-only ruling in [INPUT-PROVENANCE-CONTRACT §8](../INPUT-PROVENANCE-CONTRACT.md#8-ruling-doyle-2026-09-13-re-cut-the-same-day-on-the-operators-direction--core-only-no-token-nothing-asked-of-adapters)\n86:supersedes Amendment 1's local-readable notice and its proposed new WEB request\n87:authority for USER_INPUT. Existing MSG_OUT-backed ServeFor authorization remains\n88:unchanged.\n89:\n90:For a path-bearing existing session-authenticated input report, the broker snapshots\n91:its live remote controller connection. No quoted paths means no broker IPC and no\n92:line. An absent hosted session, local or viewer-only seat, or no controller binds\n93:nothing silently, without inspecting receiver-side paths. Requests and replies ride\n94:that same rc stream; the owner derives the audience from its locally established\n95:target. The reference origin is `user-input:<receipt-id>`. The live-reference kind,\n96:one-endpoint audience, bounded 24-hour lifetime, and receiver-side helper record\n97:remain the existing mechanisms.\n98:\n99:Core excludes peer-delivery text it physically wrote into the session, comparing\n100:exact bytes OR both byte sequences trimmed of ASCII whitespace at their ends.\n101:There is no payload-shape parsing or interior normalization. Fixed-size streaming\n102:evidence is published in physical-write order; unproven writes or exhausted evidence\n103:capacity fail closed rather than evicting evidence or guessing.\n104:\n105:Identical payload/session reports cannot renew a live reference or publish another\n106:notice. Lost or uncertain replies retain a dedup fence: absence of an answer is not\n107:proof that registration did not happen. Missing files are silent, as are the ordinary\n108:non-remote cases above. CLI broker-connect and unanswered-receipt failures are silent\n109:unless `SPT_PUMP_TRACE` enables diagnostics. Named declines apply only to failures\n110:with a remote controller; the broker's 10-second owner-reply timeout remains named.\n111:The hook receipt and owner-reply waits are bounded separately. This is receipt-seat\n112:attribution, not reconstructed keystroke provenance; adapters receive no new\n113:integration obligation.\n114:\n115:The separately admitted two-node field leg remains pending, not executed by the\n116:doc/impl/unit lane. Alongside remote serving and local silence, it must keep a remote\n117:controller seated while core physically delivers a path-bearing peer message into\n118:the session and prove that its input report causes no serve. It must also measure\n119:the added hook cost. These are deferred field obligations, not measured results or\n120:an activation of `int`."}],"details":{"totalLines":120,"displayContent":{"text":"# 0058 — File transfer is pull-model: an attachment is a served file plus a link\r\n\r\nStatus: accepted (2026-09-06 WEBSERVE grill round 1, releases#272 / #246 / #17)\r\n\r\n## Context\r\n\r\nEndpoint-to-endpoint file transfer (#246) had three candidate shapes: a push (`spt send\r\n--file`, bytes copied to the receiver through the transfer machinery), a served attachment\r\n(`spt send --attachment`, the file is registered for serving and the message envelope carries\r\nits URL; the receiver pulls if and when it wants), or a dedicated transfer verb outside\r\nmessaging. The choice also fixes what the FILE_ACCESS_HELPER now-signal (#17) tells an agent\r\nto do with a file it was handed.\r\n\r\n## Decision\r\n\r\n**`spt send --attachment` is the primary transfer UX, and it is pull-model.** The file becomes\r\na serving-registry entry (ADR-0057) and the message carries its node-prefixed URL\r\n(ADR-0056). No unsolicited bytes ever land on a receiver; a human clicks the link, an agent\r\nfetches it. One mechanism serves both audiences. A small companion verb, `spt fetch <url>\r\n[dest]`, gives a receiving agent a local copy in one command — that verb string is what\r\nFILE_ACCESS_HELPER emits.\r\n\r\n## Alternatives rejected\r\n\r\n- **Push (`--file`).** Delivers bytes the receiver did not ask for, needs receiver-side storage\r\n  policy, and duplicates the serving path for humans. Rejected.\r\n- **Dedicated transfer verb outside messaging.** A file with no message around it loses the\r\n  context a transfer almost always has. Rejected; the attachment rides the message.\r\n\r\n## Consequences\r\n\r\n- A transferred file is only as available as its owner node; an offline owner means a link\r\n  that waits, which is the honest state and is visible as such.\r\n- The pre-WEBSERVE transfer machinery is superseded by this path (its access surface's fate is\r\n  a separate decision, pending the operator's Q3 confirmation).\r\n- **Reference vs snapshot, and lifetime (round 2, Q8):** a `serve add` entry serves the file AT\r\n  ITS PATH — edits are visible, deletion answers *not found*; an attachment SNAPSHOTS the bytes\r\n  at send time into the registry's own store, because a message's attachment is as immutable\r\n  as the message. `serve add` entries live until removed; attachments carry a time-to-live,\r\n  **default 30 days** (operator-set; `--ttl` per send), reaped by the daemon's existing pulse\r\n  with the reap count logged.\r\n\r\n## Amendment 1 — a user-quoted filepath is served automatically for the receiving agent (operator directive, 2026-09-06; recorded by doyle, forks marked)\r\n\r\nThe operator directed (releases#17): when a user's message quotes a filepath, spt-core\r\nautomatically serves that file for a limited time (24h TTL) and exposes it **only to the agent\r\nthat received the message**, via the FILE_ACCESS_HELPER now-signal. This widens #17 from\r\n\"message carries attachments\" to \"user quoted a path\", and it is built as part of WEBSERVE.\r\n\r\nHow it composes with this ADR and ADR-0057:\r\n\r\n- **Still a registry entry, still pull-model.** The user quoting a path to an agent is the\r\n  deliberate act; core records it as an entry with `ttl = 24h`, `origin = <message\r\n  short-ID>`, and a new **`audience`** field naming the one endpoint allowed to fetch it. The\r\n  entry is enumerable (`spt serve list`) like any other, so \"what am I exposing\" keeps its one\r\n  answer. Audience is enforced wherever the fetch origin is proven (the cross-node stream's\r\n  handshake, ADR-0060's WEB surface); loopback remains the trusted machine.\r\n- **The signal's output does not change:** the exact `spt fetch <url>` line, once per\r\n  (message, path).\r\n- **The receiver-side record is trimmed by COUNT, not by time.** The helper line the receiving\r\n  node keeps in the endpoint's perch (`helper-fetch.jsonl`) retains the newest 256 records and\r\n  has **no expiry of its own**. A line can therefore outlive the 24h entry it names: it is a\r\n  log of what was offered, never evidence that the link still resolves. Stated here rather\r\n  than fixed with an `at_ms` sweep, because an expiry added without a rig arm to exercise it\r\n  would ship as a claim rather than a behaviour.\r\n- **Guards:** the path must exist on the user's node at signal time; absolute or `~`-rooted\r\n  paths only (a relative path has no anchor); at most 5 per message; a directory registers a\r\n  dir entry under the same TTL and audience.\r\n- **Same node vs remote — historical Am.1 proposal, superseded for USER_INPUT by Am.2.**\r\n  Am.1 proposed a local-readable notice without registration for a same-node user,\r\n  and a cross-node \"serve this path for endpoint X, 24h\" request authorized by the\r\n  user's attach session for a remote user. Am.2 replaces the local notice with\r\n  silence and uses the existing rc stream for receipt-bound USER_INPUT authority.\r\n\r\n**Ruled (operator, 2026-09-06 ~11:00Z):** a helper-registered file is **reference-served** —\r\nthe user's live file, edits visible, 404 once deleted — not a snapshot. The user said \"look at\r\nthis\", not \"keep this as it was\", and a live reference costs no copy. It is therefore a *file*\r\n(or *dir*) entry carrying a TTL and an audience, not an *attachment* entry; the TTL/audience\r\nfields are per-entry and kind-independent.\r\n\r\n## Amendment 2 — receipt-seat authority for USER_INPUT (operator re-cut, 2026-09-13; releases#300)\r\n\r\n<!-- [doc->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT] -->\r\n<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\r\nThe core-only ruling in [INPUT-PROVENANCE-CONTRACT §8](../INPUT-PROVENANCE-CONTRACT.md#8-ruling-doyle-2026-09-13-re-cut-the-same-day-on-the-operators-direction--core-only-no-token-nothing-asked-of-adapters)\r\nsupersedes Amendment 1's local-readable notice and its proposed new WEB request\r\nauthority for USER_INPUT. Existing MSG_OUT-backed ServeFor authorization remains\r\nunchanged.\r\n\r\nFor a path-bearing existing session-authenticated input report, the broker snapshots\r\nits live remote controller connection. No quoted paths means no broker IPC and no\r\nline. An absent hosted session, local or viewer-only seat, or no controller binds\r\nnothing silently, without inspecting receiver-side paths. Requests and replies ride\r\nthat same rc stream; the owner derives the audience from its locally established\r\ntarget. The reference origin is `user-input:<receipt-id>`. The live-reference kind,\r\none-endpoint audience, bounded 24-hour lifetime, and receiver-side helper record\r\nremain the existing mechanisms.\r\n\r\nCore excludes peer-delivery text it physically wrote into the session, comparing\r\nexact bytes OR both byte sequences trimmed of ASCII whitespace at their ends.\r\nThere is no payload-shape parsing or interior normalization. Fixed-size streaming\r\nevidence is published in physical-write order; unproven writes or exhausted evidence\r\ncapacity fail closed rather than evicting evidence or guessing.\r\n\r\nIdentical payload/session reports cannot renew a live reference or publish another\r\nnotice. Lost or uncertain replies retain a dedup fence: absence of an answer is not\r\nproof that registration did not happen. Missing files are silent, as are the ordinary\r\nnon-remote cases above. CLI broker-connect and unanswered-receipt failures are silent\r\nunless `SPT_PUMP_TRACE` enables diagnostics. Named declines apply only to failures\r\nwith a remote controller; the broker's 10-second owner-reply timeout remains named.\r\nThe hook receipt and owner-reply waits are bounded separately. This is receipt-seat\r\nattribution, not reconstructed keystroke provenance; adapters receive no new\r\nintegration obligation.\r\n\r\nThe separately admitted two-node field leg remains pending, not executed by the\r\ndoc/impl/unit lane. Alongside remote serving and local silence, it must keep a remote\r\ncontroller seated while core physically delivers a path-bearing peer message into\r\nthe session and prove that its input report causes no serve. It must also measure\r\nthe added hook cost. These are deferred field obligations, not measured results or\r\nan activation of `int`.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120]},"fileSize":7983,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\adr\\0058-attachments-are-pull-model.md"}}},"isError":false,"timestamp":1789402556984}}
{"type":"message","id":"d262387e","parentId":"c1d02ae9","timestamp":"2026-09-14T16:15:56.986Z","message":{"role":"toolResult","toolCallId":"call_2Naiu3cNG3m0gmEKqW1cwNg0|fc_080dc0728d55fa10016aa81dbce25c87d2aca694ea784de018","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/TRACEABILITY.md#BDB7]\n1:# Traceability development contract\n2:\n3:> How spt-core uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs) to keep every requirement traced from doc → impl → test. The manifest is `traceable-reqs.toml` (seeded from PRD `R-*` + KNOWN-HAZARDS invariants). This contract makes the trace pay off instead of rotting.\n4:\n5:## Why\n6:\n7:The whole point of the rebuild is to *not* re-introduce the sister project's bugs and to ship the PRD's scope verifiably. A requirement that isn't traced to real evidence (a doc, an implementation, a test) is a requirement you're *hoping* is met. This contract converts that hope into a build that fails when evidence is missing.\n8:\n9:## The contract (rules)\n10:\n11:1. **The manifest is the authoritative requirement registry.** Every PRD `R-*` requirement and every KNOWN-HAZARDS invariant exists in `traceable-reqs.toml` as a `REQ-*` id. No work without a REQ; no REQ without intent to satisfy. The PRD holds the prose; the manifest holds the id + `required_stages`.\n12:2. **Tag in the same change as the evidence.** When you write the function / test / doc-section that satisfies a stage, add its `[<stage>-><REQ-ID>]` tag in *that same commit*. Never \"tag later\" — this single rule is what stops the trace from drifting.\n13:3. **Evidence-proximate tags, not file-tops.** A tag sits on or immediately above the real function, test, doc section, config entry, or workflow step that proves the stage. One tag = one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.\n14:4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring `unit` (and `int` where cross-process/cross-node). A hazard cannot be \"covered\" without a test tagging it — the anti-regression promise becomes mechanical (PRD success criterion #6).\n15:5. **Activation, not premature failure.** Every requirement starts `required_stages = []` (inactive) so `check` stays green pre-code. A **milestone (M0–M5) activates** the requirements it delivers by setting their real `required_stages`. Deferred items (`docs/DEFERRED.md`) stay `[]` until promoted.\n16:6. **Scan roots stay honest.** `[scan].roots` includes every evidence location (`src`, `tests`, `docs`, and later `.github/workflows`, `scripts`). Audit roots whenever a new evidence dir appears — a missing root makes evidence *silently* vanish from the trace.\n17:7. **Scope at mint.** Every new `REQ-*` joins at least one `[[groups]]` subject scope in the same commit that mints it. The groups partition is what the confusion audit (Gate 4, 2026-07-30) turned into manifest structure: the `req-confusion-audit` skill scores confusability only *within* groups, so an unscoped requirement's confusable pairs go unexamined — roughly half of them, by the audit's measurement. `span-*` groups are the declared cross-scope cover for measured-hot pairs; add one when a new requirement is known to sit confusably against another scope. Do not derive scopes from id prefixes — subject matter, not spelling.\n18:\n19:### Stages\n20:\n21:`doc` (prose/design/rustdoc) · `impl` (production code) · `unit` (unit test) · `int` (integration / e2e / cross-node). Default activation policy: `[\"doc\",\"impl\",\"unit\"]`; networking/lifecycle/cross-node reqs add `int`.\n22:\n23:### Tag examples\n24:\n25:Real tags are `[<stage>->REQ-ID]` with a bare stage word. The examples below\n26:angle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as\n27:illustrations, not live evidence — write a bare stage word in real code.\n28:\n29:```rust\n30:// [<impl>->REQ-PAIR-1]\n31:fn spake2_pair(...) { ... }\n32:```\n33:```rust\n34:// [<unit>->REQ-HAZARD-ENVELOPE-DECODE-ORDER]\n35:#[test] fn decodes_amp_last() { ... }\n36:```\n37:```markdown\n38:<!-- [<doc>->REQ-NET-2] -->\n39:## Relay dependency\n40:```\n41:\n42:## Enforcement (four layers — defense in depth)\n43:\n44:1. **CI gate (hard backstop).** `traceable-reqs check --json` runs on every PR/push; exit-1 (`missing_stage` / `undeclared_id` / `parse_error` / `manifest_error`) **fails the build**. A PR can't merge if it leaves an *activated* req's required stage uncovered. Runs alongside the DOCS-STRATEGY CI gates.\n45:2. **Milestone activation gate.** Starting a milestone flips its reqs from `[]` to the real policy; the milestone **is not done until `check` is green for its reqs**. Coverage grows *with* the project instead of failing on day one.\n46:3. **Agent-facing rule.** The repo `CLAUDE.md` instructs every dev-agent: tag `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check` before declaring done. Since spt-core is built *with* agents, the contract must be machine-followable — agent discipline + CI as the net.\n47:4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review` + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run advisory in CI (or periodically). Presence = hard gate; quality = audited.\n48:\n49:Optional fast-feedback: a pre-push git hook running `traceable-reqs check` locally.\n50:\n51:## CI snippet (wire at M0, once there is a Rust project + CI)\n52:\n53:```yaml\n54:# .github/workflows/traceability.yml\n55:name: traceability\n56:on: [push, pull_request]\n57:jobs:\n58:  check:\n59:    runs-on: ubuntu-latest\n60:    steps:\n61:      - uses: actions/checkout@v4\n62:      - name: Install traceable-reqs\n63:        run: gh release download --repo BigscreenVR/traceable-reqs --pattern '*linux-x86_64' --output traceable-reqs && chmod +x traceable-reqs\n64:        env: { GH_TOKEN: ${{ github.token }} }\n65:      - name: Coverage gate (hard)\n66:        run: ./traceable-reqs check --json\n67:      - name: Quality audit (advisory)\n68:        run: ./traceable-reqs lint || true\n69:```\n70:\n71:## Lifecycle\n72:\n73:- **Now (planning):** manifest seeded, all reqs inactive. `check` is green (nothing required yet).\n74:- **M0:** install the CLI, validate the seed against it, wire the CI workflow, **activate M0 reqs** (`REQ-ARCH-*`, `REQ-HAZARD-*` for the commodity layer), tag as code lands.\n75:- **M1–M5:** each milestone activates + covers its requirements before it's called done."}],"details":{"totalLines":75,"displayContent":{"text":"# Traceability development contract\r\n\r\n> How spt-core uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs) to keep every requirement traced from doc → impl → test. The manifest is `traceable-reqs.toml` (seeded from PRD `R-*` + KNOWN-HAZARDS invariants). This contract makes the trace pay off instead of rotting.\r\n\r\n## Why\r\n\r\nThe whole point of the rebuild is to *not* re-introduce the sister project's bugs and to ship the PRD's scope verifiably. A requirement that isn't traced to real evidence (a doc, an implementation, a test) is a requirement you're *hoping* is met. This contract converts that hope into a build that fails when evidence is missing.\r\n\r\n## The contract (rules)\r\n\r\n1. **The manifest is the authoritative requirement registry.** Every PRD `R-*` requirement and every KNOWN-HAZARDS invariant exists in `traceable-reqs.toml` as a `REQ-*` id. No work without a REQ; no REQ without intent to satisfy. The PRD holds the prose; the manifest holds the id + `required_stages`.\r\n2. **Tag in the same change as the evidence.** When you write the function / test / doc-section that satisfies a stage, add its `[<stage>-><REQ-ID>]` tag in *that same commit*. Never \"tag later\" — this single rule is what stops the trace from drifting.\r\n3. **Evidence-proximate tags, not file-tops.** A tag sits on or immediately above the real function, test, doc section, config entry, or workflow step that proves the stage. One tag = one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.\r\n4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring `unit` (and `int` where cross-process/cross-node). A hazard cannot be \"covered\" without a test tagging it — the anti-regression promise becomes mechanical (PRD success criterion #6).\r\n5. **Activation, not premature failure.** Every requirement starts `required_stages = []` (inactive) so `check` stays green pre-code. A **milestone (M0–M5) activates** the requirements it delivers by setting their real `required_stages`. Deferred items (`docs/DEFERRED.md`) stay `[]` until promoted.\r\n6. **Scan roots stay honest.** `[scan].roots` includes every evidence location (`src`, `tests`, `docs`, and later `.github/workflows`, `scripts`). Audit roots whenever a new evidence dir appears — a missing root makes evidence *silently* vanish from the trace.\r\n7. **Scope at mint.** Every new `REQ-*` joins at least one `[[groups]]` subject scope in the same commit that mints it. The groups partition is what the confusion audit (Gate 4, 2026-07-30) turned into manifest structure: the `req-confusion-audit` skill scores confusability only *within* groups, so an unscoped requirement's confusable pairs go unexamined — roughly half of them, by the audit's measurement. `span-*` groups are the declared cross-scope cover for measured-hot pairs; add one when a new requirement is known to sit confusably against another scope. Do not derive scopes from id prefixes — subject matter, not spelling.\r\n\r\n### Stages\r\n\r\n`doc` (prose/design/rustdoc) · `impl` (production code) · `unit` (unit test) · `int` (integration / e2e / cross-node). Default activation policy: `[\"doc\",\"impl\",\"unit\"]`; networking/lifecycle/cross-node reqs add `int`.\r\n\r\n### Tag examples\r\n\r\nReal tags are `[<stage>->REQ-ID]` with a bare stage word. The examples below\r\nangle-bracket the stage (`[<impl>->...]`) only so the scanner treats them as\r\nillustrations, not live evidence — write a bare stage word in real code.\r\n\r\n```rust\r\n// [<impl>->REQ-PAIR-1]\r\nfn spake2_pair(...) { ... }\r\n```\r\n```rust\r\n// [<unit>->REQ-HAZARD-ENVELOPE-DECODE-ORDER]\r\n#[test] fn decodes_amp_last() { ... }\r\n```\r\n```markdown\r\n<!-- [<doc>->REQ-NET-2] -->\r\n## Relay dependency\r\n```\r\n\r\n## Enforcement (four layers — defense in depth)\r\n\r\n1. **CI gate (hard backstop).** `traceable-reqs check --json` runs on every PR/push; exit-1 (`missing_stage` / `undeclared_id` / `parse_error` / `manifest_error`) **fails the build**. A PR can't merge if it leaves an *activated* req's required stage uncovered. Runs alongside the DOCS-STRATEGY CI gates.\r\n2. **Milestone activation gate.** Starting a milestone flips its reqs from `[]` to the real policy; the milestone **is not done until `check` is green for its reqs**. Coverage grows *with* the project instead of failing on day one.\r\n3. **Agent-facing rule.** The repo `CLAUDE.md` instructs every dev-agent: tag `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check` before declaring done. Since spt-core is built *with* agents, the contract must be machine-followable — agent discipline + CI as the net.\r\n4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review` + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run advisory in CI (or periodically). Presence = hard gate; quality = audited.\r\n\r\nOptional fast-feedback: a pre-push git hook running `traceable-reqs check` locally.\r\n\r\n## CI snippet (wire at M0, once there is a Rust project + CI)\r\n\r\n```yaml\r\n# .github/workflows/traceability.yml\r\nname: traceability\r\non: [push, pull_request]\r\njobs:\r\n  check:\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - uses: actions/checkout@v4\r\n      - name: Install traceable-reqs\r\n        run: gh release download --repo BigscreenVR/traceable-reqs --pattern '*linux-x86_64' --output traceable-reqs && chmod +x traceable-reqs\r\n        env: { GH_TOKEN: ${{ github.token }} }\r\n      - name: Coverage gate (hard)\r\n        run: ./traceable-reqs check --json\r\n      - name: Quality audit (advisory)\r\n        run: ./traceable-reqs lint || true\r\n```\r\n\r\n## Lifecycle\r\n\r\n- **Now (planning):** manifest seeded, all reqs inactive. `check` is green (nothing required yet).\r\n- **M0:** install the CLI, validate the seed against it, wire the CI workflow, **activate M0 reqs** (`REQ-ARCH-*`, `REQ-HAZARD-*` for the commodity layer), tag as code lands.\r\n- **M1–M5:** each milestone activates + covers its requirements before it's called done.\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75]},"fileSize":6057,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\TRACEABILITY.md"}}},"isError":false,"timestamp":1789402556986}}
{"type":"message","id":"efd6b786","parentId":"d262387e","timestamp":"2026-09-14T16:15:56.991Z","message":{"role":"toolResult","toolCallId":"call_3yFikUGKH4Q2lY2tMIKy4IsI|fc_080dc0728d55fa10016aa81dbce25487d2854091b2117de6fb","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/DOCS-STRATEGY.md#94CD]\n1:# Documentation strategy\n2:\n3:> Draft (2026-05-31). A **designed-in commitment**, not an afterthought — grounded in research on critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare, Anthropic, Diátaxis, `llms.txt`, Google dev style guide). Governs the **shipped product docs** (authored as code lands); the planning docs (CONTEXT/PRD/ADRs/design docs) are internal and separate.\n4:\n5:## The defining constraint: a dual audience\n6:\n7:spt-core's docs serve **two readers at once** — human developers *and* the **AI dev-agents that build integrations with it**. This is unusual and it is the design constraint: every artifact is authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export). The agent layer is **first-class, not optional** — the strategic signal (Tailwind's docs traffic fell ~40% because assistants answer for it) is, for spt-core, *the goal*: make the agent layer so good that dev-agents integrate correctly on the first try. There is recursive fit here — the same kind of agent that spt-core *hosts* is the one that *reads its docs*.\n8:\n9:## Principles (top techniques, prioritized for spt-core)\n10:\n11:1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no placeholders. TTFHW (\"time to first hello world\") is the single most-cited conversion lever (~68% abandon trials over setup time).\n12:2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed (mixing is the most-cited cause of confusing docs).\n13:3. **Deterministic, real, copy-pasteable examples everywhere** — no `<YOUR_VALUE_HERE>` placeholders; real values that run. Serves humans *and* agents.\n14:4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full concatenated export), auto-generated in CI, plus markdown content negotiation (`.md` suffix / `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\n15:5. **One canonical way to do X** — explicitly mark deprecated/alternate paths. Non-determinism is fatal for agents.\n16:6. **Complete reference, auto-generated, all error variants** — rustdoc for the public SDK crates (`spt-proto`, `spt-runtime`, `spt-msg`) + a machine-readable manifest/contract schema. Generic placeholders in reference are a failure mode.\n17:7. **Consistent conversational voice** — adopt the Google developer style guide: second person (\"you\"), active voice, knowledgeable-friend tone.\n18:8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the messaging model, the agent lifecycle states, instances/dormancy, and P2P topology.\n19:9. **Stable, never-renamed anchors/URLs** — agents cache links.\n20:10. **Docs-as-product, gated in CI** — generation (rustdoc, `llms.txt`, schema, CLI help) is part of the build so docs can never drift from code. Drift is the #1 most-cited docs failure; this kills it structurally.\n21:\n22:## Information architecture — by capability vertical\n23:\n24:Organize by capability, each vertical carrying the same four Diátaxis modes internally (the Cloudflare pattern):\n25:\n26:**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\n27:\n28:## Killer quickstart targets (two, one per audience)\n29:\n30:- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n31:- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n32:\n33:## Agent-consumable docs (spt-core specifics)\n34:\n35:<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->\n36:\n37:- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index lets a tool answer quick questions, the full export feeds deep ingestion.\n38:- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\n39:- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\n40:- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\n41:- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\n42:\n43:## v0.1 deployment scope (grilled 2026-06-05)\n44:\n45:Deployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · **manifest JSON Schema (new code: schemars on the manifest types)** · root `llms.txt` + `llms-full.txt` CI-generated · rustdoc → Pages (no docs.rs — SDK crates are first-party-only while source is private).\n46:\n47:**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\n48:\n49:**Tier 2 (deferred, tracked):** remaining verticals' tutorials/how-tos (overview + reference stubs only in v0.1) · MCP doc server · `--help --json` mode (low marginal value over the generated CLI reference) · capstone tutorial · per-vertical `llms.txt` fan-out (root links pages directly until then).\n50:\n51:## Anti-patterns to design against (most-cited failures)\n52:\n53:Doc/code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first success; generic placeholders in reference; mixed Diátaxis modes; poor search/navigation; multiple non-canonical ways to do X (fatal for agents).\n54:\n55:## CI commitments\n56:\n57:rustdoc, the manifest JSON Schema, `llms.txt`/`llms-full.txt`, and CLI help exports are **generated and checked in CI** — a doc-drift gate. Doc quality lives on the same footing as tests.\n58:\n59:<!-- [doc->REQ-DOCS-NO-INTERNAL-CODES] -->\n60:**Public help carries no internal codes.** The public CLI `--help` (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading `--help` and ship to GitHub Pages. The `xtask check` docs gate scans the **generated** reference for these tokens and fails on any hit (regeneration alone keeps drift at zero but would faithfully republish a leak — the scan is what gives the gate teeth). The scan is scoped to the generated reference by construction, so rustdoc `///` on functions and structs is out of scope and keeps its `REQ`/`ADR` cross-references — those are source-internal, never public help. Substance stays; co…\n61:\n62:---\n63:\n64:## Concrete templates (from firsthand artifacts)\n65:\n66:Extracted by fetching the *actual* artifacts (Cloudflare/Anthropic/Mintlify `llms.txt`, FastAPI/Stripe/Supabase quickstarts, the Rust Book TOC, Django/Diátaxis, Tokio/clap rustdoc, gh/Stripe CLI). Copy the *shape*.\n67:\n68:### `llms.txt` — two-level (Cloudflare pattern, not Anthropic's flat dump)\n69:\n70:Root `llms.txt` is a **curated index that fans out to a per-vertical `llms.txt`**; `llms-full.txt` is CI-concatenated page bodies (Anthropic's overflowed a 10 MB fetch — generation-only, never hand-authored).\n71:\n72:```markdown\n73:# spt-core\n74:A harness-independent core for an agent ecosystem: messaging, live-agent\n75:lifecycle, terminal hosting, P2P networking, a runtime-manifest harness contract.\n76:\n77:> Each capability links to its own llms.txt. Full concatenated docs: /llms-full.txt.\n78:> Machine-readable contract: /manifest.schema.json. Append `.md` to any doc URL\n79:> for raw markdown (~90% fewer tokens than HTML).\n80:\n81:## Start here\n82:- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\n83:- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n84:- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n85:\n86:## Capabilities      (one bullet per vertical, each → its own llms.txt)\n87:- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\n88:- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\n89:- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\n90:\n91:## Reference (auto-generated)\n92:- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\n93:- [CLI reference](…/cli/llms.txt) · [Manifest JSON Schema](…/manifest.schema.json)\n94:```\n95:\n96:### Quickstart — whole-thing → run → verify → dissect (FastAPI), with a 2-path fork (Supabase)\n97:\n98:Zero placeholders; every value runs; complete working thing shown *before* explanation; audience fork at the top (spt-core has two readers); end with the Diátaxis trident.\n…\n114:\n…\n147:Top-level `--help` groups by audience (`AGENT COMMANDS` send/ring/ready/poll · `HARNESS COMMANDS` api … · `NODE COMMANDS` pair/subnet/update). Per-command: **USAGE · FLAGS · EXAMPLES (real values) · LEARN MORE**. Beyond gh: a **`spt <cmd> --help --json`** structured-help mode so dev-agents parse the contract without scraping, plus gh's uniform **`--json <fields>` / `--jq` / `--template`** trio on any list command. The `--json` help export is generated + CI-gated alongside rustdoc and the schema.\n\n[Showing lines 1-98 of 147. Use :99 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# Documentation strategy\r\n\r\n> Draft (2026-05-31). A **designed-in commitment**, not an afterthought — grounded in research on critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare, Anthropic, Diátaxis, `llms.txt`, Google dev style guide). Governs the **shipped product docs** (authored as code lands); the planning docs (CONTEXT/PRD/ADRs/design docs) are internal and separate.\r\n\r\n## The defining constraint: a dual audience\r\n\r\nspt-core's docs serve **two readers at once** — human developers *and* the **AI dev-agents that build integrations with it**. This is unusual and it is the design constraint: every artifact is authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export). The agent layer is **first-class, not optional** — the strategic signal (Tailwind's docs traffic fell ~40% because assistants answer for it) is, for spt-core, *the goal*: make the agent layer so good that dev-agents integrate correctly on the first try. There is recursive fit here — the same kind of agent that spt-core *hosts* is the one that *reads its docs*.\r\n\r\n## Principles (top techniques, prioritized for spt-core)\r\n\r\n1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no placeholders. TTFHW (\"time to first hello world\") is the single most-cited conversion lever (~68% abandon trials over setup time).\r\n2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed (mixing is the most-cited cause of confusing docs).\r\n3. **Deterministic, real, copy-pasteable examples everywhere** — no `<YOUR_VALUE_HERE>` placeholders; real values that run. Serves humans *and* agents.\r\n4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full concatenated export), auto-generated in CI, plus markdown content negotiation (`.md` suffix / `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\r\n5. **One canonical way to do X** — explicitly mark deprecated/alternate paths. Non-determinism is fatal for agents.\r\n6. **Complete reference, auto-generated, all error variants** — rustdoc for the public SDK crates (`spt-proto`, `spt-runtime`, `spt-msg`) + a machine-readable manifest/contract schema. Generic placeholders in reference are a failure mode.\r\n7. **Consistent conversational voice** — adopt the Google developer style guide: second person (\"you\"), active voice, knowledgeable-friend tone.\r\n8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the messaging model, the agent lifecycle states, instances/dormancy, and P2P topology.\r\n9. **Stable, never-renamed anchors/URLs** — agents cache links.\r\n10. **Docs-as-product, gated in CI** — generation (rustdoc, `llms.txt`, schema, CLI help) is part of the build so docs can never drift from code. Drift is the #1 most-cited docs failure; this kills it structurally.\r\n\r\n## Information architecture — by capability vertical\r\n\r\nOrganize by capability, each vertical carrying the same four Diátaxis modes internally (the Cloudflare pattern):\r\n\r\n**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\r\n\r\n## Killer quickstart targets (two, one per audience)\r\n\r\n- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\r\n- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\r\n\r\n## Agent-consumable docs (spt-core specifics)\r\n\r\n<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->\r\n\r\n- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index lets a tool answer quick questions, the full export feeds deep ingestion.\r\n- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\r\n- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\r\n- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\r\n- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\r\n\r\n## v0.1 deployment scope (grilled 2026-06-05)\r\n\r\nDeployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · **manifest JSON Schema (new code: schemars on the manifest types)** · root `llms.txt` + `llms-full.txt` CI-generated · rustdoc → Pages (no docs.rs — SDK crates are first-party-only while source is private).\r\n\r\n**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\r\n\r\n**Tier 2 (deferred, tracked):** remaining verticals' tutorials/how-tos (overview + reference stubs only in v0.1) · MCP doc server · `--help --json` mode (low marginal value over the generated CLI reference) · capstone tutorial · per-vertical `llms.txt` fan-out (root links pages directly until then).\r\n\r\n## Anti-patterns to design against (most-cited failures)\r\n\r\nDoc/code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first success; generic placeholders in reference; mixed Diátaxis modes; poor search/navigation; multiple non-canonical ways to do X (fatal for agents).\r\n\r\n## CI commitments\r\n\r\nrustdoc, the manifest JSON Schema, `llms.txt`/`llms-full.txt`, and CLI help exports are **generated and checked in CI** — a doc-drift gate. Doc quality lives on the same footing as tests.\r\n\r\n<!-- [doc->REQ-DOCS-NO-INTERNAL-CODES] -->\r\n**Public help carries no internal codes.** The public CLI `--help` (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading `--help` and ship to GitHub Pages. The `xtask check` docs gate scans the **generated** reference for these tokens and fails on any hit (regeneration alone keeps drift at zero but would faithfully republish a leak — the scan is what gives the gate teeth). The scan is scoped to the generated reference by construction, so rustdoc `///` on functions and structs is out of scope and keeps its `REQ`/`ADR` cross-references — those are source-internal, never public help. Substance stays; co…\n\r\n---\r\n\r\n## Concrete templates (from firsthand artifacts)\r\n\r\nExtracted by fetching the *actual* artifacts (Cloudflare/Anthropic/Mintlify `llms.txt`, FastAPI/Stripe/Supabase quickstarts, the Rust Book TOC, Django/Diátaxis, Tokio/clap rustdoc, gh/Stripe CLI). Copy the *shape*.\r\n\r\n### `llms.txt` — two-level (Cloudflare pattern, not Anthropic's flat dump)\r\n\r\nRoot `llms.txt` is a **curated index that fans out to a per-vertical `llms.txt`**; `llms-full.txt` is CI-concatenated page bodies (Anthropic's overflowed a 10 MB fetch — generation-only, never hand-authored).\r\n\r\n```markdown\r\n# spt-core\r\nA harness-independent core for an agent ecosystem: messaging, live-agent\r\nlifecycle, terminal hosting, P2P networking, a runtime-manifest harness contract.\r\n\r\n> Each capability links to its own llms.txt. Full concatenated docs: /llms-full.txt.\r\n> Machine-readable contract: /manifest.schema.json. Append `.md` to any doc URL\r\n> for raw markdown (~90% fewer tokens than HTML).\r\n\r\n## Start here\r\n- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\r\n- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\r\n- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\r\n\r\n## Capabilities      (one bullet per vertical, each → its own llms.txt)\r\n- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\r\n- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\r\n- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\r\n\r\n## Reference (auto-generated)\r\n- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\r\n- [CLI reference](…/cli/llms.txt) · [Manifest JSON Schema](…/manifest.schema.json)\r\n```\r\n\r\n### Quickstart — whole-thing → run → verify → dissect (FastAPI), with a 2-path fork (Supabase)\r\n\r\nZero placeholders; every value runs; complete working thing shown *before* explanation; audience fork at the top (spt-core has two readers); end with the Diátaxis trident.\r","truncated":true,"truncatedBy":"lines","totalLines":147,"totalBytes":10070,"outputLines":98,"outputBytes":10070,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":147,"displayContent":{"text":"# Documentation strategy\r\n\r\n> Draft (2026-05-31). A **designed-in commitment**, not an afterthought — grounded in research on critically-acclaimed developer docs (Stripe, Twilio, the Rust Book, FastAPI, Cloudflare, Anthropic, Diátaxis, `llms.txt`, Google dev style guide). Governs the **shipped product docs** (authored as code lands); the planning docs (CONTEXT/PRD/ADRs/design docs) are internal and separate.\r\n\r\n## The defining constraint: a dual audience\r\n\r\nspt-core's docs serve **two readers at once** — human developers *and* the **AI dev-agents that build integrations with it**. This is unusual and it is the design constraint: every artifact is authored **once in clean markdown** and served in **two depths** (human-rendered + agent-export). The agent layer is **first-class, not optional** — the strategic signal (Tailwind's docs traffic fell ~40% because assistants answer for it) is, for spt-core, *the goal*: make the agent layer so good that dev-agents integrate correctly on the first try. There is recursive fit here — the same kind of agent that spt-core *hosts* is the one that *reads its docs*.\r\n\r\n## Principles (top techniques, prioritized for spt-core)\r\n\r\n1. **Sub-10-minute killer quickstart** — runnable, deterministic, whole-working-thing-first, no placeholders. TTFHW (\"time to first hello world\") is the single most-cited conversion lever (~68% abandon trials over setup time).\r\n2. **Diátaxis four-mode separation** — tutorial / how-to / reference / explanation, never mixed (mixing is the most-cited cause of confusing docs).\r\n3. **Deterministic, real, copy-pasteable examples everywhere** — no `<YOUR_VALUE_HERE>` placeholders; real values that run. Serves humans *and* agents.\r\n4. **Dual-depth agent exports** — `llms.txt` (slim curated index) + `llms-full.txt` (full concatenated export), auto-generated in CI, plus markdown content negotiation (`.md` suffix / `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\r\n5. **One canonical way to do X** — explicitly mark deprecated/alternate paths. Non-determinism is fatal for agents.\r\n6. **Complete reference, auto-generated, all error variants** — rustdoc for the public SDK crates (`spt-proto`, `spt-runtime`, `spt-msg`) + a machine-readable manifest/contract schema. Generic placeholders in reference are a failure mode.\r\n7. **Consistent conversational voice** — adopt the Google developer style guide: second person (\"you\"), active voice, knowledgeable-friend tone.\r\n8. **Explain *why*, not just *what*** — conceptual docs + diagrams for the messaging model, the agent lifecycle states, instances/dormancy, and P2P topology.\r\n9. **Stable, never-renamed anchors/URLs** — agents cache links.\r\n10. **Docs-as-product, gated in CI** — generation (rustdoc, `llms.txt`, schema, CLI help) is part of the build so docs can never drift from code. Drift is the #1 most-cited docs failure; this kills it structurally.\r\n\r\n## Information architecture — by capability vertical\r\n\r\nOrganize by capability, each vertical carrying the same four Diátaxis modes internally (the Cloudflare pattern):\r\n\r\n**Messaging · Live-agent lifecycle · Terminal hosting · Networking (P2P + pairing) · Harness contract (manifest + `api`) · Instances · Shells · Self-update.**\r\n\r\n## Killer quickstart targets (two, one per audience)\r\n\r\n- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\r\n- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\r\n\r\n## Agent-consumable docs (spt-core specifics)\r\n\r\n<!-- [doc->REQ-DOCS-4] the agent-consumable layer's design; the schema leg shipped M6-D3 (schemars-derived, $id at the canonical URL), llms/CLI legs M6-D5, MCP + --json deferred (DEFERRED.md) -->\r\n\r\n- **`llms.txt` / `llms-full.txt`** — auto-emitted in CI; the slim index lets a tool answer quick questions, the full export feeds deep ingestion.\r\n- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\r\n- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\r\n- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\r\n- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\r\n\r\n## v0.1 deployment scope (grilled 2026-06-05)\r\n\r\nDeployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · **manifest JSON Schema (new code: schemars on the manifest types)** · root `llms.txt` + `llms-full.txt` CI-generated · rustdoc → Pages (no docs.rs — SDK crates are first-party-only while source is private).\r\n\r\n**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\r\n\r\n**Tier 2 (deferred, tracked):** remaining verticals' tutorials/how-tos (overview + reference stubs only in v0.1) · MCP doc server · `--help --json` mode (low marginal value over the generated CLI reference) · capstone tutorial · per-vertical `llms.txt` fan-out (root links pages directly until then).\r\n\r\n## Anti-patterns to design against (most-cited failures)\r\n\r\nDoc/code drift (#1 — solved by CI gating); *what* without *why*; too much setup before first success; generic placeholders in reference; mixed Diátaxis modes; poor search/navigation; multiple non-canonical ways to do X (fatal for agents).\r\n\r\n## CI commitments\r\n\r\nrustdoc, the manifest JSON Schema, `llms.txt`/`llms-full.txt`, and CLI help exports are **generated and checked in CI** — a doc-drift gate. Doc quality lives on the same footing as tests.\r\n\r\n<!-- [doc->REQ-DOCS-NO-INTERNAL-CODES] -->\r\n**Public help carries no internal codes.** The public CLI `--help` (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading `--help` and ship to GitHub Pages. The `xtask check` docs gate scans the **generated** reference for these tokens and fails on any hit (regeneration alone keeps drift at zero but would faithfully republish a leak — the scan is what gives the gate teeth). The scan is scoped to the generated reference by construction, so rustdoc `///` on functions and structs is out of scope and keeps its `REQ`/`ADR` cross-references — those are source-internal, never public help. Substance stays; co…\n\r\n---\r\n\r\n## Concrete templates (from firsthand artifacts)\r\n\r\nExtracted by fetching the *actual* artifacts (Cloudflare/Anthropic/Mintlify `llms.txt`, FastAPI/Stripe/Supabase quickstarts, the Rust Book TOC, Django/Diátaxis, Tokio/clap rustdoc, gh/Stripe CLI). Copy the *shape*.\r\n\r\n### `llms.txt` — two-level (Cloudflare pattern, not Anthropic's flat dump)\r\n\r\nRoot `llms.txt` is a **curated index that fans out to a per-vertical `llms.txt`**; `llms-full.txt` is CI-concatenated page bodies (Anthropic's overflowed a 10 MB fetch — generation-only, never hand-authored).\r\n\r\n```markdown\r\n# spt-core\r\nA harness-independent core for an agent ecosystem: messaging, live-agent\r\nlifecycle, terminal hosting, P2P networking, a runtime-manifest harness contract.\r\n\r\n> Each capability links to its own llms.txt. Full concatenated docs: /llms-full.txt.\r\n> Machine-readable contract: /manifest.schema.json. Append `.md` to any doc URL\r\n> for raw markdown (~90% fewer tokens than HTML).\r\n\r\n## Start here\r\n- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\r\n- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\r\n- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\r\n\r\n## Capabilities      (one bullet per vertical, each → its own llms.txt)\r\n- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\r\n- [Live-agent lifecycle](…/lifecycle/llms.txt): skeleton → live → dormant → suspended.\r\n- … Terminal · Networking · Harness contract · Instances · Shells · Self-update\r\n\r\n## Reference (auto-generated)\r\n- [spt-proto](https://docs.rs/spt-proto) · [spt-runtime](https://docs.rs/spt-runtime) · [spt-msg](https://docs.rs/spt-msg)\r\n- [CLI reference](…/cli/llms.txt) · [Manifest JSON Schema](…/manifest.schema.json)\r\n```\r\n\r\n### Quickstart — whole-thing → run → verify → dissect (FastAPI), with a 2-path fork (Supabase)\r\n\r\nZero placeholders; every value runs; complete working thing shown *before* explanation; audience fork at the top (spt-core has two readers); end with the Diátaxis trident.\r\n…\n\n…\nTop-level `--help` groups by audience (`AGENT COMMANDS` send/ring/ready/poll · `HARNESS COMMANDS` api … · `NODE COMMANDS` pair/subnet/update). Per-command: **USAGE · FLAGS · EXAMPLES (real values) · LEARN MORE**. Beyond gh: a **`spt <cmd> --help --json`** structured-help mode so dev-agents parse the contract without scraping, plus gh's uniform **`--json <fields>` / `--jq` / `--template`** trio on any list command. The `--json` help export is generated + CI-gated alongside rustdoc and the schema.","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,null,114,null,147]},"fileSize":13143,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\DOCS-STRATEGY.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":147,"totalBytes":10070,"outputLines":98,"outputBytes":10070,"shownRange":{"start":1,"end":98},"nextOffset":99},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789402556991}}
{"type":"message","id":"be56f5cb","parentId":"efd6b786","timestamp":"2026-09-14T16:15:56.991Z","message":{"role":"toolResult","toolCallId":"call_W9Fw6zoN9SNYmtrEWZFXcc90|fc_080dc0728d55fa10016aa81dbce26c87d2a94374ad025e72c1","toolName":"grep","content":[{"type":"text","text":"No matches found"}],"details":{"scopePath":".worktrees/308-registry-process-lock/docs-site/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs-site\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":0,"fileCount":0,"files":[],"truncated":false},"isError":false,"useless":true,"timestamp":1789402556991}}
{"type":"message","id":"0be77ec0","parentId":"be56f5cb","timestamp":"2026-09-14T16:15:57.008Z","message":{"role":"toolResult","toolCallId":"call_WcAxVfDm70uUe6mJB25mNfKu|fc_080dc0728d55fa10016aa81dbce26487d2ac4c3fc7c2af466d","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/crates/spt-daemon/src/livehost.rs#996D]\n1259:/// Delete every EXPIRED attachment snapshot and its registry entry together, on\n1260:/// the live host's existing tick.\n1261:///\n1262:/// Serve-time masking already refuses an expired entry, so this is not what\n1263:/// stops an expired link from working — it is what stops\n1264:/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\n1265:/// never does.\n1266:///\n1267:/// **The order is deliberate: bytes first, then the entry.** Neither step is\n1268:/// atomic with the other, so the question is which crash heals. Deleting the\n1269:/// file and then failing to save leaves an expired entry naming a file that is\n1270:/// gone: the next tick finds it expired again and `remove_file` is idempotent,\n1271:/// so the pair completes on its own. Saving first and then failing to delete\n1272:/// orphans the bytes with no entry left to name them, and no later tick can\n1273:/// repair that — it is exactly the unbounded growth this exists to prevent.\n1274:///\n1275:/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\n1276:/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\n1277:/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\n1278:/// containment check is belt-and-braces on that same point: a hand-edited or\n1279:/// corrupted registry must not be able to aim a delete at an arbitrary path.\n1280:// [impl->REQ-WEB-ATTACHMENT-PULL]\n1281:fn reap_expired_attachments() {\n1282:    // ONE WRITER. The whole load -> reap -> save runs under the registry write\n1283:    // lock every other writer in this process holds, because this pass is a\n1284:    // read-modify-write on a file `serve add` also writes: an entry registered\n1285:    // between our load and our save would be clobbered by our stale snapshot\n1286:    // and silently lost, on a 5s tick, forever.\n1287:    let held = crate::servehost::with_registry_write(|| {\n1288:        let registry_path = perch::serving_registry_file();\n1289:        if !registry_path.exists() {\n1290:            return; // nothing has ever been served on this node\n1291:        }\n1292:        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\n1293:            Ok(registry) => registry,\n1294:            Err(err) => {\n1295:                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\n1296:                return;\n1297:            }\n1298:        };\n1299:        let reaped = registry.reap_expired(now_ms());\n1300:        if reaped.is_empty() {\n1301:            return;\n1302:        }\n1303:        let snapshots = perch::serve_snapshots_dir();\n1304:        let mut bytes_removed: u64 = 0;\n1305:        for entry in &reaped {\n1306:            if entry.kind != spt_store::serving::ServedKind::Attachment {\n1307:                continue; // a reference entry's path is the user's own file\n1308:            }\n1309:            if !entry.path.starts_with(&snapshots) {\n1310:                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\n1311:                continue;\n1312:            }\n1313:            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\n1314:            match std::fs::remove_file(&entry.path) {\n1315:                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\n1316:                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n1317:                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\n1318:            }\n1319:        }\n1320:        if let Err(err) = registry.save_at(&registry_path) {\n1321:            // The bytes are gone and the entries are not. Say so rather than\n1322:            // reporting a reap that only half happened; the next tick retries.\n1323:            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\n1324:            return;\n1325:        }\n1326:        eprintln!(\n1327:            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\n1328:            reaped.len(),\n1329:            bytes_removed\n1330:        );\n1331:    });\n1332:    if let Err(err) = held {\n1333:        // A poisoned writer lock is not something a pulse may paper over: say\n1334:        // it every tick rather than reaping outside the lock.\n1335:        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\n1336:    }\n1337:}\n1338:\n\n[Showing lines 1259-1338 of 2917. Use :1339 to continue]"}],"details":{"truncation":{"content":"/// Delete every EXPIRED attachment snapshot and its registry entry together, on\r\n/// the live host's existing tick.\r\n///\r\n/// Serve-time masking already refuses an expired entry, so this is not what\r\n/// stops an expired link from working — it is what stops\r\n/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\r\n/// never does.\r\n///\r\n/// **The order is deliberate: bytes first, then the entry.** Neither step is\r\n/// atomic with the other, so the question is which crash heals. Deleting the\r\n/// file and then failing to save leaves an expired entry naming a file that is\r\n/// gone: the next tick finds it expired again and `remove_file` is idempotent,\r\n/// so the pair completes on its own. Saving first and then failing to delete\r\n/// orphans the bytes with no entry left to name them, and no later tick can\r\n/// repair that — it is exactly the unbounded growth this exists to prevent.\r\n///\r\n/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\r\n/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\r\n/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\r\n/// containment check is belt-and-braces on that same point: a hand-edited or\r\n/// corrupted registry must not be able to aim a delete at an arbitrary path.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\nfn reap_expired_attachments() {\r\n    // ONE WRITER. The whole load -> reap -> save runs under the registry write\r\n    // lock every other writer in this process holds, because this pass is a\r\n    // read-modify-write on a file `serve add` also writes: an entry registered\r\n    // between our load and our save would be clobbered by our stale snapshot\r\n    // and silently lost, on a 5s tick, forever.\r\n    let held = crate::servehost::with_registry_write(|| {\r\n        let registry_path = perch::serving_registry_file();\r\n        if !registry_path.exists() {\r\n            return; // nothing has ever been served on this node\r\n        }\r\n        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\r\n            Ok(registry) => registry,\r\n            Err(err) => {\r\n                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\r\n                return;\r\n            }\r\n        };\r\n        let reaped = registry.reap_expired(now_ms());\r\n        if reaped.is_empty() {\r\n            return;\r\n        }\r\n        let snapshots = perch::serve_snapshots_dir();\r\n        let mut bytes_removed: u64 = 0;\r\n        for entry in &reaped {\r\n            if entry.kind != spt_store::serving::ServedKind::Attachment {\r\n                continue; // a reference entry's path is the user's own file\r\n            }\r\n            if !entry.path.starts_with(&snapshots) {\r\n                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\r\n                continue;\r\n            }\r\n            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\r\n            match std::fs::remove_file(&entry.path) {\r\n                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\r\n                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\r\n                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\r\n            }\r\n        }\r\n        if let Err(err) = registry.save_at(&registry_path) {\r\n            // The bytes are gone and the entries are not. Say so rather than\r\n            // reporting a reap that only half happened; the next tick retries.\r\n            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\r\n            return;\r\n        }\r\n        eprintln!(\r\n            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\r\n            reaped.len(),\r\n            bytes_removed\r\n        );\r\n    });\r\n    if let Err(err) = held {\r\n        // A poisoned writer lock is not something a pulse may paper over: say\r\n        // it every tick rather than reaping outside the lock.\r\n        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\r\n    }\r\n}\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":1659,"totalBytes":3981,"outputLines":80,"outputBytes":3981,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":2917,"displayContent":{"text":"/// Delete every EXPIRED attachment snapshot and its registry entry together, on\r\n/// the live host's existing tick.\r\n///\r\n/// Serve-time masking already refuses an expired entry, so this is not what\r\n/// stops an expired link from working — it is what stops\r\n/// `$SPT_HOME/serve/snapshots/` from growing without bound, which masking alone\r\n/// never does.\r\n///\r\n/// **The order is deliberate: bytes first, then the entry.** Neither step is\r\n/// atomic with the other, so the question is which crash heals. Deleting the\r\n/// file and then failing to save leaves an expired entry naming a file that is\r\n/// gone: the next tick finds it expired again and `remove_file` is idempotent,\r\n/// so the pair completes on its own. Saving first and then failing to delete\r\n/// orphans the bytes with no entry left to name them, and no later tick can\r\n/// repair that — it is exactly the unbounded growth this exists to prevent.\r\n///\r\n/// **Only a snapshot is ever deleted.** `ServedKind::Attachment` marks an entry\r\n/// whose `path` this node minted into its own snapshot store; a `File` or `Dir`\r\n/// entry's `path` is the USER'S OWN file, which a reaper must never touch. The\r\n/// containment check is belt-and-braces on that same point: a hand-edited or\r\n/// corrupted registry must not be able to aim a delete at an arbitrary path.\r\n// [impl->REQ-WEB-ATTACHMENT-PULL]\r\nfn reap_expired_attachments() {\r\n    // ONE WRITER. The whole load -> reap -> save runs under the registry write\r\n    // lock every other writer in this process holds, because this pass is a\r\n    // read-modify-write on a file `serve add` also writes: an entry registered\r\n    // between our load and our save would be clobbered by our stale snapshot\r\n    // and silently lost, on a 5s tick, forever.\r\n    let held = crate::servehost::with_registry_write(|| {\r\n        let registry_path = perch::serving_registry_file();\r\n        if !registry_path.exists() {\r\n            return; // nothing has ever been served on this node\r\n        }\r\n        let mut registry = match spt_store::serving::ServingRegistry::load_at(&registry_path) {\r\n            Ok(registry) => registry,\r\n            Err(err) => {\r\n                eprintln!(\"SERVE_REAP_LOAD_FAIL: {err}\");\r\n                return;\r\n            }\r\n        };\r\n        let reaped = registry.reap_expired(now_ms());\r\n        if reaped.is_empty() {\r\n            return;\r\n        }\r\n        let snapshots = perch::serve_snapshots_dir();\r\n        let mut bytes_removed: u64 = 0;\r\n        for entry in &reaped {\r\n            if entry.kind != spt_store::serving::ServedKind::Attachment {\r\n                continue; // a reference entry's path is the user's own file\r\n            }\r\n            if !entry.path.starts_with(&snapshots) {\r\n                eprintln!(\"SERVE_REAP_SKIP_FOREIGN:{}\", entry.id);\r\n                continue;\r\n            }\r\n            let len = std::fs::metadata(&entry.path).map(|meta| meta.len()).unwrap_or(0);\r\n            match std::fs::remove_file(&entry.path) {\r\n                Ok(()) => bytes_removed = bytes_removed.saturating_add(len),\r\n                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\r\n                Err(err) => eprintln!(\"SERVE_REAP_UNLINK_FAIL:{}: {err}\", entry.id),\r\n            }\r\n        }\r\n        if let Err(err) = registry.save_at(&registry_path) {\r\n            // The bytes are gone and the entries are not. Say so rather than\r\n            // reporting a reap that only half happened; the next tick retries.\r\n            eprintln!(\"SERVE_REAP_SAVE_FAIL: {err}\");\r\n            return;\r\n        }\r\n        eprintln!(\r\n            \"SERVE_REAP: reaped {} expired entries, freed {} bytes\",\r\n            reaped.len(),\r\n            bytes_removed\r\n        );\r\n    });\r\n    if let Err(err) = held {\r\n        // A poisoned writer lock is not something a pulse may paper over: say\r\n        // it every tick rather than reaping outside the lock.\r\n        eprintln!(\"SERVE_REAP_LOCK_FAIL: {err}\");\r\n    }\r\n}\r\n\r","startLine":1259,"lineNumbers":[1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338]},"fileSize":150325,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\crates\\spt-daemon\\src\\livehost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2917,"totalBytes":3981,"outputLines":80,"outputBytes":3981,"shownRange":{"start":1259,"end":1338},"nextOffset":1339}}},"isError":false,"timestamp":1789402557007}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FnAirHtpJoG3l2VN1gxoFLpk|fc_080dc0728d55fa10016aa81dc24f5887d2bc250aac4528a677","toolName":"read","startedAt":"2026-09-14T16:16:02.488Z","args":{"path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:448-480"},"intent":"Reading regression section boundary"},"id":"68f262cc","parentId":"0be77ec0","timestamp":"2026-09-14T16:16:02.488Z"}
{"type":"message","id":"0e2afcec","parentId":"68f262cc","timestamp":"2026-09-14T16:16:02.489Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_FnAirHtpJoG3l2VN1gxoFLpk|fc_080dc0728d55fa10016aa81dc24f5887d2bc250aac4528a677","name":"read","arguments":{"i":"Reading regression section boundary","path":".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:448-480"},"intent":"Reading regression section boundary"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":13339,"output":105,"cacheRead":42240,"cacheWrite":0,"totalTokens":55684,"reasoningTokens":59,"cost":{"input":0.13339,"output":0.00525,"cacheRead":0.04224,"cacheWrite":0,"total":0.18088}},"stopReason":"toolUse","timestamp":1789402557014,"responseId":"resp_080dc0728d55fa10016aa81dbd430087d28ba3cf45e5473a7c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81dbe6ff887d2b9dfe429f36e1d28","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB3CjHYv8lMIPEGvjG2OFgJ7tdkgoDEQO_dCPKJw9mtsLp07YLXWHiqyy2vCzBlPKouZXzYOoUNmfQekKbo0VchfafNw6XArTpbE9dlcBvzghvRrnNtOadthaK2Rr2__1dm1kYgXG2WoLhxMLZkH3EBGCJIxRQwdCB8YUPuMWfJBR-iFGME09SkY0RfRObw22LDQ6AEPQhFxStADw0SPj5PfMXDQoeRhVTTUzqmuyVL2LZEs8Ha30fcJPk-4WwcR430c-HZybiS7vV3Z3Kh7NbkAKZf3nBgZfRNLrA1VdiCemncz9Rl-I1Ze6OSOP8qI9yyEmEEBuXZIumOV2DeunlViqlnkLacFO-g84WB7T9jzowpYx1iIabnxrvhqSh-UH0Fi9SYbH0Pgj6xHKUPZxlbt3PyJHnHKdIxxk5SKxztbAJeQfzOOuHp0OLv0qsIPMzTiST4wMxe0cqaCYB54S5xQoWM_YK7XXjVPZI6Duo8Ygqxibycq4p109-HNffNQZjTYof8cNY05Fx9RU-0P2dDO1CJYNMA1B8IojMo8ooOGL2_-QJeA8KChnlUOFrKoohXoVRlvQZzbqbwVN4AL3GPgSeQdKRxoNmHbUB__4dojimf2Ni72XaK6GvZSrXqRE1AwJzXGyaghfOKe7PvvCih_7sCK2sAB3mbR-a5j4nsO5Ry54n0O3t3QxeaRId4PAlzJNN5LPBQt7RKSGSU0ydab86pB16y3KiPl531YWy5KfIepdKn2S-cCqm9h_I9tw3yszeaAlKpCdVorZZQi9rwp5jb0ZO3NxHiSRs_CLFRG-luso0hDnmXgq0XeW4xMc2NDhgnTpUJBoQfnmjIj4NYQnkc65w2V9k7zNI-kpX4T31KZKQjl3Dw8i1soKMu5xKgcyojt4Gf9oNKr1nAy5ojSGbBnu2LcaItaETiiWued2-8C_Yy5xdngRoOftugoHarMUwImcNGNQ5P4q06HPc7fc6bQS8Kaxz2fIRQLJLcxEWVEBwHBqtgu9yShiS8BYnmzxVICq-SeVlTux_e5GBTI-pe0_MD7bFRd4qiw7Yk5fwvVmS_WhoAUZnng2Q7Le16wCSaGAY3QEXdtPhfUBJWstYyX6leZUvXbCAYbAkPY7vSuW6-d2KR0XFkE24pUf1bMkhqCGlKl2JXXhqslVRuq6QiDTKROBO1O4qmCEVg9ijNfnns168OvsjyBfTysR3Dxei8nN8OxlW5uJJhA-jd7QZg83d0SqfZZpU9QichORfjn03ZwCMaItTMwo-H9druDdVJVgkx9Mg7QeIOqayzHSlj8hQ-KyEnOhu6XWmypGGoMukbZAoNBOV1y-w9p8lN1meRavClVGW5MFmJmq0bkEXILGCEaz4OLFmS7FF-25if4ODjVOmPSIIErAxth_GnSR-NHpa1UQY6g4XrrCrT8YWatQmTKBly3mXSWLzoYl6c8FqrgHXF0kzYEPK1zeRMPhjuUzy7UWF7dHJk7q9OfJ_Kn2fQtdLnudQcsrHxmxn945mSFeX9UDNMHtlmTDzIf3A1x8OHWOhlK25JzeQI5YXhVWdbnlbwhsMT3G6IOAmqDhfoUMKGHH2CWZMdkIudj5pCTRAUy5J1_8SYIinYJvJDRChi4IFevgVpeYNrRp5l61UL1LLsVY_oIlmegizIj","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81dc24f5887d2bc250aac4528a677","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading regression section boundary\",\"path\":\".worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md:448-480\"}","call_id":"call_FnAirHtpJoG3l2VN1gxoFLpk","internal_chat_message_metadata_passthrough":{"create_time":1789402557.47159,"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"read","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":5473.202000007033,"ttft":1444.262999996543,"completedAt":1789402562488,"contextSnapshot":{"promptTokens":55579,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"message","id":"6c389ff0","parentId":"0e2afcec","timestamp":"2026-09-14T16:16:02.500Z","message":{"role":"toolResult","toolCallId":"call_FnAirHtpJoG3l2VN1gxoFLpk|fc_080dc0728d55fa10016aa81dc24f5887d2bc250aac4528a677","toolName":"read","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#F7C6]\n372:## 6. Documented regressions (non-obvious invariants)\n…\n441:### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n…\n447:\n448:### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`\n449:- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as stale. 6.4 named the invariant as \"the LLM is read-only; only the supervisor deletes\" and never contemplated core itself becoming a second WRITER on the Self's path; `echo.rs` read \"the mind\" as the Psyche and forgot the Self, while `docs/CONTEXT-MEMORY.md` says th…\n450:- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.\n451:- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.\n452:- **Testing note:** the pin is a **byte-identity** assertion over a pre-placed authored drop, not an existence check — the failure is an OVERWRITE, so the file still exists afterwards and `exists()` stays green straight through the defect. Restoring the write turns that arm red.\n453:- **Origin:** operator ruling 2026-09-06 on doyle's own funnel measurement (releases#276); built by todlando.\n454:<!-- [doc->REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP] -->\n455:\n456:### 6.13 IO event sequences never reset at a byte-window boundary\n457:<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->\n458:- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n459:- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.\n460:- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n461:\n462:---\n463:\n464:## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\n465:\n466:These were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\n467:\n468:### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\n469:- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\n470:- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\n471:- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\n472:- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\n473:\n474:### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\n475:- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\n476:- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\n477:- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\n478:- **Source:** codex Stage A #14.\n479:\n480:### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\n481:- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\n482:- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\n483:- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\n…\n485:\n…\n1049:\n\n[Showing lines 447-483 of 1132. Use :484 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"\r\n### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`\r\n- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as stale. 6.4 named the invariant as \"the LLM is read-only; only the supervisor deletes\" and never contemplated core itself becoming a second WRITER on the Self's path; `echo.rs` read \"the mind\" as the Psyche and forgot the Self, while `docs/CONTEXT-MEMORY.md` says th…\n- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.\r\n- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.\r\n- **Testing note:** the pin is a **byte-identity** assertion over a pre-placed authored drop, not an existence check — the failure is an OVERWRITE, so the file still exists afterwards and `exists()` stays green straight through the defect. Restoring the write turns that arm red.\r\n- **Origin:** operator ruling 2026-09-06 on doyle's own funnel measurement (releases#276); built by todlando.\r\n<!-- [doc->REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP] -->\r\n\r\n### 6.13 IO event sequences never reset at a byte-window boundary\r\n<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->\r\n- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\r\n- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.\r\n- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\r\n\r\n---\r\n\r\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\r\n\r\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\r\n\r\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\r\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\r\n- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\r\n- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\r\n- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\r\n\r\n### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\r\n- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\r\n- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\r\n- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\r\n- **Source:** codex Stage A #14.\r\n\r\n### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\r\n- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\r\n- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\r\n- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\r","truncated":true,"truncatedBy":"lines","totalLines":686,"totalBytes":6332,"outputLines":37,"outputBytes":6332,"lastLinePartial":false,"firstLineExceedsLimit":false},"totalLines":1132,"displayContent":{"text":"## 6. Documented regressions (non-obvious invariants)\n…\n### 6.11 Brain respawn must exec the APPLIED bytes, not the renamed old binary (Linux `current_exe` follows the apply-rename; readiness ≠ new-bytes) `[REQ-HAZARD-BRAIN-RESPAWN-PATH]`\n…\n\r\n### 6.12 The echo-commune brief never transits the agent's drop path (one writer on `<id>-commune.md`, and it is the agent) `[REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP]`\r\n- **Failure:** spt-core wrote its echo-commune brief to `<id>-commune.md` — the SAME path an agent writes its own boundary commune to — with no arbitration between the two writers. A Self commune authored at a `/clear` and not yet ingested was therefore overwritten **unread** by the boundary echo, which fires 25-45s later and lands exactly in that window. Measured on one endpoint 2026-09-06: **two of three Self communes lost inside one hour**, with the reborn session reporting its own wake text as stale. 6.4 named the invariant as \"the LLM is read-only; only the supervisor deletes\" and never contemplated core itself becoming a second WRITER on the Self's path; `echo.rs` read \"the mind\" as the Psyche and forgot the Self, while `docs/CONTEXT-MEMORY.md` says th…\n- **Invariant:** exactly **one** writer on the Self's drop path — the agent — and the echo's brief routes **direct** into the two-tier store on the fire. The file was never required by the design; it was an implementation convenience that re-used the ingest, and re-using it cost the Self's own words.\r\n- **spt-core mapping:** `fire_echo` (`spt-daemon/src/lifecycle.rs`) calls `spt_live::ingest_body` — the routing core shared with `ingest_drops`, so a later tiering change cannot fork — then publishes the COMMUNE io-event and consumes the write-ahead intent. `run_echo_commune` takes no directory and writes nothing on any arm. A deferred project slice (no project anchor) is a **loud discard**, never a file.\r\n- **Testing note:** the pin is a **byte-identity** assertion over a pre-placed authored drop, not an existence check — the failure is an OVERWRITE, so the file still exists afterwards and `exists()` stays green straight through the defect. Restoring the write turns that arm red.\r\n- **Origin:** operator ruling 2026-09-06 on doyle's own funnel measurement (releases#276); built by todlando.\r\n<!-- [doc->REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP] -->\r\n\r\n### 6.13 IO event sequences never reset at a byte-window boundary\r\n<!-- [doc->REQ-HAZARD-IOLOG-SEQ-MONOTONIC] -->\r\n- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\r\n- **Invariant:** decode tail bytes lossily only after discarding the leading row fragment; append above the global maximum. Detect non-increasing sequences, including equal adjacent values and resets hidden between a lower first and higher last row. Under the exclusive lock, repair retained rows in file order above their old maximum, preserving payloads. Retention keeps the newest rows by position, not sequence value.\r\n- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\r\n\r\n---\r\n\r\n## 7. Boundary & delivery integrity (added 2026-05-31 — Stage A red-team)\r\n\r\nThese were absent from the sister-project harvest; codex surfaced them as load-bearing gaps for spt-core's new daemon/network surface.\r\n\r\n### 7.1 Local `api` mutation auth  `[REQ-HAZARD-LOCAL-API-AUTH]`\r\n- **Failure:** any local process calls `spt api bind|state|session-end|history-log|poll` and binds, ends, injects, or spoofs the state of an endpoint it does not own. Local untrusted processes are explicitly in scope (shells, third-party adapters).\r\n- **Invariant:** every `api` *mutation* is authenticated to an endpoint/session — per-endpoint link token or OS-credential binding. An unrelated local process cannot bind, inject, end, or spoof state.\r\n- **spt-core mapping:** the `api` subcommand surface (PRD R-API-*) + broker IPC.\r\n- **Source:** codex Stage A #13 (`docs/reviews/STAGE-A-codex-redteam.md`).\r\n\r\n### 7.2 Idempotent delivery across brain restart  `[REQ-HAZARD-RESTART-IDEMPOTENT]`\r\n- **Failure:** broker queues, spool rows, PTY injection, remote streams, and file transfers cross the broker↔brain boundary; a brain restart duplicates or drops a side effect.\r\n- **Invariant:** every side effect crossing the boundary carries a durable ID + replay rule; replay after restart is exactly-once / idempotent. Crash the brain before/after spool write, before/after PTY write, mid-transfer, mid-registry update — no dup, no drop.\r\n- **spt-core mapping:** broker↔brain IPC (ADR-0004), self-update handoff.\r\n- **Source:** codex Stage A #14.\r\n\r\n### 7.3 Psyche outbound capture + sanitization  `[REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]`\r\n- **Failure:** the Psyche's sole outbound channel is its **stdout** (`<EVENT type=\"reply|notify\">` intents — ADR-0012). Two ways to break it: (a) a **null-stdout / detached** live-Psyche driver silently **discards every reply and notify**; (b) the daemon relays a Psyche-supplied `from=`/target **unchanged**, letting a sandboxed Psyche **spoof identity** or address arbitrary endpoints.\r\n- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the inbound message's structural sender (its `from`) only, `notify` → the agent's own user/subnet only. Body validated per 4.1.\r\n- **spt-core mapping:** the live-Psyche turn driver + daemon outbound relay (ADR-0012); `spt-proto::event` type taxonomy (`+reply`/`+notify`). The interim `runtime::spawn_session` `Stdio::null()` path is **not** the live-Psyche driver.\r\n…\n\n…\n","startLine":372,"lineNumbers":[372,null,441,null,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,null,485,null,1049]},"fileSize":360311,"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1132,"totalBytes":6332,"outputLines":37,"outputBytes":6332,"shownRange":{"start":447,"end":483},"nextOffset":484},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1789402562500}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LUJojTiyWdTU7FmkT3NsM7rP|ctc_080dc0728d55fa10016aa81dcb6bdc87d2bdee8ff7c535abb2","toolName":"edit","startedAt":"2026-09-14T16:16:58.744Z"},"id":"2bcaa99d","parentId":"6c389ff0","timestamp":"2026-09-14T16:16:58.744Z"}
{"type":"message","id":"2b23d9c1","parentId":"2bcaa99d","timestamp":"2026-09-14T16:16:58.745Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Editing registry lock details**\n\n**Updating documentation links**"},{"type":"toolCall","id":"call_LUJojTiyWdTU7FmkT3NsM7rP|ctc_080dc0728d55fa10016aa81dcb6bdc87d2bdee8ff7c535abb2","name":"edit","arguments":{"input":"*** Begin Patch\n[.worktrees/308-registry-process-lock/docs/STORAGE.md#D5CA]\nPUT 147.=150:\n+cache. The broker and brain are separate writer processes: broker control\n+requests register, remove, and reconcile entries; the brain's live-host pulse\n+reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+reads those snapshots without a writer lock. Only a genuinely missing file\n+initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+history refuses the operation rather than recycling stale URLs.\nPUT >184:\n+\n+### Writer serialization\n+\n+The releases#308 repair contract is **PREP-ONLY; no fixed-code validation has\n+run**. It preserves schema 1, the CLI, TTLs, and the existing save decisions.\n+See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+for the lost-update evidence and its limits.\n+\n+Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+the complete read-modify-write operation: load, mutation, associated snapshot\n+creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+refuses the operation; it never permits an unlocked write. The lock is\n+non-reentrant: a guarded caller must not reacquire it for the same registry.\n+\n+The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+stable sibling formed by appending `.lock` to the **full registry path**:\n+`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+publication replaces that file. Never unlink the sentinel, including on\n+unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+the guard or exiting the process releases the lock without deleting it.\n+\n+Atomic publication and mutual exclusion solve different problems. Atomic\n+replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+does not stop a stale writer from replacing a newer snapshot. Locking only\n+`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+with the brain. Direct writers, including live-daemon fixture mutations, must\n+follow the same full-operation guard contract.\n+\n+The guard **never auto-saves**, including on drop. Read-only listing, an\n+unchanged input-reference replay, and a reaper pass with no expired entries\n+do not save the registry; a missing registry remains missing after a no-op\n+reap. Existing operations that explicitly save, such as reconciliation, still\n+do so. Snapshot unlink-before-save ordering and user-source protection remain\n+the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+publication one atomic transaction.\n+\n+Protection is active only when **both running writer implementations** use\n+this protocol. Updating only the brain while an old broker survives does not\n+fully activate it; a new broker paired with an old brain is likewise unsafe.\n+The sentinel's presence is not proof that both writers participate.\n[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#F7C6]\nPUT >460:\n+\n+### 6.14 Serving-registry writers must serialize across processes\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. This repair is **PREP-ONLY**: no fixed-code validation or runtime measurement has run.\n[.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md#7349]\nPUT 118.=119:\n+The broker and brain are separate processes that write\n+`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n+entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\nPUT >126:\n+\n+The prepared writer-coordination repair requires both processes to hold the\n+same `serve/registry.lock` guard across reading and updating the registry.\n+Atomic snapshot replacement alone does not prevent one writer from losing\n+another's update. Both running writer implementations must be updated:\n+a new brain with an old broker does not fully activate protection. This is\n+a PREP-ONLY contract, not a measured fixed-runtime result. HTTP readers remain\n+lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+are unchanged.\n[.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md#7B67]\nPUT 59.=63:\n+The brain's pulse reaps expired registry entries and logs how many it removed.\n+For an attachment, it attempts to unlink the core-owned snapshot **before**\n+saving the registry without that entry. These are not one atomic transaction:\n+if saving fails after unlinking, the next pulse can finish removing the expired\n+entry even though its snapshot is already gone. Saving first could instead\n+leave untracked snapshot bytes that later pulses cannot find.\n+\n+Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+unlinking. Expiry removes a live file or directory reference from the registry\n+but **never deletes the user's source**. The\n+[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+a pass with nothing expired does not save the registry.\n+\n+An expired entry stops serving the moment it expires, whether or not the\n+reaper has run yet: between expiry and the next tick the bytes may still be\n+on disk, and serving them would make the TTL a suggestion.\n*** End Patch\n"},"customWireName":"edit"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":1983,"output":1815,"cacheRead":55424,"cacheWrite":0,"totalTokens":59222,"reasoningTokens":195,"cost":{"input":0.01983,"output":0.09075000000000001,"cacheRead":0.055423999999999994,"cacheWrite":0,"total":0.166004}},"stopReason":"toolUse","timestamp":1789402562541,"responseId":"resp_080dc0728d55fa10016aa81dc2f22887d2abcab61418faa838","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81dc4399087d29e1bb4f41ea8af8f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB3LLZItJtUi_jlUKWmBS52sgPvIYPtWMUElituJiAXxOCBElMKYhC6iapfuhCfleLPPGlLrV-GDABEoMVzD5l96fpUT117XZwQVPsvzWTLZvDoZHvxIHhrzTEZaz02hjpVb8weY8DnYSEHGksDBhdnPOWvh7dB2Qbs7vPqIGhQPzOlqb1-t_oqgmc5h_RkD0Eqdw4O2DpLlOK7PFBoNw5fcHQj8-75Zcn73wsOzqaFWFtn0pNeVKSv_ekqI_f3BRtdIXDgTTGXs8As97Z1Zh2r_AjjXRgUl4KYbAiPQ8RmUdNIHMTRnYk1r37zKdV7ANSgM2JlioeKWUXrafln3QtwpIf3pe5NZ3FjdQEf8bzS1GLJxdtyBqEtPVCS25HAG4rJvM_Aw0r7NzzqEIcQCOXs3YV5W9OQ-VQwryH2UT4QLxY7bzUh0P052wKEgDqN8p0MZcAmWIxJyoPPibsr0jb3kN4KTUHTOCiWqiyg_-KhRxNY9_BtzrlFjNHPY5nBt7Nd-bMuMolisWUuOS87IcGialiTop8vrhl6IYWkK0C6DrHLkVrAqK8jR3JlsWNpEK7qal6c71kIsXwmYVFWlSE32Ty4SNIDg8yvIBngng7NwlujrAD6z6i82IFritIe4bhbLwO5PB9j6xHJJI690G9jAEStPCGdpwCslt0O4598mYVqFHDRGS3mFyw0JP83RKqGp7I4LowEDcEHUSwaDbPwIeyDLUjmoyD4_2ZJDhBSGSyKgwgXTiqcU7Y1VeEZ2Ifrk5oTtMdquqHnq7ObvooMTeJpeNruSqeEOD8UwCb3dSjoyedwy9HAPqd5UU3Vf7J1E00udjS7KiS11BJKSBdxRTGnIfmO_RnHTFqOHJ8M_sHhVH2NPVjjcafpwo-S3A8oHPcLliUQPS9AmtV2vSe0cKsgCc5WkXMo0x0uuzQQium4s0HXwQoGGSR9qw43u9xa6h_rzre8P6g6zMmvQMW4MdoGXURo-TN8T6oAAZI47Mud9tbm9rgauw6mH9nP9I5Cfa0WwlotuE20QUhL31vi-wadbhXhXOrDxGntMrQRQtBSJoDF40YlqITGF8mcLhQBvOamcTvt_APfvB3tpeXm7PxhFrPhPS3lJ0y2n5TvcrGqqZ7b66Uu-auXFWnj1l4AcH5L2fDPS74hBYS3bSoBFqIyxZsyQ7QysTkbGEYjRADNfjL7AI4NC6-8j_FmOZ7Dw_03yFCpdi3TWGo9GOtLENzXcbRyCWjshcPnhSwcbN9sd-S5vzws6JajqCvqIXQugCgtq7dd9SDwwLOSVF5K0MF1T6Uz79OdOJvgfnVSvWfwqbUo5byhkbUrmY0V8Vz3f7DoGK6Z6b0jdrivwkLLtUEmJIzZXSwQhwzkFHYuvWdoxHS2MQr4aMbv9fSDt6zCPCkCx8oBqf8INoSLgUBzkp0Jd-EeYqEYVocKU-H3LrrXVscJV6fMxnFOxYwNtUoufpOMTih4i7OEyeS36qCG7LQU4RjzhEfbBqGCYy-6q2TD46pd0NDhYZSwduFCRwr5aE1nu4oqpzxpU3H2WWNDX2jarulPamUeRdXpEDng80iltknw8PaBnmMyEOyTFAG9-yYiCtZFGt76Sy3up9PoZXB36oHL7vZ76ySyXsb_0KGCDAXWadV8z36LZ0HcFufn7-ErKFRpOU7mjva2pPgb1nbkV53c7C4KD0QqXM-mHsb6bwPu9FsvFsCPy9M4mM_c-mb33ppL78C9Wgvw-XuXprP4LONDHy4v622a6BCUk5AmFsS9eIS0jh-ZrWZ_2ibkI6nnatNPGZHx230oEnw4AJs80o7Bum8elUkF31qrcQh0DnDkuUUNssCEEXmJRzY40nGuqCZALvNHL1Q4IrkjZ5f5rnBrN7PtwngyO946YZGm4PmhN9hSNQ_-XH1h0crB2fxcY5ZQ80SCcgcq8sDW6IE3jJB-6oyf7HYw3Emgzr_MnfnMSI6FeVGWm_91ttDOP0plf6ceV9GYkbEvLyyVbukshGW3HNFsBOGGtwqd8QP_DNEDOrwGaSaGAADtQxFLJ4kfH6plAbLcUoth0ylcIVnx-tiJbwkGJYxVfBy-7xe_Vd0ih4bw0dJUAOQ98yvWRMB9yU1Juht1MNzt5VYFeyneK44Yn3hQBTPRA_Y-ITghq7D_b4uRC0RqErIR5fUCqFPFnMFvz8IhQtnJpvBLsAH8ztBvFJ0DxPQq2e1QDdi9R1lHdXOK-Nm41sAQWo_rK_1C_7kUZ8mQJm0rkqmgYPQxLB4tR3-oZM9E1ngFYTqo6iEcCBl3YZwJkpa5m-rn6-tn0IECK2lMyPdJdfIqbpPSFksrnD5V1hkqyrtVX98j-TPA8YqRriV6-2Sm1KvukWMSZCcgSk8Ii5wgXmWLDcm1ujKZwtdiCZ8KjHobaml4yyNFBBolLYFq5Xw169VoCHZkBv0WXVCFyhyG0QT8KAb9qT2cB2K6m0ggDuVRYvjmdOo3-ONnt0w92nNVQtdcT-7aDoFmCHjbg7rGl5sBC7MXABJdn5RaXiCVNHcF3e_4TL5jud0IUqtVkv1EQ8j3dbAqOjHjrM5SJOXibaGLxn16N8JTVmeiGJZVkjYOs4mhbAV7lyBBO9n6GkgD3gPWVi54FUr3RR2fDVF12x2GulTnVaqG5GkLWibko_VsDbKgO0IdeBANYpthrUdQim7B8bJYrXySocbqZOTN0IDaCCag9XsiqeTKQ34l4ktbWQpk=","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[{"type":"summary_text","text":"**Editing registry lock details**"},{"type":"summary_text","text":"**Updating documentation links**"}],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"ctc_080dc0728d55fa10016aa81dcb6bdc87d2bdee8ff7c535abb2","type":"custom_tool_call","status":"completed","call_id":"call_LUJojTiyWdTU7FmkT3NsM7rP","input":"*** Begin Patch\n[.worktrees/308-registry-process-lock/docs/STORAGE.md#D5CA]\nPUT 147.=150:\n+cache. The broker and brain are separate writer processes: broker control\n+requests register, remove, and reconcile entries; the brain's live-host pulse\n+reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+reads those snapshots without a writer lock. Only a genuinely missing file\n+initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+history refuses the operation rather than recycling stale URLs.\nPUT >184:\n+\n+### Writer serialization\n+\n+The releases#308 repair contract is **PREP-ONLY; no fixed-code validation has\n+run**. It preserves schema 1, the CLI, TTLs, and the existing save decisions.\n+See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+for the lost-update evidence and its limits.\n+\n+Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+the complete read-modify-write operation: load, mutation, associated snapshot\n+creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+refuses the operation; it never permits an unlocked write. The lock is\n+non-reentrant: a guarded caller must not reacquire it for the same registry.\n+\n+The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+stable sibling formed by appending `.lock` to the **full registry path**:\n+`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+publication replaces that file. Never unlink the sentinel, including on\n+unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+the guard or exiting the process releases the lock without deleting it.\n+\n+Atomic publication and mutual exclusion solve different problems. Atomic\n+replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+does not stop a stale writer from replacing a newer snapshot. Locking only\n+`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+with the brain. Direct writers, including live-daemon fixture mutations, must\n+follow the same full-operation guard contract.\n+\n+The guard **never auto-saves**, including on drop. Read-only listing, an\n+unchanged input-reference replay, and a reaper pass with no expired entries\n+do not save the registry; a missing registry remains missing after a no-op\n+reap. Existing operations that explicitly save, such as reconciliation, still\n+do so. Snapshot unlink-before-save ordering and user-source protection remain\n+the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+publication one atomic transaction.\n+\n+Protection is active only when **both running writer implementations** use\n+this protocol. Updating only the brain while an old broker survives does not\n+fully activate it; a new broker paired with an old brain is likewise unsafe.\n+The sentinel's presence is not proof that both writers participate.\n[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#F7C6]\nPUT >460:\n+\n+### 6.14 Serving-registry writers must serialize across processes\n+<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. This repair is **PREP-ONLY**: no fixed-code validation or runtime measurement has run.\n[.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md#7349]\nPUT 118.=119:\n+The broker and brain are separate processes that write\n+`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n+entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\nPUT >126:\n+\n+The prepared writer-coordination repair requires both processes to hold the\n+same `serve/registry.lock` guard across reading and updating the registry.\n+Atomic snapshot replacement alone does not prevent one writer from losing\n+another's update. Both running writer implementations must be updated:\n+a new brain with an old broker does not fully activate protection. This is\n+a PREP-ONLY contract, not a measured fixed-runtime result. HTTP readers remain\n+lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+are unchanged.\n[.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md#7B67]\nPUT 59.=63:\n+The brain's pulse reaps expired registry entries and logs how many it removed.\n+For an attachment, it attempts to unlink the core-owned snapshot **before**\n+saving the registry without that entry. These are not one atomic transaction:\n+if saving fails after unlinking, the next pulse can finish removing the expired\n+entry even though its snapshot is already gone. Saving first could instead\n+leave untracked snapshot bytes that later pulses cannot find.\n+\n+Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+unlinking. Expiry removes a live file or directory reference from the registry\n+but **never deletes the user's source**. The\n+[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+a pass with nothing expired does not save the registry.\n+\n+An expired entry stops serving the moment it expires, whether or not the\n+reaper has run yet: between expiry and the next tick the bytes may still be\n+on disk, and serving them would make the TTL a suggestion.\n*** End Patch\n","internal_chat_message_metadata_passthrough":{"create_time":1789402563.119372,"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"edit","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":56200.21660000086,"ttft":1701.3286000043154,"completedAt":1789402618742,"contextSnapshot":{"promptTokens":57407,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"message","id":"64f36f47","parentId":"2b23d9c1","timestamp":"2026-09-14T16:16:58.964Z","message":{"role":"toolResult","toolCallId":"call_LUJojTiyWdTU7FmkT3NsM7rP|ctc_080dc0728d55fa10016aa81dcb6bdc87d2bdee8ff7c535abb2","toolName":"edit","content":[{"type":"text","text":"[.worktrees/308-registry-process-lock/docs/STORAGE.md#227A]\n142:## serve/registry — authoritative serving assignments\n\n145:\n146:`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n147:cache. The broker and brain are separate writer processes: broker control\n148:requests register, remove, and reconcile entries; the brain's live-host pulse\n…\n151:initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n152:history refuses the operation rather than recycling stale URLs.\n153:\n154:Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n\n166:that counter (ADR-0057 Amendment 2).\n\n183:Back up this registry with any adapter output roots you need to preserve.\n\n186:the history which prevents an old URL from naming a different resource.\n187:\n188:### Writer serialization\n189:\n…\n228:The sentinel's presence is not proof that both writers participate.\n229:\n230:## tracked/ — the context git repo\n231:\n\n235:\n\n[.worktrees/308-registry-process-lock/docs/KNOWN-HAZARDS.md#07AB]\n372:## 6. Documented regressions (non-obvious invariants)\n\n456:### 6.13 IO event sequences never reset at a byte-window boundary\n\n458:- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n\n460:- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n461:\n462:### 6.14 Serving-registry writers must serialize across processes\n463:<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n…\n468:- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. This repair is **PREP-ONLY**: no fixed-code validation or runtime measurement has run.\n469:\n470:---\n471:\n\n[.worktrees/308-registry-process-lock/docs-site/src/serving/overview.md#0EA1]\n87:## Register, inspect, remove\n\n105:- The first `report.md` keeps that name.\n\n115:- Allocation history is persisted per node. Another node has its own names;\n116:  the node prefix keeps their URLs distinct.\n117:\n118:The broker and brain are separate processes that write\n119:`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n120:entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n121:registration timestamps, optional origin endpoint ids, and allocation history.\n127:URLs changing meaning.\n128:\n129:The prepared writer-coordination repair requires both processes to hold the\n130:same `serve/registry.lock` guard across reading and updating the registry.\n…\n136:are unchanged.\n137:\n138:<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n139:## Adapter served roots\n\n161:<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->\n\n[.worktrees/308-registry-process-lock/docs-site/src/serving/attachments.md#15B1]\n8:## Attaching a file to a message\n\n49:### Lifetime\n\n55:```console\n\n57:```\n58:\n59:The brain's pulse reaps expired registry entries and logs how many it removed.\n60:For an attachment, it attempts to unlink the core-owned snapshot **before**\n…\n63:entry even though its snapshot is already gone. Saving first could instead\n64:leave untracked snapshot bytes that later pulses cannot find.\n65:\n66:Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n67:unlinking. Expiry removes a live file or directory reference from the registry\n…\n75:on disk, and serving them would make the TTL a suggestion.\n76:\n77:<!-- [doc->REQ-WEB-FETCH-VERB] -->\n78:## Pulling one: `spt fetch`\n\n109:<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->"}],"details":{"diff":" 142|## serve/registry — authoritative serving assignments\n\n 145|\n 146|`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n-147|cache. The daemon is its single writer and publishes durable atomic snapshots;\n-148|the HTTP listener and listing read those snapshots. Only a genuinely missing\n-149|file initializes an empty registry. Unreadable, corrupt, unsupported, or\n-150|inconsistent history refuses the operation rather than recycling stale URLs.\n+147|cache. The broker and brain are separate writer processes: broker control\n+148|requests register, remove, and reconcile entries; the brain's live-host pulse\n+149|reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+150|reads those snapshots without a writer lock. Only a genuinely missing file\n+151|initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+152|history refuses the operation rather than recycling stale URLs.\n 151|\n 152|Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n\n 164|that counter (ADR-0057 Amendment 2).\n\n 181|Back up this registry with any adapter output roots you need to preserve.\n\n 184|the history which prevents an old URL from naming a different resource.\n 185|\n+188|### Writer serialization\n+189|\n+190|The releases#308 repair contract is **PREP-ONLY; no fixed-code validation has\n+191|run**. It preserves schema 1, the CLI, TTLs, and the existing save decisions.\n+192|See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+193|for the lost-update evidence and its limits.\n+194|\n+195|Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+196|**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+197|the complete read-modify-write operation: load, mutation, associated snapshot\n+198|creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+199|refuses the operation; it never permits an unlocked write. The lock is\n+200|non-reentrant: a guarded caller must not reacquire it for the same registry.\n+201|\n+202|The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+203|stable sibling formed by appending `.lock` to the **full registry path**:\n+204|`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+205|publication replaces that file. Never unlink the sentinel, including on\n+206|unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+207|the guard or exiting the process releases the lock without deleting it.\n+208|\n+209|Atomic publication and mutual exclusion solve different problems. Atomic\n+210|replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+211|does not stop a stale writer from replacing a newer snapshot. Locking only\n+212|`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+213|with the brain. Direct writers, including live-daemon fixture mutations, must\n+214|follow the same full-operation guard contract.\n+215|\n+216|The guard **never auto-saves**, including on drop. Read-only listing, an\n+217|unchanged input-reference replay, and a reaper pass with no expired entries\n+218|do not save the registry; a missing registry remains missing after a no-op\n+219|reap. Existing operations that explicitly save, such as reconciliation, still\n+220|do so. Snapshot unlink-before-save ordering and user-source protection remain\n+221|the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+222|(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+223|publication one atomic transaction.\n+224|\n+225|Protection is active only when **both running writer implementations** use\n+226|this protocol. Updating only the brain while an old broker survives does not\n+227|fully activate it; a new broker paired with an old brain is likewise unsafe.\n+228|The sentinel's presence is not proof that both writers participate.\n+229|\n 186|## tracked/ — the context git repo\n 187|\n\n 191|\n 372|## 6. Documented regressions (non-obvious invariants)\n\n 456|### 6.13 IO event sequences never reset at a byte-window boundary\n\n 458|- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n\n 460|- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n 461|\n+462|### 6.14 Serving-registry writers must serialize across processes\n+463|<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+464|- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+465|- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+466|- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+467|- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+468|- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. This repair is **PREP-ONLY**: no fixed-code validation or runtime measurement has run.\n+469|\n 462|---\n 463|\n 87|## Register, inspect, remove\n\n 105|- The first `report.md` keeps that name.\n\n 115|- Allocation history is persisted per node. Another node has its own names;\n 116|  the node prefix keeps their URLs distinct.\n 117|\n-118|The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n+118|The broker and brain are separate processes that write\n+119|`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n 119|entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n 120|registration timestamps, optional origin endpoint ids, and allocation history.\n 126|URLs changing meaning.\n 127|\n+129|The prepared writer-coordination repair requires both processes to hold the\n+130|same `serve/registry.lock` guard across reading and updating the registry.\n+131|Atomic snapshot replacement alone does not prevent one writer from losing\n+132|another's update. Both running writer implementations must be updated:\n+133|a new brain with an old broker does not fully activate protection. This is\n+134|a PREP-ONLY contract, not a measured fixed-runtime result. HTTP readers remain\n+135|lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+136|are unchanged.\n+137|\n 128|<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 129|## Adapter served roots\n\n 151|<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->\n 8|## Attaching a file to a message\n\n 49|### Lifetime\n\n 55|```console\n\n 57|```\n 58|\n-59|The daemon's pulse reaps what has expired, deleting the snapshot file and its\n-60|registry entry together and logging how many it removed. An expired entry stops\n-61|serving the moment it expires, whether or not the reaper has run yet: between\n-62|expiry and the next tick the bytes are still on disk, and serving them would\n-63|make the TTL a suggestion.\n+59|The brain's pulse reaps expired registry entries and logs how many it removed.\n+60|For an attachment, it attempts to unlink the core-owned snapshot **before**\n+61|saving the registry without that entry. These are not one atomic transaction:\n+62|if saving fails after unlinking, the next pulse can finish removing the expired\n+63|entry even though its snapshot is already gone. Saving first could instead\n+64|leave untracked snapshot bytes that later pulses cannot find.\n 64|\n+66|Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+67|unlinking. Expiry removes a live file or directory reference from the registry\n+68|but **never deletes the user's source**. The\n+69|[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+70|guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+71|a pass with nothing expired does not save the registry.\n+72|\n+73|An expired entry stops serving the moment it expires, whether or not the\n+74|reaper has run yet: between expiry and the next tick the bytes may still be\n+75|on disk, and serving them would make the TTL a suggestion.\n+76|\n 65|<!-- [doc->REQ-WEB-FETCH-VERB] -->\n 66|## Pulling one: `spt fetch`\n\n 97|<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->","firstChangedLine":147,"perFileResults":[{"path":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\STORAGE.md","diff":" 142|## serve/registry — authoritative serving assignments\n\n 145|\n 146|`$SPT_HOME/serve/registry` is schema-versioned JSON, not a regenerable discovery\n-147|cache. The daemon is its single writer and publishes durable atomic snapshots;\n-148|the HTTP listener and listing read those snapshots. Only a genuinely missing\n-149|file initializes an empty registry. Unreadable, corrupt, unsupported, or\n-150|inconsistent history refuses the operation rather than recycling stale URLs.\n+147|cache. The broker and brain are separate writer processes: broker control\n+148|requests register, remove, and reconcile entries; the brain's live-host pulse\n+149|reaps expired entries. They publish durable atomic snapshots; the HTTP listener\n+150|reads those snapshots without a writer lock. Only a genuinely missing file\n+151|initializes an empty registry. Unreadable, corrupt, unsupported, or inconsistent\n+152|history refuses the operation rather than recycling stale URLs.\n 151|\n 152|Schema 1 carries `entries`, `next_suffix` (per requested-name family), and\n\n 164|that counter (ADR-0057 Amendment 2).\n\n 181|Back up this registry with any adapter output roots you need to preserve.\n\n 184|the history which prevents an old URL from naming a different resource.\n 185|\n+188|### Writer serialization\n+189|\n+190|The releases#308 repair contract is **PREP-ONLY; no fixed-code validation has\n+191|run**. It preserves schema 1, the CLI, TTLs, and the existing save decisions.\n+192|See [KNOWN-HAZARDS §6.14](KNOWN-HAZARDS.md#614-serving-registry-writers-must-serialize-across-processes)\n+193|for the lost-update evidence and its limits.\n+194|\n+195|Every writer acquires `spt_store::serving::lock_registry_at(registry_path)`\n+196|**before a fresh load**, and keeps its returned `std::fs::File` alive through\n+197|the complete read-modify-write operation: load, mutation, associated snapshot\n+198|creation or cleanup, and any explicit `save_at`. Lock acquisition failure\n+199|refuses the operation; it never permits an unlocked write. The lock is\n+200|non-reentrant: a guarded caller must not reacquire it for the same registry.\n+201|\n+202|The guard opens a fresh file handle and takes an `fs2` exclusive lock on a\n+203|stable sibling formed by appending `.lock` to the **full registry path**:\n+204|`$SPT_HOME/serve/registry.lock`. Never lock the registry inode itself: atomic\n+205|publication replaces that file. Never unlink the sentinel, including on\n+206|unlock or cleanup: waiters and new writers must lock the same file. Dropping\n+207|the guard or exiting the process releases the lock without deleting it.\n+208|\n+209|Atomic publication and mutual exclusion solve different problems. Atomic\n+210|replacement gives lock-free HTTP readers a complete old or new snapshot; it\n+211|does not stop a stale writer from replacing a newer snapshot. Locking only\n+212|`save_at` is too late, and a process-local mutex cannot coordinate the broker\n+213|with the brain. Direct writers, including live-daemon fixture mutations, must\n+214|follow the same full-operation guard contract.\n+215|\n+216|The guard **never auto-saves**, including on drop. Read-only listing, an\n+217|unchanged input-reference replay, and a reaper pass with no expired entries\n+218|do not save the registry; a missing registry remains missing after a no-op\n+219|reap. Existing operations that explicitly save, such as reconciliation, still\n+220|do so. Snapshot unlink-before-save ordering and user-source protection remain\n+221|the [attachment lifetime contract](../docs-site/src/serving/attachments.md#lifetime)\n+222|(`REQ-WEB-ATTACHMENT-PULL`); locking does not make snapshot deletion and registry\n+223|publication one atomic transaction.\n+224|\n+225|Protection is active only when **both running writer implementations** use\n+226|this protocol. Updating only the brain while an old broker survives does not\n+227|fully activate it; a new broker paired with an old brain is likewise unsafe.\n+228|The sentinel's presence is not proof that both writers participate.\n+229|\n 186|## tracked/ — the context git repo\n 187|\n\n 191|","firstChangedLine":147,"op":"update","snapshotsPruned":true},{"path":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs\\KNOWN-HAZARDS.md","diff":" 372|## 6. Documented regressions (non-obvious invariants)\n\n 456|### 6.13 IO event sequences never reset at a byte-window boundary\n\n 458|- **Failure:** seeking 256 KiB before EOF could land inside a UTF-8 codepoint; `read_to_string` returned `InvalidData`, the tail reader answered zero, and append minted sequence 1. Nine of nine measured resets on doyle's log matched this mechanism; four perches on the box were affected (releases#277). Duplicate/reset blocks blinded high cursors and replayed old COMMUNE rows to low cursors; value-based trim also undercounted or removed newer rows.\n\n 460|- **spt-core mapping:** `spt_store::iolog::{last_seq_at, append_locked, trim_locked}` and the mechanism, repair, and position-retention regression units. Repair can replay retained history once to an old cursor; it does not make old events new.\n 461|\n+462|### 6.14 Serving-registry writers must serialize across processes\n+463|<!-- [doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE] -->\n+464|- **Failure:** the broker handles serving registrations while the brain independently reaps expired entries. Each can load `serve/registry`, mutate its own snapshot, and atomically publish it; the later stale publication can erase an acknowledged registration and name-allocation history. A thread-only mutex cannot coordinate these separate processes.\n+465|- **Invariant:** every writer holds the stable `serve/registry.lock` guard from **before fresh load** through mutation, snapshot side effects, and explicit save. Atomic replacement protects readers from torn snapshots, not writers from lost updates. The authoritative [writer serialization contract](STORAGE.md#writer-serialization) defines sentinel lifetime, no-op saves, and activation requiring **both** broker and brain implementations; an old broker with a new brain is not protected.\n+466|- **Preserved requirements:** `REQ-WEB-SERVING-REGISTRY` retains entries and stable name ownership; `REQ-WEB-ATTACHMENT-PULL` retains snapshot unlink-before-save and never deletes user reference sources; `REQ-WEB-ENTRY-AUDIENCE` retains per-entry TTL/audience semantics. HTTP remains a lock-free atomic-snapshot reader.\n+467|- **spt-core mapping:** `spt_store::serving::lock_registry_at`, broker `servehost::apply_at`, brain `livehost::reap_expired_attachments`, and direct live-daemon fixture writers.\n+468|- **Evidence / status (releases#308):** the lost-update defect is proven from source; the exact original failure schedule is inferred, not captured. The observed serial-consumer cohort was **1 red in 10 runs**, a sample rather than a probability floor. This repair is **PREP-ONLY**: no fixed-code validation or runtime measurement has run.\n+469|\n 462|---\n 463|","firstChangedLine":460,"op":"update","snapshotsPruned":true},{"path":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs-site\\src\\serving\\overview.md","diff":" 87|## Register, inspect, remove\n\n 105|- The first `report.md` keeps that name.\n\n 115|- Allocation history is persisted per node. Another node has its own names;\n 116|  the node prefix keeps their URLs distinct.\n 117|\n-118|The daemon is the only writer of `$SPT_HOME/serve/registry`. The store includes\n+118|The broker and brain are separate processes that write\n+119|`$SPT_HOME/serve/registry`; CLI clients do not write it directly. The store includes\n 119|entry ids, kinds (`file`, `dir`, `attachment`), paths, assigned names,\n 120|registration timestamps, optional origin endpoint ids, and allocation history.\n 126|URLs changing meaning.\n 127|\n+129|The prepared writer-coordination repair requires both processes to hold the\n+130|same `serve/registry.lock` guard across reading and updating the registry.\n+131|Atomic snapshot replacement alone does not prevent one writer from losing\n+132|another's update. Both running writer implementations must be updated:\n+133|a new brain with an old broker does not fully activate protection. This is\n+134|a PREP-ONLY contract, not a measured fixed-runtime result. HTTP readers remain\n+135|lock-free; [attachment expiry and source protection](attachments.md#lifetime)\n+136|are unchanged.\n+137|\n 128|<!-- [doc->REQ-WEB-SERVING-REGISTRY] -->\n 129|## Adapter served roots\n\n 151|<!-- [doc->REQ-WEB-ADAPTER-DOCS-FACET] -->","firstChangedLine":118,"op":"update","snapshotsPruned":true},{"path":"C:\\Users\\decid\\Documents\\projects\\spt-core\\.worktrees\\308-registry-process-lock\\docs-site\\src\\serving\\attachments.md","diff":" 8|## Attaching a file to a message\n\n 49|### Lifetime\n\n 55|```console\n\n 57|```\n 58|\n-59|The daemon's pulse reaps what has expired, deleting the snapshot file and its\n-60|registry entry together and logging how many it removed. An expired entry stops\n-61|serving the moment it expires, whether or not the reaper has run yet: between\n-62|expiry and the next tick the bytes are still on disk, and serving them would\n-63|make the TTL a suggestion.\n+59|The brain's pulse reaps expired registry entries and logs how many it removed.\n+60|For an attachment, it attempts to unlink the core-owned snapshot **before**\n+61|saving the registry without that entry. These are not one atomic transaction:\n+62|if saving fails after unlinking, the next pulse can finish removing the expired\n+63|entry even though its snapshot is already gone. Saving first could instead\n+64|leave untracked snapshot bytes that later pulses cannot find.\n 64|\n+66|Only attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\n+67|unlinking. Expiry removes a live file or directory reference from the registry\n+68|but **never deletes the user's source**. The\n+69|[writer-coordination contract](overview.md#register-inspect-remove) keeps the\n+70|guard across the reaper's fresh load, snapshot cleanup, and explicit save;\n+71|a pass with nothing expired does not save the registry.\n+72|\n+73|An expired entry stops serving the moment it expires, whether or not the\n+74|reaper has run yet: between expiry and the next tick the bytes may still be\n+75|on disk, and serving them would make the TTL a suggestion.\n+76|\n 65|<!-- [doc->REQ-WEB-FETCH-VERB] -->\n 66|## Pulling one: `spt fetch`\n\n 97|<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->","firstChangedLine":59,"op":"update","oldText":"# Attachments and `spt fetch`\r\n\r\nSending a file with a message does not push bytes to the receiver. The file is\r\nsnapshot into this node's own store, registered for serving, and the message\r\ncarries its URL. The receiver decides whether to pull it, and when.\r\n\r\n<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\r\n## Attaching a file to a message\r\n\r\n```console\r\n$ printf 'the report you asked for' | spt send doyle --attachment ./report.md\r\nATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)\r\nSENT:doyle\r\n```\r\n\r\n`--attachment` repeats; each file becomes its own registry entry with its own\r\nURL. The message envelope grows an `attachments` attribute carrying, per file,\r\nits served name, its URL and its size in bytes — so a receiving agent can decide\r\nwhether to fetch **before** it fetches. A core that has never heard of the\r\nattribute delivers the message unchanged.\r\n\r\nAn attachment is a **snapshot**, and that is the one place the serving registry\r\ndoes not resolve at request time:\r\n\r\n| | `spt serve add` | `spt send --attachment` |\r\n|---|---|---|\r\n| What is served | the file at its path | a copy taken at send time |\r\n| Editing the source | the reader sees the edit | the reader still sees what was sent |\r\n| Deleting the source | the URL answers 404 | the URL keeps serving |\r\n| Lifetime | until removed | a TTL, 30 days by default |\r\n\r\nA message's attachment is as immutable as the message: an edit after the send\r\nmust not change what the receiver pulls, and deleting the original must not\r\nturn a delivered link into a dead one.\r\n\r\nTwo sends of the same filename get two stable, distinct URLs under the\r\nregistry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\r\nlink ever changes meaning.\r\n\r\nA missing or unreadable path **refuses the send by name** — nothing is\r\ndelivered and nothing is spooled — rather than delivering a message whose link\r\nis dead on arrival:\r\n\r\n```console\r\n$ printf 'here' | spt send doyle --attachment ./nope.md\r\nATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)\r\n```\r\n\r\n### Lifetime\r\n\r\n`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or\r\n`d`. A bare number is refused rather than guessed, because `30` meaning seconds\r\nwhen the sender meant days is a deleted attachment.\r\n\r\n```console\r\n$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\r\n```\r\n\r\nThe daemon's pulse reaps what has expired, deleting the snapshot file and its\r\nregistry entry together and logging how many it removed. An expired entry stops\r\nserving the moment it expires, whether or not the reaper has run yet: between\r\nexpiry and the next tick the bytes are still on disk, and serving them would\r\nmake the TTL a suggestion.\r\n\r\n<!-- [doc->REQ-WEB-FETCH-VERB] -->\r\n## Pulling one: `spt fetch`\r\n\r\n```console\r\n$ spt fetch hfenduleam/f/report.md\r\nreport.md\r\n$ spt fetch http://localhost:5474/hfenduleam/f/report.md ~/inbox/report.md\r\n/home/reavus/inbox/report.md\r\n```\r\n\r\nIt accepts a full node-prefixed URL or the bare `<node>/f/<name>` shorthand —\r\nthe shorthand is how a link is said between agents, and the host and port belong\r\nto whichever node is doing the fetching. The written path is printed on stdout\r\nso a caller can pipe it.\r\n\r\nWithout a destination the file lands in the current directory under its served\r\nname. An existing destination is **not** overwritten without `--force`. The body\r\nstreams to a temporary file and is renamed into place, so an interrupted or\r\nrefused fetch never leaves a truncated file where a later reader would trust it.\r\n\r\nThe three outcomes are distinct, and the distinction is the point:\r\n\r\n| Exit | Meaning |\r\n|---|---|\r\n| `0` | The file was written; its path is on stdout. |\r\n| `3` | The owner **refused** it — an access decision. Retrying is wrong. |\r\n| `1` | Anything else: unreachable owner, not found, a deadline, a local write error. |\r\n\r\nEvery fetch — local or remote — is served through the local daemon's web\r\nsurface, the same path a browser takes, so a fetch of your own node's file\r\nexercises the code a remote fetch will use.\r\n\r\n<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\r\n## The `FILE_ACCESS_HELPER` signal\r\n\r\nAn agent does not have to notice an attachment. When a delivered message carries\r\none, the now-signal hands the agent the exact command to run, taken verbatim\r\nfrom the envelope:\r\n\r\n```text\r\n<FILE_ACCESS_HELPER>\r\nspt fetch http://localhost:5474/kitsubito/f/report.md\r\n</FILE_ACCESS_HELPER>\r\n```\r\n\r\n<!-- [doc->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT] -->\r\nThe same category answers the other way a file arrives: **a user quoting a\r\nfilepath while controlling the receiving agent through remote `spt rc`**.\r\nCore binds the existing session-authenticated input report to the broker's live\r\nremote controller when the report arrives. A later controller change cannot\r\nreattribute it. Registration happens on that controller's machine, not by looking\r\nfor a similarly named file on the receiving machine.\r\n\r\nThe file is a **live reference**, not an attachment snapshot: edits are visible\r\nand deletion returns not found. The reference lasts up to **24 hours**, addressed\r\nonly to the receiving endpoint. The fetch command appears on a subsequent signal\r\npoll once the owner answers; input processing never waits for that answer.\r\n\r\n- **Absolute or `~`-rooted only.** The owner resolves `~` against its own home.\r\n  A relative path has no anchor on another node. At most **five** paths per report;\r\n  directories receive the same lifetime and audience.\r\n- **No quoted paths means no broker IPC and no line.** An absent hosted session,\r\n  local or viewer-only seat, or no controller binds nothing silently. A missing\r\n  file on the bound owner's machine is also silent, with no fallback origin.\r\n- CLI broker-connect and unanswered-receipt failures are silent unless\r\n  `SPT_PUMP_TRACE` enables diagnostics. Named declines apply only to failures with\r\n  a remote controller, such as an invalid session, uncertain delivery-byte evidence,\r\n  or owner refusal. The broker's **10-second owner-reply timeout** remains named.\r\n- A repeated payload in the same harness session does not register again, repeat\r\n  the helper notice, or extend an existing reference's deadline.\r\n- Core excludes bytes it knows it physically wrote for peer delivery: either an\r\n  exact match or a match after trimming ASCII whitespace from both byte sequences'\r\n  ends. It does not parse message shapes or normalize interior text. This is\r\n  **receipt-seat attribution**, not proof of who typed every byte: external\r\n  automation and native harness re-submission cannot be distinguished beyond the\r\n  current controller seat. No adapter token or integration change is required.\r\n\r\nEvery entry the helper registers is enumerable in `spt serve list` with its\r\norigin, so an automatic exposure is exactly as visible as a deliberate one.\r\n\r\nTwo-node field acceptance remains pending, separate from doc/impl/unit evidence.\r\nIt must include a remote controller remaining seated while core physically delivers\r\na path-bearing peer message, whose input report must cause **no serve**, and measure\r\nthe added hook cost. Neither obligation is claimed as executed here.\r\n\r\n<!-- [doc->REQ-MSG-SHORT-ID] -->\r\n## Reading a message back\r\n\r\nEvery message carries a short-ID, and an attachment link is part of what that id\r\nrenders — `spt msg show <id>` prints the message with its attachment URLs, and\r\n`/<node>/m/<id>` is the same view in a browser. See\r\n[Messaging](../messaging/overview.md) for the id itself.\r\n","newText":"# Attachments and `spt fetch`\r\n\r\nSending a file with a message does not push bytes to the receiver. The file is\r\nsnapshot into this node's own store, registered for serving, and the message\r\ncarries its URL. The receiver decides whether to pull it, and when.\r\n\r\n<!-- [doc->REQ-WEB-ATTACHMENT-PULL] -->\r\n## Attaching a file to a message\r\n\r\n```console\r\n$ printf 'the report you asked for' | spt send doyle --attachment ./report.md\r\nATTACHED:report.md: http://localhost:5474/hfenduleam/f/report.md (2481 bytes, ttl 2592000000ms)\r\nSENT:doyle\r\n```\r\n\r\n`--attachment` repeats; each file becomes its own registry entry with its own\r\nURL. The message envelope grows an `attachments` attribute carrying, per file,\r\nits served name, its URL and its size in bytes — so a receiving agent can decide\r\nwhether to fetch **before** it fetches. A core that has never heard of the\r\nattribute delivers the message unchanged.\r\n\r\nAn attachment is a **snapshot**, and that is the one place the serving registry\r\ndoes not resolve at request time:\r\n\r\n| | `spt serve add` | `spt send --attachment` |\r\n|---|---|---|\r\n| What is served | the file at its path | a copy taken at send time |\r\n| Editing the source | the reader sees the edit | the reader still sees what was sent |\r\n| Deleting the source | the URL answers 404 | the URL keeps serving |\r\n| Lifetime | until removed | a TTL, 30 days by default |\r\n\r\nA message's attachment is as immutable as the message: an edit after the send\r\nmust not change what the receiver pulls, and deleting the original must not\r\nturn a delivered link into a dead one.\r\n\r\nTwo sends of the same filename get two stable, distinct URLs under the\r\nregistry's ordinary disambiguation rule (`report.md`, then `report~1.md`), so no\r\nlink ever changes meaning.\r\n\r\nA missing or unreadable path **refuses the send by name** — nothing is\r\ndelivered and nothing is spooled — rather than delivering a message whose link\r\nis dead on arrival:\r\n\r\n```console\r\n$ printf 'here' | spt send doyle --attachment ./nope.md\r\nATTACHMENT_REFUSED:./nope.md: The system cannot find the file specified. (nothing was sent)\r\n```\r\n\r\n### Lifetime\r\n\r\n`--ttl` sets this send's lifetime and **requires a unit** — `s`, `m`, `h` or\r\n`d`. A bare number is refused rather than guessed, because `30` meaning seconds\r\nwhen the sender meant days is a deleted attachment.\r\n\r\n```console\r\n$ printf 'valid for a day' | spt send doyle --attachment ./build.log --ttl 24h\r\n```\r\n\r\nThe brain's pulse reaps expired registry entries and logs how many it removed.\r\nFor an attachment, it attempts to unlink the core-owned snapshot **before**\r\nsaving the registry without that entry. These are not one atomic transaction:\r\nif saving fails after unlinking, the next pulse can finish removing the expired\r\nentry even though its snapshot is already gone. Saving first could instead\r\nleave untracked snapshot bytes that later pulses cannot find.\r\n\r\nOnly attachment snapshots under `$SPT_HOME/serve/snapshots/` are candidates for\r\nunlinking. Expiry removes a live file or directory reference from the registry\r\nbut **never deletes the user's source**. The\r\n[writer-coordination contract](overview.md#register-inspect-remove) keeps the\r\nguard across the reaper's fresh load, snapshot cleanup, and explicit save;\r\na pass with nothing expired does not save the registry.\r\n\r\nAn expired entry stops serving the moment it expires, whether or not the\r\nreaper has run yet: between expiry and the next tick the bytes may still be\r\non disk, and serving them would make the TTL a suggestion.\r\n\r\n<!-- [doc->REQ-WEB-FETCH-VERB] -->\r\n## Pulling one: `spt fetch`\r\n\r\n```console\r\n$ spt fetch hfenduleam/f/report.md\r\nreport.md\r\n$ spt fetch http://localhost:5474/hfenduleam/f/report.md ~/inbox/report.md\r\n/home/reavus/inbox/report.md\r\n```\r\n\r\nIt accepts a full node-prefixed URL or the bare `<node>/f/<name>` shorthand —\r\nthe shorthand is how a link is said between agents, and the host and port belong\r\nto whichever node is doing the fetching. The written path is printed on stdout\r\nso a caller can pipe it.\r\n\r\nWithout a destination the file lands in the current directory under its served\r\nname. An existing destination is **not** overwritten without `--force`. The body\r\nstreams to a temporary file and is renamed into place, so an interrupted or\r\nrefused fetch never leaves a truncated file where a later reader would trust it.\r\n\r\nThe three outcomes are distinct, and the distinction is the point:\r\n\r\n| Exit | Meaning |\r\n|---|---|\r\n| `0` | The file was written; its path is on stdout. |\r\n| `3` | The owner **refused** it — an access decision. Retrying is wrong. |\r\n| `1` | Anything else: unreachable owner, not found, a deadline, a local write error. |\r\n\r\nEvery fetch — local or remote — is served through the local daemon's web\r\nsurface, the same path a browser takes, so a fetch of your own node's file\r\nexercises the code a remote fetch will use.\r\n\r\n<!-- [doc->REQ-NOW-SIGNAL-FILE-ACCESS-HELPER] -->\r\n## The `FILE_ACCESS_HELPER` signal\r\n\r\nAn agent does not have to notice an attachment. When a delivered message carries\r\none, the now-signal hands the agent the exact command to run, taken verbatim\r\nfrom the envelope:\r\n\r\n```text\r\n<FILE_ACCESS_HELPER>\r\nspt fetch http://localhost:5474/kitsubito/f/report.md\r\n</FILE_ACCESS_HELPER>\r\n```\r\n\r\n<!-- [doc->REQ-INPUT-PROVENANCE-ACCEPTANCE-REPORT] -->\r\nThe same category answers the other way a file arrives: **a user quoting a\r\nfilepath while controlling the receiving agent through remote `spt rc`**.\r\nCore binds the existing session-authenticated input report to the broker's live\r\nremote controller when the report arrives. A later controller change cannot\r\nreattribute it. Registration happens on that controller's machine, not by looking\r\nfor a similarly named file on the receiving machine.\r\n\r\nThe file is a **live reference**, not an attachment snapshot: edits are visible\r\nand deletion returns not found. The reference lasts up to **24 hours**, addressed\r\nonly to the receiving endpoint. The fetch command appears on a subsequent signal\r\npoll once the owner answers; input processing never waits for that answer.\r\n\r\n- **Absolute or `~`-rooted only.** The owner resolves `~` against its own home.\r\n  A relative path has no anchor on another node. At most **five** paths per report;\r\n  directories receive the same lifetime and audience.\r\n- **No quoted paths means no broker IPC and no line.** An absent hosted session,\r\n  local or viewer-only seat, or no controller binds nothing silently. A missing\r\n  file on the bound owner's machine is also silent, with no fallback origin.\r\n- CLI broker-connect and unanswered-receipt failures are silent unless\r\n  `SPT_PUMP_TRACE` enables diagnostics. Named declines apply only to failures with\r\n  a remote controller, such as an invalid session, uncertain delivery-byte evidence,\r\n  or owner refusal. The broker's **10-second owner-reply timeout** remains named.\r\n- A repeated payload in the same harness session does not register again, repeat\r\n  the helper notice, or extend an existing reference's deadline.\r\n- Core excludes bytes it knows it physically wrote for peer delivery: either an\r\n  exact match or a match after trimming ASCII whitespace from both byte sequences'\r\n  ends. It does not parse message shapes or normalize interior text. This is\r\n  **receipt-seat attribution**, not proof of who typed every byte: external\r\n  automation and native harness re-submission cannot be distinguished beyond the\r\n  current controller seat. No adapter token or integration change is required.\r\n\r\nEvery entry the helper registers is enumerable in `spt serve list` with its\r\norigin, so an automatic exposure is exactly as visible as a deliberate one.\r\n\r\nTwo-node field acceptance remains pending, separate from doc/impl/unit evidence.\r\nIt must include a remote controller remaining seated while core physically delivers\r\na path-bearing peer message, whose input report must cause **no serve**, and measure\r\nthe added hook cost. Neither obligation is claimed as executed here.\r\n\r\n<!-- [doc->REQ-MSG-SHORT-ID] -->\r\n## Reading a message back\r\n\r\nEvery message carries a short-ID, and an attachment link is part of what that id\r\nrenders — `spt msg show <id>` prints the message with its attachment URLs, and\r\n`/<node>/m/<id>` is the same view in a browser. See\r\n[Messaging](../messaging/overview.md) for the id itself.\r\n"}]},"isError":false,"timestamp":1789402618964}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8LB2gJW7qzC8zDWFTe8konyT|fc_080dc0728d55fa10016aa81e05d18087d2a99d0b20af4e8ea9","toolName":"yield","startedAt":"2026-09-14T16:17:15.172Z"},"id":"07efe66f","parentId":"64f36f47","timestamp":"2026-09-14T16:17:15.172Z"}
{"type":"message","id":"95bff818","parentId":"07efe66f","timestamp":"2026-09-14T16:17:15.172Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Skipping additional checks**"},{"type":"toolCall","id":"call_8LB2gJW7qzC8zDWFTe8konyT|fc_080dc0728d55fa10016aa81e05d18087d2a99d0b20af4e8ea9","name":"yield","arguments":{"data":{"status":"PREP-ONLY; no validation performed","edited_files":["docs/KNOWN-HAZARDS.md","docs/STORAGE.md","docs-site/src/serving/overview.md","docs-site/src/serving/attachments.md"],"contract_changes":["STORAGE.md now owns the detailed writer-serialization contract: separate broker/brain processes, fresh fs2-exclusive File guard on full-path-appended .lock sentinel, acquire before fresh load and retain through mutation/snapshot effects/explicit save, fail closed, non-reentrant, never unlink sentinel or lock replaced registry inode.","Distinguished lock-free atomic-snapshot publication from writer mutual exclusion; documented explicit save/no-op behavior and activation requiring both running writer implementations.","Added KNOWN-HAZARDS §6.14 with doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE and natural WEB registry, attachment, and audience requirement references.","Preserved attachment unlink-before-save ordering, user-source protection, no-op reaper behavior, and unchanged TTL/schema/CLI semantics; concise cross-links connect authoritative contract, hazard, and user-facing explanations.","Recorded source-proven lost-update defect versus inferred original schedule, 1/10 serial-consumer cohort as a sample rather than probability floor, and absence of fixed-code validation."],"constraints_observed":"Edited only the four owned existing Markdown files in the assigned worktree. No new files, commands, builds, tests, formatters, linters, docs generation, trace checks, commits, or runtime measurements."}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-6-astra","usage":{"input":3043,"output":444,"cacheRead":57216,"cacheWrite":0,"totalTokens":60703,"reasoningTokens":119,"cost":{"input":0.030430000000000002,"output":0.0222,"cacheRead":0.057215999999999996,"cacheWrite":0,"total":0.109846}},"stopReason":"toolUse","timestamp":1789402619010,"responseId":"resp_080dc0728d55fa10016aa81dfb3ac087d2acce42d9c07099ec","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_080dc0728d55fa10016aa81dfdd1d887d29d2d123977a72ac6","type":"reasoning","content":[],"encrypted_content":"gAAAAABqqB4FZ0pSxCTS9sVgJjZRRr6cpiOUpQrqwWolhLFLnetgfjZ_0pVrGWYM3TJ4T4-dw1R1tY5hDcHLmsVY7z1KCLdeYMarxbS0VIif_6-zX6CSNlnKTXdqGLyJyTOAfvwrTM7pnctL9LtVGgRTBemPLbCdaHSCyntaC8tpHFxOn0c7k0SIIzvm_pbn9Thps0LswpXaw8GN13lC6aT3UMS9CQR_0b-MnvJxeMKittIIq8uX-vKwlkH0Jymzak3UcgVlM9CaHsEJpNZFLo10u36dM3vBwv_xkARAarSA2WkGW291DkJGilXGMd8B20tLkdDkHZVa3BneovOlm3FYYR6nv8KXFh9tELTHEC8xcyyt9Dg5s8DxeZKxRzlaV7Pz00dssxxCA-SfFiHOAyYGL4nm06zvfaTyHczG-YlX2RYg9gRIPzHOkH7rvXmkp5ZgD3tkwulloyAYvaJueBPJ6VxCsMsl0pyxl0TwXxnLnKRQQYjMCI2BIZMTRLo46QQ0j01IVbxyiidGioUbukp79LKByBsR5WWb4f7ZZ1O9VFoam6q2xJ9PD9XT2M1VPv71SZqLT5A4YwcGCj97GlLR4FCYUy8_7C5XqsdCkk3U9le19yxA4eWhv8wNFkcviwjSKqI7kPFAHPJDL4qyK0O_kLGNAIlfmBTf5xtUmYRMZzI7inHZa04kTReDB8T--gS0vE1S5GRyexhakj5f9YSXCOSt-Uowv9S_t9arMKPbvd53EFcJBDRcUOLu7NYaFOtTbj5W6R5n-bt5CAzUw-mC2P_-qarHEw74YmgT8EqkRrHZFxnLghEuhMktVoKpFPjrAf-AUiGF-QzR_H5LEMGhs4eAZuflyFcP7f6xtBs2PNW5B23nfJwzHI3Ik_X62s8ZcIShPOaJPaHGwEpisxLBIBDJgzc0d7KBB04JPIJ7gJM_1OUCnkksrBZPiBkfkMkM95aOkmsa2LspeH69LW-c2a0lYJvwfm6U7CcB0F46Wc07iPSDYllRx3kEMJxtxbkwtKjoLw8fK45Ync_IH8Nq4xTodO6UpM-fhFZDA5i5gB9dvjkMmvnTNEde15gjW29RR7vujT4c-fWjFG9608bT6-I-r99uVBCcJwXW8QI9qdOWVdq75uXLCGhhiAXdKtFhaPhhNDYHadP8DxNHd2xXeY1dR6N67WlGGR7rxH61HglatPCb7WkSkG62OCqItD2SWKk-ozUeJHBA-R2z4Q7fWg-WN6IicURSJTAokFpLh5bQqsyeQDn8FGVf2LYcFKKNJvxQ624nLd2Y0YIbZEPQMrKJYiYQZGh7qMFFkt4acBQfLqV3l2njHN5qaPsdneS_TD_ivUctUeSzp7BB1qgUv56If93_n3sRhRhpL3bU4if_puxg7m_q_cW0OwBmZuwXgaJocxWRSweXG3jMNl6M3w_IpS1GPK5vw6k5Jj3oNovbfBPR-M3EQ_O1FDs82sJVSUY2eaCpA-MPZ0z5_ONfU3PzuAHkf23xwREUZvUIS2mtXlUozaNbUOaRvRQUUHA5vfbzWBtAN2j1F6gcz5r3KtX_aRYHZTO8e3tzZVohBSUAL3tZJQMRGe37twbVByS9YC1R07u4vMKL5y7JkZMONb-OoDM-B2ZoJMJ8F3Wq9ZnVyQC13II8at8DdEJuagfYUZaqwAwc2Zsc_EYyIiGb2dSH1pVdiqI-zmL8-b0LYeqvU9bpjX5GEnN8zrz6aticiE5bYgMd3YjuzQS3aBtJlosqg44n_4WR0G7dP8o9_nwOzNIjdxewFnwYHaBN26OikB6mmczXTAjlKGk-3mCBteI1OlU5K057tNZdxAgdy1CxgzfCBRCBRsT9N13YDb0FnhVyUY3Jdb4LjQhkrpo95Cznbvqnz-9XUOKTkr_54vpUbqwNYEIbzaDA-c7-gKh0ufxI81fGB5KG6iWNn9bZvK5GloyntM1DxMweH9UDbqbuBv3B_PmG7fUZ9WR7prjiMMq_FwDGVs1XR4SVyV2GuWSAT352NNR5AxUrr-oNLHCmXDXklf6SknG5lQYbhnFX18UivMRtk_C9UbOvV1cK3d4-IrMebM1hmrcT7Qo35_nPgTrP_tPc2dO91tZp5dy5yICZ7PgShu8KMP9oJxYShEH63QOyXWfv-NbDpe8JouJsYaB1mxw=","internal_chat_message_metadata_passthrough":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"summary":[{"type":"summary_text","text":"**Skipping additional checks**"}],"metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}},{"id":"fc_080dc0728d55fa10016aa81e05d18087d2a99d0b20af4e8ea9","type":"function_call","status":"completed","arguments":"{\"data\":{\"status\":\"PREP-ONLY; no validation performed\",\"edited_files\":[\"docs/KNOWN-HAZARDS.md\",\"docs/STORAGE.md\",\"docs-site/src/serving/overview.md\",\"docs-site/src/serving/attachments.md\"],\"contract_changes\":[\"STORAGE.md now owns the detailed writer-serialization contract: separate broker/brain processes, fresh fs2-exclusive File guard on full-path-appended .lock sentinel, acquire before fresh load and retain through mutation/snapshot effects/explicit save, fail closed, non-reentrant, never unlink sentinel or lock replaced registry inode.\",\"Distinguished lock-free atomic-snapshot publication from writer mutual exclusion; documented explicit save/no-op behavior and activation requiring both running writer implementations.\",\"Added KNOWN-HAZARDS §6.14 with doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE and natural WEB registry, attachment, and audience requirement references.\",\"Preserved attachment unlink-before-save ordering, user-source protection, no-op reaper behavior, and unchanged TTL/schema/CLI semantics; concise cross-links connect authoritative contract, hazard, and user-facing explanations.\",\"Recorded source-proven lost-update defect versus inferred original schedule, 1/10 serial-consumer cohort as a sample rather than probability floor, and absence of fixed-code validation.\"],\"constraints_observed\":\"Edited only the four owned existing Markdown files in the assigned worktree. No new files, commands, builds, tests, formatters, linters, docs generation, trace checks, commits, or runtime measurements.\"}}","call_id":"call_8LB2gJW7qzC8zDWFTe8konyT","internal_chat_message_metadata_passthrough":{"create_time":1789402619.442496,"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"},"name":"yield","metadata":{"turn_id":"badd13a5-ecfc-42f7-bdd9-cb5f3f8bbe71"}}]},"duration":16160.87080000341,"ttft":2829.9751000106335,"completedAt":1789402635172,"contextSnapshot":{"promptTokens":60259,"nonMessageTokens":17033,"compactionEpoch":0}}}
{"type":"message","id":"344eac06","parentId":"95bff818","timestamp":"2026-09-14T16:17:15.173Z","message":{"role":"toolResult","toolCallId":"call_8LB2gJW7qzC8zDWFTe8konyT|fc_080dc0728d55fa10016aa81e05d18087d2a99d0b20af4e8ea9","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"status":"PREP-ONLY; no validation performed","edited_files":["docs/KNOWN-HAZARDS.md","docs/STORAGE.md","docs-site/src/serving/overview.md","docs-site/src/serving/attachments.md"],"contract_changes":["STORAGE.md now owns the detailed writer-serialization contract: separate broker/brain processes, fresh fs2-exclusive File guard on full-path-appended .lock sentinel, acquire before fresh load and retain through mutation/snapshot effects/explicit save, fail closed, non-reentrant, never unlink sentinel or lock replaced registry inode.","Distinguished lock-free atomic-snapshot publication from writer mutual exclusion; documented explicit save/no-op behavior and activation requiring both running writer implementations.","Added KNOWN-HAZARDS §6.14 with doc->REQ-HAZARD-SERVE-REGISTRY-LOST-UPDATE and natural WEB registry, attachment, and audience requirement references.","Preserved attachment unlink-before-save ordering, user-source protection, no-op reaper behavior, and unchanged TTL/schema/CLI semantics; concise cross-links connect authoritative contract, hazard, and user-facing explanations.","Recorded source-proven lost-update defect versus inferred original schedule, 1/10 serial-consumer cohort as a sample rather than probability floor, and absence of fixed-code validation."],"constraints_observed":"Edited only the four owned existing Markdown files in the assigned worktree. No new files, commands, builds, tests, formatters, linters, docs generation, trace checks, commits, or runtime measurements."},"status":"success"},"isError":false,"timestamp":1789402635173}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-09-14T16:24:15.204Z"},"id":"08dd06ee","parentId":"344eac06","timestamp":"2026-09-14T16:24:15.204Z"}
