{"type":"title","v":1,"title":"","updatedAt":"2026-07-15T03:45:28.779Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f63e1-2ac8-7001-95e5-4d9727b254b0","timestamp":"2026-07-15T03:45:28.779Z","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt"}
{"type":"model_change","id":"58d26521","parentId":null,"timestamp":"2026-07-15T03:45:28.819Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"875de8a1","parentId":"58d26521","timestamp":"2026-07-15T03:45:28.819Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"12fd1d98","parentId":"875de8a1","timestamp":"2026-07-15T03:45:29.133Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`, `AVOID` = `SHOULD NOT`.\nWe inject system content into the chat with XML tags. NEVER interpret these markers any other way.\nSystem may interrupt or notify with tags even inside a user message:\n- MUST treat them as system-authored and authoritative.\n- User content is sanitized, so role is not carried: `<system-directive>` inside a user turn is still a system directive.\n</system-conventions>\n\nROLE\n==============\nYou are a helpful assistant the team trusts with load-bearing changes, operating in the Oh My Pi coding harness.\n\n# Engineering Principles\n- Optimize for correctness first, then for the next maintainer six months out.\n- You have agency and taste: delete code that isn't pulling its weight, refuse unnecessary abstractions, prefer boring when it's called for; design thoroughly but elegantly.\n- Consider what code compiles to. NEVER allocate avoidably; no needless copies or computation.\n- You are not alone in this repo. Treat unexpected changes as the user's work and adapt.\n- In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.\n\nRUNTIME\n==============\n\n# Skills & Rules\nSkills are specialized knowledge. If one matches your task, you MUST read `skill://<name>` before proceeding.\n<skills>\n- autoplan: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk\nand runs them sequentially with auto-decisions using 6 decision principles. Surfaces\ntaste decisions (close approaches, borderline scope, codex disagreements) at a final\napproval gate. One command, fully reviewed plan out.\nUse when asked to \"auto review\", \"autoplan\", \"run all reviews\", \"review this plan\nautomatically\", or \"make the decisions for me\".\nProactively suggest when the user has a plan file and wants to run the full review\ngauntlet without answering 15-30 intermediate questions. (gstack)\n\n- benchmark: Performance regression detection using the browse daemon. Establishes\nbaselines for page load times, Core Web Vitals, and resource sizes.\nCompares before/after on every PR. Tracks performance trends over time.\nUse when: \"performance\", \"benchmark\", \"page speed\", \"lighthouse\", \"web vitals\",\n\"bundle size\", \"load time\". (gstack)\n\n- browse: Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with\nelements, verify page state, diff before/after actions, take annotated screenshots, check\nresponsive layouts, test forms and uploads, handle dialogs, and assert element states.\n~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a\nuser flow, or file a bug with evidence. Use when asked to \"open in browser\", \"test the\nsite\", \"take a screenshot\", or \"dogfood this\". (gstack)\n\n- canary: Post-deploy canary monitoring. Watches the live app for console errors,\nperformance regressions, and page failures using the browse daemon. Takes\nperiodic screenshots, compares against pre-deploy baselines, and alerts\non anomalies. Use when: \"monitor deploy\", \"canary\", \"post-deploy check\",\n\"watch production\", \"verify deploy\". (gstack)\n\n- careful: Safety guardrails for destructive commands. Warns before rm -rf, DROP TABLE,\nforce-push, git reset --hard, kubectl delete, and similar destructive operations.\nUser can override each warning. Use when touching prod, debugging live systems,\nor working in a shared environment. Use when asked to \"be careful\", \"safety mode\",\n\"prod mode\", or \"careful mode\". (gstack)\n\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- caveman-commit: Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when \"why\" isn't obvious. Use when user says \"write a commit\", \"commit message\", \"generate commit\", \"/commit\", or invokes /caveman-commit. Auto-triggers when staging changes.\n\n- caveman-help: Quick-reference card for all caveman modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /caveman-help, \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n- caveman-review: Ultra-compressed code review comments. Cuts noise from PR feedback while preserving the actionable signal. Each comment is one line: location, problem, fix. Use when user says \"review this PR\", \"code review\", \"review the diff\", \"/review\", or invokes /caveman-review. Auto-triggers when reviewing pull requests.\n\n- ccs-delegation: Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, enhances prompts with context, executes via `ccs {profile} -p \"task\"` or `ccs {profile}:continue`, and reports results. Triggers on \"use ccs [task]\" patterns, typo/test/refactor keywords. Excludes complex architecture, security-critical code, performance optimization, breaking changes.\n- checkpoint: Save and resume working state checkpoints. Captures git state, decisions made,\nand remaining work so you can pick up exactly where you left off — even across\nConductor workspace handoffs between branches.\nUse when asked to \"checkpoint\", \"save progress\", \"where was I\", \"resume\",\n\"what was I working on\", or \"pick up where I left off\".\nProactively suggest when a session is ending, the user is switching context,\nor before a long break. (gstack)\n\n- codex: OpenAI Codex CLI wrapper — three modes. Code review: independent diff review via\ncodex review with pass/fail gate. Challenge: adversarial mode that tries to break\nyour code. Consult: ask codex anything with session continuity for follow-ups.\nThe \"200 IQ autistic developer\" second opinion. Use when asked to \"codex review\",\n\"codex challenge\", \"ask codex\", \"second opinion\", or \"consult codex\". (gstack)\n\n- commune: Push a context update to your Psyche so it can brief your resume across a reset. Use when the\nuser says \"commune\" or \"update psyche\", or — as a live agent — after a significant body of work,\nbefore a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the\nfreshest commune (an agent-driven context reset, no operator needed).\n\n- compress: Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format to save input tokens. Preserves all technical substance, code, URLs, and structure. Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. Trigger: /caveman:compress <filepath> or \"compress memory file\"\n\n- cso: Chief Security Officer mode. Infrastructure-first security audit: secrets archaeology,\ndependency supply chain, CI/CD pipeline security, LLM/AI security, skill supply chain\nscanning, plus OWASP Top 10, STRIDE threat modeling, and active verification.\nTwo modes: daily (zero-noise, 8/10 confidence gate) and comprehensive (monthly deep\nscan, 2/10 bar). Trend tracking across audit runs.\nUse when: \"security audit\", \"threat model\", \"pentest review\", \"OWASP\", \"CSO review\". (gstack)\n\n- design-consultation: Design consultation: understands your product, researches the landscape, proposes a\ncomplete design system (aesthetic, typography, color, layout, spacing, motion), and\ngenerates font+color preview pages. Creates DESIGN.md as your project's design source\nof truth. For existing sites, use /plan-design-review to infer the system instead.\nUse when asked to \"design system\", \"brand guidelines\", or \"create DESIGN.md\".\nProactively suggest when starting a new project's UI with no existing\ndesign system or DESIGN.md. (gstack)\n\n- design-html: Design finalization: generates production-quality Pretext-native HTML/CSS.\nWorks with approved mockups from /design-shotgun, CEO plans from /plan-ceo-review,\ndesign review context from /plan-design-review, or from scratch with a user\ndescription. Text actually reflows, heights are computed, layouts are dynamic.\n30KB overhead, zero deps. Smart API routing: picks the right Pretext patterns\nfor each design type. Use when: \"finalize this design\", \"turn this into HTML\",\n\"build me a page\", \"implement this design\", or after any planning skill.\nProactively suggest when user has approved a design or has a plan ready. (gstack)\n\n- design-review: Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems,\nAI slop patterns, and slow interactions — then fixes them. Iteratively fixes issues\nin source code, committing each fix atomically and re-verifying with before/after\nscreenshots. For plan-mode design review (before implementation), use /plan-design-review.\nUse when asked to \"audit the design\", \"visual QA\", \"check if it looks good\", or \"design polish\".\nProactively suggest when the user mentions visual inconsistencies or\nwants to polish the look of a live site. (gstack)\n\n- design-shotgun: Design shotgun: generate multiple AI design variants, open a comparison board,\ncollect structured feedback, and iterate. Standalone design exploration you can\nrun anytime. Use when: \"explore designs\", \"show me options\", \"design variants\",\n\"visual brainstorm\", or \"I don't like how this looks\".\nProactively suggest when the user describes a UI feature but hasn't seen\nwhat it could look like. (gstack)\n\n- devex-review: Live developer experience audit. Uses the browse tool to actually TEST the\ndeveloper experience: navigates docs, tries the getting started flow, times\nTTHW, screenshots error messages, evaluates CLI help text. Produces a DX\nscorecard with evidence. Compares against /plan-devex-review scores if they\nexist (the boomerang: plan said 3 minutes, reality says 8). Use when asked to\n\"test the DX\", \"DX audit\", \"developer experience test\", or \"try the\nonboarding\". Proactively suggest after shipping a developer-facing feature. (gstack)\n\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- document-release: Post-ship documentation update. Reads all project docs, cross-references the\ndiff, updates README/ARCHITECTURE/CONTRIBUTING/CLAUDE.md to match what shipped,\npolishes CHANGELOG voice, cleans up TODOS, and optionally bumps VERSION. Use when\nasked to \"update the docs\", \"sync documentation\", or \"post-ship docs\".\nProactively suggest after a PR is merged or code is shipped. (gstack)\n\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- force-stop: Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user\nsays \"stop listening\", \"kill live agent\", \"force stop\", or \"tear down perch\". Session-aware: a\nlive target tears its Psyche down too.\n\n- freeze: Restrict file edits to a specific directory for the session. Blocks Edit and\nWrite outside the allowed path. Use when debugging to prevent accidentally\n\"fixing\" unrelated code, or when you want to scope changes to one module.\nUse when asked to \"freeze\", \"restrict edits\", \"only edit this folder\",\nor \"lock down edits\". (gstack)\n\n- frontend-design: Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.\n- grill-me: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions \"grill me\".\n- grill-with-docs: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.\n- gsd-add-tests: Generate tests for a completed phase based on UAT criteria and implementation\n- gsd-ai-integration-phase: Generate an AI-SPEC.md design contract for phases that involve building AI systems.\n- gsd-audit-fix: Autonomous audit-to-fix pipeline — find issues, classify, fix, test, commit\n- gsd-audit-milestone: Audit milestone completion against original intent before archiving\n- gsd-audit-uat: Cross-phase audit of all outstanding UAT and verification items\n- gsd-autonomous: Run all remaining phases autonomously — discuss→plan→execute per phase\n- gsd-capture: Capture ideas, tasks, notes, and seeds to their destination\n- gsd-cleanup: Archive accumulated phase directories from completed milestones\n- gsd-code-review: Review source files changed during a phase for bugs, security issues, and code quality problems\n- gsd-complete-milestone: Archive completed milestone and prepare for next version\n- gsd-config: Configure GSD settings — workflow toggles, advanced knobs, integrations, and model profile\n- gsd-debug: Systematic debugging with persistent state across context resets\n- gsd-discuss-phase: Gather phase context through adaptive questioning before planning.\n- gsd-docs-update: Generate or update project documentation verified against the codebase\n- gsd-eval-review: Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan.\n- gsd-execute-phase: Execute all plans in a phase with wave-based parallelization\n- gsd-explore: Socratic ideation and idea routing — think through ideas before committing to plans\n- gsd-extract-learnings: Extract decisions, lessons, patterns, and surprises from completed phase artifacts\n- gsd-fast: Execute a trivial task inline — no subagents, no planning overhead\n- gsd-forensics: Post-mortem investigation for failed GSD workflows — diagnoses what went wrong.\n- gsd-graphify: Build, query, and inspect the project knowledge graph in .planning/graphs/\n- gsd-health: Diagnose planning directory health and optionally repair issues\n- gsd-help: Show available GSD commands and usage guide\n- gsd-import: Ingest external plans with conflict detection against project decisions before writing anything.\n- gsd-inbox: Triage and review open GitHub issues and PRs against project templates and contribution guidelines.\n- gsd-ingest-docs: Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo.\n- gsd-manager: Interactive command center for managing multiple phases from one terminal\n- gsd-map-codebase: Analyze codebase with parallel mapper agents to produce .planning/codebase/ documents\n- gsd-milestone-summary: Generate a comprehensive project summary from milestone artifacts for team onboarding and review\n- gsd-mvp-phase: Plan a phase as a vertical MVP slice — user story, SPIDR splitting, then plan-phase\n- gsd-new-milestone: Start a new milestone cycle — update PROJECT.md and route to requirements\n- gsd-new-project: Initialize a new project with deep context gathering and PROJECT.md\n- gsd-ns-context: codebase intelligence | map graphify docs learnings\n- gsd-ns-ideate: exploration capture | explore sketch spike spec capture\n- gsd-ns-manage: config workspace | workstreams thread update ship inbox\n- gsd-ns-project: project lifecycle | milestones audits summary\n- gsd-ns-review: quality gates | code review debug audit security eval ui\n- gsd-ns-workflow: workflow | discuss plan execute verify phase progress\n- gsd-pause-work: Create context handoff when pausing work mid-phase\n- gsd-phase: CRUD for phases in ROADMAP.md — add, insert, remove, or edit phases\n- gsd-plan-phase: Create detailed phase plan (PLAN.md) with verification loop\n- gsd-plan-review-convergence: Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain.\n- gsd-pr-branch: Create a clean PR branch by filtering out .planning/ commits — ready for code review\n- gsd-profile-user: Generate developer behavioral profile and create Claude-discoverable artifacts\n- gsd-progress: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command\n- gsd-quick: Execute a quick task with GSD guarantees (atomic commits, state tracking) but skip optional agents\n- gsd-resume-work: Resume work from previous session with full context restoration\n- gsd-review: Request cross-AI peer review of phase plans from external AI CLIs\n- gsd-review-backlog: Review and promote backlog items to active milestone\n- gsd-secure-phase: Retroactively verify threat mitigations for a completed phase\n- gsd-settings: Configure GSD workflow toggles and model profile\n- gsd-ship: Create PR, run review, and prepare for merge after verification passes\n- gsd-sketch: Sketch UI/design ideas with throwaway HTML mockups, or propose what to sketch next (frontier mode)\n- gsd-spec-phase: Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase.\n- gsd-spike: Spike an idea through experiential exploration, or propose what to spike next (frontier mode)\n- gsd-stats: Display project statistics — phases, plans, requirements, git metrics, and timeline\n- gsd-surface: Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall\n- gsd-thread: Manage persistent context threads for cross-session work\n- gsd-ui-phase: Generate UI design contract (UI-SPEC.md) for frontend phases\n- gsd-ui-review: Retroactive 6-pillar visual audit of implemented frontend code\n- gsd-ultraplan-phase: [BETA] Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back.\n- gsd-undo: Safe git revert. Roll back phase or plan commits using the phase manifest with dependency checks.\n- gsd-update: Update GSD to latest version with changelog display\n- gsd-validate-phase: Retroactively audit and fill Nyquist validation gaps for a completed phase\n- gsd-verify-work: Validate built features through conversational UAT\n- gsd-workspace: Manage GSD workspaces — create, list, or remove isolated workspace environments\n- gsd-workstreams: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume\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- gstack-upgrade: Upgrade gstack to the latest version. Detects global vs vendored install,\nruns the upgrade, and shows what's new. Use when asked to \"upgrade gstack\",\n\"update gstack\", or \"get latest version\".\n\n- guard: Full safety mode: destructive command warnings + directory-scoped edits.\nCombines /careful (warns before rm -rf, DROP TABLE, force-push, etc.) with\n/freeze (blocks edits outside a specified directory). Use for maximum safety\nwhen touching prod or debugging live systems. Use when asked to \"guard mode\",\n\"full safety\", \"lock it down\", or \"maximum safety\". (gstack)\n\n- handoff: Compact the current conversation into a handoff document for another agent to pick up.\n- health: Code quality dashboard. Wraps existing project tools (type checker, linter,\ntest runner, dead code detector, shell linter), computes a weighted composite\n0-10 score, and tracks trends over time. Use when: \"health check\",\n\"code quality\", \"how healthy is the codebase\", \"run all checks\",\n\"quality score\". (gstack)\n\n- improve-codebase-architecture: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.\n- investigate: Systematic debugging with root cause investigation. Four phases: investigate,\nanalyze, hypothesize, implement. Iron Law: no fixes without root cause.\nUse when asked to \"debug this\", \"fix this bug\", \"why is this broken\",\n\"investigate this error\", or \"root cause analysis\".\nProactively invoke this skill (do NOT debug directly) when the user reports\nerrors, 500 errors, stack traces, unexpected behavior, \"it was working\nyesterday\", or is troubleshooting why something stopped working. (gstack)\n\n- land-and-deploy: Land and deploy workflow. Merges the PR, waits for CI and deploy,\nverifies production health via canary checks. Takes over after /ship\ncreates the PR. Use when: \"merge\", \"land\", \"deploy\", \"merge and verify\",\n\"land it\", \"ship it to production\". (gstack)\n\n- learn: Manage project learnings. Review, search, prune, and export what gstack\nhas learned across sessions. Use when asked to \"what have we learned\",\n\"show learnings\", \"prune stale learnings\", or \"export learnings\".\nProactively suggest when the user asks about past patterns or wonders\n\"didn't we fix this before?\"\n\n- list-agents: List SPT agents and endpoints reachable from this node. Use when the user says \"list agents\",\n\"who's live\", \"who's listening\", or wants a roster of active perches.\n\n- live: Run a live agent session. For past sessions, restores a summarized context.\n\nEXPLICIT START phrases (route to /sptc:live <id>):\n- \"live as\"\n- \"start live\"\n- \"go live\"\n- \"start a live agent\"\n\nAUTO-RESUME phrases (route to /sptc:live --auto, resumes most-recently-active live agent):\n- continue live work\n- resume live work\n- continue live agent\n- resume live agent\n- live agent continue\n- live agent resume\n- live work continue\n- live work resume\n\nDoes NOT route here (too ambiguous — require BOTH \"live\" AND (\"agent\" or \"work\")):\n- \"keep going\"\n- \"resume work\"\n- \"continue\" (bare)\n\n- mempalace: MemPalace — mine projects and conversations into a searchable memory palace. Use when asked about mempalace, memory palace, mining memories, searching memories, or palace setup.\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- office-hours: YC Office Hours — two modes. Startup mode: six forcing questions that expose\ndemand reality, status quo, desperate specificity, narrowest wedge, observation,\nand future-fit. Builder mode: design thinking brainstorming for side projects,\nhackathons, learning, and open source. Saves a design doc.\nUse when asked to \"brainstorm this\", \"I have an idea\", \"help me think through\nthis\", \"office hours\", or \"is this worth building\".\nProactively invoke this skill (do NOT answer directly) when the user describes\na new product idea, asks whether something is worth building, wants to think\nthrough design decisions for something that doesn't exist yet, or is exploring\na concept before any code is written.\nUse before /plan-ceo-review or /plan-eng-review. (gstack)\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- pair-agent: Pair a remote AI agent with your browser. One command generates a setup key and\nprints instructions the other agent can follow to connect. Works with OpenClaw,\nHermes, Codex, Cursor, or any agent that can make HTTP requests. The remote agent\ngets its own tab with scoped access (read+write by default, admin on request).\nUse when asked to \"pair agent\", \"connect agent\", \"share browser\", \"remote browser\",\n\"let another agent use my browser\", or \"give browser access\". (gstack)\n\n- plan-ceo-review: CEO/founder-mode plan review. Rethink the problem, find the 10-star product,\nchallenge premises, expand scope when it creates a better product. Four modes:\nSCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick\nexpansions), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials).\nUse when asked to \"think bigger\", \"expand scope\", \"strategy review\", \"rethink this\",\nor \"is this ambitious enough\".\nProactively suggest when the user is questioning scope or ambition of a plan,\nor when the plan feels like it could be thinking bigger. (gstack)\n\n- plan-design-review: Designer's eye plan review — interactive, like CEO and Eng review.\nRates each design dimension 0-10, explains what would make it a 10,\nthen fixes the plan to get there. Works in plan mode. For live site\nvisual audits, use /design-review. Use when asked to \"review the design plan\"\nor \"design critique\".\nProactively suggest when the user has a plan with UI/UX components that\nshould be reviewed before implementation. (gstack)\n\n- plan-devex-review: Interactive developer experience plan review. Explores developer personas,\nbenchmarks against competitors, designs magical moments, and traces friction\npoints before scoring. Three modes: DX EXPANSION (competitive advantage),\nDX POLISH (bulletproof every touchpoint), DX TRIAGE (critical gaps only).\nUse when asked to \"DX review\", \"developer experience audit\", \"devex review\",\nor \"API design review\".\nProactively suggest when the user has a plan for developer-facing products\n(APIs, CLIs, SDKs, libraries, platforms, docs). (gstack)\n\n- plan-eng-review: Eng manager-mode plan review. Lock in the execution plan — architecture,\ndata flow, diagrams, edge cases, test coverage, performance. Walks through\nissues interactively with opinionated recommendations. Use when asked to\n\"review the architecture\", \"engineering review\", or \"lock in the plan\".\nProactively suggest when the user has a plan or design doc and is about to\nstart coding — to catch architecture issues before implementation. (gstack)\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- qa: Systematically QA test a web application and fix bugs found. Runs QA testing,\nthen iteratively fixes bugs in source code, committing each fix atomically and\nre-verifying. Use when asked to \"qa\", \"QA\", \"test this site\", \"find bugs\",\n\"test and fix\", or \"fix what's broken\".\nProactively suggest when the user says a feature is ready for testing\nor asks \"does this work?\". Three tiers: Quick (critical/high only),\nStandard (+ medium), Exhaustive (+ cosmetic). Produces before/after health scores,\nfix evidence, and a ship-readiness summary. For report-only mode, use /qa-only. (gstack)\n\n- qa-only: Report-only QA testing. Systematically tests a web application and produces a\nstructured report with health score, screenshots, and repro steps — but never\nfixes anything. Use when asked to \"just report bugs\", \"qa report only\", or\n\"test but don't fix\". For the full test-fix-verify loop, use /qa instead.\nProactively suggest when the user wants a bug report without any code changes. (gstack)\n\n- ready: Make this Claude Code session reachable for inter-agent messages (register a perch and\nlisten). Use when the user says \"listen as\", \"ready as\", or wants to receive messages from\nother agents.\n\n- retro: Weekly engineering retrospective. Analyzes commit history, work patterns,\nand code quality metrics with persistent history and trend tracking.\nTeam-aware: breaks down per-person contributions with praise and growth areas.\nUse when asked to \"weekly retro\", \"what did we ship\", or \"engineering retrospective\".\nProactively suggest at the end of a work week or sprint. (gstack)\n\n- review: Pre-landing PR review. Analyzes diff against the base branch for SQL safety, LLM trust\nboundary violations, conditional side effects, and other structural issues. Use when\nasked to \"review this PR\", \"code review\", \"pre-landing review\", or \"check my diff\".\nProactively suggest when the user is about to merge or land code changes. (gstack)\n\n- role: Show or edit this endpoint's durable role — its statement of purpose in the mind. Use when the user\nsays \"show my role\", \"edit my role\", \"update my role\", or runs /sptc:role. Pass a directive to change\nit in words (e.g. /sptc:role make it senior); a bare /sptc:role opens an in-input-box editor\nround-trip on a live session; add --include-desc to also edit the service description.\n\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- search: Instantly search files and folders on Windows via voidtools Everything 1.5a HTTP JSON API. Use when the user wants to find files by name, path, size, date, or extension across the whole system — much faster than Glob/Grep for filesystem-wide queries. Triggers on \"find file\", \"where is\", \"locate\", \"search my drive\", \"find all .ext files\", \"everything search\".\n- send: Send a message to another SPT agent. Use when the user says \"send to\", \"message\",\n\"tell <agent>\", or when you need to reach or reply to another agent yourself.\n\n- setup: Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session\n(mid-session installer + post-install activation).\n\n- setup-browser-cookies: Import cookies from your real Chromium browser into the headless browse session.\nOpens an interactive picker UI where you select which cookie domains to import.\nUse before QA testing authenticated pages. Use when asked to \"import cookies\",\n\"login to the site\", or \"authenticate the browser\". (gstack)\n\n- setup-deploy: Configure deployment settings for /land-and-deploy. Detects your deploy\nplatform (Fly.io, Render, Vercel, Netlify, Heroku, GitHub Actions, custom),\nproduction URL, health check endpoints, and deploy status commands. Writes\nthe configuration to CLAUDE.md so all future deploys are automatic.\nUse when: \"setup deploy\", \"configure deployment\", \"set up land-and-deploy\",\n\"how do I deploy with gstack\", \"add deploy config\".\n\n- ship: Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION,\nupdate CHANGELOG, commit, push, create PR. Use when asked to \"ship\", \"deploy\",\n\"push to main\", \"create a PR\", \"merge and push\", or \"get it deployed\".\nProactively invoke this skill (do NOT push/PR directly) when the user says code\nis ready, asks about deploying, wants to push code up, or asks to create a PR. (gstack)\n\n- signoff: Gracefully shut down your live session, saving a final context summary. Use when the user says\n\"sign off\" or \"graceful stop\", or when you (a live agent) are done and want to go offline cleanly.\n\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- subnet: Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join\nan existing one. Use when the user wants to pair machines, set up a subnet, or reach agents\nacross machines. Cross-machine /sptc:send and live agents depend on it.\n\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 Claude Code 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- unfreeze: Clear the freeze boundary set by /freeze, allowing edits to all directories\nagain. Use when you want to widen edit scope without ending the session.\nUse when asked to \"unfreeze\", \"unlock edits\", \"remove freeze\", or\n\"allow all edits\". (gstack)\n\n- version: Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin\nversion). Use when the user asks the spt or adapter version, or runs /sptc:version.\n\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- 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</skills>\n# Internal URLs\nSpecial URLs for internal resources; with most FS/bash tools they auto-resolve to FS paths.\n- `skill://<name>`: skill instructions; `/<path>` = file within\n- `rule://<name>`: rule details\n- `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field\n- `artifact://<id>`: artifact content\n- `local://<name>.md`: plan artifacts or shared content for subagents\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue, disk-cached. Bare lists recent issues; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR, same cache; `?comments=0` drops comments. Bare lists recent PRs; `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless the user asks about the harness itself.\n\n# Tool Inventory\n- Read: `read`\n- Grep: `grep`\n- Glob: `glob`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- IRC: `irc`\n- mempalace:mempalace/mempalace_add_drawer: `mcp__mempalace_mempalace_mempalace_add_drawer`\n- mempalace:mempalace/mempalace_check_duplicate: `mcp__mempalace_mempalace_mempalace_check_duplicate`\n- mempalace:mempalace/mempalace_delete_drawer: `mcp__mempalace_mempalace_mempalace_delete_drawer`\n- mempalace:mempalace/mempalace_diary_read: `mcp__mempalace_mempalace_mempalace_diary_read`\n- mempalace:mempalace/mempalace_diary_write: `mcp__mempalace_mempalace_mempalace_diary_write`\n- mempalace:mempalace/mempalace_find_tunnels: `mcp__mempalace_mempalace_mempalace_find_tunnels`\n- mempalace:mempalace/mempalace_get_aaak_spec: `mcp__mempalace_mempalace_mempalace_get_aaak_spec`\n- mempalace:mempalace/mempalace_get_taxonomy: `mcp__mempalace_mempalace_mempalace_get_taxonomy`\n- mempalace:mempalace/mempalace_graph_stats: `mcp__mempalace_mempalace_mempalace_graph_stats`\n- mempalace:mempalace/mempalace_kg_add: `mcp__mempalace_mempalace_mempalace_kg_add`\n- mempalace:mempalace/mempalace_kg_invalidate: `mcp__mempalace_mempalace_mempalace_kg_invalidate`\n- mempalace:mempalace/mempalace_kg_query: `mcp__mempalace_mempalace_mempalace_kg_query`\n- mempalace:mempalace/mempalace_kg_stats: `mcp__mempalace_mempalace_mempalace_kg_stats`\n- mempalace:mempalace/mempalace_kg_timeline: `mcp__mempalace_mempalace_mempalace_kg_timeline`\n- mempalace:mempalace/mempalace_list_rooms: `mcp__mempalace_mempalace_mempalace_list_rooms`\n- mempalace:mempalace/mempalace_list_wings: `mcp__mempalace_mempalace_mempalace_list_wings`\n- mempalace:mempalace/mempalace_search: `mcp__mempalace_mempalace_mempalace_search`\n- mempalace:mempalace/mempalace_status: `mcp__mempalace_mempalace_mempalace_status`\n- mempalace:mempalace/mempalace_traverse: `mcp__mempalace_mempalace_mempalace_traverse`\n- GenerateImage: `generate_image`\n\nTOOL POLICY\n==============\n\n# General\nUse tools whenever they improve correctness, completeness, or grounding.\n- You MUST complete the task using available tools.\n- SHOULD resolve prerequisites before acting.\n- NEVER stop at the first plausible answer if another call would cut uncertainty.\n- Empty, partial, or suspiciously narrow lookup? Retry with a different strategy.\n- SHOULD parallelize independent calls.\n# Tool I/O\n- Prefer relative paths for `path`-like fields.\n- Most tools take `i`: a concise intent, present participle, 2–6 words, no period, capitalized.\n# Specialized Tools\nYou MUST use the specialized tool over its shell equivalent:\n- File or directory reads → `read` (a directory path lists entries).\n- Regex search → `grep`, not `grep`, `rg`, or `awk`.\n- Globbing → `glob`, not `ls **/*.ext` or `fd`.\n# Exploration\nYou NEVER open a file hoping. Hope is not a strategy.\n- You MUST load only what's necessary; AVOID reading files or sections you don't need.\n- Use `grep` to locate targets.\n- Use `glob` to map structure.\n- Use `read` with offset/limit instead of whole-file reads.\n# Delegation\n\nEXECUTION WORKFLOW\n==============\n\n# 1. Scope\n- Read relevant skills first.\n- For multi-file work, plan before touching files; research existing code and conventions first.\n\n# 2. Research Before Editing\n- Read sections, not snippets. You MUST reuse existing patterns; a second convention beside an existing one is PROHIBITED.\n\n- Re-read before acting if a tool fails or a file changed since you read it.\n\n# 3. Decompose\n- Update todos as you go; skip them for trivial requests. Marking a todo done is a transition: start the next in the same turn.\n- NEVER abandon phases under scope pressure—delegate, don't shrink.\n\n- Plan only what makes the request work. Cleanup—changelog, tests, docs—is NOT planned up front; it belongs to the final phase below.\n\n# 4. Implement\n- Fix problems at the source. Remove obsolete code—no leftover comments, aliases, or re-exports.\n- Prefer updating existing files over creating new ones.\n- Review changes from the user's perspective.\n- Grep instead of guessing.\n- Don't run destructive git commands or delete code you didn't write.\n\n# 5. Verify\n- NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA.\n- Every test MUST defend an observable contract and fail on a plausible bug.\n- Test behavior, boundaries, invariants, transitions, precedence, and real errors—not plumbing, source text, or incidental defaults.\n- Match existing conventions; keep tests deterministic, isolated, and full-suite safe.\n- Run only touched tests; small/no-test changes still REQUIRE a focused behavioral smoke test.\n\n# 6. Cleanup\nChangelog, tests, docs, and removing scaffolding are the LAST phase—NEVER skipped, but gated on the request demonstrably working.\n\n- NEVER start, pre-plan, or pre-allocate todos for cleanup before you've made the request work and smoke-tested it. Until then, every edit serves correctness; housekeeping NEVER steers the design.\n- Once your smoke test confirms “it works,” do the cleanup in full before yielding.\n\nDELIVERY CONTRACT\n==============\n\n<contract>\nInviolable.\n- NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or sub-step is NEVER a yield point—continue in the same turn.\n- NEVER fabricate outputs. Claims about code, tools, tests, docs, or sources MUST be grounded.\n- NEVER substitute an easier or more familiar problem:\n  - Don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—because it changes the contract.\n  - Don't solve the symptom—suppress a warning or exception, special-case an input—unless asked. Do the real ask.\n- NEVER ask for what tools, repo context, or files can provide.\n- NEVER punt half-solved work back.\n- Default to clean cutover: migrate every caller; leave no shims, aliases, or deprecated paths.\n</contract>\n\n<completeness>\n- “Done” means the deliverable behaves as specified end to end—not that a scaffold compiles or a narrowed test passes.\n- A named plan, phase list, checklist, or spec MUST satisfy every acceptance criterion. A plausible subset is failure, not partial success.\n- NEVER silently shrink scope. Reduce scope only with explicit user approval in this conversation; otherwise do the full work—exhaust every tool and angle.\n- NEVER ship stubs, placeholders, mocks, no-ops, fake fallbacks, or `TODO: implement` as delivered work. If real implementation needs unavailable information, state the missing prerequisite and implement everything else.\n- NEVER relabel unfinished work—“scaffold,” “MVP,” “v1,” “foundation,” “follow-up”—to imply completion. Not done? Say so.\n</completeness>\n\n<evidence-and-output>\n- Output format MUST match the ask.\n- Every claim about code, tools, tests, docs, or sources MUST be grounded.\n- Mark any claim not directly observed or established as `[INFERENCE]`.\n- Verification claims MUST match what was exercised, preferably smoke tested.\n- No required tool lookup may be skipped when it would cut uncertainty.\n- Be brief in prose, not in evidence, verification, or blocking details.\n</evidence-and-output>\n\n<yielding>\nBefore yielding, verify:\n- All requested deliverables are complete; no partial implementation is presented as complete.\n- All affected artifacts—callsites, tests, docs—are updated or intentionally left unchanged.\n- The output and evidence requirements above are satisfied.\n\nBefore declaring blocked:\n- Be sure the information is unreachable through tools, context, or anything in reach. One failing check does not mean blocked—finish all remaining work first.\n- Still stuck? State exactly what's missing and what you tried.\n</yielding>\n<critical>\n- NEVER narrate or consider session limits, token or tool budgets, effort estimates, or how much you can finish. Not your concern—start as if unbounded; execute or delegate.\n- NEVER re-audit an applied edit; NEVER run git subcommands as routine validation. Tool results are THE verification.\n</critical>\n\nROLE\n===================================\n\nInvestigate the codebase rapidly. Return structured findings another agent can use without re-reading everything.\n\n<directives>\n- You MUST use tools for broad pattern matching / code search as much as possible.\n- You SHOULD invoke tools in parallel—this is a short investigation, and you are supposed to finish in a few seconds.\n- If a search returns empty results, you MUST try at least one alternate strategy (different pattern, broader path, or AST search) before concluding the target doesn't exist.\n</directives>\n\n<thoroughness>\nYou MUST infer the thoroughness from the task; default to medium:\n- **Quick**: Targeted lookups, key files only\n- **Medium**: Follow imports, read critical sections\n- **Thorough**: Trace all dependencies, check tests/types.\n</thoroughness>\n\n<procedure>\n1. Locate relevant code using tools.\n2. Read key sections. NEVER read full files unless they're tiny.\n3. Identify types/interfaces/key functions.\n4. Note dependencies between files.\n</procedure>\n\n<critical>\nYou MUST operate as read-only. You NEVER write, edit, or modify files, nor execute any state-changing commands, via git, build system, package manager, etc.\nYou MUST keep going until complete.\n</critical>\n\nYou are specializing as: **Domain Decision Auditor**. Bring exactly that expertise to the assignment — let it shape how you investigate, decide, and what you produce.\n\nCONTEXT\n===================================\n\n# Goal\nAudit omp-spt thoroughly against the published spt-core harness checklist, Oh My Pi public integration surfaces, and its sister claude-spt adapter, then identify concrete gaps to grill.\n# Constraints\nRead-only investigation. Never inspect spt-core source; only https://sabermage.github.io/spt-releases and SaberMage/spt-releases published artifacts. Follow existing CONTEXT.md/SCOPE.md/ADRs. Skip formatters, linters, and project-wide tests. Current repo is C:/Users/decid/Documents/projects/omp-spt. The user called the sister ../claude-spt, but that exact path was not found; locate plausible sibling without guessing and report if absent.\n# Contract\nReturn evidence by exact path/symbol or public-doc URL. Classify each checklist/surface as implemented, partial, missing, intentionally inapplicable, or blocked by public-contract gap. Separate factual defects from design decisions needing user input.\nCOOP\n===================================\n\nYou are operating on a piece of work assigned to you by the main agent.\n# IRC Peers\nYou can reach other live agents via the `irc` tool. Your id is `DecisionLedgerAudit`. Currently visible peers:\n- `Main` — main (main, running)\n- `OmpSurfaceAudit` — Oh My Pi Integration Auditor (sub, running)\n- `SisterParityAudit` — Adapter Port Parity Auditor (sub, running)\n- `CoreContractAudit` — Harness Contract Auditor (sub, running)\n\nUse `irc` only for quick coordination, never long-form content. Address peers by id or use `\"all\"` to broadcast.\n- Discovery: the roster above shows each peer's role and what it is doing now; `irc` op:\"list\" refreshes it.\n- Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide.\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\nCOMPLETION\n===================================\n\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 `result.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 `result.data`.\n\nYour terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:\n```ts\nresult: {\n  data: {\n    summary: string;\n    files: { path: string; description: string; }[];\n    architecture: string;\n  };\n}\n```\n\nGiving up is a last resort. If truly blocked, you MUST terminal-yield `result.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\n<workstation>\n- OS: win32 10.0.26200\n- Distro: Windows_NT\n- Kernel: Windows 11 Pro\n- Arch: x64\n- GPU: Parsec Virtual Display Adapter\n- Model: openai-codex/gpt-5.6-sol\n</workstation>\n\n<context>\nYou MUST follow the context files below for all tasks:\n<file path=\"C:\\Users\\decid\\.claude\\CLAUDE.md\">\n## Reference Documents\n\nThe `~/.claude/reference_docs/` directory contains reference guides available across all projects.\n\n- **[claude-code-hooks.md](reference_docs/claude-code-hooks.md)** -- Complete Claude Code hooks reference: all 27 event types, input/output JSON schemas, matcher patterns, exit code behavior, environment variables, and caveats. Consult this when building or debugging hooks.\n</file>\n</context>\nThe context files above are loaded automatically. You NEVER `grep`/`glob` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.\nToday is 2026-07-14, and the current working directory is 'C:/Users/decid/Documents/projects/omp-spt'.\n\n<critical>\n- Each response MUST advance the task. There is no stopping condition other than completion.\n- You MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- You MUST verify the effect of significant behavioral changes before yielding: run the specific test, command, or scenario that covers your change.\n</critical>","task":"Complete the assignment below, thoroughly:\n\n# Target\nAudit CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, tests, and traceable-reqs.toml for unresolved or contradictory omp-spt commitments.\n# Change\nExtract decisions already locked so they are not re-asked. Identify fuzzy/conflicting domain terms, stale Claude-specific glossary language, open questions still live, known field issues not demonstrably closed, and requirements/tests gaps.\n# Acceptance\nReturn a decision ledger: locked, superseded, unresolved, contradicted-by-code, and glossary updates needed. Propose the dependency order for one-question-at-a-time grilling. No edits or commands.","tools":["read","grep","glob","web_search","yield","irc","mcp__mempalace_mempalace_mempalace_add_drawer","mcp__mempalace_mempalace_mempalace_check_duplicate","mcp__mempalace_mempalace_mempalace_delete_drawer","mcp__mempalace_mempalace_mempalace_diary_read","mcp__mempalace_mempalace_mempalace_diary_write","mcp__mempalace_mempalace_mempalace_find_tunnels","mcp__mempalace_mempalace_mempalace_get_aaak_spec","mcp__mempalace_mempalace_mempalace_get_taxonomy","mcp__mempalace_mempalace_mempalace_graph_stats","mcp__mempalace_mempalace_mempalace_kg_add","mcp__mempalace_mempalace_mempalace_kg_invalidate","mcp__mempalace_mempalace_mempalace_kg_query","mcp__mempalace_mempalace_mempalace_kg_stats","mcp__mempalace_mempalace_mempalace_kg_timeline","mcp__mempalace_mempalace_mempalace_list_rooms","mcp__mempalace_mempalace_mempalace_list_wings","mcp__mempalace_mempalace_mempalace_search","mcp__mempalace_mempalace_mempalace_status","mcp__mempalace_mempalace_mempalace_traverse","generate_image"],"spawns":"","readSummarize":false,"outputSchema":{"properties":{"summary":{"metadata":{"description":"Brief summary of findings and conclusions"},"type":"string"},"files":{"metadata":{"description":"Files examined with relevant code references"},"elements":{"properties":{"path":{"metadata":{"description":"Project-relative path or paths to the most relevant code reference(s), optionally suffixed with line ranges like `:12-34` when relevant"},"type":"string"},"description":{"metadata":{"description":"Section contents"},"type":"string"}}}},"architecture":{"metadata":{"description":"Brief explanation of how pieces connect"},"type":"string"}}}}
{"type":"message","id":"03be8a04","parentId":"12fd1d98","timestamp":"2026-07-15T03:45:31.427Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nAudit CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, tests, and traceable-reqs.toml for unresolved or contradictory omp-spt commitments.\n# Change\nExtract decisions already locked so they are not re-asked. Identify fuzzy/conflicting domain terms, stale Claude-specific glossary language, open questions still live, known field issues not demonstrably closed, and requirements/tests gaps.\n# Acceptance\nReturn a decision ledger: locked, superseded, unresolved, contradicted-by-code, and glossary updates needed. Propose the dependency order for one-question-at-a-time grilling. No edits or commands."}],"attribution":"agent","timestamp":1784087130284}}
{"type":"message","id":"2e4ac66e","parentId":"03be8a04","timestamp":"2026-07-15T03:45:39.838Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning file reading and auditing**"},{"type":"toolCall","id":"call_9nHWyIRRjrBZu9iCsP9w06H2|fc_0ca4af36969c80fa016a5702639fa0819889cc1ec45a9bb0b4","name":"read","arguments":{"i":"Mapping repository root","path":".","selector":null}},{"type":"toolCall","id":"call_hXhMgVsGPn6FgPTt71Fr2VHa|fc_0ca4af36969c80fa016a5702639fbc8198a17bb61ac822ba44","name":"glob","arguments":{"i":"Locating audit documents","path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr/**;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml","hidden":true,"gitignore":true,"limit":200}},{"type":"toolCall","id":"call_hCbBsjjwLG6w1j6azUDwfGzJ|fc_0ca4af36969c80fa016a5702639fd08198b9dacaf03765d6ec","name":"glob","arguments":{"i":"Mapping test suite","path":"tests/**/*","hidden":true,"gitignore":true,"limit":500}},{"type":"toolCall","id":"call_ZypqesFCAxRM3NpVRng9pPAq|fc_0ca4af36969c80fa016a5702639fe48198b2bf9b04463baeba","name":"grep","arguments":{"i":"Finding decision markers","pattern":"(?i)\\b(TODO|TBD|open question|unresolved|decision|must|should|out of scope|in scope|supersed|deprecated|hazard|known issue|field issue|Claude|glossary)\\b","path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs;traceable-reqs.toml","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":17146,"output":358,"cacheRead":0,"cacheWrite":0,"totalTokens":17504,"reasoningTokens":81,"cost":{"input":0.08573,"output":0.01074,"cacheRead":0,"cacheWrite":0,"total":0.09647}},"stopReason":"toolUse","timestamp":1784087131478,"responseId":"resp_0ca4af36969c80fa016a57025bb45c8198965e82f97258af93","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a57025d27048198b5d4ec406737fce0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwJj39Nwyo7Klmh_PT5DbSChQ_k-0Rssnilqpz_EJDI6o-wWnGiXoQ7_8d_Gy8X6EWLmuVAusgC-IpMdalNU77wLtt3P5MZzeIW7BAHiUK3nWn6Hb8fVpzq9jBMbxsPdzcgVrHUoEGYpkTxRVEgcHONEd-c7gVn3eVd2NYKUUxa-kMeJWY8wdbhG9fNYvkcS8mXvEaIkGxapLqsZ08v2ZA69dbmUSTjdqIVCEwFvmSlScBUZa97eBw4cb47R0O98-IDxBd6kEBWod2uWCSKKy1lz5eiKGJtxT4CXU2CTMBJjlEqFnO-Mok9tFN_VxFE94PmnkL5r_Inv9EG4S_STT5K693u_rFrYhhsw__ndXlGO4-OmamzOgHebfIovPiNgelwj0DX_lz2Jw3s8DoFRWB3cL4DQ-w_eUhRoN1oHWov66YVhuMkg8xFtjcrukT6KNEYUCK3_MTm605NZ3MCuBJLefUe1E9wbry2_vcXsh07YaZR_41FkItdW4q506Uoe_jM5I0DWfiW6-WT8hDC2pK0EwzYBHPIsKfv_3e9AY87_SnX-APMO9HEOtJyxfXp3caQcKr81H8Kuf1_9r20jQgSVlBbAc-Y8BJpUnyUE29bOk7Dqng77nVevDbgaaCCygGEiqnTDOndEmUSJRxNEP3G2phtXww-GBLdrQeo--ct4JRi6ICtGhfABRjxV4EZT5Iag11g5P-gl6o0YQeYV5UGe1Bq7AqmEk94RcEd2vCSjyeP0d8HB5po8KWXOzqhINCYdth0YsP1u_Ih8q9nFGlerdL1kNprbU44o3LpX7LSOVOqu24pE12zgx1-72FCX4UfobuBSsSTPQeCECJA38k-UithXCE-k8ogxOm760N5s3A-hVOS3sEKLC1LTOAvO6Hyp5JYkaGWNSmAAxaLGe-ldInDr2WAIYEi1BDPv3E7Fa8PSYxdQ7omsTFe8q3vupIdUTwYDeKo3yBuEGFBANdZ_w9X1eGnh8x9Rh-9mwHAxs_y7Li21A29iUJsAQ_uzc_oXWY4Zs2sq5t1PDuubeTmDRANQMBVf3q8yJ12xbft3zgm8QbaRPvFbooPonGCpIkZZxJBZC6FnCCFTs8UvJZEViLQ-vrNGieoQ6Qrll7se71HDErqsNxxmPjCso6R_4_1bFmsrpzJ2jLOvyanDo_12HaVJJ6jIXzBE4pRrAOAjUeYU1Ch4vI12_fe8iXm4Fp6qWLU32Ytdni77Hary-XzWWqweIVJl_OTnv004Kxwdtrs2LemkbUErzK1iC6guQyEhnsFy71Nze5qLZGQjOwgnuoJgPqVMouXdLi6FAqm0JA38U6nou7sPCCJDjybynyW8GijuFvwAn1fBkAkfxtk5-RBB7710g5cjpIlTM-Urs0y47w14PssMzMpreoj7F3AU","summary":[{"type":"summary_text","text":"**Planning file reading and auditing**"}]},{"id":"fc_0ca4af36969c80fa016a5702639fa0819889cc1ec45a9bb0b4","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping repository root\",\"path\":\".\",\"selector\":null}","call_id":"call_9nHWyIRRjrBZu9iCsP9w06H2","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702639fbc8198a17bb61ac822ba44","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating audit documents\",\"path\":\"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr/**;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml\",\"hidden\":true,\"gitignore\":true,\"limit\":200}","call_id":"call_hXhMgVsGPn6FgPTt71Fr2VHa","name":"glob"},{"id":"fc_0ca4af36969c80fa016a5702639fd08198b9dacaf03765d6ec","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping test suite\",\"path\":\"tests/**/*\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_hCbBsjjwLG6w1j6azUDwfGzJ","name":"glob"},{"id":"fc_0ca4af36969c80fa016a5702639fe48198b2bf9b04463baeba","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding decision markers\",\"pattern\":\"(?i)\\\\b(TODO|TBD|open question|unresolved|decision|must|should|out of scope|in scope|supersed|deprecated|hazard|known issue|field issue|Claude|glossary)\\\\b\",\"path\":\"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs;traceable-reqs.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_ZypqesFCAxRM3NpVRng9pPAq","name":"grep"}]},"duration":8319.986900000018,"ttft":1683.7097000000067,"contextSnapshot":{"promptTokens":17146,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9nHWyIRRjrBZu9iCsP9w06H2|fc_0ca4af36969c80fa016a5702639fa0819889cc1ec45a9bb0b4","toolName":"read","startedAt":"2026-07-15T03:45:39.838Z","args":{"path":"."},"intent":"Mapping repository root"},"id":"834d3d90","parentId":"2e4ac66e","timestamp":"2026-07-15T03:45:39.838Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hXhMgVsGPn6FgPTt71Fr2VHa|fc_0ca4af36969c80fa016a5702639fbc8198a17bb61ac822ba44","toolName":"glob","startedAt":"2026-07-15T03:45:39.838Z","args":{"path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr/**;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml"},"intent":"Locating audit documents"},"id":"254a33e8","parentId":"834d3d90","timestamp":"2026-07-15T03:45:39.838Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hCbBsjjwLG6w1j6azUDwfGzJ|fc_0ca4af36969c80fa016a5702639fd08198b9dacaf03765d6ec","toolName":"glob","startedAt":"2026-07-15T03:45:39.838Z","args":{"path":"tests/**/*"},"intent":"Mapping test suite"},"id":"ad064d6d","parentId":"254a33e8","timestamp":"2026-07-15T03:45:39.838Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZypqesFCAxRM3NpVRng9pPAq|fc_0ca4af36969c80fa016a5702639fe48198b2bf9b04463baeba","toolName":"grep","startedAt":"2026-07-15T03:45:39.838Z","args":{"path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs;traceable-reqs.toml"},"intent":"Finding decision markers"},"id":"46f06c55","parentId":"ad064d6d","timestamp":"2026-07-15T03:45:39.838Z"}
{"type":"message","id":"ef28738a","parentId":"46f06c55","timestamp":"2026-07-15T03:45:39.842Z","message":{"role":"toolResult","toolCallId":"call_hCbBsjjwLG6w1j6azUDwfGzJ|fc_0ca4af36969c80fa016a5702639fd08198b9dacaf03765d6ec","toolName":"glob","content":[{"type":"text","text":"# tests/\nomp-extension.mjs\nmanifest-shortcut.sh\nhooks-dispatch.sh\nbridge-acceptance.sh\nadapter-archive.sh\nskeleton-validate.sh\nmsys-hazard.sh\nmanifest-schema.sh\nci-gates.sh\ndocs-gen.sh\nacceptance-harness.sh"}],"details":{"scopePath":"tests","fileCount":11,"files":["tests/omp-extension.mjs","tests/manifest-shortcut.sh","tests/hooks-dispatch.sh","tests/bridge-acceptance.sh","tests/adapter-archive.sh","tests/skeleton-validate.sh","tests/msys-hazard.sh","tests/manifest-schema.sh","tests/ci-gates.sh","tests/docs-gen.sh","tests/acceptance-harness.sh"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt"},"isError":false,"timestamp":1784087139842}}
{"type":"message","id":"ec8849ea","parentId":"ef28738a","timestamp":"2026-07-15T03:45:39.842Z","message":{"role":"toolResult","toolCallId":"call_9nHWyIRRjrBZu9iCsP9w06H2|fc_0ca4af36969c80fa016a5702639fa0819889cc1ec45a9bb0b4","toolName":"read","content":[{"type":"text","text":".\n  - .claude/                                 25m ago\n  - traceable-reqs.toml            127.6KB   4h ago\n  - tests/                                   4h ago\n    - omp-extension.mjs            1.0KB     4h ago\n    - manifest-shortcut.sh         8.7KB     4h ago\n    - hooks-dispatch.sh            7.6KB     4h ago\n    - bridge-acceptance.sh         6.1KB     4h ago\n    - adapter-archive.sh           4.3KB     4h ago\n    - skeleton-validate.sh         2.7KB     5d ago\n    - msys-hazard.sh               2.7KB     5d ago\n    - manifest-schema.sh           2.8KB     5d ago\n    - ci-gates.sh                  1.4KB     5d ago\n    - docs-gen.sh                  1.5KB     5d ago\n    - acceptance-harness.sh        3.4KB     5d ago\n  - docs/                                    4h ago\n    - adr/                                   4h ago\n    - OMP-OPENROUTER.md            2.8KB     4h ago\n    - OMP-BRIDGE-FIELD-ISSUES.md   9.7KB     4h ago\n    - formats/                               5d ago\n    - SPT-CORE-FINDINGS.md         136.8KB   5d ago\n    - TRACEABILITY.md              5.3KB     5d ago\n    - RELEASE-RUNBOOK.md           18.2KB    5d ago\n    - SKILL-AUTHORING.md           5.2KB     5d ago\n    - PARITY.md                    7.7KB     5d ago\n    - DOCS-STRATEGY.md             6.2KB     5d ago\n    - KNOWN-HAZARDS.md             33.5KB    5d ago\n    - CI.md                        5.4KB     5d ago\n  - adapter/                                 4h ago\n    - strings/                               4h ago\n    - omp-spt.toml                 83.8KB    4h ago\n    - manifest.schema.json         39.5KB    5d ago\n  - OMP-ADAPTER-PLAN.md            10.4KB    4h ago\n  - CHANGELOG.md                   49.2KB    4h ago\n  - .gitignore                     924B      4h ago\n  - tools/                                   5d ago\n    - claude-spt/                            4h ago\n  - plugin/                                  5d ago\n    - omps/                                  5d ago\n  - docs-site/                               5d ago\n    - src/                                   5d ago\n    - theme/                                 5d ago\n    - book.toml                    771B      5d ago\n    - llms.txt                     347B      5d ago\n    - README.md                    427B      5d ago\n  - ci/                                      5d ago\n    - publish/                               4h ago\n    - manifest/                              4h ago\n    - idle-translate/                        4h ago\n    - hooks/                                 4h ago\n    - run-gates.sh                 2.8KB     5d ago\n    - setup/                                 5d ago\n    - subnet/                                5d ago\n    - psyche/                                5d ago\n    - psyche-download/                       5d ago\n    - launcher/                              5d ago\n    - lib/                                   5d ago\n    - … 4 more\n    - acceptance/                            5d ago\n  - UNIFY-CONSOLIDATE-PLAN.md      11.6KB    5d ago\n  - UPDATE-NAMING-DOYLE-ASKS.md    13.2KB    5d ago\n  - V09-PLAN.md                    6.1KB     5d ago\n  - V13-ADAPTER-PLAN.md            3.8KB     5d ago\n  - SKILLS-SLICE-PLAN.md           10.4KB    5d ago\n  - SPT_HARNESS_ADAPTER_TIPS.md    18.2KB    5d ago\n  - TRUNCATION-PROBE-PLAN.md       6.6KB     5d ago\n  - SESSIONSTART-BRIEF-PLAN.md     4.1KB     5d ago\n  - SETUP-SLICE-PLAN.md            13.1KB    5d ago\n  - SKELETON-PLAN.md               5.8KB     5d ago\n  - PEROS-RERELEASE-PLAN.md        3.7KB     5d ago\n  - PREP4-PLAN.md                  6.9KB     5d ago\n  - PSYCHE-EPHEMERAL-PLAN.md       13.1KB    5d ago\n  - README.md                      3.9KB     5d ago\n  - SCOPE.md                       13.3KB    5d ago\n  - PARITY-PUBLISH-PLAN.md         9.2KB     5d ago\n  - PEROS-PLAN.md                  2.7KB     5d ago\n  - HANDOFF.md                     8.5KB     5d ago\n  - MANIFEST-PARITY-PLAN.md        9.9KB     5d ago\n  - MDBOOK-PLAN.md                 2.6KB     5d ago\n  - NEXT-WORKLOAD-PLAN.md          12.0KB    5d ago\n  - DIGEST-FETCHER-PLAN.md         3.1KB     5d ago\n  - ECHO-COMMUNE-PLAN.md           3.1KB     5d ago\n  - CLAUDE.md                      286B      5d ago\n  - CONTEXT.md                     7.7KB     5d ago\n  - CPLUGS-PREP-PLAN.md            2.7KB     5d ago\n  - CHECKPOINT-CLEAR-RACE-PLAN.md  5.6KB     5d ago\n  - CHECKPOINT-COMMUNE-PLAN.md     11.1KB    5d ago\n  - CI-ACCEPTANCE-PLAN.md          3.2KB     5d ago\n  - BUGS-v091.md                   6.1KB     5d ago\n  - .gitattributes                 917B      5d ago\n  - AGENTS.md                      4.4KB     5d ago\n\n[1 results limit reached. Use limit=2 for more]"}],"details":{"isDirectory":true,"resolvedPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt"},"limits":{"resultLimit":{"reached":1,"suggestion":2}}}},"isError":false,"timestamp":1784087139842}}
{"type":"message","id":"e7fd0cf4","parentId":"ec8849ea","timestamp":"2026-07-15T03:45:39.844Z","message":{"role":"toolResult","toolCallId":"call_hXhMgVsGPn6FgPTt71Fr2VHa|fc_0ca4af36969c80fa016a5702639fbc8198a17bb61ac822ba44","toolName":"glob","content":[{"type":"text","text":"traceable-reqs.toml\nOMP-ADAPTER-PLAN.md\nSCOPE.md\nCONTEXT.md\n# docs/\nOMP-BRIDGE-FIELD-ISSUES.md\nKNOWN-HAZARDS.md\n## adr/\n0007-native-omp-tui-hosts-spt-extension.md\n0005-name-unification.md\n0006-one-command-update-and-consolidated-binary.md\n0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n0003-psyche-is-a-constrained-auto-approving-companion.md\n0004-checkpoint-via-self-send-loopback.md\n0000-template.md\n0001-distribution-splits-by-volatility.md"}],"details":{"scopePath":"CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr/**, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, traceable-reqs.toml","fileCount":14,"files":["traceable-reqs.toml","docs/adr/0007-native-omp-tui-hosts-spt-extension.md","docs/OMP-BRIDGE-FIELD-ISSUES.md","OMP-ADAPTER-PLAN.md","docs/adr/0005-name-unification.md","docs/adr/0006-one-command-update-and-consolidated-binary.md","docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","docs/adr/0004-checkpoint-via-self-send-loopback.md","docs/adr/0000-template.md","docs/adr/0001-distribution-splits-by-volatility.md","docs/KNOWN-HAZARDS.md","SCOPE.md","CONTEXT.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt"},"isError":false,"timestamp":1784087139844}}
{"type":"message","id":"ca8eb418","parentId":"e7fd0cf4","timestamp":"2026-07-15T03:45:39.974Z","message":{"role":"toolResult","toolCallId":"call_ZypqesFCAxRM3NpVRng9pPAq|fc_0ca4af36969c80fa016a5702639fe48198b2bf9b04463baeba","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n*1|# spt-claude-code — glossary\n 2|\n*3|> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n 4|> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n 5|\n*6|**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n 7|adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n 8|parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n 9|installer. Built by maintainer perri from the public spt-releases surface only.\n*10|(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n 11|[[naming-domains]].)\n 12|\n*13|**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n 14|invocation + the manifest). The CC adapter's identity inside spt-core. The\n 15|**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n 16|\n*17|**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n*18|spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n*19|**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n 20|`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n 21|`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n 22|shortcut (`cc-<id>`) is decoupled from both.\n...\n 38|that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that\n*39|its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint\n 40|roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity\n 41|piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\n 42|flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n...\n 50|\n*51|**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n 52|that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n*53|NOT its own adapter — ccs is structurally Claude Code.\n 54|\n 55|**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n 56|spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n...\n 64|exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65|profile. (Realized here by the `claude-spt-psyche` runner.)\n 66|\n 67|**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68|parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n...\n 91|\n*92|**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:\n 93|Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap\n 94|pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors\n 95|legacy owl's psyche box; see `docs/adr/0003-*`.\n...\n 97|**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n*98|adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n 99|users: install the plugin, get spt-core for free.\n 100|\n 101|**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102|extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103|traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104|same-repo published-docs. Defined in its own folder, not here.\n 105|\n...\n 115|turn: the session digest records a turn whose input is the literal `[Request interrupted by\n*116|user]` (the normalized form of the TUI's \"Interrupted · What should Claude do instead?\"). The\n 117|sole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`\n 118|hook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the\n 119|fetcher re-reads the transcript), never daemon-pushed.\n\n# SCOPE.md\n*1|# spt-claude-code — scoping decision ledger\n 2|\n 3|> Running source of truth for the scoping session (started 2026-06-13, doyle).\n 4|> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n...\n 9|\n*10|**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n*11|`claude-spt` adapter) — simultaneously:\n 12|1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n 13|   delegating all core to `spt.exe`).\n 14|2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n...\n 30|\n*31|**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n 32|in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n 33|\n 34|| Layer | Contents | Home | Update path | Churn |\n 35||---|---|---|---|---|\n*36|| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n 37|| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n 38|| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n 39|\n 40|- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n*41|  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n 42|- The \"bulk\" spt-core installs/updates = **manifest + binary**, not the skills.\n 43|- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.\n 44|- → ADR-0001 (to write).\n...\n 53|  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n*54|  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n 55|  empirically at build time**, but design on it.\n 56|- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n 57|  manifest doesn't bloat.\n...\n 65|SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n*66|(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n 67|\n 68|## ccs integration\n 69|\n*70|**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n*71|`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n 72|model/billing backend differ, which is exactly the profile seam (leaf-replace\n 73|spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n 74|leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n 75|profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n*76|Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n 77|not only bare adapter_name (profile model already supports composite addressing).\n 78|\n 79|## `/spt:setup` skill\n...\n 99|full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100|`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n*101|flag if verify should fold inline._\n 102|\n 103|### `cc` launcher (capsule-style) — **LOCKED (design)**\n 104|\n...\n 125|- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.\n*126|- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the\n 127|  **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.\n 128|- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods\n 129|  the product as its own CI nervous system.\n*130|- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131|  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132|  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133|  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n...\n 135|  gates → reports over spt. Manual \"run gates\" = fallback. (Polling rejected: latency + waste.)\n*136|  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137|    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n*138|    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139|- This CI pattern → reusable, goes in `experimplate`.\n 140|\n 141|## Parity-trim (→ REQ seed / acceptance bar)\n...\n 153|**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n*154|harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n 155|\n 156|**KEEP — FINALIZED** (post-investigation 2026-06-14):\n 157|- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`\n...\n 173|**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174|spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175|`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176|\n 177|## spt-core upstream deps\n\n# OMP-ADAPTER-PLAN.md\n*1|# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n 2|\n*3|> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n 4|> **oh-my-pi (omp)** — the terminal coding agent at\n 5|> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n 6|> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n...\n 16|\n*17|claude-spt exists because Claude Code has **no control protocol** — the adapter\n 18|reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n 19|`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n 20|digest, and pushes received messages back via `additionalContext`. Most of its\n...\n 46|\n*47|### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n 48|\n 49|**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n*50|spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n 51|runs `omp --mode rpc` as a child, and translates in both directions:\n 52|\n 53|```\n...\n 70|\n*71|Rationale, in one line: **the process whose UI the operator must see must own the\n 72|PTY.** See ADR-0007. <!-- [doc->REQ-OMP-NATIVE-TUI] -->\n 73|\n*74|## Seam map — claude-spt value → omp-spt target\n 75|\n*76|| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77||---|---|---|\n*78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n*79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n*83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n*84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n*86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n 87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n...\n 110|\n*111|## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112|\n*113|- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114|  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115|  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116|- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117|  (bridge command), `[identity]`. Prove with\n 118|  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119|  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120|- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121|  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122|  Acceptance: from another node, message the perch → get an omp answer; assert\n 123|  `spt` state transitions (isolated CI identity per tips doc §Testing).\n...\n 143|    models:\n*144|      - id: anthropic/claude-sonnet-5\n*145|        name: Claude Sonnet 5 (OpenRouter)\n 146|        contextWindow: 200000\n 147|        maxTokens: 8192\n 148|```\n\n# docs/\n## CI.md\n 2|\n*3|> spt-claude-code's CI is **wholly agent-driven and autonomous** on the existing Windows +\n 4|> Linux fleet (hfenduleam + kitsubito). **GitHub-hosted runners are NOT used** — a stock\n*5|> hosted runner physically can't run Claude Code (auth + interactivity), so only a real\n 6|> harness on the fleet reaches the acceptance bar. **Deterministic gates** run by a fleet\n 7|> runner-agent, triggered by a git post-push hook, reporting over **spt messaging**. No LLM\n 8|> sits in the gate path — the gates are plain scripts; the \"agent\" is just the autonomous\n...\n 27|5. **Docs-drift** — generated docs (API reference, `llms.txt`, schema, CLI help) are regenerated\n*28|   and must match what's checked in; a diff fails (see `docs/DOCS-STRATEGY.md`).\n 29|\n*30|## Acceptance (the system-under-test is a real `claude` session)\n 31|\n*32|Acceptance is **scripted orchestration that spawns real `claude` / headless sessions as the\n 33|system-under-test**, then asserts spt-state / `[digest]` output. The **LLM is the SUT, never\n 34|the runner** — the orchestration is deterministic; only the thing it drives is a real harness.\n 35|This is what a stock hosted runner can't do, and why the fleet is mandatory. (spt-core's\n...\n 38|\n*39|**Disposable-identity invariant (mandatory).** Every nested `claude` the harness spawns runs\n 40|under a **throwaway perch id** (`SPT_AGENT_ID=sptc-ci-<n>`), never a live agent's name. A nested\n 41|session that resolves a live id establishes a perch under that name and — perches being\n 42|name-keyed, last-establish-wins — **tears down the live agent's perch and poll stream** (the\n 43|self-inflicted collision diagnosed 2026-06-15; see `docs/KNOWN-HAZARDS.md` §2.1 /\n*44|`REQ-HAZARD-PERCH-COLLISION`). `ci/acceptance/lib.sh` `sptc_ci_identity` enforces it.\n 45|\n*46|**Slice 1** (`ci/acceptance/run-acceptance.sh`) asserts a real `claude` **fires the\n 47|UserPromptSubmit hook** via a hook-written digest marker — the harness-contract entry point\n 48|working inside a real harness — independent of the spt bus or model text. It is **env-gated**\n*49|(`SPTC_ACCEPTANCE=1`) so the deterministic gate run stays green on hosts without `claude`/auth.\n 50|Bus-delivery acceptance (poll→additionalContext over real spt) is a later slice, gated on\n 51|`REQ-MSG-ENVELOPE`.\n 52|\n...\n 77|The bus binary's path **changes between versions** — legacy spt lives in a per-version\n*78|`~/.claude/` or `~/.ccs/` plugins folder whose path moves each release. No further\n*79|legacy-spt changes are anticipated, but the hook and runner **must locate it robustly**:\n 80|resolve it at run time (search the known plugins/install roots / a configured path / `PATH`)\n 81|rather than hard-coding a versioned location. A stale hard-coded path is the most likely cause\n 82|of a silently dead trigger.\n...\n 85|\n*86|A **manual \"run gates\" command** must always exist — the same gate scripts, runnable by hand on\n 87|any fleet host. Use it when the hook didn't fire, when reproducing a failure, or before a hook is\n 88|wired at all.\n 89|\n## DOCS-STRATEGY.md\n 31|   alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\n*32|5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism\n 33|   is fatal for agents.\n 34|6. **Complete reference, auto-generated, all error variants** — generate API reference from the\n 35|   code for your public surface, plus any machine-readable contract/schema. Generic placeholders\n...\n 45|    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt\n*46|         deterministically and fails the build on any diff vs the committed file; the book must\n 47|         also build. This is the structural anti-drift commitment, enforced in run-gates. -->\n 48|\n 49|## Information architecture — by capability vertical\n## KNOWN-HAZARDS.md\n 2|\n*3|> A **conformance checklist, not advice.** Each hazard below is a first-class\n*4|> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n 5|> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n 6|> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n*7|> and `traceable-reqs check` will say so once the hazard is activated.\n 8|\n*9|A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\n 10|incident) or one you have *committed never to introduce*. State it so a test can prove it.\n 11|\n 12|## Entry format\n...\n 16|- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n*17|- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n*18|  `REQ-HAZARD-*` requires).\n 19|- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n 20|  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n 21|- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n...\n 27|[[requirements]]\n*28|id = \"REQ-HAZARD-EXAMPLE\"\n 29|title = \"The invariant, stated so a test can prove it\"\n 30|required_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n 31|```\n...\n 40|  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n*41|  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n*42|  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n 43|  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n 44|  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n*45|  documents the identical hazard in `new-alarm`.)\n*46|- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n 47|  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n*48|  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n 49|  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n 50|- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n...\n 54|- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n*55|  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n 56|\n 57|---\n 58|\n...\n 60|\n*61|### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n 62|\n*63|<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n 64|\n*65|- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n 66|  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n 67|  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n 68|  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n...\n 71|  operator as self-inflicted, NOT a legacy-substrate bug.\n*72|- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n 73|  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n*74|  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75|  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n 76|  overrides both to a `sptc-ci-` id and never emits a live id.\n 77|- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n*78|  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n*79|  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n 80|  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n 81|  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n 82|  dir is not part of the public surface, so isolation rides on identity.\n## OMP-BRIDGE-FIELD-ISSUES.md\n 5|\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n*6|and could not be switched back to `claude-spt`. This documents the root causes,\n 7|grounded in this repo's code, so the bridge can be hardened.\n 8|\n*9|All line references are against `tools/claude-spt/src/bridge.rs` and\n 10|`adapter/omp-spt.toml` at commit `f0109d1`.\n 11|\n 12|## Incident summary\n...\n 14|1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15|   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16|2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17|   way to tell working vs. wedged.\n 18|3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19|   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20|   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21|   endpoint looked alive but was dead.\n*22|4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23|5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24|\n 25|Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n...\n 30|\n*31|## HIGH-1 — Bridge binary basename collides with `claude-spt`\n 32|\n*33|**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n*34|indistinguishable from a genuine claude-spt process. During the incident this\n*35|made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n 36|todlando's perch was actually the omp bridge. It directly caused the\n 37|misdiagnosis.\n 38|\n...\n 40|```\n*41|command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42|```\n*43|The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44|This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n 45|bridge (S2) is `omp-spt`\"*).\n 46|\n...\n 58|object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n*59|`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n 60|`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n 61|When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62|those lines never run. On Windows, killing a process does not kill its children,\n...\n 149|**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150|restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151|editing spt-core's session ledger by hand.\n 152|\n 153|**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n...\n 160|**Fix.** Provide (and document) a clean unbind/switch so an operator can move an\n*161|endpoint off omp-spt without ledger surgery. Ideally a failed bringup should not\n 162|leave a poisoning ledger row — bind only once the omp `ready` + `get_state`\n 163|handshake has actually succeeded (it already binds after `get_state`, but see\n 164|HIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).\n## OMP-OPENROUTER.md\n 18|    models:\n*19|      - id: anthropic/claude-sonnet-5\n*20|        name: Claude Sonnet 5 (OpenRouter)\n 21|        contextWindow: 200000\n 22|        maxTokens: 8192\n 23|      # add more OpenRouter model ids as needed; omp fuzzy-matches --model against them\n## PARITY.md\n*1|# Parity audit — claude-spt vs the legacy owl surface\n 2|\n 3|> The reconciliation record: the adapter's operative surface measured against the **LOCKED\n 4|> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **\"proven parity\"**\n 5|> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +\n*6|> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved\n 7|> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.\n 8|>\n 9|> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is\n...\n 28|| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |\n*29|| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |\n 30|| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |\n 31|| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸️ DEFER + remove stub |\n 32|| `/fork` | DROP | absent | ✅ confirmed absent |\n...\n 37|TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +\n*38|`shortcut_basename=\"cc\"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→\n 39|`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅\n 40|\n 41|## Open divergences and dispositions\n...\n 52|supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is\n*53|`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT\n 54|`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile\n 55|authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good\n 56|sister project **claude_skill_owl**:\n*57|- **Spawn seam.** `[profiles.ccs.session.self].command = \"ccs\"` leaf-replaces the base `claude`.\n*58|  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`\n 59|  Tier-2 PULSE recovery latches `cli_binary = \"ccs\"`); `SPT_ENDPOINT_ID` rides inherited env through\n 60|  the wrapper unchanged.\n 61|- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree\n*62|  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),\n 63|  a per-account runtime value with no static catalog path. SCOPE's \"per-profile `~/.ccs` log dir\"\n*64|  is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/\n 65|  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The\n 66|  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.\n 67|  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`\n## RELEASE-RUNBOOK.md\n 2|\n*3|> How an spt-claude-code release ships. The spine is fixed (changelog · bump · regenerate\n 4|> docs · tag · publish · docs publish). **Release here is LIGHT: signing is DELEGATED to the\n*5|> `spt` binary — there is NO two-key signing ceremony in this project.** spt-claude-code is a\n 6|> thin adapter; the heavy signed-release machinery lives in spt-core, not here.\n 7|\n 8|<!-- [<doc>->REQ-EXAMPLE-REL] (illustration; escaped per docs/TRACEABILITY.md) -->\n...\n 11|\n*12|An spt-claude-code release lands in **two places**, by volatility:\n 13|\n 14|1. **The `SaberMage/cplugs` marketplace** — the thin **skeleton plugin** (namespaced\n 15|   `/spt:*` skill skeletons, `hooks.json`, the SessionStart bootstrap, `plugin.json`). This\n...\n 42|   - **No internal lingo** — no requirement ids, internal module names, commit hashes, or\n*43|     milestone / hazard codes. A reader who has never seen the source must understand every line.\n 44|   - **Use spt nomenclature** (operator rule, 2026-07-03) — product terms (Psyche, live agent,\n 45|     perch, endpoint, node, subnet) ARE the user-facing language; never substitute an invented\n 46|     softer synonym (e.g. \"companion\" for Psyche). \"No internal lingo\" bans *implementation*\n...\n 80|     floor bump + changelog to match). For the ephemeral-psyche case the signature is the manifest\n*81|     table: `git show <release-HEAD>:adapter/claude-spt.toml | grep -q 'session.psyche_resume' &&\n 82|     echo \"PARKED WORK PRESENT — do not tag\" || echo \"clean\"`. Generalize per parked feature:\n 83|     grep the tag tree for the feature's defining seam.\n 84|   - **When a release DOES ship formerly-parked work**, it is deliberate: the `min_spt_core` floor\n...\n 103|The concrete cplugs marketplace steps, captured from the sister project's\n*104|`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs\n 105|target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart\n 106|bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest\n 107|ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so\n...\n 122|# Clone the marketplace repo\n*123|git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs\n*124|# Register a \"cplugs\" entry in ~/.claude/plugins/known_marketplaces.json\n 125|#   source: { source: \"github\", repo: \"SaberMage/cplugs\" }\n 126|```\n 127|\n...\n 133|# 2. Copy skeleton files (NO binary) into the marketplace clone:\n*134|MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc\n 135|cp -r skills/*  \"$MARKET/skills/\"\n 136|cp -r hooks/*   \"$MARKET/hooks/\"\n*137|cp .claude-plugin/plugin.json \"$MARKET/.claude-plugin/\"\n 138|# 3. Commit + push the cplugs repo:\n*139|cd ~/.claude/plugins/marketplaces/cplugs && git add plugins/sptc/ \\\n 140|  && git commit -m \"sptc: <change>\" && git push\n 141|```\n 142|\n...\n 145|```bash\n*146|claude plugin install sptc@cplugs   # un-orphans + rewrites installed_plugins.json atomically\n*147|# then, inside Claude Code:\n 148|/reload-plugins\n 149|```\n 150|\n...\n 152|- **Never hand-patch `installed_plugins.json`** (`jq`/`sed` are brittle; CC may silently reject).\n*153|  Use the `claude plugin install` CLI — it's the atomic pointer flip + orphan-marker cleanup.\n*154|- **No legacy manual-install dir alongside the marketplace install.** A `~/.claude/plugins/<name>/`\n 155|  regular dir coexisting with the marketplace cache install makes `/plugin` report the plugin\n 156|  **not installed** (CC enumerates both; the untracked one wins the conflict). Keep the two exclusive.\n*157|- **Restart the Claude Code session after install** so the SessionStart hook re-runs and repopulates\n 158|  the plugin's env (`$OWL`/`$LIVE` equivalents) in Bash subprocesses.\n 159|\n 160|**Explicitly NOT ours** (owl ships a live self-migrating binary; the thin skeleton has none, so\n...\n 173|The CC adapter manifest (publish target #2) ships to end users as an **`adapter.spt`** GitHub\n*174|**release asset** on this monorepo, acquired with **`spt adapter add --release SaberMage/claude-spt`**\n 175|(doyle's `--release` source; needs **spt v0.7.3+ / counter 15** — not in 0.7.2). No dedicated\n 176|adapter repo: the asset is packed straight from `adapter/`.\n 177|\n...\n 184|  archive needs it), requires **both** platforms' built tool binaries, and tars the archive **ROOT** =\n*185|  `manifest.toml` (renamed from `claude-spt.toml` — `adapter add` is root-only + exact-name) +\n 186|  `strings/` **shared at root**, plus each recognized target-triple's binaries under a `<triple>/` dir.\n 187|  Never uploads (operator's step).\n 188|<!-- [doc->REQ-DIST-ADAPTER-PEROS] -->\n...\n 191|  archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install\n*192|  dir**, so the bare-name command token (`claude-spt` — its digest/psyche/post-update/translate subcommands) still resolves at `<install_dir>/`\n 193|  (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root\n 194|  entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform\n 195|  beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.\n...\n 197|  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →\n*198|    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate\n 199|    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)\n 200|  - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target\n 201|    x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the\n## SKILL-AUTHORING.md\n 7|\n*8|## What a skill body must NOT contain\n 9|\n 10|- **Plugin / spt-core internals.** No adapter resolution (`--adapter`, parent-pid / `host_binaries`\n 11|  matching), no perch-state stamping (`state=live_agent`), no daemon/poll-vs-relay coordination, no\n## SPT-CORE-FINDINGS.md\n 20|| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:\"host not resident within 5s ...\", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit ...\n*21|| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises \"with substitution\"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + ...\n*22|| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os...\n 23|| F-016 | 2026-06-22 | **RESOLVED — both fixes SHIPPED** (doyle, broker.rs-confirmed). The published `[message-idle-translation-binary]` doc omitted `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **(i) adapter bi...\n*24|| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bo...\n*25|| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails...\n*26|| F-020 | 2026-06-24 | **RESOLVED-PENDING — frozen Tier-1 contract, ships v0.15.0 W5** (doyle ruled + grounded; verb committed by todlando @20bfc1f). spt-core publishes NO `spt api` verb to pull resume/psyche context at SessionStart — `download_psyche_context` (resume.rs:88) is exported-but-unexposed (ZERO `ApiCmd` callers); `resume.rs:9` documents the \"adapter pulls it in its SessionStart hook\" intent but no verb was ever published. **Consequence: claude-spt live agents rehydrate NO durable context ...\n 27|\n 28|| F-023 | 2026-07-01 | **CONFIRMED + RCA'd (doyle, same day) — build dispatched to todlando (`BUILD-F023-WANIDLE`)**. Both ranked hypotheses real: `receive_wan` has deliver_tcp + spool ONLY (the spt-hosted broker-inject leg exists solely in local `cmd_send`) AND no idle-edge drain exists anywhere. Fix = shared inject leg called from WAN ingress + ACTIVE→IDLE spool drain. Adapter exoneration stands verbatim. `from_id=\"\"` sibling ruled: compose stamps `cli@<node-label>`; ingress renders empty-from as o...\n 29|| F-024 | 2026-07-01 | **CLOSED-REFRAMED (doyle, same day) — transport EXONERATED; symptom = F-023 on the remote node.** The `(spooled)` token in `SENT(WAN)` output is RECEIVER-CONFIRMED (only prints on a confirmed round-trip): probe `DIAG-7c1e` reached ENLYZEAM post-restart and sits in ball-b's spool undelivered (`spooled` not `delivered` = no live TCP listener at receive time). Operator workaround: any prompt typed into ball-b UPS-drains the stuck messages. Residues stay real as separate diagnostics ...\n*30|| F-031 | 2026-07-08 | **REPORTED to doyle (perri) — docs gap; adapter worked around in v0.17.3 (REQ-HAZARD-SEND-STATUS-STDERR).** `spt send`'s result token (`SENT:<id>` / `QUEUED:<id>` / `DEFERRED:<id>` / `NO_PERCH:<reason>`) is written to **STDERR**, not stdout — stdout is empty on a plain `spt send`. This is undocumented in the published harness-contract / CLI reference (they describe the tokens as the send's *result* without naming the stream). It bit the claude-spt tag-messaging hook: the hook c...\n 31|\n*32|> **F-021 / F-022 (NOT spt-core findings — claude-spt parity items, tracked as REQs; surfaced during the 2026-06-24 checkpoint-commune grill)** — (F-021) claude-spt polls only on `UserPromptSubmit` (between turns); legacy spt also polls+injects on **PreToolUse** = the mid-turn half of live-agent reachability (the main path by which a live agent receives a message WHILE working). `api poll` already exists on the public surface → adapter wiring, not an spt-core gap → **`REQ-DIST-PRETOOL-POLL`**. (F...\n 33|\n 34|> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as sp...\n 35|\n...\n 37|\n*38|## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter\n 39|\n 40|**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.\n 41|\n...\n 60|   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the\n*61|   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author\n 62|   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:\n*63|   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side\n 64|   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills\n 65|   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption \"the plugin ships a `hooks.json`\n 66|   calling `spt api`\" — it may instead be manifest-declared + spt-materialized.)\n...\n 85|  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`\n*86|  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle\n 87|  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /\n 88|  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit\n 89|  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.\n...\n 109|\n*110|Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary\n 111|(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the\n 112|residual: the **published docs still omit** two things a manifest author needs, both learnable only\n 113|from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:\n...\n 129|   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with\n*130|   `source = \"{home}/.claude/projects\"` — CC's cwd-slug subdir is not expressible as a flat\n 131|   `locate_template`, so `source` names the per-project root and the extractor finds\n 132|   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less\n 133|   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);\n...\n 146|**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has\n*147|raised the framing as a contract decision to the operator.\n 148|\n 149|**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained\n 150|message with `print!(\"{msg}\")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**\n...\n 254|$ spt adapter add <dir>\n*255|#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative\n 256|#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered\n 257|```\n 258|\n...\n 284|\n*285|**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor.\n 286|\n 287|**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample\n*288|path) into the extractor command and hard-fails on any other key. The claude-spt extractor command\n 289|is the production-correct, published-example shape:\n 290|\n 291|```\n*292|extractor = \"claude-spt-digest --session {session_id} --in {source}\"\n 293|```\n 294|\n*295|→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.\n 296|\n 297|**Isolation (same adapter, only the command line differs):**\n 298|- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,\n...\n 302|\n*303|**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**\n## TRACEABILITY.md\n 2|\n*3|> How spt-claude-code uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs)\n 4|> to keep every requirement traced from doc → impl → test. The manifest is\n 5|> `traceable-reqs.toml` (seed it from your requirements source + your KNOWN-HAZARDS\n 6|> invariants). This contract makes the trace pay off instead of rotting.\n...\n 26|   one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.\n*27|4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring\n*28|   `unit` (and `int` where cross-process / cross-node). A hazard cannot be \"covered\" without a\n 29|   test tagging it — the anti-regression promise becomes mechanical.\n 30|5. **Activation, not premature failure.** Every requirement starts `required_stages = []`\n 31|   (inactive) so `check` stays green pre-code. A **milestone activates** the requirements it\n...\n 53|```rust\n*54|// [<unit>->REQ-HAZARD-EXAMPLE]\n 55|#[test] fn upholds_the_invariant() { ... }\n 56|```\n 57|```markdown\n...\n 72|   `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check`\n*73|   before declaring done. If this project is built *with* agents, the contract must be\n 74|   machine-followable — agent discipline + CI as the net.\n 75|4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review`\n 76|   + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run\n...\n 105|- **First milestone:** install the CLI, validate the seed against it, wire the CI gate,\n*106|  **activate that milestone's reqs** (and the `REQ-HAZARD-*` invariants it must uphold), tag as\n 107|  code lands.\n 108|- **Each later milestone:** activates + covers its requirements before it's called done.\n\n## adr/\n### 0000-template.md\n*1|# <Title — the decision stated as its outcome>\n 2|\n*3|<!-- Title states the decision, not the question. e.g. \"Sessions are pinned at first bind\",\n*4|     not \"How should sessions bind?\". Number files sequentially: 0001-, 0002-, … -->\n 5|\n 6|## Status\n 7|\n...\n 9|proposed (`<YYYY-MM-DD>`)\n*10|<!-- e.g. accepted (2026-01-15) · supersedes [ADR-NNNN](NNNN-old-decision.md) -->\n 11|\n 12|## Context\n 13|\n*14|<!-- The forces at play: what made this decision necessary, the constraints, the real\n 15|     problems (numbered if several). Enough that a reader who wasn't there understands WHY.\n 16|     Reference CONTEXT.md terms by their canonical names. -->\n 17|\n*18|## Decision\n 19|\n 20|<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,\n 21|     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->\n### 0001-distribution-splits-by-volatility.md\n 12|\n*13|`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n*14|its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n*15|invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n 16|inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n 17|plugin author.\n 18|\n...\n 24|   marketplace or letting logic rot.\n*25|2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n 26|   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n 27|   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n 28|   need to ride the marketplace at all.\n*29|3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under\n*30|   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills\n*31|   must stay **plugin-provided**.\n 32|4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed\n*33|   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.\n 34|\n*35|## Decision\n 36|\n 37|Split the adapter across **three layers by volatility** (see `SCOPE.md` §\"Distribution\n 38|architecture\"):\n...\n 65|\n*66|- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,\n 67|  collision-prone). Rejected.\n 68|- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:\n 69|  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen\n...\n 97|  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n*98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n 99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n*102|  `claude plugin update` (skeleton sync only).\n 103|<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n 104|- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105|  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106|  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n 107|  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108|  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109|- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n...\n 111|  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112|  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n 113|  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114|  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n 115|  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 12|primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n*13|formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n 14|payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n*15|public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n 16|(the adapter_name — distinct from the plugin name `sptc`).\n 17|\n 18|Two hard constraints shape the wiring:\n...\n 22|   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n\n[Showing lines 1-742 of 964 (49.9KB limit). Use :743 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs, traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":246,"fileCount":20,"files":["CONTEXT.md","SCOPE.md","OMP-ADAPTER-PLAN.md","docs/CI.md","docs/DOCS-STRATEGY.md","docs/KNOWN-HAZARDS.md","docs/OMP-BRIDGE-FIELD-ISSUES.md","docs/OMP-OPENROUTER.md","docs/PARITY.md","docs/RELEASE-RUNBOOK.md","docs/SKILL-AUTHORING.md","docs/SPT-CORE-FINDINGS.md","docs/TRACEABILITY.md","docs/adr/0000-template.md","docs/adr/0001-distribution-splits-by-volatility.md","docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","docs/adr/0004-checkpoint-via-self-send-loopback.md","docs/adr/0005-name-unification.md","docs/adr/0006-one-command-update-and-consolidated-binary.md"],"fileMatches":[{"path":"CONTEXT.md","count":16},{"path":"SCOPE.md","count":20},{"path":"OMP-ADAPTER-PLAN.md","count":20},{"path":"docs/CI.md","count":12},{"path":"docs/DOCS-STRATEGY.md","count":2},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","count":14},{"path":"docs/OMP-OPENROUTER.md","count":2},{"path":"docs/PARITY.md","count":9},{"path":"docs/RELEASE-RUNBOOK.md","count":20},{"path":"docs/SKILL-AUTHORING.md","count":1},{"path":"docs/SPT-CORE-FINDINGS.md","count":20},{"path":"docs/TRACEABILITY.md","count":6},{"path":"docs/adr/0000-template.md","count":6},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","count":14},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","count":13},{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","count":18},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","count":5},{"path":"docs/adr/0005-name-unification.md","count":13},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","count":15}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# CONTEXT.md\n  *1│# spt-claude-code — glossary\n   2│\n  *3│> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n   4│> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n   5│\n  *6│**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n   7│adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n   8│parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n   9│installer. Built by maintainer perri from the public spt-releases surface only.\n *10│(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n  11│[[naming-domains]].)\n  12│\n *13│**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n  14│invocation + the manifest). The CC adapter's identity inside spt-core. The\n  15│**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n  16│\n *17│**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n *18│spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n *19│**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n  20│`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n  21│`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n  22│shortcut (`cc-<id>`) is decoupled from both.\n    │...\n  38│that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that\n *39│its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint\n  40│roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity\n  41│piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\n  42│flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n    │...\n  50│\n *51│**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n  52│that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n *53│NOT its own adapter — ccs is structurally Claude Code.\n  54│\n  55│**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n  56│spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n    │...\n  64│exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n *65│profile. (Realized here by the `claude-spt-psyche` runner.)\n  66│\n  67│**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n  68│parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n    │...\n  91│\n *92│**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:\n  93│Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap\n  94│pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors\n  95│legacy owl's psyche box; see `docs/adr/0003-*`.\n    │...\n  97│**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n *98│adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n  99│users: install the plugin, get spt-core for free.\n 100│\n 101│**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102│extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103│traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104│same-repo published-docs. Defined in its own folder, not here.\n 105│\n    │...\n 115│turn: the session digest records a turn whose input is the literal `[Request interrupted by\n*116│user]` (the normalized form of the TUI's \"Interrupted · What should Claude do instead?\"). The\n 117│sole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`\n 118│hook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the\n 119│fetcher re-reads the transcript), never daemon-pushed.\n\n# SCOPE.md\n  *1│# spt-claude-code — scoping decision ledger\n   2│\n   3│> Running source of truth for the scoping session (started 2026-06-13, doyle).\n   4│> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n    │...\n   9│\n *10│**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n *11│`claude-spt` adapter) — simultaneously:\n  12│1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n  13│   delegating all core to `spt.exe`).\n  14│2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n    │...\n  30│\n *31│**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n  32│in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n  33│\n  34│| Layer | Contents | Home | Update path | Churn |\n  35│|---|---|---|---|---|\n *36│| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n  37│| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n  38│| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n  39│\n  40│- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n *41│  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n  42│- The \"bulk\" spt-core installs/updates = **manifest + binary**, not the skills.\n  43│- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.\n  44│- → ADR-0001 (to write).\n    │...\n  53│  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n *54│  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n  55│  empirically at build time**, but design on it.\n  56│- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n  57│  manifest doesn't bloat.\n    │...\n  65│SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n *66│(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n  67│\n  68│## ccs integration\n  69│\n *70│**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n *71│`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n  72│model/billing backend differ, which is exactly the profile seam (leaf-replace\n  73│spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n  74│leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n  75│profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n *76│Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n  77│not only bare adapter_name (profile model already supports composite addressing).\n  78│\n  79│## `/spt:setup` skill\n    │...\n  99│full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100│`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n*101│flag if verify should fold inline._\n 102│\n 103│### `cc` launcher (capsule-style) — **LOCKED (design)**\n 104│\n    │...\n 125│- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.\n*126│- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the\n 127│  **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.\n 128│- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods\n 129│  the product as its own CI nervous system.\n*130│- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131│  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132│  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133│  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n    │...\n 135│  gates → reports over spt. Manual \"run gates\" = fallback. (Polling rejected: latency + waste.)\n*136│  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137│    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n*138│    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139│- This CI pattern → reusable, goes in `experimplate`.\n 140│\n 141│## Parity-trim (→ REQ seed / acceptance bar)\n    │...\n 153│**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n*154│harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n 155│\n 156│**KEEP — FINALIZED** (post-investigation 2026-06-14):\n 157│- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`\n    │...\n 173│**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174│spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175│`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176│\n 177│## spt-core upstream deps\n\n# OMP-ADAPTER-PLAN.md\n  *1│# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n   2│\n  *3│> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n   4│> **oh-my-pi (omp)** — the terminal coding agent at\n   5│> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n   6│> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n    │...\n  16│\n *17│claude-spt exists because Claude Code has **no control protocol** — the adapter\n  18│reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n  19│`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n  20│digest, and pushes received messages back via `additionalContext`. Most of its\n    │...\n  46│\n *47│### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n  48│\n  49│**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n *50│spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n  51│runs `omp --mode rpc` as a child, and translates in both directions:\n  52│\n  53│```\n    │...\n  70│\n *71│Rationale, in one line: **the process whose UI the operator must see must own the\n  72│PTY.** See ADR-0007. <!-- [doc->REQ-OMP-NATIVE-TUI] -->\n  73│\n *74│## Seam map — claude-spt value → omp-spt target\n  75│\n *76│| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n  77│|---|---|---|\n *78│| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n *79│| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n  80│| `[adapter].shortcut_basename` | `cc` | `omp` |\n  81│| `[adapter].kind` | `harness` | `harness` (unchanged) |\n *82│| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n *83│| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n *84│| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n  85│| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n *86│| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n  87│| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n  88│| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n  89│\n    │...\n 110│\n*111│## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112│\n*113│- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114│  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115│  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116│- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117│  (bridge command), `[identity]`. Prove with\n 118│  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119│  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120│- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121│  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122│  Acceptance: from another node, message the perch → get an omp answer; assert\n 123│  `spt` state transitions (isolated CI identity per tips doc §Testing).\n    │...\n 143│    models:\n*144│      - id: anthropic/claude-sonnet-5\n*145│        name: Claude Sonnet 5 (OpenRouter)\n 146│        contextWindow: 200000\n 147│        maxTokens: 8192\n 148│```\n\n# docs/\n## CI.md\n  2│\n *3│> spt-claude-code's CI is **wholly agent-driven and autonomous** on the existing Windows +\n  4│> Linux fleet (hfenduleam + kitsubito). **GitHub-hosted runners are NOT used** — a stock\n *5│> hosted runner physically can't run Claude Code (auth + interactivity), so only a real\n  6│> harness on the fleet reaches the acceptance bar. **Deterministic gates** run by a fleet\n  7│> runner-agent, triggered by a git post-push hook, reporting over **spt messaging**. No LLM\n  8│> sits in the gate path — the gates are plain scripts; the \"agent\" is just the autonomous\n   │...\n 27│5. **Docs-drift** — generated docs (API reference, `llms.txt`, schema, CLI help) are regenerated\n*28│   and must match what's checked in; a diff fails (see `docs/DOCS-STRATEGY.md`).\n 29│\n*30│## Acceptance (the system-under-test is a real `claude` session)\n 31│\n*32│Acceptance is **scripted orchestration that spawns real `claude` / headless sessions as the\n 33│system-under-test**, then asserts spt-state / `[digest]` output. The **LLM is the SUT, never\n 34│the runner** — the orchestration is deterministic; only the thing it drives is a real harness.\n 35│This is what a stock hosted runner can't do, and why the fleet is mandatory. (spt-core's\n   │...\n 38│\n*39│**Disposable-identity invariant (mandatory).** Every nested `claude` the harness spawns runs\n 40│under a **throwaway perch id** (`SPT_AGENT_ID=sptc-ci-<n>`), never a live agent's name. A nested\n 41│session that resolves a live id establishes a perch under that name and — perches being\n 42│name-keyed, last-establish-wins — **tears down the live agent's perch and poll stream** (the\n 43│self-inflicted collision diagnosed 2026-06-15; see `docs/KNOWN-HAZARDS.md` §2.1 /\n*44│`REQ-HAZARD-PERCH-COLLISION`). `ci/acceptance/lib.sh` `sptc_ci_identity` enforces it.\n 45│\n*46│**Slice 1** (`ci/acceptance/run-acceptance.sh`) asserts a real `claude` **fires the\n 47│UserPromptSubmit hook** via a hook-written digest marker — the harness-contract entry point\n 48│working inside a real harness — independent of the spt bus or model text. It is **env-gated**\n*49│(`SPTC_ACCEPTANCE=1`) so the deterministic gate run stays green on hosts without `claude`/auth.\n 50│Bus-delivery acceptance (poll→additionalContext over real spt) is a later slice, gated on\n 51│`REQ-MSG-ENVELOPE`.\n 52│\n   │...\n 77│The bus binary's path **changes between versions** — legacy spt lives in a per-version\n*78│`~/.claude/` or `~/.ccs/` plugins folder whose path moves each release. No further\n*79│legacy-spt changes are anticipated, but the hook and runner **must locate it robustly**:\n 80│resolve it at run time (search the known plugins/install roots / a configured path / `PATH`)\n 81│rather than hard-coding a versioned location. A stale hard-coded path is the most likely cause\n 82│of a silently dead trigger.\n   │...\n 85│\n*86│A **manual \"run gates\" command** must always exist — the same gate scripts, runnable by hand on\n 87│any fleet host. Use it when the hook didn't fire, when reproducing a failure, or before a hook is\n 88│wired at all.\n 89│\n## DOCS-STRATEGY.md\n 31│   alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\n*32│5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism\n 33│   is fatal for agents.\n 34│6. **Complete reference, auto-generated, all error variants** — generate API reference from the\n 35│   code for your public surface, plus any machine-readable contract/schema. Generic placeholders\n   │...\n 45│    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt\n*46│         deterministically and fails the build on any diff vs the committed file; the book must\n 47│         also build. This is the structural anti-drift commitment, enforced in run-gates. -->\n 48│\n 49│## Information architecture — by capability vertical\n## KNOWN-HAZARDS.md\n  2│\n *3│> A **conformance checklist, not advice.** Each hazard below is a first-class\n *4│> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n  5│> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n  6│> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n *7│> and `traceable-reqs check` will say so once the hazard is activated.\n  8│\n *9│A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\n 10│incident) or one you have *committed never to introduce*. State it so a test can prove it.\n 11│\n 12│## Entry format\n   │...\n 16│- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n*17│- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n*18│  `REQ-HAZARD-*` requires).\n 19│- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n 20│  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n 21│- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n   │...\n 27│[[requirements]]\n*28│id = \"REQ-HAZARD-EXAMPLE\"\n 29│title = \"The invariant, stated so a test can prove it\"\n 30│required_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n 31│```\n   │...\n 40│  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n*41│  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n*42│  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n 43│  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n 44│  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n*45│  documents the identical hazard in `new-alarm`.)\n*46│- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n 47│  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n*48│  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n 49│  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n 50│- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51│  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n   │...\n 54│- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n*55│  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n 56│\n 57│---\n 58│\n   │...\n 60│\n*61│### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n 62│\n*63│<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n 64│\n*65│- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n 66│  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n 67│  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n 68│  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n   │...\n 71│  operator as self-inflicted, NOT a legacy-substrate bug.\n*72│- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n 73│  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n*74│  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75│  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n 76│  overrides both to a `sptc-ci-` id and never emits a live id.\n 77│- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n*78│  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n*79│  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n 80│  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n 81│  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n 82│  dir is not part of the public surface, so isolation rides on identity.\n## OMP-BRIDGE-FIELD-ISSUES.md\n   5│\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n  *6│and could not be switched back to `claude-spt`. This documents the root causes,\n   7│grounded in this repo's code, so the bridge can be hardened.\n   8│\n  *9│All line references are against `tools/claude-spt/src/bridge.rs` and\n  10│`adapter/omp-spt.toml` at commit `f0109d1`.\n  11│\n  12│## Incident summary\n    │...\n  14│1. Operator started `todlando` under omp-spt (`[session.self]` →\n *15│   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n  16│2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n  17│   way to tell working vs. wedged.\n  18│3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n *19│   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n  20│   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n  21│   endpoint looked alive but was dead.\n *22│4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n  23│5. Starting fresh after switching the harness adapter still launched omp-spt.\n  24│\n  25│Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n    │...\n  30│\n *31│## HIGH-1 — Bridge binary basename collides with `claude-spt`\n  32│\n *33│**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n *34│indistinguishable from a genuine claude-spt process. During the incident this\n *35│made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n  36│todlando's perch was actually the omp bridge. It directly caused the\n  37│misdiagnosis.\n  38│\n    │...\n  40│```\n *41│command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n  42│```\n *43│The executable is named `claude-spt`, the exact basename of the upstream adapter.\n  44│This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n  45│bridge (S2) is `omp-spt`\"*).\n  46│\n    │...\n  58│object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n *59│`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n  60│`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n  61│When the broker or operator terminates the bridge process (the normal \"stop\"),\n  62│those lines never run. On Windows, killing a process does not kill its children,\n    │...\n 149│**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150│restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151│editing spt-core's session ledger by hand.\n 152│\n 153│**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n    │...\n 160│**Fix.** Provide (and document) a clean unbind/switch so an operator can move an\n*161│endpoint off omp-spt without ledger surgery. Ideally a failed bringup should not\n 162│leave a poisoning ledger row — bind only once the omp `ready` + `get_state`\n 163│handshake has actually succeeded (it already binds after `get_state`, but see\n 164│HIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).\n## OMP-OPENROUTER.md\n 18│    models:\n*19│      - id: anthropic/claude-sonnet-5\n*20│        name: Claude Sonnet 5 (OpenRouter)\n 21│        contextWindow: 200000\n 22│        maxTokens: 8192\n 23│      # add more OpenRouter model ids as needed; omp fuzzy-matches --model against them\n## PARITY.md\n *1│# Parity audit — claude-spt vs the legacy owl surface\n  2│\n  3│> The reconciliation record: the adapter's operative surface measured against the **LOCKED\n  4│> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **\"proven parity\"**\n  5│> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +\n *6│> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved\n  7│> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.\n  8│>\n  9│> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is\n   │...\n 28│| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |\n*29│| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |\n 30│| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |\n 31│| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸️ DEFER + remove stub |\n 32│| `/fork` | DROP | absent | ✅ confirmed absent |\n   │...\n 37│TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +\n*38│`shortcut_basename=\"cc\"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→\n 39│`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅\n 40│\n 41│## Open divergences and dispositions\n   │...\n 52│supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is\n*53│`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT\n 54│`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile\n 55│authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good\n 56│sister project **claude_skill_owl**:\n*57│- **Spawn seam.** `[profiles.ccs.session.self].command = \"ccs\"` leaf-replaces the base `claude`.\n*58│  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`\n 59│  Tier-2 PULSE recovery latches `cli_binary = \"ccs\"`); `SPT_ENDPOINT_ID` rides inherited env through\n 60│  the wrapper unchanged.\n 61│- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree\n*62│  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),\n 63│  a per-account runtime value with no static catalog path. SCOPE's \"per-profile `~/.ccs` log dir\"\n*64│  is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/\n 65│  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The\n 66│  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.\n 67│  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`\n## RELEASE-RUNBOOK.md\n   2│\n  *3│> How an spt-claude-code release ships. The spine is fixed (changelog · bump · regenerate\n   4│> docs · tag · publish · docs publish). **Release here is LIGHT: signing is DELEGATED to the\n  *5│> `spt` binary — there is NO two-key signing ceremony in this project.** spt-claude-code is a\n   6│> thin adapter; the heavy signed-release machinery lives in spt-core, not here.\n   7│\n   8│<!-- [<doc>->REQ-EXAMPLE-REL] (illustration; escaped per docs/TRACEABILITY.md) -->\n    │...\n  11│\n *12│An spt-claude-code release lands in **two places**, by volatility:\n  13│\n  14│1. **The `SaberMage/cplugs` marketplace** — the thin **skeleton plugin** (namespaced\n  15│   `/spt:*` skill skeletons, `hooks.json`, the SessionStart bootstrap, `plugin.json`). This\n    │...\n  42│   - **No internal lingo** — no requirement ids, internal module names, commit hashes, or\n *43│     milestone / hazard codes. A reader who has never seen the source must understand every line.\n  44│   - **Use spt nomenclature** (operator rule, 2026-07-03) — product terms (Psyche, live agent,\n  45│     perch, endpoint, node, subnet) ARE the user-facing language; never substitute an invented\n  46│     softer synonym (e.g. \"companion\" for Psyche). \"No internal lingo\" bans *implementation*\n    │...\n  80│     floor bump + changelog to match). For the ephemeral-psyche case the signature is the manifest\n *81│     table: `git show <release-HEAD>:adapter/claude-spt.toml | grep -q 'session.psyche_resume' &&\n  82│     echo \"PARKED WORK PRESENT — do not tag\" || echo \"clean\"`. Generalize per parked feature:\n  83│     grep the tag tree for the feature's defining seam.\n  84│   - **When a release DOES ship formerly-parked work**, it is deliberate: the `min_spt_core` floor\n    │...\n 103│The concrete cplugs marketplace steps, captured from the sister project's\n*104│`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs\n 105│target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart\n 106│bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest\n 107│ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so\n    │...\n 122│# Clone the marketplace repo\n*123│git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs\n*124│# Register a \"cplugs\" entry in ~/.claude/plugins/known_marketplaces.json\n 125│#   source: { source: \"github\", repo: \"SaberMage/cplugs\" }\n 126│```\n 127│\n    │...\n 133│# 2. Copy skeleton files (NO binary) into the marketplace clone:\n*134│MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc\n 135│cp -r skills/*  \"$MARKET/skills/\"\n 136│cp -r hooks/*   \"$MARKET/hooks/\"\n*137│cp .claude-plugin/plugin.json \"$MARKET/.claude-plugin/\"\n 138│# 3. Commit + push the cplugs repo:\n*139│cd ~/.claude/plugins/marketplaces/cplugs && git add plugins/sptc/ \\\n 140│  && git commit -m \"sptc: <change>\" && git push\n 141│```\n 142│\n    │...\n 145│```bash\n*146│claude plugin install sptc@cplugs   # un-orphans + rewrites installed_plugins.json atomically\n*147│# then, inside Claude Code:\n 148│/reload-plugins\n 149│```\n 150│\n    │...\n 152│- **Never hand-patch `installed_plugins.json`** (`jq`/`sed` are brittle; CC may silently reject).\n*153│  Use the `claude plugin install` CLI — it's the atomic pointer flip + orphan-marker cleanup.\n*154│- **No legacy manual-install dir alongside the marketplace install.** A `~/.claude/plugins/<name>/`\n 155│  regular dir coexisting with the marketplace cache install makes `/plugin` report the plugin\n 156│  **not installed** (CC enumerates both; the untracked one wins the conflict). Keep the two exclusive.\n*157│- **Restart the Claude Code session after install** so the SessionStart hook re-runs and repopulates\n 158│  the plugin's env (`$OWL`/`$LIVE` equivalents) in Bash subprocesses.\n 159│\n 160│**Explicitly NOT ours** (owl ships a live self-migrating binary; the thin skeleton has none, so\n    │...\n 173│The CC adapter manifest (publish target #2) ships to end users as an **`adapter.spt`** GitHub\n*174│**release asset** on this monorepo, acquired with **`spt adapter add --release SaberMage/claude-spt`**\n 175│(doyle's `--release` source; needs **spt v0.7.3+ / counter 15** — not in 0.7.2). No dedicated\n 176│adapter repo: the asset is packed straight from `adapter/`.\n 177│\n    │...\n 184│  archive needs it), requires **both** platforms' built tool binaries, and tars the archive **ROOT** =\n*185│  `manifest.toml` (renamed from `claude-spt.toml` — `adapter add` is root-only + exact-name) +\n 186│  `strings/` **shared at root**, plus each recognized target-triple's binaries under a `<triple>/` dir.\n 187│  Never uploads (operator's step).\n 188│<!-- [doc->REQ-DIST-ADAPTER-PEROS] -->\n    │...\n 191│  archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install\n*192│  dir**, so the bare-name command token (`claude-spt` — its digest/psyche/post-update/translate subcommands) still resolves at `<install_dir>/`\n 193│  (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root\n 194│  entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform\n 195│  beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.\n    │...\n 197│  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →\n*198│    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate\n 199│    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)\n 200│  - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target\n 201│    x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the\n## SKILL-AUTHORING.md\n  7│\n *8│## What a skill body must NOT contain\n  9│\n 10│- **Plugin / spt-core internals.** No adapter resolution (`--adapter`, parent-pid / `host_binaries`\n 11│  matching), no perch-state stamping (`state=live_agent`), no daemon/poll-vs-relay coordination, no\n## SPT-CORE-FINDINGS.md\n  20│| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:\"host not resident within 5s ...\", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit ...\n *21│| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises \"with substitution\"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + ...\n *22│| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os...\n  23│| F-016 | 2026-06-22 | **RESOLVED — both fixes SHIPPED** (doyle, broker.rs-confirmed). The published `[message-idle-translation-binary]` doc omitted `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **(i) adapter bi...\n *24│| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bo...\n *25│| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails...\n *26│| F-020 | 2026-06-24 | **RESOLVED-PENDING — frozen Tier-1 contract, ships v0.15.0 W5** (doyle ruled + grounded; verb committed by todlando @20bfc1f). spt-core publishes NO `spt api` verb to pull resume/psyche context at SessionStart — `download_psyche_context` (resume.rs:88) is exported-but-unexposed (ZERO `ApiCmd` callers); `resume.rs:9` documents the \"adapter pulls it in its SessionStart hook\" intent but no verb was ever published. **Consequence: claude-spt live agents rehydrate NO durable context ...\n  27│\n  28│| F-023 | 2026-07-01 | **CONFIRMED + RCA'd (doyle, same day) — build dispatched to todlando (`BUILD-F023-WANIDLE`)**. Both ranked hypotheses real: `receive_wan` has deliver_tcp + spool ONLY (the spt-hosted broker-inject leg exists solely in local `cmd_send`) AND no idle-edge drain exists anywhere. Fix = shared inject leg called from WAN ingress + ACTIVE→IDLE spool drain. Adapter exoneration stands verbatim. `from_id=\"\"` sibling ruled: compose stamps `cli@<node-label>`; ingress renders empty-from as o...\n  29│| F-024 | 2026-07-01 | **CLOSED-REFRAMED (doyle, same day) — transport EXONERATED; symptom = F-023 on the remote node.** The `(spooled)` token in `SENT(WAN)` output is RECEIVER-CONFIRMED (only prints on a confirmed round-trip): probe `DIAG-7c1e` reached ENLYZEAM post-restart and sits in ball-b's spool undelivered (`spooled` not `delivered` = no live TCP listener at receive time). Operator workaround: any prompt typed into ball-b UPS-drains the stuck messages. Residues stay real as separate diagnostics ...\n *30│| F-031 | 2026-07-08 | **REPORTED to doyle (perri) — docs gap; adapter worked around in v0.17.3 (REQ-HAZARD-SEND-STATUS-STDERR).** `spt send`'s result token (`SENT:<id>` / `QUEUED:<id>` / `DEFERRED:<id>` / `NO_PERCH:<reason>`) is written to **STDERR**, not stdout — stdout is empty on a plain `spt send`. This is undocumented in the published harness-contract / CLI reference (they describe the tokens as the send's *result* without naming the stream). It bit the claude-spt tag-messaging hook: the hook c...\n  31│\n *32│> **F-021 / F-022 (NOT spt-core findings — claude-spt parity items, tracked as REQs; surfaced during the 2026-06-24 checkpoint-commune grill)** — (F-021) claude-spt polls only on `UserPromptSubmit` (between turns); legacy spt also polls+injects on **PreToolUse** = the mid-turn half of live-agent reachability (the main path by which a live agent receives a message WHILE working). `api poll` already exists on the public surface → adapter wiring, not an spt-core gap → **`REQ-DIST-PRETOOL-POLL`**. (F...\n  33│\n  34│> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as sp...\n  35│\n    │...\n  37│\n *38│## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter\n  39│\n  40│**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.\n  41│\n    │...\n  60│   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the\n *61│   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author\n  62│   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:\n *63│   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side\n  64│   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills\n  65│   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption \"the plugin ships a `hooks.json`\n  66│   calling `spt api`\" — it may instead be manifest-declared + spt-materialized.)\n    │...\n  85│  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`\n *86│  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle\n  87│  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /\n  88│  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit\n  89│  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.\n    │...\n 109│\n*110│Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary\n 111│(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the\n 112│residual: the **published docs still omit** two things a manifest author needs, both learnable only\n 113│from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:\n    │...\n 129│   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with\n*130│   `source = \"{home}/.claude/projects\"` — CC's cwd-slug subdir is not expressible as a flat\n 131│   `locate_template`, so `source` names the per-project root and the extractor finds\n 132│   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less\n 133│   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);\n    │...\n 146│**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has\n*147│raised the framing as a contract decision to the operator.\n 148│\n 149│**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained\n 150│message with `print!(\"{msg}\")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**\n    │...\n 254│$ spt adapter add <dir>\n*255│#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative\n 256│#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered\n 257│```\n 258│\n    │...\n 284│\n*285│**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor.\n 286│\n 287│**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample\n*288│path) into the extractor command and hard-fails on any other key. The claude-spt extractor command\n 289│is the production-correct, published-example shape:\n 290│\n 291│```\n*292│extractor = \"claude-spt-digest --session {session_id} --in {source}\"\n 293│```\n 294│\n*295│→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.\n 296│\n 297│**Isolation (same adapter, only the command line differs):**\n 298│- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,\n    │...\n 302│\n*303│**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**\n## TRACEABILITY.md\n   2│\n  *3│> How spt-claude-code uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs)\n   4│> to keep every requirement traced from doc → impl → test. The manifest is\n   5│> `traceable-reqs.toml` (seed it from your requirements source + your KNOWN-HAZARDS\n   6│> invariants). This contract makes the trace pay off instead of rotting.\n    │...\n  26│   one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.\n *27│4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring\n *28│   `unit` (and `int` where cross-process / cross-node). A hazard cannot be \"covered\" without a\n  29│   test tagging it — the anti-regression promise becomes mechanical.\n  30│5. **Activation, not premature failure.** Every requirement starts `required_stages = []`\n  31│   (inactive) so `check` stays green pre-code. A **milestone activates** the requirements it\n    │...\n  53│```rust\n *54│// [<unit>->REQ-HAZARD-EXAMPLE]\n  55│#[test] fn upholds_the_invariant() { ... }\n  56│```\n  57│```markdown\n    │...\n  72│   `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check`\n *73│   before declaring done. If this project is built *with* agents, the contract must be\n  74│   machine-followable — agent discipline + CI as the net.\n  75│4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review`\n  76│   + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run\n    │...\n 105│- **First milestone:** install the CLI, validate the seed against it, wire the CI gate,\n*106│  **activate that milestone's reqs** (and the `REQ-HAZARD-*` invariants it must uphold), tag as\n 107│  code lands.\n 108│- **Each later milestone:** activates + covers its requirements before it's called done.\n\n## adr/\n### 0000-template.md\n *1│# <Title — the decision stated as its outcome>\n  2│\n *3│<!-- Title states the decision, not the question. e.g. \"Sessions are pinned at first bind\",\n *4│     not \"How should sessions bind?\". Number files sequentially: 0001-, 0002-, … -->\n  5│\n  6│## Status\n  7│\n   │...\n  9│proposed (`<YYYY-MM-DD>`)\n*10│<!-- e.g. accepted (2026-01-15) · supersedes [ADR-NNNN](NNNN-old-decision.md) -->\n 11│\n 12│## Context\n 13│\n*14│<!-- The forces at play: what made this decision necessary, the constraints, the real\n 15│     problems (numbered if several). Enough that a reader who wasn't there understands WHY.\n 16│     Reference CONTEXT.md terms by their canonical names. -->\n 17│\n*18│## Decision\n 19│\n 20│<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,\n 21│     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->\n### 0001-distribution-splits-by-volatility.md\n  12│\n *13│`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n *14│its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n *15│invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n  16│inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n  17│plugin author.\n  18│\n    │...\n  24│   marketplace or letting logic rot.\n *25│2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n  26│   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n  27│   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n  28│   need to ride the marketplace at all.\n *29│3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under\n *30│   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills\n *31│   must stay **plugin-provided**.\n  32│4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed\n *33│   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.\n  34│\n *35│## Decision\n  36│\n  37│Split the adapter across **three layers by volatility** (see `SCOPE.md` §\"Distribution\n  38│architecture\"):\n    │...\n  65│\n *66│- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,\n  67│  collision-prone). Rejected.\n  68│- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:\n  69│  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen\n    │...\n  97│  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n *98│  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n  99│  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100│  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101│- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n*102│  `claude plugin update` (skeleton sync only).\n 103│<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n 104│- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105│  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106│  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n 107│  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108│  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109│- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n    │...\n 111│  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112│  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n 113│  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114│  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n 115│  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n  12│primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n *13│formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n  14│payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n *15│public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n  16│(the adapter_name — distinct from the plugin name `sptc`).\n  17│\n  18│Two hard constraints shape the wiring:\n    │...\n  22│   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n *23│2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n  24│   L52/L181. The plugin hand-writes it.\n  25│\n *26│## Decision\n  27│\n  28│**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n *29│`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n  30│(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n  31│substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n  32│\n    │...\n  34│\n *35│| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n  36│|---|---|---|\n  37│| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n  38│| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n    │...\n  43│| SessionEnd | `session-end <id>` | soft teardown; `shutdown <id>` on graceful signoff |\n *44│| PreToolUse | — | **out of scope v1** (UPS covers delivery) |\n  45│\n  46│`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n  47│**not** from a hook.\n    │...\n  88│\n *89│Handlers must map CC stdin JSON → `api` flags. The plugin ships **no binary**, so the mapping is a\n  90│thin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is\n  91│settled during impl (see Open).\n  92│\n    │...\n  99│- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n*100│  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n 101│- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n 102│  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n 103│  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n    │...\n 108│\n*109│## Validation results (throwaway `claude -p` session, 2026-06-15)\n 110│\n 111│Ran an isolated temp-project rig (UserPromptSubmit marker hook + a registered `/send` project\n 112│skill) on the real CC 2.1.177 binary:\n 113│\n*114│- ✅ **UPS fires on a `/`-slash-command.** `MSYS_NO_PATHCONV=1 claude -p \"/send hi\"` → the hook\n 115│  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n 116│  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n 117│  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n    │...\n 124│  `C:/Program Files/Git/send`. Test artifact (wrappers read stdin, not argv) but a real Windows\n*125│  hazard → `docs/KNOWN-HAZARDS.md` 1.1 + `REQ-HAZARD-MSYS-PATHCONV` (test: `tests/msys-hazard.sh`).\n 126│\n 127│## Open / resolved `int`\n 128│\n    │...\n 138│   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n*139│   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n 140│   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n 141│   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n 142│   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n  1│<!-- [doc->REQ-SKILL-LIVE] -->\n *2│<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n  3│# The Psyche runs as a constrained, auto-approving companion (legacy owl parity)\n  4│\n  5│## Status\n   │...\n 10│\n*11│A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\n*12│hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\n*13│subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\n*14│the runner seeds one headless `claude` turn from the daemon-supplied prompt,\n*15│then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\n 16│on each pulse.\n 17│\n 18│Forces:\n   │...\n 27│   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.\n*28│3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;\n 29│   leaving the model unpinned lets it inherit whatever the parent uses.\n 30│4. **A proven reference exists.** The legacy sibling `claude_skill_owl`\n*31│   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final\n 32│   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe\n 33│   default, not a novel design.\n 34│\n*35│## Decision\n 36│\n*37│Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\n*38│legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n 39│`pulse_cmd`:\n 40│\n 41│- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n   │...\n 48│The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n*49│`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\n*50│the flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\n*51│recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n 52│\n*53│Rejected: launching the Psyche as a bare, unconstrained `claude` (the pre-2026-06-18 state). It gave\n 54│the unattended companion the full toolset *and* left it without skip-permissions — broader blast\n 55│radius and a latent detached-deadlock at once.\n 56│\n   │...\n 62│- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination\n*63│  with the tool cap. The two must move together — never widen the tools without re-justifying the\n 64│  auto-approve, and never drop the cap while keeping skip-permissions.\n*65│- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66│  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67│  sibling, and surfaces loudly (no commune produced).\n*68│- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n 69│  (the deadlock invariant). `unit` evidence is the\n 70│  `every_turn_is_sandboxed_to_legacy_owl_parity` test plus the manifest assertion on the two\n 71│  `[session.self]` commands.\n### 0004-checkpoint-via-self-send-loopback.md\n 16│   `stdin`-driven (`init`/`event`/`input` lines from spt-core) with **no filesystem or commune\n*17│   knowledge** — it cannot read `.claude/<id>-commune.md` to detect a trigger. The daemon is the\n*18│   single-writer that ingests+deletes the drop (`REQ-HAZARD-DROP-FILE-SINGLE-WRITER`), so any\n 19│   adapter-side reader races it (TOCTOU).\n 20│2. **Only IDLE delivery routes to the binary.** While the agent is mid-turn, messages route to the\n*21│   poll/hook channel (additionalContext), not the binary. A trigger must reach the binary *as an idle\n 22│   delivery* to fire the macro.\n 23│3. **Re-seed staleness.** If the commune is authored *after* `/clear` (the boundary/resume-Self\n 24│   refresh path), the legacy `psyche-stale-after-clear` gap reappears (LLM-latency-bounded): the\n   │...\n 33│\n*34│## Decision\n 35│\n 36│Checkpoint is driven by a **self-sent loopback message**, with the commune **authored inline\n 37│pre-clear**:\n   │...\n 56│defers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale\n*57│gap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.\n 58│\n 59│**Rejected alternatives:**\n 60│- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's\n### 0005-name-unification.md\n  1│<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n *2│# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n  3│\n  4│## Status\n  5│\n *6│accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n *7│`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n  8│\n  9│## Context\n 10│\n   │...\n 14│|---|---|---|\n*15│| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n*16│| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n*17│| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n 18│| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n 19│\n*20│Three names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The ski...\n 21│\n*22│## Decision\n 23│\n 24│Collapse three names to **two**, by domain:\n 25│\n*26│- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27│- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n 28│- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29│\n*30│End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n 31│\n 32│## Considered and rejected\n 33│\n*34│- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n 35│- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n 36│\n 37│## Consequences\n 38│\n*39│- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n 40│- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41│- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n  6│\n *7│**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n  8│dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n  9│**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n 10│`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\n 11│let the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\n*12│in `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n*13│`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\n*14│hook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\n 15│hook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n 16│<!-- [doc->REQ-DIST-HOOK-BINARY] -->\n 17│See `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n   │...\n 20│\n*21│ADR-0001 split distribution into a low-churn **cplugs skeleton** (hooks, skill skeletons, `plugin.json`, bootstrap — updated by `claude plugin update`) and an spt-conducted **adapter** (manifest + binaries + strings — updated by `spt adapter update`). The operator wants the whole thing kept current with effectively **one lever**, and wants install and update to feel **harmonious** (no hunting for the update command after install) and to work from the **CLI** (not only inside an endpoint session).\n 22│\n 23│Two facts bound the design:\n 24│\n*25│1. **Hook *logic* must live in the CC plugin dir** (CC loads hooks from there), outside `spt adapter update`'s reach. So \"one command updates everything\" is unreachable purely inside this repo — it needs spt-core capabilities → findings, not workarounds (public-surface-only).\n*26│2. **Raw skills/commands can't reliably replace the plugin.** Reopened and re-rejected: user-scope skills are flat (no namespace), and CC slash-command subdirectory namespacing is broken ([anthropics/claude-code#2422] — subdirs show in the description but don't namespace the invocation). The plugin is the only reliable `/spt:*` + the only clean hooks bundle. The eloquent thing to borrow from suite installers (e.g. gsd-core) is their **installer/onboarding UX**, not going raw.\n 27│\n*28│## Decision\n 29│\n 30│**Keep the plugin** (reaffirms ADR-0001). Make the update path one-lever-shaped and consolidate the adapter's binaries.\n 31│\n   │...\n 34│- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n*35│- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n 36│- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37│- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38│\n 39│### Binary consolidation\n 40│\n*41│Collapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n 42│\n*43│- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44│- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45│\n*46│The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47│\n 48│### The spt-core asks (findings to doyle — decided in shape, gated on delivery)\n 49│\n 50│1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.\n*51│2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **\"changed\"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.\n 52│3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.\n 53│\n 54│## Consequences","truncation":{"content":"# CONTEXT.md\n*1|# spt-claude-code — glossary\n 2|\n*3|> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n 4|> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n 5|\n*6|**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n 7|adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n 8|parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n 9|installer. Built by maintainer perri from the public spt-releases surface only.\n*10|(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n 11|[[naming-domains]].)\n 12|\n*13|**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n 14|invocation + the manifest). The CC adapter's identity inside spt-core. The\n 15|**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n 16|\n*17|**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n*18|spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n*19|**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n 20|`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n 21|`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n 22|shortcut (`cc-<id>`) is decoupled from both.\n...\n 38|that already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that\n*39|its perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint\n 40|roster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity\n 41|piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\n 42|flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n...\n 50|\n*51|**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n 52|that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n*53|NOT its own adapter — ccs is structurally Claude Code.\n 54|\n 55|**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n 56|spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n...\n 64|exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65|profile. (Realized here by the `claude-spt-psyche` runner.)\n 66|\n 67|**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68|parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n...\n 91|\n*92|**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:\n 93|Read/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap\n 94|pinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors\n 95|legacy owl's psyche box; see `docs/adr/0003-*`.\n...\n 97|**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n*98|adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n 99|users: install the plugin, get spt-core for free.\n 100|\n 101|**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102|extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103|traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104|same-repo published-docs. Defined in its own folder, not here.\n 105|\n...\n 115|turn: the session digest records a turn whose input is the literal `[Request interrupted by\n*116|user]` (the normalized form of the TUI's \"Interrupted · What should Claude do instead?\"). The\n 117|sole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`\n 118|hook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the\n 119|fetcher re-reads the transcript), never daemon-pushed.\n\n# SCOPE.md\n*1|# spt-claude-code — scoping decision ledger\n 2|\n 3|> Running source of truth for the scoping session (started 2026-06-13, doyle).\n 4|> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n...\n 9|\n*10|**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n*11|`claude-spt` adapter) — simultaneously:\n 12|1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n 13|   delegating all core to `spt.exe`).\n 14|2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n...\n 30|\n*31|**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n 32|in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n 33|\n 34|| Layer | Contents | Home | Update path | Churn |\n 35||---|---|---|---|---|\n*36|| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n 37|| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n 38|| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n 39|\n 40|- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n*41|  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n 42|- The \"bulk\" spt-core installs/updates = **manifest + binary**, not the skills.\n 43|- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.\n 44|- → ADR-0001 (to write).\n...\n 53|  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n*54|  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n 55|  empirically at build time**, but design on it.\n 56|- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n 57|  manifest doesn't bloat.\n...\n 65|SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n*66|(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n 67|\n 68|## ccs integration\n 69|\n*70|**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n*71|`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n 72|model/billing backend differ, which is exactly the profile seam (leaf-replace\n 73|spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n 74|leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n 75|profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n*76|Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n 77|not only bare adapter_name (profile model already supports composite addressing).\n 78|\n 79|## `/spt:setup` skill\n...\n 99|full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100|`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n*101|flag if verify should fold inline._\n 102|\n 103|### `cc` launcher (capsule-style) — **LOCKED (design)**\n 104|\n...\n 125|- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.\n*126|- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the\n 127|  **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.\n 128|- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods\n 129|  the product as its own CI nervous system.\n*130|- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131|  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132|  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133|  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n...\n 135|  gates → reports over spt. Manual \"run gates\" = fallback. (Polling rejected: latency + waste.)\n*136|  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137|    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n*138|    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139|- This CI pattern → reusable, goes in `experimplate`.\n 140|\n 141|## Parity-trim (→ REQ seed / acceptance bar)\n...\n 153|**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n*154|harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n 155|\n 156|**KEEP — FINALIZED** (post-investigation 2026-06-14):\n 157|- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`\n...\n 173|**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174|spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175|`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176|\n 177|## spt-core upstream deps\n\n# OMP-ADAPTER-PLAN.md\n*1|# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n 2|\n*3|> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n 4|> **oh-my-pi (omp)** — the terminal coding agent at\n 5|> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n 6|> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n...\n 16|\n*17|claude-spt exists because Claude Code has **no control protocol** — the adapter\n 18|reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n 19|`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n 20|digest, and pushes received messages back via `additionalContext`. Most of its\n...\n 46|\n*47|### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n 48|\n 49|**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n*50|spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n 51|runs `omp --mode rpc` as a child, and translates in both directions:\n 52|\n 53|```\n...\n 70|\n*71|Rationale, in one line: **the process whose UI the operator must see must own the\n 72|PTY.** See ADR-0007. <!-- [doc->REQ-OMP-NATIVE-TUI] -->\n 73|\n*74|## Seam map — claude-spt value → omp-spt target\n 75|\n*76|| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77||---|---|---|\n*78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n*79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n*83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n*84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n*86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n 87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n...\n 110|\n*111|## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112|\n*113|- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114|  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115|  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116|- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117|  (bridge command), `[identity]`. Prove with\n 118|  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119|  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120|- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121|  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122|  Acceptance: from another node, message the perch → get an omp answer; assert\n 123|  `spt` state transitions (isolated CI identity per tips doc §Testing).\n...\n 143|    models:\n*144|      - id: anthropic/claude-sonnet-5\n*145|        name: Claude Sonnet 5 (OpenRouter)\n 146|        contextWindow: 200000\n 147|        maxTokens: 8192\n 148|```\n\n# docs/\n## CI.md\n 2|\n*3|> spt-claude-code's CI is **wholly agent-driven and autonomous** on the existing Windows +\n 4|> Linux fleet (hfenduleam + kitsubito). **GitHub-hosted runners are NOT used** — a stock\n*5|> hosted runner physically can't run Claude Code (auth + interactivity), so only a real\n 6|> harness on the fleet reaches the acceptance bar. **Deterministic gates** run by a fleet\n 7|> runner-agent, triggered by a git post-push hook, reporting over **spt messaging**. No LLM\n 8|> sits in the gate path — the gates are plain scripts; the \"agent\" is just the autonomous\n...\n 27|5. **Docs-drift** — generated docs (API reference, `llms.txt`, schema, CLI help) are regenerated\n*28|   and must match what's checked in; a diff fails (see `docs/DOCS-STRATEGY.md`).\n 29|\n*30|## Acceptance (the system-under-test is a real `claude` session)\n 31|\n*32|Acceptance is **scripted orchestration that spawns real `claude` / headless sessions as the\n 33|system-under-test**, then asserts spt-state / `[digest]` output. The **LLM is the SUT, never\n 34|the runner** — the orchestration is deterministic; only the thing it drives is a real harness.\n 35|This is what a stock hosted runner can't do, and why the fleet is mandatory. (spt-core's\n...\n 38|\n*39|**Disposable-identity invariant (mandatory).** Every nested `claude` the harness spawns runs\n 40|under a **throwaway perch id** (`SPT_AGENT_ID=sptc-ci-<n>`), never a live agent's name. A nested\n 41|session that resolves a live id establishes a perch under that name and — perches being\n 42|name-keyed, last-establish-wins — **tears down the live agent's perch and poll stream** (the\n 43|self-inflicted collision diagnosed 2026-06-15; see `docs/KNOWN-HAZARDS.md` §2.1 /\n*44|`REQ-HAZARD-PERCH-COLLISION`). `ci/acceptance/lib.sh` `sptc_ci_identity` enforces it.\n 45|\n*46|**Slice 1** (`ci/acceptance/run-acceptance.sh`) asserts a real `claude` **fires the\n 47|UserPromptSubmit hook** via a hook-written digest marker — the harness-contract entry point\n 48|working inside a real harness — independent of the spt bus or model text. It is **env-gated**\n*49|(`SPTC_ACCEPTANCE=1`) so the deterministic gate run stays green on hosts without `claude`/auth.\n 50|Bus-delivery acceptance (poll→additionalContext over real spt) is a later slice, gated on\n 51|`REQ-MSG-ENVELOPE`.\n 52|\n...\n 77|The bus binary's path **changes between versions** — legacy spt lives in a per-version\n*78|`~/.claude/` or `~/.ccs/` plugins folder whose path moves each release. No further\n*79|legacy-spt changes are anticipated, but the hook and runner **must locate it robustly**:\n 80|resolve it at run time (search the known plugins/install roots / a configured path / `PATH`)\n 81|rather than hard-coding a versioned location. A stale hard-coded path is the most likely cause\n 82|of a silently dead trigger.\n...\n 85|\n*86|A **manual \"run gates\" command** must always exist — the same gate scripts, runnable by hand on\n 87|any fleet host. Use it when the hook didn't fire, when reproducing a failure, or before a hook is\n 88|wired at all.\n 89|\n## DOCS-STRATEGY.md\n 31|   alongside each `.html`, or `Accept: text/markdown`) which cuts agent token use ~90% vs HTML.\n*32|5. **One canonical way to do X** — explicitly mark deprecated / alternate paths. Non-determinism\n 33|   is fatal for agents.\n 34|6. **Complete reference, auto-generated, all error variants** — generate API reference from the\n 35|   code for your public surface, plus any machine-readable contract/schema. Generic placeholders\n...\n 45|    <!-- [doc->REQ-DOCS-DRIFT] The drift gate (ci/docs/check-docs.sh) regenerates llms.txt\n*46|         deterministically and fails the build on any diff vs the committed file; the book must\n 47|         also build. This is the structural anti-drift commitment, enforced in run-gates. -->\n 48|\n 49|## Information architecture — by capability vertical\n## KNOWN-HAZARDS.md\n 2|\n*3|> A **conformance checklist, not advice.** Each hazard below is a first-class\n*4|> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n 5|> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n 6|> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n*7|> and `traceable-reqs check` will say so once the hazard is activated.\n 8|\n*9|A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\n 10|incident) or one you have *committed never to introduce*. State it so a test can prove it.\n 11|\n 12|## Entry format\n...\n 16|- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n*17|- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n*18|  `REQ-HAZARD-*` requires).\n 19|- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n 20|  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n 21|- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n...\n 27|[[requirements]]\n*28|id = \"REQ-HAZARD-EXAMPLE\"\n 29|title = \"The invariant, stated so a test can prove it\"\n 30|required_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n 31|```\n...\n 40|  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n*41|  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n*42|  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n 43|  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n 44|  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n*45|  documents the identical hazard in `new-alarm`.)\n*46|- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n 47|  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n*48|  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n 49|  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n 50|- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n...\n 54|- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n*55|  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n 56|\n 57|---\n 58|\n...\n 60|\n*61|### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n 62|\n*63|<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n 64|\n*65|- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n 66|  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n 67|  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n 68|  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n...\n 71|  operator as self-inflicted, NOT a legacy-substrate bug.\n*72|- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n 73|  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n*74|  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75|  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n 76|  overrides both to a `sptc-ci-` id and never emits a live id.\n 77|- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n*78|  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n*79|  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n 80|  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n 81|  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n 82|  dir is not part of the public surface, so isolation rides on identity.\n## OMP-BRIDGE-FIELD-ISSUES.md\n 5|\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n*6|and could not be switched back to `claude-spt`. This documents the root causes,\n 7|grounded in this repo's code, so the bridge can be hardened.\n 8|\n*9|All line references are against `tools/claude-spt/src/bridge.rs` and\n 10|`adapter/omp-spt.toml` at commit `f0109d1`.\n 11|\n 12|## Incident summary\n...\n 14|1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15|   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16|2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17|   way to tell working vs. wedged.\n 18|3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19|   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20|   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21|   endpoint looked alive but was dead.\n*22|4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23|5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24|\n 25|Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n...\n 30|\n*31|## HIGH-1 — Bridge binary basename collides with `claude-spt`\n 32|\n*33|**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n*34|indistinguishable from a genuine claude-spt process. During the incident this\n*35|made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n 36|todlando's perch was actually the omp bridge. It directly caused the\n 37|misdiagnosis.\n 38|\n...\n 40|```\n*41|command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42|```\n*43|The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44|This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n 45|bridge (S2) is `omp-spt`\"*).\n 46|\n...\n 58|object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n*59|`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n 60|`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n 61|When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62|those lines never run. On Windows, killing a process does not kill its children,\n...\n 149|**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150|restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151|editing spt-core's session ledger by hand.\n 152|\n 153|**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n...\n 160|**Fix.** Provide (and document) a clean unbind/switch so an operator can move an\n*161|endpoint off omp-spt without ledger surgery. Ideally a failed bringup should not\n 162|leave a poisoning ledger row — bind only once the omp `ready` + `get_state`\n 163|handshake has actually succeeded (it already binds after `get_state`, but see\n 164|HIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).\n## OMP-OPENROUTER.md\n 18|    models:\n*19|      - id: anthropic/claude-sonnet-5\n*20|        name: Claude Sonnet 5 (OpenRouter)\n 21|        contextWindow: 200000\n 22|        maxTokens: 8192\n 23|      # add more OpenRouter model ids as needed; omp fuzzy-matches --model against them\n## PARITY.md\n*1|# Parity audit — claude-spt vs the legacy owl surface\n 2|\n 3|> The reconciliation record: the adapter's operative surface measured against the **LOCKED\n 4|> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **\"proven parity\"**\n 5|> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +\n*6|> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved\n 7|> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.\n 8|>\n 9|> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is\n...\n 28|| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |\n*29|| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |\n 30|| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |\n 31|| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸️ DEFER + remove stub |\n 32|| `/fork` | DROP | absent | ✅ confirmed absent |\n...\n 37|TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +\n*38|`shortcut_basename=\"cc\"`); echo-commune→`[digest]` extractor (`claude-spt digest`); psyche-sync→\n 39|`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅\n 40|\n 41|## Open divergences and dispositions\n...\n 52|supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is\n*53|`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT\n 54|`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile\n 55|authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good\n 56|sister project **claude_skill_owl**:\n*57|- **Spawn seam.** `[profiles.ccs.session.self].command = \"ccs\"` leaf-replaces the base `claude`.\n*58|  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`\n 59|  Tier-2 PULSE recovery latches `cli_binary = \"ccs\"`); `SPT_ENDPOINT_ID` rides inherited env through\n 60|  the wrapper unchanged.\n 61|- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree\n*62|  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),\n 63|  a per-account runtime value with no static catalog path. SCOPE's \"per-profile `~/.ccs` log dir\"\n*64|  is therefore honored **in `claude-spt digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/\n 65|  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The\n 66|  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.\n 67|  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`\n## RELEASE-RUNBOOK.md\n 2|\n*3|> How an spt-claude-code release ships. The spine is fixed (changelog · bump · regenerate\n 4|> docs · tag · publish · docs publish). **Release here is LIGHT: signing is DELEGATED to the\n*5|> `spt` binary — there is NO two-key signing ceremony in this project.** spt-claude-code is a\n 6|> thin adapter; the heavy signed-release machinery lives in spt-core, not here.\n 7|\n 8|<!-- [<doc>->REQ-EXAMPLE-REL] (illustration; escaped per docs/TRACEABILITY.md) -->\n...\n 11|\n*12|An spt-claude-code release lands in **two places**, by volatility:\n 13|\n 14|1. **The `SaberMage/cplugs` marketplace** — the thin **skeleton plugin** (namespaced\n 15|   `/spt:*` skill skeletons, `hooks.json`, the SessionStart bootstrap, `plugin.json`). This\n...\n 42|   - **No internal lingo** — no requirement ids, internal module names, commit hashes, or\n*43|     milestone / hazard codes. A reader who has never seen the source must understand every line.\n 44|   - **Use spt nomenclature** (operator rule, 2026-07-03) — product terms (Psyche, live agent,\n 45|     perch, endpoint, node, subnet) ARE the user-facing language; never substitute an invented\n 46|     softer synonym (e.g. \"companion\" for Psyche). \"No internal lingo\" bans *implementation*\n...\n 80|     floor bump + changelog to match). For the ephemeral-psyche case the signature is the manifest\n*81|     table: `git show <release-HEAD>:adapter/claude-spt.toml | grep -q 'session.psyche_resume' &&\n 82|     echo \"PARKED WORK PRESENT — do not tag\" || echo \"clean\"`. Generalize per parked feature:\n 83|     grep the tag tree for the feature's defining seam.\n 84|   - **When a release DOES ship formerly-parked work**, it is deliberate: the `min_spt_core` floor\n...\n 103|The concrete cplugs marketplace steps, captured from the sister project's\n*104|`claude_skill_owl/docs/DEPLOY.md`. **Take only the SKELETON SUBSET.** spt-claude-code's cplugs\n 105|target is the **thin skeleton** — `/spt:*` skill skeletons, `hooks.json`, the SessionStart\n 106|bootstrap, `plugin.json`. **No binary, no manifest in cplugs** — the binary + adapter manifest\n 107|ride the **spt-core adapter registry** (spt-conducted; the *other* publish target above), so\n...\n 122|# Clone the marketplace repo\n*123|git clone https://github.com/SaberMage/cplugs.git ~/.claude/plugins/marketplaces/cplugs\n*124|# Register a \"cplugs\" entry in ~/.claude/plugins/known_marketplaces.json\n 125|#   source: { source: \"github\", repo: \"SaberMage/cplugs\" }\n 126|```\n 127|\n...\n 133|# 2. Copy skeleton files (NO binary) into the marketplace clone:\n*134|MARKET=~/.claude/plugins/marketplaces/cplugs/plugins/sptc\n 135|cp -r skills/*  \"$MARKET/skills/\"\n 136|cp -r hooks/*   \"$MARKET/hooks/\"\n*137|cp .claude-plugin/plugin.json \"$MARKET/.claude-plugin/\"\n 138|# 3. Commit + push the cplugs repo:\n*139|cd ~/.claude/plugins/marketplaces/cplugs && git add plugins/sptc/ \\\n 140|  && git commit -m \"sptc: <change>\" && git push\n 141|```\n 142|\n...\n 145|```bash\n*146|claude plugin install sptc@cplugs   # un-orphans + rewrites installed_plugins.json atomically\n*147|# then, inside Claude Code:\n 148|/reload-plugins\n 149|```\n 150|\n...\n 152|- **Never hand-patch `installed_plugins.json`** (`jq`/`sed` are brittle; CC may silently reject).\n*153|  Use the `claude plugin install` CLI — it's the atomic pointer flip + orphan-marker cleanup.\n*154|- **No legacy manual-install dir alongside the marketplace install.** A `~/.claude/plugins/<name>/`\n 155|  regular dir coexisting with the marketplace cache install makes `/plugin` report the plugin\n 156|  **not installed** (CC enumerates both; the untracked one wins the conflict). Keep the two exclusive.\n*157|- **Restart the Claude Code session after install** so the SessionStart hook re-runs and repopulates\n 158|  the plugin's env (`$OWL`/`$LIVE` equivalents) in Bash subprocesses.\n 159|\n 160|**Explicitly NOT ours** (owl ships a live self-migrating binary; the thin skeleton has none, so\n...\n 173|The CC adapter manifest (publish target #2) ships to end users as an **`adapter.spt`** GitHub\n*174|**release asset** on this monorepo, acquired with **`spt adapter add --release SaberMage/claude-spt`**\n 175|(doyle's `--release` source; needs **spt v0.7.3+ / counter 15** — not in 0.7.2). No dedicated\n 176|adapter repo: the asset is packed straight from `adapter/`.\n 177|\n...\n 184|  archive needs it), requires **both** platforms' built tool binaries, and tars the archive **ROOT** =\n*185|  `manifest.toml` (renamed from `claude-spt.toml` — `adapter add` is root-only + exact-name) +\n 186|  `strings/` **shared at root**, plus each recognized target-triple's binaries under a `<triple>/` dir.\n 187|  Never uploads (operator's step).\n 188|<!-- [doc->REQ-DIST-ADAPTER-PEROS] -->\n...\n 191|  archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install\n*192|  dir**, so the bare-name command token (`claude-spt` — its digest/psyche/post-update/translate subcommands) still resolves at `<install_dir>/`\n 193|  (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root\n 194|  entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform\n 195|  beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.\n...\n 197|  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →\n*198|    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate\n 199|    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)\n 200|  - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target\n 201|    x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the\n## SKILL-AUTHORING.md\n 7|\n*8|## What a skill body must NOT contain\n 9|\n 10|- **Plugin / spt-core internals.** No adapter resolution (`--adapter`, parent-pid / `host_binaries`\n 11|  matching), no perch-state stamping (`state=live_agent`), no daemon/poll-vs-relay coordination, no\n## SPT-CORE-FINDINGS.md\n 20|| F-010 | 2026-06-16 | **RESOLVED-SHIPPED + RE-VALIDATED (spt v0.8.2, 2026-06-17)**. A spawn-then-exit psyche now stamps `psyche_host_error{reason:\"host not resident within 5s ...\", attempts:2}` on the parent perch (rendered `psyche-host: FAILED (...)` by `endpoint list`/`whoami`); status stays online (liveness authoritative). Forced fast-exit confirmed it | Silent-exit still maskable: `psyche_host_error` stays clear when the detached spawn() succeeds but the child exits IMMEDIATELY (e.g. arg-parse exit ...\n*21|| F-013 | 2026-06-17 | **ROOT-CAUSED (perri) → RULED spt-core BUG (doyle 2026-06-17): fork (a)**. spt-core must honor `[env].value` substitution in endpoint-run (the schema already promises \"with substitution\"; not applying it is a silent correctness bug). **Adapter manifest is CORRECT as-is — no wrapper** (b rejected: a shim would dodge a bug every `[env]`-routing adapter hits). Dispatched **`REQ-HAZARD-ENV-SUBST` → todlando, v0.11.0-findings** (pairs with REQ-SEND-SPT-HOSTED). **VERIFIED FIXED + ...\n*22|| F-011 | 2026-06-17 | **CONFIRMED + ROOT-CAUSED (doyle, spt-core source) — case-3 robustness, NON-blocking**. doyle: `registry.rs` `manifest_dir` — Pointer/GhReleaseManaged adapters read the manifest LIVE from `source_dir`; a deferred install whose manifest isn't extracted yet → `load_manifest` fails → `registered()` `filter_map(...ok())` **SILENTLY DROPS** the adapter → zero host_binaries candidates (`ADAPTER_UNRESOLVED`) AND `resolve_option/set_active` reads the absent manifest → bare **os...\n 23|| F-016 | 2026-06-22 | **RESOLVED — both fixes SHIPPED** (doyle, broker.rs-confirmed). The published `[message-idle-translation-binary]` doc omitted `{commit}` from the stdout vocabulary AND its degenerate baseline `{text}{key:enter}` would itself FAULT. `{commit:true}` is the MANDATORY inject-sequence terminator (`run_inject_worker` broker.rs:1075-1090; no-commit → 5s `INJECT_COMMIT_DEADLINE` FAULT, broker.rs:151-169; reference `{text}{key:enter}{commit:true}` translation.rs:74-78). **(i) adapter bi...\n*24|| F-017 | 2026-06-22 | **RESOLVED-SHIPPED (spt-core v0.14.0, counter 30) + VALIDATED on real claude-spt (perri, 2026-06-23)**. v0.14.0's endpoint-creation-flow milestone (REQ-RUN-MULTISUBNET-HOME, ADRs 0026/0027) closed it: `spt endpoint run` now HOMES at creation — multi-subnet node w/o `--subnet` → instant **`MULTI_SUBNET_HOME`** refuse + subnet list (0.058s, NOT the old ~25s silent `ENDPOINT_RUN_ONLINE_TIMEOUT`); `--subnet <name>` → homes + harness binds (UNBOUND → online), no HOME_REFUSED. Bo...\n*25|| F-018 | 2026-06-22 | **REPORTED to doyle (perri) — destructive footgun, os-2 family (F-011 sibling)**. Surfaced while constructing the F-015 brick repro on v0.13.2. `spt adapter add --github <user/repo>` on an **already-registered, already-extracted gh_release Pointer adapter** is destructive-then-cryptic: it git-clones the SOURCE repo over the extracted install dir (wiping the root `manifest.toml` + the 3 runtime `.exe` binaries the Pointer resolves), then the post-add first-`[update]` conduct fails...\n*26|| F-020 | 2026-06-24 | **RESOLVED-PENDING — frozen Tier-1 contract, ships v0.15.0 W5** (doyle ruled + grounded; verb committed by todlando @20bfc1f). spt-core publishes NO `spt api` verb to pull resume/psyche context at SessionStart — `download_psyche_context` (resume.rs:88) is exported-but-unexposed (ZERO `ApiCmd` callers); `resume.rs:9` documents the \"adapter pulls it in its SessionStart hook\" intent but no verb was ever published. **Consequence: claude-spt live agents rehydrate NO durable context ...\n 27|\n 28|| F-023 | 2026-07-01 | **CONFIRMED + RCA'd (doyle, same day) — build dispatched to todlando (`BUILD-F023-WANIDLE`)**. Both ranked hypotheses real: `receive_wan` has deliver_tcp + spool ONLY (the spt-hosted broker-inject leg exists solely in local `cmd_send`) AND no idle-edge drain exists anywhere. Fix = shared inject leg called from WAN ingress + ACTIVE→IDLE spool drain. Adapter exoneration stands verbatim. `from_id=\"\"` sibling ruled: compose stamps `cli@<node-label>`; ingress renders empty-from as o...\n 29|| F-024 | 2026-07-01 | **CLOSED-REFRAMED (doyle, same day) — transport EXONERATED; symptom = F-023 on the remote node.** The `(spooled)` token in `SENT(WAN)` output is RECEIVER-CONFIRMED (only prints on a confirmed round-trip): probe `DIAG-7c1e` reached ENLYZEAM post-restart and sits in ball-b's spool undelivered (`spooled` not `delivered` = no live TCP listener at receive time). Operator workaround: any prompt typed into ball-b UPS-drains the stuck messages. Residues stay real as separate diagnostics ...\n*30|| F-031 | 2026-07-08 | **REPORTED to doyle (perri) — docs gap; adapter worked around in v0.17.3 (REQ-HAZARD-SEND-STATUS-STDERR).** `spt send`'s result token (`SENT:<id>` / `QUEUED:<id>` / `DEFERRED:<id>` / `NO_PERCH:<reason>`) is written to **STDERR**, not stdout — stdout is empty on a plain `spt send`. This is undocumented in the published harness-contract / CLI reference (they describe the tokens as the send's *result* without naming the stream). It bit the claude-spt tag-messaging hook: the hook c...\n 31|\n*32|> **F-021 / F-022 (NOT spt-core findings — claude-spt parity items, tracked as REQs; surfaced during the 2026-06-24 checkpoint-commune grill)** — (F-021) claude-spt polls only on `UserPromptSubmit` (between turns); legacy spt also polls+injects on **PreToolUse** = the mid-turn half of live-agent reachability (the main path by which a live agent receives a message WHILE working). `api poll` already exists on the public surface → adapter wiring, not an spt-core gap → **`REQ-DIST-PRETOOL-POLL`**. (F...\n 33|\n 34|> **F-012 (NOT logged as spt-core)** — legacy-owl 1.11.25 poll-loop exits 1 / orphans the Psyche across daemon churn (`/spt:revive` started gen-7 wrapper+psyche fine but the foreground poll died with a non-fatal `sessions log seal failed: git failed (continuing)` line). doyle ruled this is the **legacy owl listener** (a separate daemon from spt-core), NOT an spt-core public-surface finding; the seal line is non-fatal/continues so isn't the exit cause; it dies with legacy owl's retirement. Re-open as sp...\n 35|\n...\n 37|\n*38|## F-001 — Harness hook-wiring contract incomplete for a Claude Code adapter\n 39|\n 40|**Reported:** 2026-06-14 to doyle (owl). **Status:** awaiting ruling / docs fix.\n 41|\n...\n 60|   `{key}` substitution from `reads`. It is **silent** on whether spt-core *materializes* the\n*61|   harness-native config (Claude Code `hooks.json`) from the manifest, or the adapter author\n 62|   hand-writes `hooks.json` that shells out to `spt api`. These imply different substitution models:\n*63|   Claude Code delivers hook data as JSON on **stdin**, but the manifest model is spt-core-side\n 64|   `{placeholder}` fill — so if `hooks.json` is hand-written, it is unclear who fills\n 65|   `--pid {parent_pid}`. (Also reframes the `SCOPE.md` assumption \"the plugin ships a `hooks.json`\n 66|   calling `spt api`\" — it may instead be manifest-declared + spt-materialized.)\n...\n 85|  adapter glue (L181). Owned mappings: SessionStart → `api seed` (+ env aliases); `/sptc:ready|live`\n*86|  → `api listen <id>` (the blocking poll loop — *not* SessionStart, which must not block); Stop/Idle\n 87|  → `api state idle|busy`; PreCompact/clear → `api boundary`; SessionEnd → `api session-end` /\n 88|  signoff → `api shutdown`; SubagentStart/Stop → `api worker-start`/`worker-stop`; UserPromptSubmit\n 89|  → `api poll`; **PreToolUse out-of-scope v1** (UPS covers delivery). See `docs/adr/0002-*`.\n...\n 109|\n*110|Authored + registered `adapter/claude-spt.toml` against the real v0.7.0 binary\n 111|(`REQ-DIST-MANIFEST-SCHEMA` int — `ci/manifest/registration-int.sh`, 6/6 green). Confirms the\n 112|residual: the **published docs still omit** two things a manifest author needs, both learnable only\n 113|from the **shipped mock-adapter source** (which IS public surface) + live `adapter add` errors:\n...\n 129|   locate_template`. Not documented prose-side; surfaced only at registration. (Worked around with\n*130|   `source = \"{home}/.claude/projects\"` — CC's cwd-slug subdir is not expressible as a flat\n 131|   `locate_template`, so `source` names the per-project root and the extractor finds\n 132|   `{session_id}.jsonl` within. A CC-shaped extractor is the right design vs the mock's log-less\n 133|   `[history] native`.) **Non-blocking** (resolved by reading the shipped mock + the error text);\n...\n 146|**Reported:** 2026-06-15 to doyle. **Status:** open — **spt-core CODE gap** (not docs); doyle has\n*147|raised the framing as a contract decision to the operator.\n 148|\n 149|**The gap (confirmed from source by doyle):** the **agent** `api poll` path emits each drained\n 150|message with `print!(\"{msg}\")` (`delivery.rs:192`) and `format_row` adds **no trailing newline**\n...\n 254|$ spt adapter add <dir>\n*255|#   ADAPTER_ADD_FAIL: invalid [strings] file pointer: pointer ../outside.txt must be a relative\n 256|#   path inside the strings/ dir (no absolute paths, no `..` traversal)   ← manifest-first: nothing registered\n 257|```\n 258|\n...\n 284|\n*285|**Surfaced:** 2026-06-15, authoring the claude-spt `[digest]` extractor.\n 286|\n 287|**Symptom.** `spt adapter digest-proof --sample <log>` substitutes only `{source}` (= the sample\n*288|path) into the extractor command and hard-fails on any other key. The claude-spt extractor command\n 289|is the production-correct, published-example shape:\n 290|\n 291|```\n*292|extractor = \"claude-spt-digest --session {session_id} --in {source}\"\n 293|```\n 294|\n*295|→ `DIGEST_PROOF_EXTRACT_FAIL:claude-spt: digest extractor failed: no value for substitution key {session_id}`.\n 296|\n 297|**Isolation (same adapter, only the command line differs):**\n 298|- **Variant A** — `<exe> --in {source}` (source-only) → `DIGEST_PROOF_OK`, **parsed 5 / dropped 0**,\n...\n 302|\n*303|**Why claude-spt needs `{session_id}` on the command (not a workaround — the only correct shape).**\n## TRACEABILITY.md\n 2|\n*3|> How spt-claude-code uses [`traceable-reqs`](https://github.com/BigscreenVR/traceable-reqs)\n 4|> to keep every requirement traced from doc → impl → test. The manifest is\n 5|> `traceable-reqs.toml` (seed it from your requirements source + your KNOWN-HAZARDS\n 6|> invariants). This contract makes the trace pay off instead of rotting.\n...\n 26|   one piece of real evidence. Tags at file tops to satisfy coverage are noise and usually wrong.\n*27|4. **KNOWN-HAZARDS are first-class requirements.** Each invariant is a `REQ-HAZARD-*` requiring\n*28|   `unit` (and `int` where cross-process / cross-node). A hazard cannot be \"covered\" without a\n 29|   test tagging it — the anti-regression promise becomes mechanical.\n 30|5. **Activation, not premature failure.** Every requirement starts `required_stages = []`\n 31|   (inactive) so `check` stays green pre-code. A **milestone activates** the requirements it\n...\n 53|```rust\n*54|// [<unit>->REQ-HAZARD-EXAMPLE]\n 55|#[test] fn upholds_the_invariant() { ... }\n 56|```\n 57|```markdown\n...\n 72|   `[<stage>-><REQ-ID>]` in the same change as the evidence, and run `traceable-reqs check`\n*73|   before declaring done. If this project is built *with* agents, the contract must be\n 74|   machine-followable — agent discipline + CI as the net.\n 75|4. **Quality audit (anti-box-ticking).** `check` proves a tag *exists*; `traceable-reqs review`\n 76|   + `lint` audit whether tags sit near *real* evidence and whether titles are meaningful. Run\n...\n 105|- **First milestone:** install the CLI, validate the seed against it, wire the CI gate,\n*106|  **activate that milestone's reqs** (and the `REQ-HAZARD-*` invariants it must uphold), tag as\n 107|  code lands.\n 108|- **Each later milestone:** activates + covers its requirements before it's called done.\n\n## adr/\n### 0000-template.md\n*1|# <Title — the decision stated as its outcome>\n 2|\n*3|<!-- Title states the decision, not the question. e.g. \"Sessions are pinned at first bind\",\n*4|     not \"How should sessions bind?\". Number files sequentially: 0001-, 0002-, … -->\n 5|\n 6|## Status\n 7|\n...\n 9|proposed (`<YYYY-MM-DD>`)\n*10|<!-- e.g. accepted (2026-01-15) · supersedes [ADR-NNNN](NNNN-old-decision.md) -->\n 11|\n 12|## Context\n 13|\n*14|<!-- The forces at play: what made this decision necessary, the constraints, the real\n 15|     problems (numbered if several). Enough that a reader who wasn't there understands WHY.\n 16|     Reference CONTEXT.md terms by their canonical names. -->\n 17|\n*18|## Decision\n 19|\n 20|<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,\n 21|     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->\n### 0001-distribution-splits-by-volatility.md\n 12|\n*13|`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n*14|its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n*15|invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n 16|inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n 17|plugin author.\n 18|\n...\n 24|   marketplace or letting logic rot.\n*25|2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n 26|   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n 27|   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n 28|   need to ride the marketplace at all.\n*29|3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under\n*30|   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills\n*31|   must stay **plugin-provided**.\n 32|4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed\n*33|   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.\n 34|\n*35|## Decision\n 36|\n 37|Split the adapter across **three layers by volatility** (see `SCOPE.md` §\"Distribution\n 38|architecture\"):\n...\n 65|\n*66|- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,\n 67|  collision-prone). Rejected.\n 68|- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:\n 69|  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen\n...\n 97|  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n*98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n 99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n*102|  `claude plugin update` (skeleton sync only).\n 103|<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n 104|- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105|  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106|  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n 107|  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108|  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109|- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n...\n 111|  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112|  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n 113|  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114|  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n 115|  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 12|primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n*13|formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n 14|payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n*15|public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n 16|(the adapter_name — distinct from the plugin name `sptc`).\n 17|\n 18|Two hard constraints shape the wiring:\n...\n 22|   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).","truncated":true,"truncatedBy":"bytes","totalLines":964,"totalBytes":69062,"outputLines":742,"outputBytes":51145,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":964,"totalBytes":69062,"outputLines":742,"outputBytes":51145,"shownRange":{"start":1,"end":742},"nextOffset":743}}},"isError":false,"timestamp":1784087139973}}
{"type":"message","id":"7de7cfb2","parentId":"ca8eb418","timestamp":"2026-07-15T03:45:52.167Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning comprehensive codebase audit**"},{"type":"toolCall","id":"call_d67b0l76l69bqq55p3Ofm8ge|fc_0ca4af36969c80fa016a5702700af08198a7c00878803c51a0","name":"read","arguments":{"i":"Reading canonical glossary","path":"CONTEXT.md","selector":"1-180"}},{"type":"toolCall","id":"call_0c5RzMlPI4vBtlI9oYlUggb9|fc_0ca4af36969c80fa016a5702700b088198a6592f0056d4853a","name":"read","arguments":{"i":"Reading scope ledger","path":"SCOPE.md","selector":"1-260"}},{"type":"toolCall","id":"call_e42F4IaMa5qs0QA6bnhyMw28|fc_0ca4af36969c80fa016a5702700b108198992fc6f291b41337","name":"read","arguments":{"i":"Reading adapter plan","path":"OMP-ADAPTER-PLAN.md","selector":"1-260"}},{"type":"toolCall","id":"call_61MhbWTH8AYrHXwaaUwt2OoT|fc_0ca4af36969c80fa016a5702700b1881989bd8ffe2eefb4ce2","name":"read","arguments":{"i":"Reading bridge incidents","path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","selector":"1-260"}},{"type":"toolCall","id":"call_53ZhN7IVQS8RQM8fOnCK3MqV|fc_0ca4af36969c80fa016a5702700b2481988099c9280d5c0d5a","name":"read","arguments":{"i":"Reading hazard headings","path":"docs/KNOWN-HAZARDS.md","selector":"1-220"}},{"type":"toolCall","id":"call_yAVRhbHchohhakARhACzhWeP|fc_0ca4af36969c80fa016a5702700b2c8198bdba49b088840754","name":"read","arguments":{"i":"Reading remaining hazards","path":"docs/KNOWN-HAZARDS.md","selector":"221-520"}},{"type":"toolCall","id":"call_82g6i7IwvDQSDZw4ScXwLDD3|fc_0ca4af36969c80fa016a5702700b3881989fe14cb0463a8c7a","name":"read","arguments":{"i":"Reading ADR decisions","path":"docs/adr/0001-distribution-splits-by-volatility.md","selector":"1-180"}},{"type":"toolCall","id":"call_lNmAqghwwn7aDRoXt8KRBcx0|fc_0ca4af36969c80fa016a5702700b4081988cc25ce7c199c8b9","name":"read","arguments":{"i":"Reading hook architecture ADR","path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","selector":"1-200"}},{"type":"toolCall","id":"call_pQm8b1aOJg3cdyoHmESa4pyJ|fc_0ca4af36969c80fa016a5702700b4881989c86812fad9a8363","name":"read","arguments":{"i":"Reading psyche ADR","path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","selector":"1-200"}},{"type":"toolCall","id":"call_RRW3vzZoOELH9TN6npmLZf90|fc_0ca4af36969c80fa016a5702700b5081989bb7b29d71921daa","name":"read","arguments":{"i":"Reading checkpoint ADR","path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","selector":"1-200"}},{"type":"toolCall","id":"call_V9ttSIkjl3d8jYBWOyOkSdRl|fc_0ca4af36969c80fa016a5702700b588198b6ebd179f254a035","name":"read","arguments":{"i":"Reading naming ADR","path":"docs/adr/0005-name-unification.md","selector":"1-220"}},{"type":"toolCall","id":"call_Ida6lqZAsBvP4UG8jDiU42X5|fc_0ca4af36969c80fa016a5702700b64819896a23e545a6d8646","name":"read","arguments":{"i":"Reading binary consolidation ADR","path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","selector":"1-220"}},{"type":"toolCall","id":"call_wqpdtDQmdZIkrHF9O2fPkqmO|fc_0ca4af36969c80fa016a5702700b708198a56013432a6f488a","name":"read","arguments":{"i":"Reading native TUI ADR","path":"docs/adr/0007-native-omp-tui-hosts-spt-extension.md","selector":"1-240"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":17474,"output":551,"cacheRead":16896,"cacheWrite":0,"totalTokens":34921,"reasoningTokens":61,"cost":{"input":0.08737,"output":0.01653,"cacheRead":0.008447999999999999,"cacheWrite":0,"total":0.112348}},"stopReason":"toolUse","timestamp":1784087140022,"responseId":"resp_0ca4af36969c80fa016a5702641be88198a2f53eb0f5299b63","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a57026616fc819896e8dc4cb7fa3a61","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwJwvwky9hJ5e8ktNcTcosgW-_eYvJNWcBrXPi_UdyGXrXdw2_zv1ZwBBXyvmTSgo9DHYRRO0PxtlapnWekhoVwF9yAKIT9iJ2K1BBMvBwGkVfDG1dgokGvUWUOTwr5Or0xR44QOhfXHiRvftJ9f-PG38U-HCgW3l3eAvzhwlDevQXY2u4bEAPJu3cXcSAZFOaOSGvu1oRCM9u_Y0OsQUxXCma-bhOiieqy_-_M6yQkoA1jPIxfXftF1NI_HsAKPvHQ7j4wi7T-E_e5srJd39wxMoxg4JMz9ZANwDtvhMMxLR7VsWz4pKlEd7UGkAHQvHKuQus9_PQ6QVhJqHtSimwjsOGBZmTiBS_jRSlfNRZY-anKDPBObSpAAzVWcXVXF1h4NRcFJ5kLGuSkgqSVR4n7ieSWxo9LTdHAdo273LOnndySIEx5dxvZ4GqeUw7FWNcUHsVtqMoQqby6tdLUIlmZBBjjpdvhA3oSulnuqiRj7IY9x6Pxj4FFW0kwbU2F9o5dBzsORrI9D04dYcDvDjBa8btIiACXxQF7CfPGnPMJgLkp6SXPtYd4a79_-EmLha8OZx-YtiSpV7ws9BpHcWy2xQ-SLxXNm11zxN8ahkDpd5Klj4YA7UsNsFJrekn8Ej1uNSDNlwKp73kTjOsyI-tfjYdhCQgQXcqsNvhywPJMjWqqp77LCmkGSvReHms1hRsMj1ERAB27Qcsqe_d2GOMxlBzOOy2LK_59vEjVohlFaGB6sU_uvw0ifYZmC66dR_3k-LTuwfPNlzER42ACVgsAH5k9yvdOGFLlf8mppUUsQ6q_Ab9tZGhiwRWuo5KMrZ-ViykDT3hwfbgznyAbNRnOO58DLfcFxEFpR0Qnm3S_03SB7Zua-AM3r3Vcf_txXIru0zX7ogdgM7Us_tBH-0dwwH_mlxe201LkE1qAwAqRSLC3j0K-6kKeghURi7kSN3I8GhAubrQ6zO9VpTDPKHLF0Hzc6Q8HkhsfwT34HNhFkqy5-JPSOdiZwVS6IXR-Nm3GX0qOY6xOEVL6Exn8IO9p-43_m-aw4FMaZN1BgmeE_fx-q6JADXR1EMHvHxvEsb_KgUrvPvkOaxpTN-HEasyFy4tcJDilm6rQhxcBD0KSlveclB9KDx3061rZ7wiu1niVFukdIAWqIOclk-etuTnz7Em_3UyEah5xq09Syj6ZpOpHZhWKYcxBkeiRreOA3RN2CzZg42UwgmOBjYKAEZPNjjOBgKrp1YNBMNx6VLS5x0A9Io-_NhcRlVDtV8QuOovDIIS0KeY26vGrQfyHjrBf8vQ==","summary":[{"type":"summary_text","text":"**Planning comprehensive codebase audit**"}]},{"id":"fc_0ca4af36969c80fa016a5702700af08198a7c00878803c51a0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading canonical glossary\",\"path\":\"CONTEXT.md\",\"selector\":\"1-180\"}","call_id":"call_d67b0l76l69bqq55p3Ofm8ge","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b088198a6592f0056d4853a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading scope ledger\",\"path\":\"SCOPE.md\",\"selector\":\"1-260\"}","call_id":"call_0c5RzMlPI4vBtlI9oYlUggb9","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b108198992fc6f291b41337","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading adapter plan\",\"path\":\"OMP-ADAPTER-PLAN.md\",\"selector\":\"1-260\"}","call_id":"call_e42F4IaMa5qs0QA6bnhyMw28","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b1881989bd8ffe2eefb4ce2","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading bridge incidents\",\"path\":\"docs/OMP-BRIDGE-FIELD-ISSUES.md\",\"selector\":\"1-260\"}","call_id":"call_61MhbWTH8AYrHXwaaUwt2OoT","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b2481988099c9280d5c0d5a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hazard headings\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"1-220\"}","call_id":"call_53ZhN7IVQS8RQM8fOnCK3MqV","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b2c8198bdba49b088840754","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading remaining hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"221-520\"}","call_id":"call_yAVRhbHchohhakARhACzhWeP","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b3881989fe14cb0463a8c7a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading ADR decisions\",\"path\":\"docs/adr/0001-distribution-splits-by-volatility.md\",\"selector\":\"1-180\"}","call_id":"call_82g6i7IwvDQSDZw4ScXwLDD3","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b4081988cc25ce7c199c8b9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hook architecture ADR\",\"path\":\"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\",\"selector\":\"1-200\"}","call_id":"call_lNmAqghwwn7aDRoXt8KRBcx0","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b4881989c86812fad9a8363","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading psyche ADR\",\"path\":\"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md\",\"selector\":\"1-200\"}","call_id":"call_pQm8b1aOJg3cdyoHmESa4pyJ","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b5081989bb7b29d71921daa","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading checkpoint ADR\",\"path\":\"docs/adr/0004-checkpoint-via-self-send-loopback.md\",\"selector\":\"1-200\"}","call_id":"call_RRW3vzZoOELH9TN6npmLZf90","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b588198b6ebd179f254a035","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading naming ADR\",\"path\":\"docs/adr/0005-name-unification.md\",\"selector\":\"1-220\"}","call_id":"call_V9ttSIkjl3d8jYBWOyOkSdRl","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b64819896a23e545a6d8646","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading binary consolidation ADR\",\"path\":\"docs/adr/0006-one-command-update-and-consolidated-binary.md\",\"selector\":\"1-220\"}","call_id":"call_Ida6lqZAsBvP4UG8jDiU42X5","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702700b708198a56013432a6f488a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading native TUI ADR\",\"path\":\"docs/adr/0007-native-omp-tui-hosts-spt-extension.md\",\"selector\":\"1-240\"}","call_id":"call_wqpdtDQmdZIkrHF9O2fPkqmO","name":"read"}]},"duration":12122.857200000028,"ttft":2078.206999999995,"contextSnapshot":{"promptTokens":34370,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_d67b0l76l69bqq55p3Ofm8ge|fc_0ca4af36969c80fa016a5702700af08198a7c00878803c51a0","toolName":"read","startedAt":"2026-07-15T03:45:52.168Z","args":{"path":"CONTEXT.md"},"intent":"Reading canonical glossary"},"id":"f4f6b427","parentId":"7de7cfb2","timestamp":"2026-07-15T03:45:52.168Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_0c5RzMlPI4vBtlI9oYlUggb9|fc_0ca4af36969c80fa016a5702700b088198a6592f0056d4853a","toolName":"read","startedAt":"2026-07-15T03:45:52.168Z","args":{"path":"SCOPE.md"},"intent":"Reading scope ledger"},"id":"84dfbb36","parentId":"f4f6b427","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_e42F4IaMa5qs0QA6bnhyMw28|fc_0ca4af36969c80fa016a5702700b108198992fc6f291b41337","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"OMP-ADAPTER-PLAN.md"},"intent":"Reading adapter plan"},"id":"831b832b","parentId":"84dfbb36","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_61MhbWTH8AYrHXwaaUwt2OoT|fc_0ca4af36969c80fa016a5702700b1881989bd8ffe2eefb4ce2","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md"},"intent":"Reading bridge incidents"},"id":"15dcd661","parentId":"831b832b","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_53ZhN7IVQS8RQM8fOnCK3MqV|fc_0ca4af36969c80fa016a5702700b2481988099c9280d5c0d5a","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading hazard headings"},"id":"5aba3dfb","parentId":"15dcd661","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yAVRhbHchohhakARhACzhWeP|fc_0ca4af36969c80fa016a5702700b2c8198bdba49b088840754","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading remaining hazards"},"id":"f8838aab","parentId":"5aba3dfb","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_82g6i7IwvDQSDZw4ScXwLDD3|fc_0ca4af36969c80fa016a5702700b3881989fe14cb0463a8c7a","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0001-distribution-splits-by-volatility.md"},"intent":"Reading ADR decisions"},"id":"fe7aaaf3","parentId":"f8838aab","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lNmAqghwwn7aDRoXt8KRBcx0|fc_0ca4af36969c80fa016a5702700b4081988cc25ce7c199c8b9","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md"},"intent":"Reading hook architecture ADR"},"id":"28438001","parentId":"fe7aaaf3","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pQm8b1aOJg3cdyoHmESa4pyJ|fc_0ca4af36969c80fa016a5702700b4881989c86812fad9a8363","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md"},"intent":"Reading psyche ADR"},"id":"533cb0b2","parentId":"28438001","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RRW3vzZoOELH9TN6npmLZf90|fc_0ca4af36969c80fa016a5702700b5081989bb7b29d71921daa","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md"},"intent":"Reading checkpoint ADR"},"id":"1924efbd","parentId":"533cb0b2","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_V9ttSIkjl3d8jYBWOyOkSdRl|fc_0ca4af36969c80fa016a5702700b588198b6ebd179f254a035","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0005-name-unification.md"},"intent":"Reading naming ADR"},"id":"b4e94e45","parentId":"1924efbd","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Ida6lqZAsBvP4UG8jDiU42X5|fc_0ca4af36969c80fa016a5702700b64819896a23e545a6d8646","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md"},"intent":"Reading binary consolidation ADR"},"id":"86f28340","parentId":"b4e94e45","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wqpdtDQmdZIkrHF9O2fPkqmO|fc_0ca4af36969c80fa016a5702700b708198a56013432a6f488a","toolName":"read","startedAt":"2026-07-15T03:45:52.169Z","args":{"path":"docs/adr/0007-native-omp-tui-hosts-spt-extension.md"},"intent":"Reading native TUI ADR"},"id":"ea051623","parentId":"86f28340","timestamp":"2026-07-15T03:45:52.169Z"}
{"type":"message","id":"042ab0b3","parentId":"ea051623","timestamp":"2026-07-15T03:45:52.175Z","message":{"role":"toolResult","toolCallId":"call_d67b0l76l69bqq55p3Ofm8ge|fc_0ca4af36969c80fa016a5702700af08198a7c00878803c51a0","toolName":"read","content":[{"type":"text","text":"# spt-claude-code — glossary\n\n> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n\n**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\nadapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\nparity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\ninstaller. Built by maintainer perri from the public spt-releases surface only.\n(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n[[naming-domains]].)\n\n**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\ninvocation + the manifest). The CC adapter's identity inside spt-core. The\n**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n\n**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\nspt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\nshortcut (`cc-<id>`) is decoupled from both.\n\n**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration\n(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a\n1:1 port — most of its machinery now lives in spt.exe.\n\n**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced\n`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;\nthe volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.\n\n**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real\ninstructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).\nKeeps skill files static while instructions update via spt-core. Distinct from a `[hints]`\nentry (proactive, keyword-triggered, UPS-hook-delivered).\n\n**identity brief** — the agent-facing text the adapter injects at SessionStart into a session\nthat already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that\nits perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint\nroster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity\npiece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\nflavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n**ring brief** (the no-perch sibling).\n\n**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`\ntopology): a node-local agent that hasn't readied still learns how to reach other agents via\n`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually\nparticipates in a subnet (has reachable peers), so a solo casual end user is never told how to\nring agents that don't exist.\n\n**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\nthat leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\nNOT its own adapter — ccs is structurally Claude Code.\n\n**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\nspawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\nattach). The spt-core realization of legacy's unbuilt \"Capsule\" milestone. _Avoid_: equating\nit with psmux/sendkeys — spt-core's broker is the terminal host.\n\n**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own\nperch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a\n*commune*. It never replies or notifies (that is the echo-commune, a different actor) and\nexits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\nprofile. (Realized here by the `claude-spt-psyche` runner.)\n\n**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\nparent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\ndaemon ingests, not an `api` verb.\n\n**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to\nrequest that its own working context be wiped and rebuilt from that latest commune, so it\ncontinues coherently without the operator manually running `/clear`. The agent-driven sibling\nof the operator's manual clear; only meaningful for a live agent (which has a Psyche + a\ncommune to rebuild from). See [[commune]].\n\n**checkpoint trigger** — the marker a live agent embeds in a commune to request a\n[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the\ndelivery layer, which is an implementation concern).\n\n**session name** — the operator-facing display name an spt-hosted endpoint carries in the\nharness TUI (prompt box / resume picker): the visual anchor tying a terminal to its agent.\nShape: `<id> @ <node> (<project>/)`, degrading to bare `<id>` when the node is unknown.\nDistinct from the remote-control name (a character-safe token) and the endpoint id (spt-core's\nidentity — attach/inject key by it, never by this name).\n\n**boundary rename** — re-asserting the [[session name]] on a `/clear` boundary. The harness\ndrops the display name when a clear rotates the session, leaving the operator no rendered\nanchor; the adapter re-applies the same name to the fresh session, before any [[checkpoint]]\nwake lands. `/compact` retains the name, so it has no rename.\n\n**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:\nRead/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap\npinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors\nlegacy owl's psyche box; see `docs/adr/0003-*`.\n\n**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\nadapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\nusers: install the plugin, get spt-core for free.\n\n**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\nextracted from spt-core's working style. spt-claude-code is its first consumer. Carries\ntraceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\nsame-repo published-docs. Defined in its own folder, not here.\n\n**tagged-input round-trip** — the harness pattern where the translation binary types\nnamespaced-XML-tagged content into the operator's *input box* without submitting, the operator\nedits the payload between the tags, and the UserPromptSubmit hook recognizes the tags on the\nresulting submit to consume the edited result. The binary-inject leg and the UPS leg are\ndecoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive\nedit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s\nadditionalContext (which the operator never edits).\n\n**interrupt marker** — the digest-visible signal that the operator pressed Esc to interrupt a\nturn: the session digest records a turn whose input is the literal `[Request interrupted by\nuser]` (the normalized form of the TUI's \"Interrupted · What should Claude do instead?\"). The\nsole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`\nhook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the\nfetcher re-reads the transcript), never daemon-pushed.\n"}],"details":{"displayContent":{"text":"# spt-claude-code — glossary\n\n> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n\n**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\nadapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\nparity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\ninstaller. Built by maintainer perri from the public spt-releases surface only.\n(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n[[naming-domains]].)\n\n**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\ninvocation + the manifest). The CC adapter's identity inside spt-core. The\n**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n\n**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\nspt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\nshortcut (`cc-<id>`) is decoupled from both.\n\n**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration\n(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a\n1:1 port — most of its machinery now lives in spt.exe.\n\n**skeleton plugin** — the thin marketplace artifact on `SaberMage/cplugs`: namespaced\n`/spt:*` skill stubs + hooks + a SessionStart bootstrap that installs spt-core. Low-churn;\nthe volatile bulk (binary + manifest) is spt-core-conducted, not in the plugin.\n\n**fetch-stub skill** — a `/spt:X` SKILL.md that is a 2-line stub fetching its real\ninstructions at runtime from the adapter `[strings]` (`spt adapter get-string`/`skill-help`).\nKeeps skill files static while instructions update via spt-core. Distinct from a `[hints]`\nentry (proactive, keyword-triggered, UPS-hook-delivered).\n\n**identity brief** — the agent-facing text the adapter injects at SessionStart into a session\nthat already owns a perch (the `bind` + `boundary` topologies). Tells the agent who it is, that\nits perch is already live (so it must not re-arm), and how to message (send + reply + the endpoint\nroster). Adapter-string-backed (`[strings.briefs]`), composed from an `{id}`-templated identity\npiece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\nflavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n**ring brief** (the no-perch sibling).\n\n**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`\ntopology): a node-local agent that hasn't readied still learns how to reach other agents via\n`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually\nparticipates in a subnet (has reachable peers), so a solo casual end user is never told how to\nring agents that don't exist.\n\n**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\nthat leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\nNOT its own adapter — ccs is structurally Claude Code.\n\n**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\nspawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\nattach). The spt-core realization of legacy's unbuilt \"Capsule\" milestone. _Avoid_: equating\nit with psmux/sendkeys — spt-core's broker is the terminal host.\n\n**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own\nperch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a\n*commune*. It never replies or notifies (that is the echo-commune, a different actor) and\nexits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\nprofile. (Realized here by the `claude-spt-psyche` runner.)\n\n**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\nparent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\ndaemon ingests, not an `api` verb.\n\n**checkpoint** — a live agent's *self-initiated* context reset: the agent flags a commune to\nrequest that its own working context be wiped and rebuilt from that latest commune, so it\ncontinues coherently without the operator manually running `/clear`. The agent-driven sibling\nof the operator's manual clear; only meaningful for a live agent (which has a Psyche + a\ncommune to rebuild from). See [[commune]].\n\n**checkpoint trigger** — the marker a live agent embeds in a commune to request a\n[[checkpoint]] (the request signal; distinct from the wire payload that carries it to the\ndelivery layer, which is an implementation concern).\n\n**session name** — the operator-facing display name an spt-hosted endpoint carries in the\nharness TUI (prompt box / resume picker): the visual anchor tying a terminal to its agent.\nShape: `<id> @ <node> (<project>/)`, degrading to bare `<id>` when the node is unknown.\nDistinct from the remote-control name (a character-safe token) and the endpoint id (spt-core's\nidentity — attach/inject key by it, never by this name).\n\n**boundary rename** — re-asserting the [[session name]] on a `/clear` boundary. The harness\ndrops the display name when a clear rotates the session, leaving the operator no rendered\nanchor; the adapter re-applies the same name to the fresh session, before any [[checkpoint]]\nwake lands. `/compact` retains the name, so it has no rename.\n\n**psyche sandbox** — the constrained surface a Psyche's `claude` turns run under:\nRead/Edit/Write tools only, slash-commands disabled, permissions auto-approved, cheap\npinned model. Deliberately narrower than the parent agent (which is unconstrained). Mirrors\nlegacy owl's psyche box; see `docs/adr/0003-*`.\n\n**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\nadapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\nusers: install the plugin, get spt-core for free.\n\n**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\nextracted from spt-core's working style. spt-claude-code is its first consumer. Carries\ntraceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\nsame-repo published-docs. Defined in its own folder, not here.\n\n**tagged-input round-trip** — the harness pattern where the translation binary types\nnamespaced-XML-tagged content into the operator's *input box* without submitting, the operator\nedits the payload between the tags, and the UserPromptSubmit hook recognizes the tags on the\nresulting submit to consume the edited result. The binary-inject leg and the UPS leg are\ndecoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive\nedit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s\nadditionalContext (which the operator never edits).\n\n**interrupt marker** — the digest-visible signal that the operator pressed Esc to interrupt a\nturn: the session digest records a turn whose input is the literal `[Request interrupted by\nuser]` (the normalized form of the TUI's \"Interrupted · What should Claude do instead?\"). The\nsole reliable idle-after-interrupt signal, since an Esc fires no `Stop` and no `idle_prompt`\nhook; the receive-heal keys off it to re-assert idle. Surfaced only on a digest pull (the\nfetcher re-reads the transcript), never daemon-pushed.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\CONTEXT.md"}}},"isError":false,"timestamp":1784087152175}}
{"type":"message","id":"22c27c18","parentId":"042ab0b3","timestamp":"2026-07-15T03:45:52.176Z","message":{"role":"toolResult","toolCallId":"call_82g6i7IwvDQSDZw4ScXwLDD3|fc_0ca4af36969c80fa016a5702700b3881989fe14cb0463a8c7a","toolName":"read","content":[{"type":"text","text":"<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->\n<!-- [doc->REQ-DIST-HOOKS-API] -->\n<!-- [doc->REQ-DIST-BOOTSTRAP-INSTALL] -->\n<!-- [doc->REQ-UPS-INJECTION] -->\n# Distribution splits by volatility: a thin skeleton plugin, an spt-conducted manifest + binary\n\n## Status\n\naccepted (2026-06-14)\n\n## Context\n\n`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\nits first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\ninvisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\ninside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\nplugin author.\n\nForces:\n\n1. **Two very different churn rates.** Skill *instructions*, the `[digest]` extractor, profiles,\n   strings, and hints change often. The plugin's *structure* (which skills exist, hook wiring,\n   bootstrap) changes rarely. Coupling them to one release cadence means either over-bumping the\n   marketplace or letting logic rot.\n2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n   need to ride the marketplace at all.\n3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under\n   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills\n   must stay **plugin-provided**.\n4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed\n   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.\n\n## Decision\n\nSplit the adapter across **three layers by volatility** (see `SCOPE.md` §\"Distribution\narchitecture\"):\n\n| Layer | Contents | Home | Churn |\n|---|---|---|---|\n| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |\n| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n\nConcretely:\n\n- **The marketplace plugin is a true thin skeleton.** It carries skill *skeletons* (frontmatter\n  + a stub body), `hooks.json`, a SessionStart bootstrap, and `plugin.json` — **no binary, no\n  manifest, no embedded logic.** [`REQ-DIST-PLUGIN-SKELETON`]\n- **`hooks.json` delegates to `spt api`** rather than carrying adapter behavior. The hook commands\n  invoke the spt binary's published `api` surface; the plugin ships no binary of its own (contrast\n  legacy, which shipped `owl.exe` and called it directly). [`REQ-DIST-HOOKS-API`]\n- **A SessionStart bootstrap installs spt-core when absent** — the invisible-installer pattern.\n  A user who installs the plugin gets spt-core for free. [`REQ-DIST-BOOTSTRAP-INSTALL`]\n- **Skill instructions are delivered by UPS-injection.** A `UserPromptSubmit` hook detects `/spt:X`\n  in the prompt and injects X's real instructions as `additionalContext`, sourced from the adapter\n  `[strings]` (file-backed, so the manifest doesn't bloat). SKILL.md files stay skeletons; the\n  volatile instruction text lives in spt-core-conducted strings. [`REQ-UPS-INJECTION`]\n- **The \"bulk\" install/update is manifest + binary**, conducted by spt-core's own framework (the\n  file-pull channel), not a marketplace bump. The marketplace plugin rarely needs a version bump\n  and never orphans an active install.\n\n### Rejected alternatives\n\n- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,\n  collision-prone). Rejected.\n- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:\n  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen\n  instead. (`SCOPE.md` §\"Skill-instruction delivery\".)\n- **A fat plugin shipping the binary** (legacy `owl.exe` model) — couples high-churn logic to the\n  marketplace cadence and forces the binary/handoff/cache-prune machinery the thin skeleton has no\n  need for. Rejected; logic rides the spt binary + adapter registry.\n\n### Open / to-confirm\n\n- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`\n  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but\n  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)\n- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction\n  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.\n  **Update 2026-06-15 — SATISFIED.** M12 (`spt 0.7.0`) ships file-backed `[strings]` as a\n  value-position table pointer: `key = { file = \"rel\" }` over the per-adapter aux dir\n  `adapters/<adapter>/strings/`, resolved lazily at `get-string` (containment-checked; copied on\n  `adapter add`). Confirmed by live byte-test + spt-core ruling (`REQ-MANIFEST-5`, M12-W3). The\n  dependency is met — skill bodies are externalized via this pointer (no manifest bloat); the\n  interim-inline fallback is retired. The pointer syntax was merely **undocumented** on the published\n  surface (now finding **F-003**, docs-only, doyle publishing). See `docs/SPT-CORE-FINDINGS.md`.\n\n## Consequences\n\n<!-- [doc->REQ-DIST-MANIFEST-SCHEMA] -->\n- The marketplace plugin can sit static across many manifest/binary updates → the\n  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /\n  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n  `claude plugin update` (skeleton sync only).\n<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —\n  no override; confirmed against the official plugin docs), the marketplace plugin is named\n  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the\n  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,\n  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.\n"}],"details":{"displayContent":{"text":"<!-- [doc->REQ-DIST-PLUGIN-SKELETON] -->\n<!-- [doc->REQ-DIST-HOOKS-API] -->\n<!-- [doc->REQ-DIST-BOOTSTRAP-INSTALL] -->\n<!-- [doc->REQ-UPS-INJECTION] -->\n# Distribution splits by volatility: a thin skeleton plugin, an spt-conducted manifest + binary\n\n## Status\n\naccepted (2026-06-14)\n\n## Context\n\n`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\nits first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\ninvisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\ninside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\nplugin author.\n\nForces:\n\n1. **Two very different churn rates.** Skill *instructions*, the `[digest]` extractor, profiles,\n   strings, and hints change often. The plugin's *structure* (which skills exist, hook wiring,\n   bootstrap) changes rarely. Coupling them to one release cadence means either over-bumping the\n   marketplace or letting logic rot.\n2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n   need to ride the marketplace at all.\n3. **The skill `/spt:` namespace must be preserved** (legacy parity). User-scope skills under\n   `~/.claude/skills` cannot namespace (they surface as bare `/live`, collision-prone), so skills\n   must stay **plugin-provided**.\n4. **Users install mid-session**, so the plugin cannot assume a SessionStart fire has installed\n   spt-core; the bootstrap and a `/spt:setup` skill must cover the gap.\n\n## Decision\n\nSplit the adapter across **three layers by volatility** (see `SCOPE.md` §\"Distribution\narchitecture\"):\n\n| Layer | Contents | Home | Churn |\n|---|---|---|---|\n| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |\n| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n\nConcretely:\n\n- **The marketplace plugin is a true thin skeleton.** It carries skill *skeletons* (frontmatter\n  + a stub body), `hooks.json`, a SessionStart bootstrap, and `plugin.json` — **no binary, no\n  manifest, no embedded logic.** [`REQ-DIST-PLUGIN-SKELETON`]\n- **`hooks.json` delegates to `spt api`** rather than carrying adapter behavior. The hook commands\n  invoke the spt binary's published `api` surface; the plugin ships no binary of its own (contrast\n  legacy, which shipped `owl.exe` and called it directly). [`REQ-DIST-HOOKS-API`]\n- **A SessionStart bootstrap installs spt-core when absent** — the invisible-installer pattern.\n  A user who installs the plugin gets spt-core for free. [`REQ-DIST-BOOTSTRAP-INSTALL`]\n- **Skill instructions are delivered by UPS-injection.** A `UserPromptSubmit` hook detects `/spt:X`\n  in the prompt and injects X's real instructions as `additionalContext`, sourced from the adapter\n  `[strings]` (file-backed, so the manifest doesn't bloat). SKILL.md files stay skeletons; the\n  volatile instruction text lives in spt-core-conducted strings. [`REQ-UPS-INJECTION`]\n- **The \"bulk\" install/update is manifest + binary**, conducted by spt-core's own framework (the\n  file-pull channel), not a marketplace bump. The marketplace plugin rarely needs a version bump\n  and never orphans an active install.\n\n### Rejected alternatives\n\n- **Global user-scope skills** (`~/.claude/skills`) — lose the `/spt:` namespace (bare `/live`,\n  collision-prone). Rejected.\n- **fetch-stub skills** (SKILL.md fetches its real body at runtime) — VETOED by the operator:\n  adds terminal noise, latency, and an extra tool call per invocation. UPS-injection chosen\n  instead. (`SCOPE.md` §\"Skill-instruction delivery\".)\n- **A fat plugin shipping the binary** (legacy `owl.exe` model) — couples high-churn logic to the\n  marketplace cadence and forces the binary/handoff/cache-prune machinery the thin skeleton has no\n  need for. Rejected; logic rides the spt binary + adapter registry.\n\n### Open / to-confirm\n\n- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`\n  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but\n  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)\n- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction\n  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.\n  **Update 2026-06-15 — SATISFIED.** M12 (`spt 0.7.0`) ships file-backed `[strings]` as a\n  value-position table pointer: `key = { file = \"rel\" }` over the per-adapter aux dir\n  `adapters/<adapter>/strings/`, resolved lazily at `get-string` (containment-checked; copied on\n  `adapter add`). Confirmed by live byte-test + spt-core ruling (`REQ-MANIFEST-5`, M12-W3). The\n  dependency is met — skill bodies are externalized via this pointer (no manifest bloat); the\n  interim-inline fallback is retired. The pointer syntax was merely **undocumented** on the published\n  surface (now finding **F-003**, docs-only, doyle publishing). See `docs/SPT-CORE-FINDINGS.md`.\n\n## Consequences\n\n<!-- [doc->REQ-DIST-MANIFEST-SCHEMA] -->\n- The marketplace plugin can sit static across many manifest/binary updates → the\n  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /\n  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n  `claude plugin update` (skeleton sync only).\n<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —\n  no override; confirmed against the official plugin docs), the marketplace plugin is named\n  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the\n  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,\n  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0001-distribution-splits-by-volatility.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"77311a4c","parentId":"22c27c18","timestamp":"2026-07-15T03:45:52.176Z","message":{"role":"toolResult","toolCallId":"call_RRW3vzZoOELH9TN6npmLZf90|fc_0ca4af36969c80fa016a5702700b5081989bb7b29d71921daa","toolName":"read","content":[{"type":"text","text":"# Checkpoint clears the session via a self-sent loopback message, authored inline pre-clear\n\n## Status\n\nproposed (2026-06-24)\n\n## Context\n\nThe operator asked for **checkpoint** (see `CONTEXT.md`): a live agent flags a commune so its own\nsession is automatically cleared and re-seeded — the agent-driven sibling of a manual `/clear` —\nwithout the operator intervening. Building it on the **public spt-core surface only** (AGENTS.md)\nsurfaced four hard forces:\n\n1. **Only the translation binary can drive the spt-hosted PTY.** Typing `/clear` + a wake prompt into\n   the broker-held PTY is the `[message-idle-translation-binary]`'s job. But that binary is\n   `stdin`-driven (`init`/`event`/`input` lines from spt-core) with **no filesystem or commune\n   knowledge** — it cannot read `.claude/<id>-commune.md` to detect a trigger. The daemon is the\n   single-writer that ingests+deletes the drop (`REQ-HAZARD-DROP-FILE-SINGLE-WRITER`), so any\n   adapter-side reader races it (TOCTOU).\n2. **Only IDLE delivery routes to the binary.** While the agent is mid-turn, messages route to the\n   poll/hook channel (additionalContext), not the binary. A trigger must reach the binary *as an idle\n   delivery* to fire the macro.\n3. **Re-seed staleness.** If the commune is authored *after* `/clear` (the boundary/resume-Self\n   refresh path), the legacy `psyche-stale-after-clear` gap reappears (LLM-latency-bounded): the\n   durable store and the drop file may both be absent/stale at the post-clear SessionStart.\n4. **No daemon-push primitive.** The clean design — daemon detects the trigger in the commune it\n   ingests and pushes a new `{\"type\":\"checkpoint\"}` stdin message to the binary — needs an spt-core\n   capability that does not exist on the published surface.\n\nEmpirically (proven this session): cyan coloring of the EVENT block is impossible (SGR bytes are\neaten by CC's input box; user-turns are theme-fixed); and a **self-addressed `spt send` loops back\nthrough the endpoint's own translation binary** and lands as a delivered EVENT.\n\n## Decision\n\nCheckpoint is driven by a **self-sent loopback message**, with the commune **authored inline\npre-clear**:\n\n1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own\n   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a\n   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**\n   the clear.\n2. A new **PostToolUse hook** detects the trigger in the Write's `tool_input.content` (not a file\n   re-read — sidesteps the daemon-delete race), flips the perch idle (`spt api state idle`, so the\n   next delivery routes to the binary, not the poll channel), and **self-sends** a reserved\n   wire-sentinel: `spt send --from <id> <id> <wire-sentinel[+wake-text]>`.\n3. The message loops back into the endpoint's **own** translation binary, which recognizes the\n   wire-sentinel and emits the **clear+wake macro** — `ctrl+s · 50ms · /clear · enter · 500ms ·\n   <wake-text> · enter · commit` — instead of normal delivery. (`ctrl+s` first is required to stash\n   any lingering input.)\n4. Post-clear re-seed is carried by spt-core's `psyche-download` verb (v0.15.0 Tier-1 contract), **not**\n   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized\n   `<pending-commune>` (trigger stripped core-side), race-free.\n\n**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never\ndefers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale\ngap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.\n\n**Rejected alternatives:**\n- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's\n  single-writer ingest+delete.\n- *Daemon-push (`{\"type\":\"checkpoint\"}`)* — cleaner, but needs an spt-core capability absent from the\n  public surface; would block the feature on a core change.\n- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the\n  race-free re-seed (the `psyche-download` `<pending-*>` append).\n\n## Consequences\n\n- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n- **Cross-team contracts depended on:** `!!checkpoint!!` marker stripped core-side (todlando, W5\n  follow-up); `psyche-download` Tier-1 verb (doyle/todlando, ships v0.15.0 W5). The checkpoint\n  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.\n- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no\n  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted\n  lifecycle on purpose.\n- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n  clear is non-obvious) so it is not \"simplified\" away by a future reader.\n"}],"details":{"displayContent":{"text":"# Checkpoint clears the session via a self-sent loopback message, authored inline pre-clear\n\n## Status\n\nproposed (2026-06-24)\n\n## Context\n\nThe operator asked for **checkpoint** (see `CONTEXT.md`): a live agent flags a commune so its own\nsession is automatically cleared and re-seeded — the agent-driven sibling of a manual `/clear` —\nwithout the operator intervening. Building it on the **public spt-core surface only** (AGENTS.md)\nsurfaced four hard forces:\n\n1. **Only the translation binary can drive the spt-hosted PTY.** Typing `/clear` + a wake prompt into\n   the broker-held PTY is the `[message-idle-translation-binary]`'s job. But that binary is\n   `stdin`-driven (`init`/`event`/`input` lines from spt-core) with **no filesystem or commune\n   knowledge** — it cannot read `.claude/<id>-commune.md` to detect a trigger. The daemon is the\n   single-writer that ingests+deletes the drop (`REQ-HAZARD-DROP-FILE-SINGLE-WRITER`), so any\n   adapter-side reader races it (TOCTOU).\n2. **Only IDLE delivery routes to the binary.** While the agent is mid-turn, messages route to the\n   poll/hook channel (additionalContext), not the binary. A trigger must reach the binary *as an idle\n   delivery* to fire the macro.\n3. **Re-seed staleness.** If the commune is authored *after* `/clear` (the boundary/resume-Self\n   refresh path), the legacy `psyche-stale-after-clear` gap reappears (LLM-latency-bounded): the\n   durable store and the drop file may both be absent/stale at the post-clear SessionStart.\n4. **No daemon-push primitive.** The clean design — daemon detects the trigger in the commune it\n   ingests and pushes a new `{\"type\":\"checkpoint\"}` stdin message to the binary — needs an spt-core\n   capability that does not exist on the published surface.\n\nEmpirically (proven this session): cyan coloring of the EVENT block is impossible (SGR bytes are\neaten by CC's input box; user-turns are theme-fixed); and a **self-addressed `spt send` loops back\nthrough the endpoint's own translation binary** and lands as a delivered EVENT.\n\n## Decision\n\nCheckpoint is driven by a **self-sent loopback message**, with the commune **authored inline\npre-clear**:\n\n1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own\n   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a\n   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**\n   the clear.\n2. A new **PostToolUse hook** detects the trigger in the Write's `tool_input.content` (not a file\n   re-read — sidesteps the daemon-delete race), flips the perch idle (`spt api state idle`, so the\n   next delivery routes to the binary, not the poll channel), and **self-sends** a reserved\n   wire-sentinel: `spt send --from <id> <id> <wire-sentinel[+wake-text]>`.\n3. The message loops back into the endpoint's **own** translation binary, which recognizes the\n   wire-sentinel and emits the **clear+wake macro** — `ctrl+s · 50ms · /clear · enter · 500ms ·\n   <wake-text> · enter · commit` — instead of normal delivery. (`ctrl+s` first is required to stash\n   any lingering input.)\n4. Post-clear re-seed is carried by spt-core's `psyche-download` verb (v0.15.0 Tier-1 contract), **not**\n   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized\n   `<pending-commune>` (trigger stripped core-side), race-free.\n\n**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never\ndefers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale\ngap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.\n\n**Rejected alternatives:**\n- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's\n  single-writer ingest+delete.\n- *Daemon-push (`{\"type\":\"checkpoint\"}`)* — cleaner, but needs an spt-core capability absent from the\n  public surface; would block the feature on a core change.\n- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the\n  race-free re-seed (the `psyche-download` `<pending-*>` append).\n\n## Consequences\n\n- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n- **Cross-team contracts depended on:** `!!checkpoint!!` marker stripped core-side (todlando, W5\n  follow-up); `psyche-download` Tier-1 verb (doyle/todlando, ships v0.15.0 W5). The checkpoint\n  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.\n- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no\n  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted\n  lifecycle on purpose.\n- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n  clear is non-obvious) so it is not \"simplified\" away by a future reader.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0004-checkpoint-via-self-send-loopback.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"bec3811f","parentId":"77311a4c","timestamp":"2026-07-15T03:45:52.176Z","message":{"role":"toolResult","toolCallId":"call_V9ttSIkjl3d8jYBWOyOkSdRl|fc_0ca4af36969c80fa016a5702700b588198b6ebd179f254a035","toolName":"read","content":[{"type":"text","text":"<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n\n## Status\n\naccepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n\n## Context\n\nThe project carried **three** names for one thing, plus a launcher shortcut:\n\n| Identifier | Value | Where it shows |\n|---|---|---|\n| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n\nThree names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The skill prefix and the adapter id genuinely want different tokens.\n\n## Decision\n\nCollapse three names to **two**, by domain:\n\n- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n\nEnd state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n\n## Considered and rejected\n\n- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n\n## Consequences\n\n- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n"}],"details":{"displayContent":{"text":"<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n\n## Status\n\naccepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n\n## Context\n\nThe project carried **three** names for one thing, plus a launcher shortcut:\n\n| Identifier | Value | Where it shows |\n|---|---|---|\n| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n\nThree names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The skill prefix and the adapter id genuinely want different tokens.\n\n## Decision\n\nCollapse three names to **two**, by domain:\n\n- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n\nEnd state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n\n## Considered and rejected\n\n- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n\n## Consequences\n\n- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0005-name-unification.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"80ba7e45","parentId":"bec3811f","timestamp":"2026-07-15T03:45:52.176Z","message":{"role":"toolResult","toolCallId":"call_0c5RzMlPI4vBtlI9oYlUggb9|fc_0ca4af36969c80fa016a5702700b088198a6592f0056d4853a","toolName":"read","content":[{"type":"text","text":"# spt-claude-code — scoping decision ledger\n\n> Running source of truth for the scoping session (started 2026-06-13, doyle).\n> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n> This project will be re-homed onto the `experimplate` template structure once\n> that template is authored — until then this ledger is the interim record.\n\n## What this project is\n\n**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n`claude-spt` adapter) — simultaneously:\n1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n   delegating all core to `spt.exe`).\n2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n   `SaberMage/cplugs`, like legacy spt today.\n3. Invisibly an **spt-core installer** for users who don't have it. This is the\n   intended pattern for *all* casual-facing harness/shell adapters.\n\nParity is **user-facing feature parity, NOT 1:1** — most legacy machinery moved\nINTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy\nconcepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**\nfrom `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the\nspt-core source tree.\n\nLegacy parity inventory: captured in this session (claude_skill_owl v1.11.25 —\n7 hooks, 12 `/spt:*` skills, owl.exe, cplugs marketplace; messaging / live-agent /\nworking-perches / binary-handoff / psyche-sync / doctor).\n\n## Distribution architecture\n\n**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\nin-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n\n| Layer | Contents | Home | Update path | Churn |\n|---|---|---|---|---|\n| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n\n- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n- The \"bulk\" spt-core installs/updates = **manifest + binary**, not the skills.\n- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.\n- → ADR-0001 (to write).\n\n### Skill-instruction delivery — **LOCKED: UPS-injection** (fetch-stub VETOED)\n\nA `UserPromptSubmit` hook detects `/spt:X` in the prompt and injects X's real\ninstructions as additionalContext. SKILL.md files stay skeletons; instruction churn\nlives in spt-core-conducted `[strings]` (file-backed — see M12 dep).\n- **fetch-stub VETOED** (operator): adds terminal noise + latency + an extra tool call.\n- UPS-injection chosen on operator's empirical confidence from legacy: UPS *does* pick up\n  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n  empirically at build time**, but design on it.\n- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n  manifest doesn't bloat.\n- `[hints]` channel (same UPS hook, keyword-triggered) kept as-is — legacy proves it works.\n\n### Update notification — **LOCKED**\n\nspt-core conducts updates seamlessly → users don't check. Version-of-truth =\nmanifest/binary version spt-core tracks (`spt adapter list` / `/spt:version`), NOT the\n~static marketplace skeleton version. On update applied: spt-core announces via one-time\nSessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n\n## ccs integration\n\n**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\nmodel/billing backend differ, which is exactly the profile seam (leaf-replace\nspawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\nleaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\nprofile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\nReconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\nnot only bare adapter_name (profile model already supports composite addressing).\n\n## `/spt:setup` skill\n\nNeeded because most users install the plugin **mid-session** (no SessionStart fire).\nProposed paths:\n1. Generate `cc`/`cc <id>` launcher (capsule-style) at project root\n2. Offer `.gitignore` the launchers\n3. Offer create first subnet — surfaces a **QR code** of the TOTP seed → spawns a window\n   (self-elevating; see elevation + M12 dep #2)\n4. If spt-core already installed → branch: new subnet / **join subnet (= add this machine)** /\n   show join-code / just-add-endpoint  *(join == add-this-machine — collapsed)*\n5. Legacy migration — detect claude_skill_owl/owl → migrate identity+agents+psyche\n   (spt-core CONTEXT.md first-class commitment)\n6. OS-service registration (always-on daemon)\n7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value\n   prop) if absent\n8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n9. Doctor/verify at end\n\n**LOCKED:** v1 = {1,2,3,4,5,6,7}, defer {8,9}. Item #1 (`cc` launcher) IS in v1 — depends on\nfull-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\nflag if verify should fold inline._\n\n### `cc` launcher (capsule-style) — **LOCKED (design)**\n\n= thin wrapper over spt-core's **spt-hosted topology** (broker PTY + inject + attach),\nNOT a reimplemented psmux. `cc <id>` → `spt` spawns-or-attaches a CC endpoint in a broker\nPTY. Reattach-if-exists / `--live` / `--resume` = spt-core spawn/resume seam semantics.\nNo-id picker = reuse spt-core's built-in id-resolution (don't reimplement legacy SEED-001).\nSendkeys hazards = spt-core's inject concern, not the adapter's.\n**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\nhosted CC session (not just headless PTY).\n\n### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n\nDetect (interactive? elevated? desktop?) → least-friction path:\nWindows = self-elevating UAC window; Linux+desktop = pkexec/polkit or x-terminal-emulator;\nLinux+TTY = inline sudo; headless/no-TTY = print exact command, agent relays to user.\nQuestion the premise: scope elevation to only steps that need it (service-install /\nfirewall / privileged-port) — subnet-create itself likely unprivileged.\n\n## CI model\n\n**LOCKED.** Wholly **agent-driven, autonomous, no LLM-in-the-loop**, on the existing\nWin+Linux fleet (hfenduleam + kitsubito). GH runners DROPPED.\n- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.\n- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the\n  **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.\n- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods\n  the product as its own CI nervous system.\n- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n- **Trigger (LOCKED):** git post-push hook → `$OWL send` pings a fleet runner-agent → runs\n  gates → reports over spt. Manual \"run gates\" = fallback. (Polling rejected: latency + waste.)\n  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n- This CI pattern → reusable, goes in `experimplate`.\n\n## Parity-trim (→ REQ seed / acceptance bar)\n\n**DROP** (LOCKED): Spine & Touch; binary-handoff/owl.exe trampoline; TCP transport + SQLite\nspool + registry + listener internals; localhost-only networking. (all spt.exe/daemon now)\n\n**TRANSFORM** (LOCKED): Capsule→spt-hosted + `cc`; echo-commune Haiku JSONL→`[digest]`\nextractor (M10); psyche-sync→subnet/`/spt:setup`; `doctor`→`spt doctor`/setup-verify;\nworking-perches→`api worker-start/stop`.\n\n**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),\nprofiles + strings + hints wiring, ccs profiles.\n\n**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\nharness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n\n**KEEP — FINALIZED** (post-investigation 2026-06-14):\n- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`\n  → `spt endpoint list`.)\n- `/signoff` — KEEP. v1 = harness-hosted semantics (writes file; consumed at teardown). The\n  spt-hosted \"drop user from PTY\" UX only applies once `cc`/local-attach lands (fast-follow).\n- `/force-stop` — KEEP, topology-aware. spt-hosted routing already has CLI: `spt endpoint\n  shutdown` (graceful) + `spt endpoint stop` (soft), both exist.\n- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to\n  recover); restart = re-`ready`/re-`listen` or `endpoint stop`+bringup. Confirm.\n- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.\n- `/whoami` — **DROP skill**. Core `spt whoami` STAYS in hot-path but → **becomes an alias for\n  `spt endpoint list`** (M12), whose SELF-pin output gains the Self `endpoint description`.\n\n### v1 topology scope — **LOCKED** (operator vetoed harness-hosted-only)\nv1 acceptance = legacy parity **AND** cross-subnet/PTY proof (spt-core's central value prop is\nnetworking + PTY ownership). So **spt-hosted mode + local PTY attach + `spt endpoint run`\npicker are MANDATORY v1.** → `cc` launcher is IN v1; `/spt:setup` #1 back in v1.\n**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\nspt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n\n## spt-core upstream deps\nRecorded in `../spt-core/M12-CANDIDATES.md` (1: file-backed strings · 2: subnet QR +\nself-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets\nadapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.\n\n## experimplate (template) — resolved content\n- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH\n  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n  (not spt-core's two-key/counter runbook).\n- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;\n  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.\n- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.\n- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).\n\n## Decisions — 2026-06-18\n\n- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`\n  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,\n  which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a\n  profile any time) — recorded so the absence reads as intent, not omission.\n- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both\n  `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC\n  into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as\n  the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.\n- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every\n  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands\n  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded\n  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was\n  previously bare `claude`).\n\n## Open threads (grill queue)\n\n- [ ] CI trigger mechanism (git-hook signal vs polling routine)\n- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)\n- [ ] Docs split: what lands in experimplate vs spt-claude-code\n- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md\n- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred\n"}],"details":{"displayContent":{"text":"# spt-claude-code — scoping decision ledger\n\n> Running source of truth for the scoping session (started 2026-06-13, doyle).\n> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n> This project will be re-homed onto the `experimplate` template structure once\n> that template is authored — until then this ledger is the interim record.\n\n## What this project is\n\n**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n`claude-spt` adapter) — simultaneously:\n1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n   delegating all core to `spt.exe`).\n2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n   `SaberMage/cplugs`, like legacy spt today.\n3. Invisibly an **spt-core installer** for users who don't have it. This is the\n   intended pattern for *all* casual-facing harness/shell adapters.\n\nParity is **user-facing feature parity, NOT 1:1** — most legacy machinery moved\nINTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy\nconcepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**\nfrom `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the\nspt-core source tree.\n\nLegacy parity inventory: captured in this session (claude_skill_owl v1.11.25 —\n7 hooks, 12 `/spt:*` skills, owl.exe, cplugs marketplace; messaging / live-agent /\nworking-perches / binary-handoff / psyche-sync / doctor).\n\n## Distribution architecture\n\n**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\nin-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n\n| Layer | Contents | Home | Update path | Churn |\n|---|---|---|---|---|\n| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n\n- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n- The \"bulk\" spt-core installs/updates = **manifest + binary**, not the skills.\n- Plugin is a true thin skeleton (wrappers + bootstrap) → rarely needs a marketplace bump.\n- → ADR-0001 (to write).\n\n### Skill-instruction delivery — **LOCKED: UPS-injection** (fetch-stub VETOED)\n\nA `UserPromptSubmit` hook detects `/spt:X` in the prompt and injects X's real\ninstructions as additionalContext. SKILL.md files stay skeletons; instruction churn\nlives in spt-core-conducted `[strings]` (file-backed — see M12 dep).\n- **fetch-stub VETOED** (operator): adds terminal noise + latency + an extra tool call.\n- UPS-injection chosen on operator's empirical confidence from legacy: UPS *does* pick up\n  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n  empirically at build time**, but design on it.\n- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n  manifest doesn't bloat.\n- `[hints]` channel (same UPS hook, keyword-triggered) kept as-is — legacy proves it works.\n\n### Update notification — **LOCKED**\n\nspt-core conducts updates seamlessly → users don't check. Version-of-truth =\nmanifest/binary version spt-core tracks (`spt adapter list` / `/spt:version`), NOT the\n~static marketplace skeleton version. On update applied: spt-core announces via one-time\nSessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n\n## ccs integration\n\n**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\nmodel/billing backend differ, which is exactly the profile seam (leaf-replace\nspawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\nleaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\nprofile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\nReconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\nnot only bare adapter_name (profile model already supports composite addressing).\n\n## `/spt:setup` skill\n\nNeeded because most users install the plugin **mid-session** (no SessionStart fire).\nProposed paths:\n1. Generate `cc`/`cc <id>` launcher (capsule-style) at project root\n2. Offer `.gitignore` the launchers\n3. Offer create first subnet — surfaces a **QR code** of the TOTP seed → spawns a window\n   (self-elevating; see elevation + M12 dep #2)\n4. If spt-core already installed → branch: new subnet / **join subnet (= add this machine)** /\n   show join-code / just-add-endpoint  *(join == add-this-machine — collapsed)*\n5. Legacy migration — detect claude_skill_owl/owl → migrate identity+agents+psyche\n   (spt-core CONTEXT.md first-class commitment)\n6. OS-service registration (always-on daemon)\n7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value\n   prop) if absent\n8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n9. Doctor/verify at end\n\n**LOCKED:** v1 = {1,2,3,4,5,6,7}, defer {8,9}. Item #1 (`cc` launcher) IS in v1 — depends on\nfull-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\nflag if verify should fold inline._\n\n### `cc` launcher (capsule-style) — **LOCKED (design)**\n\n= thin wrapper over spt-core's **spt-hosted topology** (broker PTY + inject + attach),\nNOT a reimplemented psmux. `cc <id>` → `spt` spawns-or-attaches a CC endpoint in a broker\nPTY. Reattach-if-exists / `--live` / `--resume` = spt-core spawn/resume seam semantics.\nNo-id picker = reuse spt-core's built-in id-resolution (don't reimplement legacy SEED-001).\nSendkeys hazards = spt-core's inject concern, not the adapter's.\n**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\nhosted CC session (not just headless PTY).\n\n### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n\nDetect (interactive? elevated? desktop?) → least-friction path:\nWindows = self-elevating UAC window; Linux+desktop = pkexec/polkit or x-terminal-emulator;\nLinux+TTY = inline sudo; headless/no-TTY = print exact command, agent relays to user.\nQuestion the premise: scope elevation to only steps that need it (service-install /\nfirewall / privileged-port) — subnet-create itself likely unprivileged.\n\n## CI model\n\n**LOCKED.** Wholly **agent-driven, autonomous, no LLM-in-the-loop**, on the existing\nWin+Linux fleet (hfenduleam + kitsubito). GH runners DROPPED.\n- Gates (build, unit, `traceable-reqs check`, manifest-schema) = deterministic scripts.\n- Acceptance = scripted orchestration spawning real `claude`/headless sessions as the\n  **system-under-test** (LLM is SUT, never the runner); assert spt-state/digest output.\n- **Reporting bus = legacy spt** (`$OWL send` CI progress to responsible agent) — dogfoods\n  the product as its own CI nervous system.\n- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n- **Trigger (LOCKED):** git post-push hook → `$OWL send` pings a fleet runner-agent → runs\n  gates → reports over spt. Manual \"run gates\" = fallback. (Polling rejected: latency + waste.)\n  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n- This CI pattern → reusable, goes in `experimplate`.\n\n## Parity-trim (→ REQ seed / acceptance bar)\n\n**DROP** (LOCKED): Spine & Touch; binary-handoff/owl.exe trampoline; TCP transport + SQLite\nspool + registry + listener internals; localhost-only networking. (all spt.exe/daemon now)\n\n**TRANSFORM** (LOCKED): Capsule→spt-hosted + `cc`; echo-commune Haiku JSONL→`[digest]`\nextractor (M10); psyche-sync→subnet/`/spt:setup`; `doctor`→`spt doctor`/setup-verify;\nworking-perches→`api worker-start/stop`.\n\n**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),\nprofiles + strings + hints wiring, ccs profiles.\n\n**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\nharness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n\n**KEEP — FINALIZED** (post-investigation 2026-06-14):\n- `/live`, `/commune`, `/ready`, `/send`, `/new-alarm`, `/list-agents` — KEEP. (`/list-agents`\n  → `spt endpoint list`.)\n- `/signoff` — KEEP. v1 = harness-hosted semantics (writes file; consumed at teardown). The\n  spt-hosted \"drop user from PTY\" UX only applies once `cc`/local-attach lands (fast-follow).\n- `/force-stop` — KEEP, topology-aware. spt-hosted routing already has CLI: `spt endpoint\n  shutdown` (graceful) + `spt endpoint stop` (soft), both exist.\n- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to\n  recover); restart = re-`ready`/re-`listen` or `endpoint stop`+bringup. Confirm.\n- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.\n- `/whoami` — **DROP skill**. Core `spt whoami` STAYS in hot-path but → **becomes an alias for\n  `spt endpoint list`** (M12), whose SELF-pin output gains the Self `endpoint description`.\n\n### v1 topology scope — **LOCKED** (operator vetoed harness-hosted-only)\nv1 acceptance = legacy parity **AND** cross-subnet/PTY proof (spt-core's central value prop is\nnetworking + PTY ownership). So **spt-hosted mode + local PTY attach + `spt endpoint run`\npicker are MANDATORY v1.** → `cc` launcher is IN v1; `/spt:setup` #1 back in v1.\n**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\nspt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n\n## spt-core upstream deps\nRecorded in `../spt-core/M12-CANDIDATES.md` (1: file-backed strings · 2: subnet QR +\nself-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets\nadapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.\n\n## experimplate (template) — resolved content\n- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH\n  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n  (not spt-core's two-key/counter runbook).\n- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;\n  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.\n- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.\n- Must teach acquisition of `traceable-reqs` + the grill-with-docs skill (toml/INSTANTIATE).\n\n## Decisions — 2026-06-18\n\n- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`\n  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,\n  which `:ccs` already proves. `:ccs` is now the **sole shipped overlay**. Reversible (re-add a\n  profile any time) — recorded so the absence reads as intent, not omission.\n- **Non-interactive spt-spawned CC carries `--dangerously-skip-permissions` — LOCKED.** Both\n  `[session.self]` bringup commands (base `claude`, the `ccs` profile) pass it: the broker spawns CC\n  into a PTY with no operator, so the permission gate would deadlock the launch. Same root cause as\n  the Psyche (below); cross-cutting invariant in `docs/KNOWN-HAZARDS.md` §2.2.\n- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every\n  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands\n  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded\n  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was\n  previously bare `claude`).\n\n## Open threads (grill queue)\n\n- [ ] CI trigger mechanism (git-hook signal vs polling routine)\n- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)\n- [ ] Docs split: what lands in experimplate vs spt-claude-code\n- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md\n- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred\n","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,167,168,169,170,171,172,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\SCOPE.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"00a3201b","parentId":"80ba7e45","timestamp":"2026-07-15T03:45:52.176Z","message":{"role":"toolResult","toolCallId":"call_Ida6lqZAsBvP4UG8jDiU42X5|fc_0ca4af36969c80fa016a5702700b64819896a23e545a6d8646","toolName":"read","content":[{"type":"text","text":"# One-command-ish update + a single consolidated adapter binary\n\n## Status\n\naccepted (2026-06-24) — extends ADR-0001 (distribution splits by volatility). Some pieces depend on spt-core capabilities not yet shipped (the \"doyle asks\" below); those parts are decided in shape but gated on delivery.\n\n**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\ndispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\nlet the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\nin `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\nhook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\nhook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n<!-- [doc->REQ-DIST-HOOK-BINARY] -->\nSee `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n\n## Context\n\nADR-0001 split distribution into a low-churn **cplugs skeleton** (hooks, skill skeletons, `plugin.json`, bootstrap — updated by `claude plugin update`) and an spt-conducted **adapter** (manifest + binaries + strings — updated by `spt adapter update`). The operator wants the whole thing kept current with effectively **one lever**, and wants install and update to feel **harmonious** (no hunting for the update command after install) and to work from the **CLI** (not only inside an endpoint session).\n\nTwo facts bound the design:\n\n1. **Hook *logic* must live in the CC plugin dir** (CC loads hooks from there), outside `spt adapter update`'s reach. So \"one command updates everything\" is unreachable purely inside this repo — it needs spt-core capabilities → findings, not workarounds (public-surface-only).\n2. **Raw skills/commands can't reliably replace the plugin.** Reopened and re-rejected: user-scope skills are flat (no namespace), and CC slash-command subdirectory namespacing is broken ([anthropics/claude-code#2422] — subdirs show in the description but don't namespace the invocation). The plugin is the only reliable `/spt:*` + the only clean hooks bundle. The eloquent thing to borrow from suite installers (e.g. gsd-core) is their **installer/onboarding UX**, not going raw.\n\n## Decision\n\n**Keep the plugin** (reaffirms ADR-0001). Make the update path one-lever-shaped and consolidate the adapter's binaries.\n\n### Update mechanics\n\n- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n\n### Binary consolidation\n\nCollapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n\n- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n\nThe **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n\n### The spt-core asks (findings to doyle — decided in shape, gated on delivery)\n\n1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.\n2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **\"changed\"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.\n3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.\n\n## Consequences\n\n- The thin-skeleton goal advances even before the asks land: skill bodies already ride strings; the **reactive skills** (`commune`/`send`/`signoff`, full-fat in the plugin because they are invoked without a typed slash-command) can be thinned to stubs by moving their bodies into the perched SessionStart brief (adapter strings) plus the `/spt:live` UPS body — with a known **delivery-timing wrinkle** (the perched brief fires on bind|boundary, not the instant a seed session goes live), tracked as a plan item, not blocking.\n- Until asks #1/#3 land, hook logic and the translate binary stay on their current channels; the design degrades gracefully (the `message` field bridges the manual steps).\n- One artifact per OS simplifies packaging but couples all seams' build/release — acceptable, they already ship in one `.spt`.\n"}],"details":{"displayContent":{"text":"# One-command-ish update + a single consolidated adapter binary\n\n## Status\n\naccepted (2026-06-24) — extends ADR-0001 (distribution splits by volatility). Some pieces depend on spt-core capabilities not yet shipped (the \"doyle asks\" below); those parts are decided in shape but gated on delivery.\n\n**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\ndispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\nlet the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\nin `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\nhook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\nhook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n<!-- [doc->REQ-DIST-HOOK-BINARY] -->\nSee `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n\n## Context\n\nADR-0001 split distribution into a low-churn **cplugs skeleton** (hooks, skill skeletons, `plugin.json`, bootstrap — updated by `claude plugin update`) and an spt-conducted **adapter** (manifest + binaries + strings — updated by `spt adapter update`). The operator wants the whole thing kept current with effectively **one lever**, and wants install and update to feel **harmonious** (no hunting for the update command after install) and to work from the **CLI** (not only inside an endpoint session).\n\nTwo facts bound the design:\n\n1. **Hook *logic* must live in the CC plugin dir** (CC loads hooks from there), outside `spt adapter update`'s reach. So \"one command updates everything\" is unreachable purely inside this repo — it needs spt-core capabilities → findings, not workarounds (public-surface-only).\n2. **Raw skills/commands can't reliably replace the plugin.** Reopened and re-rejected: user-scope skills are flat (no namespace), and CC slash-command subdirectory namespacing is broken ([anthropics/claude-code#2422] — subdirs show in the description but don't namespace the invocation). The plugin is the only reliable `/spt:*` + the only clean hooks bundle. The eloquent thing to borrow from suite installers (e.g. gsd-core) is their **installer/onboarding UX**, not going raw.\n\n## Decision\n\n**Keep the plugin** (reaffirms ADR-0001). Make the update path one-lever-shaped and consolidate the adapter's binaries.\n\n### Update mechanics\n\n- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n\n### Binary consolidation\n\nCollapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n\n- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n\nThe **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n\n### The spt-core asks (findings to doyle — decided in shape, gated on delivery)\n\n1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.\n2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **\"changed\"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.\n3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.\n\n## Consequences\n\n- The thin-skeleton goal advances even before the asks land: skill bodies already ride strings; the **reactive skills** (`commune`/`send`/`signoff`, full-fat in the plugin because they are invoked without a typed slash-command) can be thinned to stubs by moving their bodies into the perched SessionStart brief (adapter strings) plus the `/spt:live` UPS body — with a known **delivery-timing wrinkle** (the perched brief fires on bind|boundary, not the instant a seed session goes live), tracked as a plan item, not blocking.\n- Until asks #1/#3 land, hook logic and the translate binary stay on their current channels; the design degrades gracefully (the `message` field bridges the manual steps).\n- One artifact per OS simplifies packaging but couples all seams' build/release — acceptable, they already ship in one `.spt`.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0006-one-command-update-and-consolidated-binary.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"a202ddcc","parentId":"00a3201b","timestamp":"2026-07-15T03:45:52.177Z","message":{"role":"toolResult","toolCallId":"call_pQm8b1aOJg3cdyoHmESa4pyJ|fc_0ca4af36969c80fa016a5702700b4881989c86812fad9a8363","toolName":"read","content":[{"type":"text","text":"<!-- [doc->REQ-SKILL-LIVE] -->\n<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n# The Psyche runs as a constrained, auto-approving companion (legacy owl parity)\n\n## Status\n\naccepted (2026-06-18)\n\n## Context\n\nA live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\nhosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\nsubcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\nthe runner seeds one headless `claude` turn from the daemon-supplied prompt,\nthen drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\non each pulse.\n\nForces:\n\n1. **No operator is attached.** The Psyche is detached and its stdio is discarded. Anything that\n   would normally prompt the user — a tool-permission gate — has nothing to answer it, so the turn\n   would block forever and the daemon would believe it hosts a working companion that silently never\n   communes. (Generalizes to the `[session.self]` bringup, which spawns CC into a broker PTY with no\n   operator at spawn.)\n2. **A commune needs file IO, nothing more.** The Psyche reads context and writes its commune\n   delta. It does not run the build, hit the network, or message other agents. Granting it the\n   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.\n3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;\n   leaving the model unpinned lets it inherit whatever the parent uses.\n4. **A proven reference exists.** The legacy sibling `claude_skill_owl`\n   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final\n   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe\n   default, not a novel design.\n\n## Decision\n\nEvery Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\nlegacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n`pulse_cmd`:\n\n- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n- `--disable-slash-commands` — the Psyche is driven by its prompt, not a slash surface.\n- `--dangerously-skip-permissions` — **required**, not cosmetic: with no operator/stdin (force 1),\n  an interactive permission prompt would deadlock the turn. Auto-approve is *bounded* because it sits\n  inside the Read/Edit/Write cap.\n- `--model sonnet --fallback-model opus --effort medium` — pin the cheap companion (force 3).\n\nThe same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\nthe flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\nrecorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n\nRejected: launching the Psyche as a bare, unconstrained `claude` (the pre-2026-06-18 state). It gave\nthe unattended companion the full toolset *and* left it without skip-permissions — broader blast\nradius and a latent detached-deadlock at once.\n\n## Consequences\n\n- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future\n  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a\n  deliberate change to `sandbox_flags()` + this ADR, not an accident.\n- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination\n  with the tool cap. The two must move together — never widen the tools without re-justifying the\n  auto-approve, and never drop the cap while keeping skip-permissions.\n- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n  sibling, and surfaces loudly (no commune produced).\n- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n  (the deadlock invariant). `unit` evidence is the\n  `every_turn_is_sandboxed_to_legacy_owl_parity` test plus the manifest assertion on the two\n  `[session.self]` commands.\n- The shipped `dist/*.spt` still embed the pre-sandbox psyche binary; they pick this up on the next\n  release rebuild (held for a versioned release).\n"}],"details":{"displayContent":{"text":"<!-- [doc->REQ-SKILL-LIVE] -->\n<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n# The Psyche runs as a constrained, auto-approving companion (legacy owl parity)\n\n## Status\n\naccepted (2026-06-18)\n\n## Context\n\nA live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\nhosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\nsubcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\nthe runner seeds one headless `claude` turn from the daemon-supplied prompt,\nthen drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\non each pulse.\n\nForces:\n\n1. **No operator is attached.** The Psyche is detached and its stdio is discarded. Anything that\n   would normally prompt the user — a tool-permission gate — has nothing to answer it, so the turn\n   would block forever and the daemon would believe it hosts a working companion that silently never\n   communes. (Generalizes to the `[session.self]` bringup, which spawns CC into a broker PTY with no\n   operator at spawn.)\n2. **A commune needs file IO, nothing more.** The Psyche reads context and writes its commune\n   delta. It does not run the build, hit the network, or message other agents. Granting it the\n   parent agent's full toolset is unnecessary blast radius for an unattended, auto-approving process.\n3. **It must not silently ride the parent's heavy model.** The companion is a cheap, frequent actor;\n   leaving the model unpinned lets it inherit whatever the parent uses.\n4. **A proven reference exists.** The legacy sibling `claude_skill_owl`\n   (`src/live/wrapper/claude.rs`) already solved this exact shape — its psyche init/resume/final\n   invocations are uniformly sandboxed and auto-approving. Parity with that posture is the safe\n   default, not a novel design.\n\n## Decision\n\nEvery Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\nlegacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n`pulse_cmd`:\n\n- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n- `--disable-slash-commands` — the Psyche is driven by its prompt, not a slash surface.\n- `--dangerously-skip-permissions` — **required**, not cosmetic: with no operator/stdin (force 1),\n  an interactive permission prompt would deadlock the turn. Auto-approve is *bounded* because it sits\n  inside the Read/Edit/Write cap.\n- `--model sonnet --fallback-model opus --effort medium` — pin the cheap companion (force 3).\n\nThe same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\nthe flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\nrecorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n\nRejected: launching the Psyche as a bare, unconstrained `claude` (the pre-2026-06-18 state). It gave\nthe unattended companion the full toolset *and* left it without skip-permissions — broader blast\nradius and a latent detached-deadlock at once.\n\n## Consequences\n\n- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future\n  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a\n  deliberate change to `sandbox_flags()` + this ADR, not an accident.\n- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination\n  with the tool cap. The two must move together — never widen the tools without re-justifying the\n  auto-approve, and never drop the cap while keeping skip-permissions.\n- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n  sibling, and surfaces loudly (no commune produced).\n- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n  (the deadlock invariant). `unit` evidence is the\n  `every_turn_is_sandboxed_to_legacy_owl_parity` test plus the manifest assertion on the two\n  `[session.self]` commands.\n- The shipped `dist/*.spt` still embed the pre-sandbox psyche binary; they pick this up on the next\n  release rebuild (held for a versioned release).\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0003-psyche-is-a-constrained-auto-approving-companion.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"b0bffc97","parentId":"a202ddcc","timestamp":"2026-07-15T03:45:52.177Z","message":{"role":"toolResult","toolCallId":"call_e42F4IaMa5qs0QA6bnhyMw28|fc_0ca4af36969c80fa016a5702700b108198992fc6f291b41337","toolName":"read","content":[{"type":"text","text":"# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n\n> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n> **oh-my-pi (omp)** — the terminal coding agent at\n> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n> `omp v16.3.11` (`C:\\Users\\Admin\\AppData\\Local\\omp\\omp.exe`, config `~/.omp`).\n>\n> **Why:** the interim engine for the Athenaeum **Librarian** (Athenaeum\n> DESIGN.md D10) — a persistent, SPT-addressable agent — while spt-core does not\n> yet speak ACP. When/if spt-core gains an ACP client, an ACP bridge supersedes\n> this (omp's RPC mode is already ACP-shaped, so this is a stepping stone, not a\n> dead end).\n\n## The decisive finding: drive omp over its RPC mode, not by scraping hooks\n\nclaude-spt exists because Claude Code has **no control protocol** — the adapter\nreverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\ndigest, and pushes received messages back via `additionalContext`. Most of its\nversion history (0.6→0.17, the `SPT_HARNESS_ADAPTER_TIPS.md` hazards, the\necho-gate, force-native decay, truncation spills) is fighting that indirect,\ninjection-based coupling.\n\n**omp has a native host-drives-agent surface that removes all of that:**\n`omp --mode rpc` (per `docs/rpc.md`) is a newline-delimited JSON protocol over\nstdio —\n\n| Direction | Frame |\n|---|---|\n| omp → host (on start) | `{ \"type\": \"ready\" }` |\n| host → omp (drive a turn) | `{ \"type\": \"prompt\", \"message\": \"…\" }` (acked immediately, `data.agentInvoked`) |\n| host → omp (mid-turn) | `{ \"type\": \"steer\", … }` (interrupt) · `{ \"type\": \"follow_up\", … }` (post-turn) |\n| omp → host (turn stream) | `agent_start` → `message_update`… → `agent_end` |\n| host → omp (introspect) | `{ \"type\": \"get_state\" }` → `sessionId`, `sessionFile`, model, tools |\n\nThis maps **directly** onto what SPT needs from a driven agent:\n\n- **ready-agent** = a resident `omp --mode rpc`; each inbound SPT message →\n  `{type:\"prompt\"}`; `agent_end` → `spt api state idle`, `agent_start` →\n  `state busy`.\n- **live/psyche resume-turn** = the daemon (or bridge) sends one `{type:\"prompt\"}`\n  per event against the held session; continuity is omp's own session file + id.\n- **mid-turn delivery** = `steer` / `follow_up` — no `additionalContext`, no\n  truncation/echo-gate class at all.\n\n### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n\n**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\nspawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\nruns `omp --mode rpc` as a child, and translates in both directions:\n\n```\nSPT daemon ──spawn──▶ omp-spt-bridge ──stdio JSON──▶ omp --mode rpc\n                          │  spt api seed/bind/poll/state/worker-*   (to spt-core)\n                          └  prompt/steer/follow_up/get_state        (to omp)\n```\n\nThis remains useful when no operator terminal is expected. It cannot satisfy an\nattached endpoint because `omp --mode rpc` creates no native TUI.\n\n**Style B — native omp loads an SPT extension (CHOSEN for `[session.self]`).** omp\n*does* have a hook system (`docs/hooks.md`): a TS factory `export default function\nhook(pi){ pi.on(\"session_start\"|\"turn_start\"|\"turn_end\"|\"tool_call\"|\"context\"|\n\"session_shutdown\", …) }`, and the `context` event can inject via\n`return { messages }`. The delivered extension instead uses the public\n`sendUserMessage` action, so OMP handles the prompt normally while remaining the\nPTY leader. It binds/listens through SPT and routes the final assistant response\nto the sender.\n\nRationale, in one line: **the process whose UI the operator must see must own the\nPTY.** See ADR-0007. <!-- [doc->REQ-OMP-NATIVE-TUI] -->\n\n## Seam map — claude-spt value → omp-spt target\n\n| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n|---|---|---|\n| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n| `[adapter].shortcut_basename` | `cc` | `omp` |\n| `[adapter].kind` | `harness` | `harness` (unchanged) |\n| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n\n### Session/transcript (for the `[digest]` extractor)\n\nPer `docs/session.md`: omp writes **JSONL** at\n`~/.omp/agent/sessions/<dir-encoded>/<timestamp>_<sessionId>.jsonl`\n(`<dir-encoded>` = home-relative path, `/ \\ :` → `-`; `--session-dir` overrides).\nLine 1 = header `{type:\"session\", id, timestamp, cwd, version}`; rest = entries\n`{type, id, parentId, timestamp}`, message entries carrying an `AgentMessage`\n(`role`, `provider`, `model`, `content`). `sessionId` = 8-hex; resume by id\nprefix / filename prefix. **This is close to CC's JSONL** — the extractor rework\nis: resolve the dir-encoding, parse header+entries, emit the neutral\n`{role∈input|agent|tool, text?, tool?, ts?}` NDJSON spt-core expects (UTF-8\nstdout — the Windows cp1252 trap from the tips doc applies).\n\n### Identity binding (no native endpoint-id flag)\n\nomp has no \"inject an endpoint id\" flag, but the bridge is the hosted process and\ncan call `get_state` → `sessionId`, then `spt api bind <id> --set-session-id\n<sessionId>` (broker-spawned auth is intrinsic — tips doc §Bringup). Pass the SPT\nendpoint id into the bridge via `[env.SPT_ENDPOINT_ID]` (`direction=\"inject\"`,\n`value=\"{id}\"`).\n\n## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n\n- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n  (bridge command), `[identity]`. Prove with\n  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n  Acceptance: from another node, message the perch → get an omp answer; assert\n  `spt` state transitions (isolated CI identity per tips doc §Testing).\n- **S3 — digest/history extractor.** omp-JSONL → neutral NDJSON; validate with\n  `spt adapter digest-proof omp-spt --sample <session.jsonl>`.\n- **S4 — live/psyche.** `[session.psyche_init]` gate + the bridge as the\n  per-event resume-turn runner; verify daemon-driven turns don't starve.\n- **S5 — OpenRouter provider (#3).** Ship a documented `models.yml` fragment +\n  `.env` convention; **no adapter code** (see below). Role routing\n  (default/smol/slow/plan) + fallback chains are native.\n\n## #3 OpenRouter billing — it's omp config, not adapter code\n\nPer `docs/providers.md`, `~/.omp/agent/models.yml` takes custom\nopenai-completions providers; OpenRouter is:\n\n```yaml\nproviders:\n  openrouter:\n    baseUrl: https://openrouter.ai/api/v1\n    api: openai-completions\n    apiKey: OPENROUTER_API_KEY   # env-var-name-or-literal\n    models:\n      - id: anthropic/claude-sonnet-5\n        name: Claude Sonnet 5 (OpenRouter)\n        contextWindow: 200000\n        maxTokens: 8192\n```\n\nKey precedence (first wins): CLI `--api-key` → `models.yml apiKey` → stored cred →\nOAuth → provider env/`.env` → fallback resolver. `.env` layering:\n`cwd/.env` → `~/.omp/agent/.env` → `~/.omp/.env` → `~/.env`. So the adapter's job\nfor #3 is only to (a) ship/document this fragment and (b) select roles to the\nOpenRouter models at Librarian bringup — the billing routing itself is omp's.\n\n## Open questions — verify against the live `omp` + `spt` binaries\n\nPer the tips doc discipline (JSON-schema validity ≠ runtime; validate on the live\nbinary), confirm before/while building:\n\n1. **RPC session lifecycle across turns** — does a resident `omp --mode rpc` hold\n   one session for many `prompt`s (ready-agent), and can the bridge start it on a\n   *resumed* session id for the live path? (`get_state` + `switch_session`/\n   `new_session`/`branch` in `docs/rpc.md`; test locally.)\n2. **Daemon hosting a resident bridge** — spt-core's broker spawns\n   `[session.self].command` into a PTY; confirm a bridge that itself holds a child\n   `omp --mode rpc` survives as the hosted process (vs the ephemeral per-event\n   psyche model). Decide ready-resident vs live-ephemeral per role.\n3. **dir-encoding exactness** — reproduce omp's `<dir-encoded>` rule precisely for\n   the digest locate (home-relative, `/ \\ :`→`-`); handle `--session-dir` override.\n4. **Hook fallback need** — anything the RPC stream doesn't expose that we still\n   want (e.g. `tool_call` gating, `session_shutdown` cleanup) → a thin omp hook\n   extension (Style B, `docs/hooks.md`).\n5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest\n   `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2\n   actually calls).\n\n## Validation harness (all on the public surface, no live model needed)\n\n- `spt api --adapter omp-spt --manifest <file> capability` — hostable-types check.\n- `spt adapter add adapter/` → `spt adapter list` (adapter + profiles resolve) →\n  `spt adapter get-string` (base + overlay diffs) → `spt adapter remove`.\n- `spt adapter digest-proof omp-spt --sample <omp-session.jsonl>` — extractor E2E.\n- Acceptance: spawn a real `omp --mode rpc` as SUT under a **disposable identity**\n  (never the operator's — perches are name-keyed, last-establish-wins); assert on a\n  hook/state side-effect, not model output.\n"}],"details":{"displayContent":{"text":"# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n\n> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n> **oh-my-pi (omp)** — the terminal coding agent at\n> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n> `omp v16.3.11` (`C:\\Users\\Admin\\AppData\\Local\\omp\\omp.exe`, config `~/.omp`).\n>\n> **Why:** the interim engine for the Athenaeum **Librarian** (Athenaeum\n> DESIGN.md D10) — a persistent, SPT-addressable agent — while spt-core does not\n> yet speak ACP. When/if spt-core gains an ACP client, an ACP bridge supersedes\n> this (omp's RPC mode is already ACP-shaped, so this is a stepping stone, not a\n> dead end).\n\n## The decisive finding: drive omp over its RPC mode, not by scraping hooks\n\nclaude-spt exists because Claude Code has **no control protocol** — the adapter\nreverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\ndigest, and pushes received messages back via `additionalContext`. Most of its\nversion history (0.6→0.17, the `SPT_HARNESS_ADAPTER_TIPS.md` hazards, the\necho-gate, force-native decay, truncation spills) is fighting that indirect,\ninjection-based coupling.\n\n**omp has a native host-drives-agent surface that removes all of that:**\n`omp --mode rpc` (per `docs/rpc.md`) is a newline-delimited JSON protocol over\nstdio —\n\n| Direction | Frame |\n|---|---|\n| omp → host (on start) | `{ \"type\": \"ready\" }` |\n| host → omp (drive a turn) | `{ \"type\": \"prompt\", \"message\": \"…\" }` (acked immediately, `data.agentInvoked`) |\n| host → omp (mid-turn) | `{ \"type\": \"steer\", … }` (interrupt) · `{ \"type\": \"follow_up\", … }` (post-turn) |\n| omp → host (turn stream) | `agent_start` → `message_update`… → `agent_end` |\n| host → omp (introspect) | `{ \"type\": \"get_state\" }` → `sessionId`, `sessionFile`, model, tools |\n\nThis maps **directly** onto what SPT needs from a driven agent:\n\n- **ready-agent** = a resident `omp --mode rpc`; each inbound SPT message →\n  `{type:\"prompt\"}`; `agent_end` → `spt api state idle`, `agent_start` →\n  `state busy`.\n- **live/psyche resume-turn** = the daemon (or bridge) sends one `{type:\"prompt\"}`\n  per event against the held session; continuity is omp's own session file + id.\n- **mid-turn delivery** = `steer` / `follow_up` — no `additionalContext`, no\n  truncation/echo-gate class at all.\n\n### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n\n**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\nspawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\nruns `omp --mode rpc` as a child, and translates in both directions:\n\n```\nSPT daemon ──spawn──▶ omp-spt-bridge ──stdio JSON──▶ omp --mode rpc\n                          │  spt api seed/bind/poll/state/worker-*   (to spt-core)\n                          └  prompt/steer/follow_up/get_state        (to omp)\n```\n\nThis remains useful when no operator terminal is expected. It cannot satisfy an\nattached endpoint because `omp --mode rpc` creates no native TUI.\n\n**Style B — native omp loads an SPT extension (CHOSEN for `[session.self]`).** omp\n*does* have a hook system (`docs/hooks.md`): a TS factory `export default function\nhook(pi){ pi.on(\"session_start\"|\"turn_start\"|\"turn_end\"|\"tool_call\"|\"context\"|\n\"session_shutdown\", …) }`, and the `context` event can inject via\n`return { messages }`. The delivered extension instead uses the public\n`sendUserMessage` action, so OMP handles the prompt normally while remaining the\nPTY leader. It binds/listens through SPT and routes the final assistant response\nto the sender.\n\nRationale, in one line: **the process whose UI the operator must see must own the\nPTY.** See ADR-0007. <!-- [doc->REQ-OMP-NATIVE-TUI] -->\n\n## Seam map — claude-spt value → omp-spt target\n\n| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n|---|---|---|\n| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n| `[adapter].shortcut_basename` | `cc` | `omp` |\n| `[adapter].kind` | `harness` | `harness` (unchanged) |\n| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n\n### Session/transcript (for the `[digest]` extractor)\n\nPer `docs/session.md`: omp writes **JSONL** at\n`~/.omp/agent/sessions/<dir-encoded>/<timestamp>_<sessionId>.jsonl`\n(`<dir-encoded>` = home-relative path, `/ \\ :` → `-`; `--session-dir` overrides).\nLine 1 = header `{type:\"session\", id, timestamp, cwd, version}`; rest = entries\n`{type, id, parentId, timestamp}`, message entries carrying an `AgentMessage`\n(`role`, `provider`, `model`, `content`). `sessionId` = 8-hex; resume by id\nprefix / filename prefix. **This is close to CC's JSONL** — the extractor rework\nis: resolve the dir-encoding, parse header+entries, emit the neutral\n`{role∈input|agent|tool, text?, tool?, ts?}` NDJSON spt-core expects (UTF-8\nstdout — the Windows cp1252 trap from the tips doc applies).\n\n### Identity binding (no native endpoint-id flag)\n\nomp has no \"inject an endpoint id\" flag, but the bridge is the hosted process and\ncan call `get_state` → `sessionId`, then `spt api bind <id> --set-session-id\n<sessionId>` (broker-spawned auth is intrinsic — tips doc §Bringup). Pass the SPT\nendpoint id into the bridge via `[env.SPT_ENDPOINT_ID]` (`direction=\"inject\"`,\n`value=\"{id}\"`).\n\n## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n\n- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n  (bridge command), `[identity]`. Prove with\n  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n  Acceptance: from another node, message the perch → get an omp answer; assert\n  `spt` state transitions (isolated CI identity per tips doc §Testing).\n- **S3 — digest/history extractor.** omp-JSONL → neutral NDJSON; validate with\n  `spt adapter digest-proof omp-spt --sample <session.jsonl>`.\n- **S4 — live/psyche.** `[session.psyche_init]` gate + the bridge as the\n  per-event resume-turn runner; verify daemon-driven turns don't starve.\n- **S5 — OpenRouter provider (#3).** Ship a documented `models.yml` fragment +\n  `.env` convention; **no adapter code** (see below). Role routing\n  (default/smol/slow/plan) + fallback chains are native.\n\n## #3 OpenRouter billing — it's omp config, not adapter code\n\nPer `docs/providers.md`, `~/.omp/agent/models.yml` takes custom\nopenai-completions providers; OpenRouter is:\n\n```yaml\nproviders:\n  openrouter:\n    baseUrl: https://openrouter.ai/api/v1\n    api: openai-completions\n    apiKey: OPENROUTER_API_KEY   # env-var-name-or-literal\n    models:\n      - id: anthropic/claude-sonnet-5\n        name: Claude Sonnet 5 (OpenRouter)\n        contextWindow: 200000\n        maxTokens: 8192\n```\n\nKey precedence (first wins): CLI `--api-key` → `models.yml apiKey` → stored cred →\nOAuth → provider env/`.env` → fallback resolver. `.env` layering:\n`cwd/.env` → `~/.omp/agent/.env` → `~/.omp/.env` → `~/.env`. So the adapter's job\nfor #3 is only to (a) ship/document this fragment and (b) select roles to the\nOpenRouter models at Librarian bringup — the billing routing itself is omp's.\n\n## Open questions — verify against the live `omp` + `spt` binaries\n\nPer the tips doc discipline (JSON-schema validity ≠ runtime; validate on the live\nbinary), confirm before/while building:\n\n1. **RPC session lifecycle across turns** — does a resident `omp --mode rpc` hold\n   one session for many `prompt`s (ready-agent), and can the bridge start it on a\n   *resumed* session id for the live path? (`get_state` + `switch_session`/\n   `new_session`/`branch` in `docs/rpc.md`; test locally.)\n2. **Daemon hosting a resident bridge** — spt-core's broker spawns\n   `[session.self].command` into a PTY; confirm a bridge that itself holds a child\n   `omp --mode rpc` survives as the hosted process (vs the ephemeral per-event\n   psyche model). Decide ready-resident vs live-ephemeral per role.\n3. **dir-encoding exactness** — reproduce omp's `<dir-encoded>` rule precisely for\n   the digest locate (home-relative, `/ \\ :`→`-`); handle `--session-dir` override.\n4. **Hook fallback need** — anything the RPC stream doesn't expose that we still\n   want (e.g. `tool_call` gating, `session_shutdown` cleanup) → a thin omp hook\n   extension (Style B, `docs/hooks.md`).\n5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest\n   `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2\n   actually calls).\n\n## Validation harness (all on the public surface, no live model needed)\n\n- `spt api --adapter omp-spt --manifest <file> capability` — hostable-types check.\n- `spt adapter add adapter/` → `spt adapter list` (adapter + profiles resolve) →\n  `spt adapter get-string` (base + overlay diffs) → `spt adapter remove`.\n- `spt adapter digest-proof omp-spt --sample <omp-session.jsonl>` — extractor E2E.\n- Acceptance: spawn a real `omp --mode rpc` as SUT under a **disposable identity**\n  (never the operator's — perches are name-keyed, last-establish-wins); assert on a\n  hook/state side-effect, not model output.\n","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,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\OMP-ADAPTER-PLAN.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"71ae8620","parentId":"b0bffc97","timestamp":"2026-07-15T03:45:52.177Z","message":{"role":"toolResult","toolCallId":"call_wqpdtDQmdZIkrHF9O2fPkqmO|fc_0ca4af36969c80fa016a5702700b708198a56013432a6f488a","toolName":"read","content":[{"type":"text","text":"# Native OMP TUI hosts the SPT extension\n\n## Status\n\naccepted (2026-07-13) — supersedes the interactive-hosting choice in `OMP-ADAPTER-PLAN.md`; the RPC bridge remains a headless integration surface.\n\n## Context\n\nIssue #2 requires `spt endpoint run --adapter omp-spt ...` to expose the harness PTY. The shipped session command instead made `omp-spt bridge` the PTY leader and launched `omp --mode rpc` behind pipes. Attaching therefore showed bridge logs, while standalone `omp` showed the expected native terminal UI. No attach flag can recover a UI that the hosted process never created.\n\n## Decision\n\nRun native interactive `omp` as `[session.self]` and `[session.resume]`. Load a packaged OMP extension that binds the OMP session to the SPT endpoint, listens for SPT messages, submits each message through `sendUserMessage`, tracks busy/idle state, and sends the final assistant response back to the sender.\n\nThe extension uses OMP's public extension API. It does not render or proxy the terminal: OMP remains the PTY leader and owns its complete native TUI. The old RPC bridge remains in the binary for explicitly headless callers, but endpoint bringup no longer routes through it.\n\n<!-- [doc->REQ-OMP-NATIVE-TUI] -->\n\n## Consequences\n\n- `spt rc <id>` and attach-default endpoint bringup display the same native TUI as standalone `omp`.\n- SPT delivery is serialized so two inbound messages cannot collapse into one OMP turn or receive the wrong reply.\n- Extension code ships under the already-packaged `strings/` directory, avoiding a new archive layout or dependency.\n- OMP profile/model flags belong directly on the manifest's `omp` command instead of after the bridge's `--` separator.\n"}],"details":{"displayContent":{"text":"# Native OMP TUI hosts the SPT extension\n\n## Status\n\naccepted (2026-07-13) — supersedes the interactive-hosting choice in `OMP-ADAPTER-PLAN.md`; the RPC bridge remains a headless integration surface.\n\n## Context\n\nIssue #2 requires `spt endpoint run --adapter omp-spt ...` to expose the harness PTY. The shipped session command instead made `omp-spt bridge` the PTY leader and launched `omp --mode rpc` behind pipes. Attaching therefore showed bridge logs, while standalone `omp` showed the expected native terminal UI. No attach flag can recover a UI that the hosted process never created.\n\n## Decision\n\nRun native interactive `omp` as `[session.self]` and `[session.resume]`. Load a packaged OMP extension that binds the OMP session to the SPT endpoint, listens for SPT messages, submits each message through `sendUserMessage`, tracks busy/idle state, and sends the final assistant response back to the sender.\n\nThe extension uses OMP's public extension API. It does not render or proxy the terminal: OMP remains the PTY leader and owns its complete native TUI. The old RPC bridge remains in the binary for explicitly headless callers, but endpoint bringup no longer routes through it.\n\n<!-- [doc->REQ-OMP-NATIVE-TUI] -->\n\n## Consequences\n\n- `spt rc <id>` and attach-default endpoint bringup display the same native TUI as standalone `omp`.\n- SPT delivery is serialized so two inbound messages cannot collapse into one OMP turn or receive the wrong reply.\n- Extension code ships under the already-packaged `strings/` directory, avoiding a new archive layout or dependency.\n- OMP profile/model flags belong directly on the manifest's `omp` command instead of after the bridge's `--` separator.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0007-native-omp-tui-hosts-spt-extension.md"}}},"isError":false,"timestamp":1784087152176}}
{"type":"message","id":"d4e6f573","parentId":"71ae8620","timestamp":"2026-07-15T03:45:52.177Z","message":{"role":"toolResult","toolCallId":"call_lNmAqghwwn7aDRoXt8KRBcx0|fc_0ca4af36969c80fa016a5702700b4081988cc25ce7c199c8b9","toolName":"read","content":[{"type":"text","text":"<!-- [doc->REQ-DIST-HOOKS-API] -->\n<!-- [doc->REQ-UPS-INJECTION] -->\n# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`\n\n## Status\n\naccepted (2026-06-15) · grounds the F-001 resolution (`docs/SPT-CORE-FINDINGS.md`)\n\n## Context\n\nspt-core is **harness-independent** (`CONTEXT.md` L52/L181): it supplies the agnostic `spt api`\nprimitives + their I/O format; the **adapter** authors the harness-specific wiring and output\nformatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\npayload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\npublic 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n(the adapter_name — distinct from the plugin name `sptc`).\n\nTwo hard constraints shape the wiring:\n\n1. **`api listen` blocks** (\"consume a seed and hold the perch + relay loop (blocks)\"). It therefore\n   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,\n   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n   L52/L181. The plugin hand-writes it.\n\n## Decision\n\n**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\nsubstitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n\n### CC-event → `spt api` mapping (authored adapter glue)\n\n| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n|---|---|---|\n| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n| UserPromptSubmit | `poll <id>` → stdout → `additionalContext` | the message-delivery + UPS-injection path (L149) |\n| Stop | `state idle <id>` | arms echo-gate fallback (L137) since Stop can't inject |\n| (activity) | `state busy <id>` | on prompt/tool start |\n| SubagentStart / SubagentStop | `worker-start <parent> <id>` / `worker-stop <id>` | nested worker perches |\n| SessionEnd | `session-end <id>` | soft teardown; `shutdown <id>` on graceful signoff |\n| PreToolUse | — | **out of scope v1** (UPS covers delivery) |\n\n`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n**not** from a hook.\n\n### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook\n\nThe `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers\nqueued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter\n`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is\nours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.\n\n### `api poll` parse contract — the `<EVENT>` envelope (operator-ruled 2026-06-15)\n\n> **Supersedes** the earlier `__REPLY_TO__` framing (a mis-elevated relic, now deleted from\n> spt-core — see `docs/SPT-CORE-FINDINGS.md` F-002, resolved-by-design). Confirmed by doyle as the\n> deliberate poll-surface contract. **Transitional:** the current 0.6.0 binary still\n> emits the `__REPLY_TO__` relic at poll until `REQ-MSG-ENVELOPE` ships; build to `<EVENT>` but\n> validate against poll only post-refactor.\n\nThe **canonical format at every surface, including `api poll`, is the `spt-proto::event` envelope**\n(the ADR-0001 grammar the live listener already emits):\n\n- **One whole, single-line `<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>` per message.** Interior\n  newlines are `<br>`-escaped, so the envelope never breaks across lines.\n- **Self-delimiting** → a multi-message drain splits cleanly on `</EVENT>` (no delimiter, no\n  `F-002` ambiguity).\n- **No `<EVENT-PART>` chunking at the poll surface** (doyle Q2): `poll`/`worker-poll` emit whole\n  `<EVENT>`s. `<EVENT-PART seq=\"N/M\">` exists only for the *listener stream* (the `« spt event »`\n  Monitor's ~500-char `EVENT_LINE_THRESHOLD`); the hook-drain injects via `additionalContext` which\n  has no per-line cap. **No id+seq reassembly on the poll path.**\n\nParser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\nthe `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\nunescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\ngate, Psyche routing), never silently stripped. Covered by\n`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).\n\n**Harness injection-size limit is ours** (harness-agnostic boundary — spt-core emits whole\n`<EVENT>`s regardless): CC `additionalContext` caps at **10,000 chars** (larger output is spilled to\na file by CC). A large multi-message drain can exceed it → adapter-side follow-up: truncate with a\nmarker or spill. (`REQ-UPS-INJECTION` `int` item.)\n\n### Portability\n\nHandlers must map CC stdin JSON → `api` flags. The plugin ships **no binary**, so the mapping is a\nthin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is\nsettled during impl (see Open).\n\n### Hook-side id-resolution — RESOLVED (observed on the 0.6.0 binary)\n\n`spt whoami` \"Print this session's own perch id. **Resolved from `$OWL_SESSION_ID` /\n`$SPT_AGENT_ID`.**\" That is the id-resolver: the per-prompt hooks do not need a positional `<id>`\nthreaded in — they resolve it from the session env. Wiring:\n\n- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n  no capability token is needed from the hook.\n\nThe frame contract is corroborated at the binary level too: `spt send --from <FROM>` = \"Sender id\n**written into `__REPLY_TO__`**\" — matching the `spool.rs` frame doyle provided.\n\n## Validation results (throwaway `claude -p` session, 2026-06-15)\n\nRan an isolated temp-project rig (UserPromptSubmit marker hook + a registered `/send` project\nskill) on the real CC 2.1.177 binary:\n\n- ✅ **UPS fires on a `/`-slash-command.** `MSYS_NO_PATHCONV=1 claude -p \"/send hi\"` → the hook\n  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n  packaging on Windows (no per-OS branch needed for the hook to run).\n- ✅ **Hook stdin schema confirmed**: `{session_id, transcript_path, cwd, permission_mode,\n  hook_event_name, prompt}` — `json_str` targets the right fields; `session_id` sourcing confirmed.\n- ⚠️ **MSYS `/`-arg mangling** observed (run A): a `/send` passed as a Git-Bash *argument* became\n  `C:/Program Files/Git/send`. Test artifact (wrappers read stdin, not argv) but a real Windows\n  hazard → `docs/KNOWN-HAZARDS.md` 1.1 + `REQ-HAZARD-MSYS-PATHCONV` (test: `tests/msys-hazard.sh`).\n\n## Open / resolved `int`\n\n1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** The canonical poll envelope shipped\n   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed\n   the canonical `<EVENT type=\"msg\" from=…>body</EVENT>\\n` envelope (no `__REPLY_TO__`, no\n   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-\n   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`\n   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.\n2. **Large-drain injection size — RESOLVED (2026-06-15).** CC spills `additionalContext` over ~10k\n   chars to a file (evicting it from the inline context the agent sees), silently losing messages.\n   The UPS wrapper now pre-empts: `sptc_cap_output` (`_common.sh`) buffers the combined skill-body +\n   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n\n## Consequences\n\n- `REQ-DIST-HOOKS-API` / `REQ-UPS-INJECTION` gain a concrete, grounded `doc` design here; their\n  `impl` activates when the validated `hooks.json` + wrappers land (gated on Open #1).\n- Supersedes the `ADR-0001` \"hooks.json delegates to `spt api`\" stub with the full wiring; ADR-0001's\n  UPS-injection open item (UPS-fires empirical) is carried here as Open #2.\n- No spt-core dependency for the wiring itself — only the (non-blocking) residual docs publish of the\n  `api poll` frame format, which we can substitute with observed behavior until it lands.\n"}],"details":{"displayContent":{"text":"<!-- [doc->REQ-DIST-HOOKS-API] -->\n<!-- [doc->REQ-UPS-INJECTION] -->\n# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`\n\n## Status\n\naccepted (2026-06-15) · grounds the F-001 resolution (`docs/SPT-CORE-FINDINGS.md`)\n\n## Context\n\nspt-core is **harness-independent** (`CONTEXT.md` L52/L181): it supplies the agnostic `spt api`\nprimitives + their I/O format; the **adapter** authors the harness-specific wiring and output\nformatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\npayload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\npublic 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n(the adapter_name — distinct from the plugin name `sptc`).\n\nTwo hard constraints shape the wiring:\n\n1. **`api listen` blocks** (\"consume a seed and hold the perch + relay loop (blocks)\"). It therefore\n   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,\n   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n   L52/L181. The plugin hand-writes it.\n\n## Decision\n\n**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\nsubstitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n\n### CC-event → `spt api` mapping (authored adapter glue)\n\n| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n|---|---|---|\n| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n| UserPromptSubmit | `poll <id>` → stdout → `additionalContext` | the message-delivery + UPS-injection path (L149) |\n| Stop | `state idle <id>` | arms echo-gate fallback (L137) since Stop can't inject |\n| (activity) | `state busy <id>` | on prompt/tool start |\n| SubagentStart / SubagentStop | `worker-start <parent> <id>` / `worker-stop <id>` | nested worker perches |\n| SessionEnd | `session-end <id>` | soft teardown; `shutdown <id>` on graceful signoff |\n| PreToolUse | — | **out of scope v1** (UPS covers delivery) |\n\n`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n**not** from a hook.\n\n### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook\n\nThe `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers\nqueued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter\n`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is\nours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.\n\n### `api poll` parse contract — the `<EVENT>` envelope (operator-ruled 2026-06-15)\n\n> **Supersedes** the earlier `__REPLY_TO__` framing (a mis-elevated relic, now deleted from\n> spt-core — see `docs/SPT-CORE-FINDINGS.md` F-002, resolved-by-design). Confirmed by doyle as the\n> deliberate poll-surface contract. **Transitional:** the current 0.6.0 binary still\n> emits the `__REPLY_TO__` relic at poll until `REQ-MSG-ENVELOPE` ships; build to `<EVENT>` but\n> validate against poll only post-refactor.\n\nThe **canonical format at every surface, including `api poll`, is the `spt-proto::event` envelope**\n(the ADR-0001 grammar the live listener already emits):\n\n- **One whole, single-line `<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>` per message.** Interior\n  newlines are `<br>`-escaped, so the envelope never breaks across lines.\n- **Self-delimiting** → a multi-message drain splits cleanly on `</EVENT>` (no delimiter, no\n  `F-002` ambiguity).\n- **No `<EVENT-PART>` chunking at the poll surface** (doyle Q2): `poll`/`worker-poll` emit whole\n  `<EVENT>`s. `<EVENT-PART seq=\"N/M\">` exists only for the *listener stream* (the `« spt event »`\n  Monitor's ~500-char `EVENT_LINE_THRESHOLD`); the hook-drain injects via `additionalContext` which\n  has no per-line cap. **No id+seq reassembly on the poll path.**\n\nParser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\nthe `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\nunescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\ngate, Psyche routing), never silently stripped. Covered by\n`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).\n\n**Harness injection-size limit is ours** (harness-agnostic boundary — spt-core emits whole\n`<EVENT>`s regardless): CC `additionalContext` caps at **10,000 chars** (larger output is spilled to\na file by CC). A large multi-message drain can exceed it → adapter-side follow-up: truncate with a\nmarker or spill. (`REQ-UPS-INJECTION` `int` item.)\n\n### Portability\n\nHandlers must map CC stdin JSON → `api` flags. The plugin ships **no binary**, so the mapping is a\nthin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is\nsettled during impl (see Open).\n\n### Hook-side id-resolution — RESOLVED (observed on the 0.6.0 binary)\n\n`spt whoami` \"Print this session's own perch id. **Resolved from `$OWL_SESSION_ID` /\n`$SPT_AGENT_ID`.**\" That is the id-resolver: the per-prompt hooks do not need a positional `<id>`\nthreaded in — they resolve it from the session env. Wiring:\n\n- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n  no capability token is needed from the hook.\n\nThe frame contract is corroborated at the binary level too: `spt send --from <FROM>` = \"Sender id\n**written into `__REPLY_TO__`**\" — matching the `spool.rs` frame doyle provided.\n\n## Validation results (throwaway `claude -p` session, 2026-06-15)\n\nRan an isolated temp-project rig (UserPromptSubmit marker hook + a registered `/send` project\nskill) on the real CC 2.1.177 binary:\n\n- ✅ **UPS fires on a `/`-slash-command.** `MSYS_NO_PATHCONV=1 claude -p \"/send hi\"` → the hook\n  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n  packaging on Windows (no per-OS branch needed for the hook to run).\n- ✅ **Hook stdin schema confirmed**: `{session_id, transcript_path, cwd, permission_mode,\n  hook_event_name, prompt}` — `json_str` targets the right fields; `session_id` sourcing confirmed.\n- ⚠️ **MSYS `/`-arg mangling** observed (run A): a `/send` passed as a Git-Bash *argument* became\n  `C:/Program Files/Git/send`. Test artifact (wrappers read stdin, not argv) but a real Windows\n  hazard → `docs/KNOWN-HAZARDS.md` 1.1 + `REQ-HAZARD-MSYS-PATHCONV` (test: `tests/msys-hazard.sh`).\n\n## Open / resolved `int`\n\n1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** The canonical poll envelope shipped\n   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed\n   the canonical `<EVENT type=\"msg\" from=…>body</EVENT>\\n` envelope (no `__REPLY_TO__`, no\n   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-\n   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`\n   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.\n2. **Large-drain injection size — RESOLVED (2026-06-15).** CC spills `additionalContext` over ~10k\n   chars to a file (evicting it from the inline context the agent sees), silently losing messages.\n   The UPS wrapper now pre-empts: `sptc_cap_output` (`_common.sh`) buffers the combined skill-body +\n   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n\n## Consequences\n\n- `REQ-DIST-HOOKS-API` / `REQ-UPS-INJECTION` gain a concrete, grounded `doc` design here; their\n  `impl` activates when the validated `hooks.json` + wrappers land (gated on Open #1).\n- Supersedes the `ADR-0001` \"hooks.json delegates to `spt api`\" stub with the full wiring; ADR-0001's\n  UPS-injection open item (UPS-fires empirical) is carried here as Open #2.\n- No spt-core dependency for the wiring itself — only the (non-blocking) residual docs publish of the\n  `api poll` frame format, which we can substitute with observed behavior until it lands.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\adr\\0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md"}}},"isError":false,"timestamp":1784087152177}}
{"type":"message","id":"f4d487d0","parentId":"d4e6f573","timestamp":"2026-07-15T03:45:52.177Z","message":{"role":"toolResult","toolCallId":"call_61MhbWTH8AYrHXwaaUwt2OoT|fc_0ca4af36969c80fa016a5702700b1881989bd8ffe2eefb4ce2","toolName":"read","content":[{"type":"text","text":"# omp-spt bridge — field issues (doyle, 2026-07-09)\n\nReport from an spt-core maintainer (doyle @ HFENDULEAM) after the operator ran a\nlive agent (`todlando`) under the experimental **omp-spt** adapter. The endpoint\n\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\nand could not be switched back to `claude-spt`. This documents the root causes,\ngrounded in this repo's code, so the bridge can be hardened.\n\nAll line references are against `tools/claude-spt/src/bridge.rs` and\n`adapter/omp-spt.toml` at commit `f0109d1`.\n\n## Incident summary\n\n1. Operator started `todlando` under omp-spt (`[session.self]` →\n   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n   way to tell working vs. wedged.\n3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n   endpoint looked alive but was dead.\n4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n5. Starting fresh after switching the harness adapter still launched omp-spt.\n\nSteps 3–5 required manual intervention on the spt-core side (kill the orphan\ntree, repair the endpoint's session ledger) to recover. Findings below explain\neach symptom.\n\n---\n\n## HIGH-1 — Bridge binary basename collides with `claude-spt`\n\n**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\nindistinguishable from a genuine claude-spt process. During the incident this\nmade it impossible to tell, at a glance, that the \"claude-spt\" process holding\ntodlando's perch was actually the omp bridge. It directly caused the\nmisdiagnosis.\n\n**Root.** `adapter/omp-spt.toml:693` (and `:722` for resume) ship:\n```\ncommand = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n```\nThe executable is named `claude-spt`, the exact basename of the upstream adapter.\nThis contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\nbridge (S2) is `omp-spt`\"*).\n\n**Fix.** Rename the shipped bridge binary to `omp-spt` (or `omp-spt-bridge`) and\nupdate `[session.self]`/`[session.resume]` `command` accordingly. A distinct\nbasename makes the process self-identifying and removes the collision.\n\n## HIGH-2 — No teardown / signal handling → orphaned children on stop\n\n**Symptom.** Stopping the endpoint left the `omp --mode rpc` child and the\n`spt ready <id>` child running; the perch stayed `READY`. Recovery needed a\nmanual `taskkill /T` of the bridge tree.\n\n**Root.** `bridge.rs` installs no signal/console-control handler and no OS job\nobject — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\nWhen the broker or operator terminates the bridge process (the normal \"stop\"),\nthose lines never run. On Windows, killing a process does not kill its children,\nso both the omp child and the `spt ready` child are leaked. The leaked\n`spt ready` keeps the perch registered, so the dead endpoint still advertises as\nlistening.\n\n**Fix.** Tie child lifetime to the bridge: on Windows assign the omp + `spt ready`\nchildren to a `JobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; on Unix use\na process group + `kill_on_drop`. Install a console-control/signal handler that\ntears both children down and unbinds the perch. Consider a `[session.stop]`\nmanifest seam so the broker drives an orderly unbind rather than a bare kill.\n\n## HIGH-3 — Headless design gives the operator zero visibility\n\n**Symptom.** The literal reported symptom: `READY:todlando` and nothing else,\nforever.\n\n**Root.** The bridge runs omp in `--mode rpc` (no TTY UI) and only *inherits*\nomp's stderr (`bridge.rs:132`). The sole thing printed to the broker PTY is the\n`spt ready` child's `READY:<id>` banner (its stderr is inherited,\n`bridge.rs:192`). All turn activity — message received, prompt dispatched, reply\nsent, errors — is emitted only under `OMP_SPT_DEBUG` via the `trace!` macro\n(`bridge.rs:48-54`). With debug off (the default), a working bridge, an idle\nbridge, a wedged bridge, and a bridge whose omp child has died all look\nidentical: `READY:todlando`.\n\n**Fix.** Emit a minimal, always-on operational log to stdout: one line per\ninbound message (from, byte count), per reply (target, byte count), and per error\n(bind/state/send failure, omp exit). A periodic status/heartbeat line would let\nthe operator distinguish idle from wedged.\n\n## HIGH-4 — Unbounded blocking reads, no timeouts → silent permanent wedge\n\n**Symptom.** A bringup or a turn can hang forever with no failure surfaced —\nconsistent with \"READY then nothing\".\n\n**Root.** Every omp read is a blocking `read_line` with no deadline:\n- `wait_for_ready` (`bridge.rs:312`) loops until the `ready` frame or EOF.\n- `get_state_session_id` (`bridge.rs:326`) loops until the `get_state` response.\n- `drive_turn` (`bridge.rs:339`) loops until `agent_end`.\n\nIf omp stalls before `ready` (auth prompt, wrong binary — see MEDIUM-3, model\nhang) the bridge blocks in startup forever: it never binds, never reaches\n`spt ready`, and never fails. If a *turn* never produces `agent_end`, the single\nrelay thread wedges with the perch stuck `busy` (state set at `bridge.rs:226`,\ncleared only after `drive_turn` returns at `:228`).\n\n**Fix.** Bound every wait: a dedicated reader thread feeding a channel with\n`recv_timeout`, plus a ready-deadline and a per-turn deadline. On breach, fail\nloud (startup) or abort the turn and recover the perch to `idle`/offline.\n\n## MEDIUM-1 — Single-thread head-of-line relay; `steer`/`follow_up` unimplemented\n\n**Root.** The relay loop (`bridge.rs:211-239`) is strictly\nread-line → `drive_turn` (blocks to `agent_end`) → next line. While a turn runs,\nno further deliveries are drained, and the mid-turn `steer` / `follow_up`\ninterrupts advertised in `OMP-ADAPTER-PLAN.md` are not wired. A long or hung turn\nstalls all delivery for the endpoint.\n\n**Fix.** Decouple delivery from turn execution; wire `steer` so a hung/rescoped\nturn can be interrupted.\n\n## MEDIUM-2 — Dead omp child detected only on the next message\n\n**Root.** `child_exited()` is checked only at the top of the loop, *after* a new\n`spt ready` line arrives (`bridge.rs:216`). While the perch is idle the bridge is\nblocked on `ready_out.lines()`; if the omp child dies there, the bridge doesn't\nnotice. The perch stays online/idle and only discovers the dead child when the\nnext message arrives (then returns an empty reply — see MEDIUM-5).\n\n**Fix.** Watch the omp child concurrently (the reader thread from HIGH-4 sees its\nEOF); on exit, mark the perch offline and tear down.\n\n## MEDIUM-3 — `omp` binary resolution is PATH-last with a known alias collision\n\n**Root.** `omp_bin()` (`bridge.rs:486-504`) resolves `OMP_SPT_OMP_BIN` → install\npaths → PATH `omp`. The code's own comment (`:482-485`) notes PATH `omp` may be\nthe OneCLI **gateway alias**, not the coding agent. If the install paths are\nabsent and only the gateway alias is on PATH, the bridge spawns the wrong binary,\nwhich does not speak `--mode rpc` → it wedges at `wait_for_ready` (HIGH-4) or\nexits to `FAILURE`. Nothing validates that the resolved binary is actually\noh-my-pi.\n\n**Fix.** After spawn, validate the `ready` handshake within a deadline and/or\nprobe `--version`; refuse loudly with the resolved path if it is not omp.\n\n## MEDIUM-4 — omp-spt bind traps the endpoint's adapter (spt-core interop)\n\n**Symptom.** After the broken omp session, the endpoint could not be resumed or\nrestarted under claude-spt — every attempt relaunched omp-spt. Recovery required\nediting spt-core's session ledger by hand.\n\n**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n(`bridge.rs:167`), so spt-core records `omp-spt` in the endpoint's session ledger\n(`sessions.log`). spt-core's resume path re-stamps the endpoint to the\n*last-recorded* session adapter before spawning (REQ-RESUME-ADAPTER-FOLLOWS-SESSION),\nso a broken omp-spt session pins the endpoint on omp-spt. The adapter provides no\nsupported stop/unbind/switch path back.\n\n**Fix.** Provide (and document) a clean unbind/switch so an operator can move an\nendpoint off omp-spt without ledger surgery. Ideally a failed bringup should not\nleave a poisoning ledger row — bind only once the omp `ready` + `get_state`\nhandshake has actually succeeded (it already binds after `get_state`, but see\nHIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).\n\n## LOW-1 — Errors universally swallowed\n\nEvery `spt_call` result is discarded with `let _ =` (`bridge.rs:180, 226, 228,\n233, 437`). Bind/state/send failures are invisible unless `OMP_SPT_DEBUG` is set.\nThis compounds HIGH-3. Surface failures on the always-on log.\n\n## LOW-2 — Empty reply sent on a failed turn\n\n`drive_turn` returns `None` on EOF-before-`agent_end`; the caller does\n`.unwrap_or_default()` (`bridge.rs:227`) and sends an **empty** reply to the\nsender, then flips the perch `idle`. A failed turn is indistinguishable from a\nsuccessful empty answer. Distinguish turn failure from an empty result and report\nit.\n\n---\n\n## Suggested priority\n\n1. HIGH-2 (orphan reaping) and HIGH-1 (binary rename) — these two caused the\n   unrecoverable-without-intervention state.\n2. HIGH-4 (timeouts) and HIGH-3 (visibility) — these turn a silent wedge into a\n   diagnosable, self-healing one.\n3. MEDIUM-4 (clean adapter switch) — so a bad session is recoverable by the\n   operator alone.\n\nThe core Style-A architecture (drive omp over `--mode rpc` rather than scraping\nhooks) is sound; every issue above is in the bridge's process-lifecycle and\nobservability plumbing, not the protocol mapping.\n"}],"details":{"displayContent":{"text":"# omp-spt bridge — field issues (doyle, 2026-07-09)\n\nReport from an spt-core maintainer (doyle @ HFENDULEAM) after the operator ran a\nlive agent (`todlando`) under the experimental **omp-spt** adapter. The endpoint\n\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\nand could not be switched back to `claude-spt`. This documents the root causes,\ngrounded in this repo's code, so the bridge can be hardened.\n\nAll line references are against `tools/claude-spt/src/bridge.rs` and\n`adapter/omp-spt.toml` at commit `f0109d1`.\n\n## Incident summary\n\n1. Operator started `todlando` under omp-spt (`[session.self]` →\n   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n   way to tell working vs. wedged.\n3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n   endpoint looked alive but was dead.\n4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n5. Starting fresh after switching the harness adapter still launched omp-spt.\n\nSteps 3–5 required manual intervention on the spt-core side (kill the orphan\ntree, repair the endpoint's session ledger) to recover. Findings below explain\neach symptom.\n\n---\n\n## HIGH-1 — Bridge binary basename collides with `claude-spt`\n\n**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\nindistinguishable from a genuine claude-spt process. During the incident this\nmade it impossible to tell, at a glance, that the \"claude-spt\" process holding\ntodlando's perch was actually the omp bridge. It directly caused the\nmisdiagnosis.\n\n**Root.** `adapter/omp-spt.toml:693` (and `:722` for resume) ship:\n```\ncommand = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n```\nThe executable is named `claude-spt`, the exact basename of the upstream adapter.\nThis contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\nbridge (S2) is `omp-spt`\"*).\n\n**Fix.** Rename the shipped bridge binary to `omp-spt` (or `omp-spt-bridge`) and\nupdate `[session.self]`/`[session.resume]` `command` accordingly. A distinct\nbasename makes the process self-identifying and removes the collision.\n\n## HIGH-2 — No teardown / signal handling → orphaned children on stop\n\n**Symptom.** Stopping the endpoint left the `omp --mode rpc` child and the\n`spt ready <id>` child running; the perch stayed `READY`. Recovery needed a\nmanual `taskkill /T` of the bridge tree.\n\n**Root.** `bridge.rs` installs no signal/console-control handler and no OS job\nobject — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\nWhen the broker or operator terminates the bridge process (the normal \"stop\"),\nthose lines never run. On Windows, killing a process does not kill its children,\nso both the omp child and the `spt ready` child are leaked. The leaked\n`spt ready` keeps the perch registered, so the dead endpoint still advertises as\nlistening.\n\n**Fix.** Tie child lifetime to the bridge: on Windows assign the omp + `spt ready`\nchildren to a `JobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; on Unix use\na process group + `kill_on_drop`. Install a console-control/signal handler that\ntears both children down and unbinds the perch. Consider a `[session.stop]`\nmanifest seam so the broker drives an orderly unbind rather than a bare kill.\n\n## HIGH-3 — Headless design gives the operator zero visibility\n\n**Symptom.** The literal reported symptom: `READY:todlando` and nothing else,\nforever.\n\n**Root.** The bridge runs omp in `--mode rpc` (no TTY UI) and only *inherits*\nomp's stderr (`bridge.rs:132`). The sole thing printed to the broker PTY is the\n`spt ready` child's `READY:<id>` banner (its stderr is inherited,\n`bridge.rs:192`). All turn activity — message received, prompt dispatched, reply\nsent, errors — is emitted only under `OMP_SPT_DEBUG` via the `trace!` macro\n(`bridge.rs:48-54`). With debug off (the default), a working bridge, an idle\nbridge, a wedged bridge, and a bridge whose omp child has died all look\nidentical: `READY:todlando`.\n\n**Fix.** Emit a minimal, always-on operational log to stdout: one line per\ninbound message (from, byte count), per reply (target, byte count), and per error\n(bind/state/send failure, omp exit). A periodic status/heartbeat line would let\nthe operator distinguish idle from wedged.\n\n## HIGH-4 — Unbounded blocking reads, no timeouts → silent permanent wedge\n\n**Symptom.** A bringup or a turn can hang forever with no failure surfaced —\nconsistent with \"READY then nothing\".\n\n**Root.** Every omp read is a blocking `read_line` with no deadline:\n- `wait_for_ready` (`bridge.rs:312`) loops until the `ready` frame or EOF.\n- `get_state_session_id` (`bridge.rs:326`) loops until the `get_state` response.\n- `drive_turn` (`bridge.rs:339`) loops until `agent_end`.\n\nIf omp stalls before `ready` (auth prompt, wrong binary — see MEDIUM-3, model\nhang) the bridge blocks in startup forever: it never binds, never reaches\n`spt ready`, and never fails. If a *turn* never produces `agent_end`, the single\nrelay thread wedges with the perch stuck `busy` (state set at `bridge.rs:226`,\ncleared only after `drive_turn` returns at `:228`).\n\n**Fix.** Bound every wait: a dedicated reader thread feeding a channel with\n`recv_timeout`, plus a ready-deadline and a per-turn deadline. On breach, fail\nloud (startup) or abort the turn and recover the perch to `idle`/offline.\n\n## MEDIUM-1 — Single-thread head-of-line relay; `steer`/`follow_up` unimplemented\n\n**Root.** The relay loop (`bridge.rs:211-239`) is strictly\nread-line → `drive_turn` (blocks to `agent_end`) → next line. While a turn runs,\nno further deliveries are drained, and the mid-turn `steer` / `follow_up`\ninterrupts advertised in `OMP-ADAPTER-PLAN.md` are not wired. A long or hung turn\nstalls all delivery for the endpoint.\n\n**Fix.** Decouple delivery from turn execution; wire `steer` so a hung/rescoped\nturn can be interrupted.\n\n## MEDIUM-2 — Dead omp child detected only on the next message\n\n**Root.** `child_exited()` is checked only at the top of the loop, *after* a new\n`spt ready` line arrives (`bridge.rs:216`). While the perch is idle the bridge is\nblocked on `ready_out.lines()`; if the omp child dies there, the bridge doesn't\nnotice. The perch stays online/idle and only discovers the dead child when the\nnext message arrives (then returns an empty reply — see MEDIUM-5).\n\n**Fix.** Watch the omp child concurrently (the reader thread from HIGH-4 sees its\nEOF); on exit, mark the perch offline and tear down.\n\n## MEDIUM-3 — `omp` binary resolution is PATH-last with a known alias collision\n\n**Root.** `omp_bin()` (`bridge.rs:486-504`) resolves `OMP_SPT_OMP_BIN` → install\npaths → PATH `omp`. The code's own comment (`:482-485`) notes PATH `omp` may be\nthe OneCLI **gateway alias**, not the coding agent. If the install paths are\nabsent and only the gateway alias is on PATH, the bridge spawns the wrong binary,\nwhich does not speak `--mode rpc` → it wedges at `wait_for_ready` (HIGH-4) or\nexits to `FAILURE`. Nothing validates that the resolved binary is actually\noh-my-pi.\n\n**Fix.** After spawn, validate the `ready` handshake within a deadline and/or\nprobe `--version`; refuse loudly with the resolved path if it is not omp.\n\n## MEDIUM-4 — omp-spt bind traps the endpoint's adapter (spt-core interop)\n\n**Symptom.** After the broken omp session, the endpoint could not be resumed or\nrestarted under claude-spt — every attempt relaunched omp-spt. Recovery required\nediting spt-core's session ledger by hand.\n\n**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n(`bridge.rs:167`), so spt-core records `omp-spt` in the endpoint's session ledger\n(`sessions.log`). spt-core's resume path re-stamps the endpoint to the\n*last-recorded* session adapter before spawning (REQ-RESUME-ADAPTER-FOLLOWS-SESSION),\nso a broken omp-spt session pins the endpoint on omp-spt. The adapter provides no\nsupported stop/unbind/switch path back.\n\n**Fix.** Provide (and document) a clean unbind/switch so an operator can move an\nendpoint off omp-spt without ledger surgery. Ideally a failed bringup should not\nleave a poisoning ledger row — bind only once the omp `ready` + `get_state`\nhandshake has actually succeeded (it already binds after `get_state`, but see\nHIGH-4: without a deadline the failure mode is a hang, not a clean no-bind).\n\n## LOW-1 — Errors universally swallowed\n\nEvery `spt_call` result is discarded with `let _ =` (`bridge.rs:180, 226, 228,\n233, 437`). Bind/state/send failures are invisible unless `OMP_SPT_DEBUG` is set.\nThis compounds HIGH-3. Surface failures on the always-on log.\n\n## LOW-2 — Empty reply sent on a failed turn\n\n`drive_turn` returns `None` on EOF-before-`agent_end`; the caller does\n`.unwrap_or_default()` (`bridge.rs:227`) and sends an **empty** reply to the\nsender, then flips the perch `idle`. A failed turn is indistinguishable from a\nsuccessful empty answer. Distinguish turn failure from an empty result and report\nit.\n\n---\n\n## Suggested priority\n\n1. HIGH-2 (orphan reaping) and HIGH-1 (binary rename) — these two caused the\n   unrecoverable-without-intervention state.\n2. HIGH-4 (timeouts) and HIGH-3 (visibility) — these turn a silent wedge into a\n   diagnosable, self-healing one.\n3. MEDIUM-4 (clean adapter switch) — so a bad session is recoverable by the\n   operator alone.\n\nThe core Style-A architecture (drive omp over `--mode rpc` rather than scraping\nhooks) is sound; every issue above is in the bridge's process-lifecycle and\nobservability plumbing, not the protocol mapping.\n","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,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\OMP-BRIDGE-FIELD-ISSUES.md"}}},"isError":false,"timestamp":1784087152177}}
{"type":"message","id":"8fa92b70","parentId":"f4d487d0","timestamp":"2026-07-15T03:45:52.185Z","message":{"role":"toolResult","toolCallId":"call_53ZhN7IVQS8RQM8fOnCK3MqV|fc_0ca4af36969c80fa016a5702700b2481988099c9280d5c0d5a","toolName":"read","content":[{"type":"text","text":"# Known Hazards\n\n> A **conformance checklist, not advice.** Each hazard below is a first-class\n> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n> and `traceable-reqs check` will say so once the hazard is activated.\n\nA hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\nincident) or one you have *committed never to introduce*. State it so a test can prove it.\n\n## Entry format\n\nEach entry is one numbered subsection with these fields:\n\n- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n  `REQ-HAZARD-*` requires).\n- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n  reference only — the binding evidence is the tagged test.\n\nMirror each entry as a requirement:\n\n```toml\n[[requirements]]\nid = \"REQ-HAZARD-EXAMPLE\"\ntitle = \"The invariant, stated so a test can prove it\"\nrequired_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n```\n\n---\n\n## 1. Windows / MSYS shell environment\n\n### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments\n\n- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`\n  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n  documents the identical hazard in `new-alarm`.)\n- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  A test asserts the stdin path is honored (no argv `/`-token dependency).\n- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n\n---\n\n## 2. Live-agent perch / CI orchestration\n\n### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n\n<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n\n- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a\n  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the\n  operator as self-inflicted, NOT a legacy-substrate bug.\n- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n  overrides both to a `sptc-ci-` id and never emits a live id.\n- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n  dir is not part of the public surface, so isolation rides on identity.\n- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`\n  loading the spt plugin). Reference only — binding evidence is the tagged test under\n  `REQ-HAZARD-PERCH-COLLISION`.\n\n### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt\n\n<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n\n- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n  discarded): the daemon believes it hosts a working companion that never produces a commune.\n- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n  trust (see `docs/adr/0003-*`).\n- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass\n  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference\n  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.\n\n### 2.3 A missed `/clear` boundary rotation pins the perch to a dead session id — silently and permanently\n\n<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n\n- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart\n  boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**\n  session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the\n  departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two\n  stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22\n  (the new sid is unregistered until the rotation succeeds) that resolved `self:null` and **skipped\n  the rotation with zero diagnostics** — and even when the id resolved it presented **no auth\n  proof**. Either way the perch stays pinned to the dead sid, after which EVERY id-scoped api call\n  (`poll`/`state`/`boundary`) from the live session is silently `AUTH_REFUSED` (stderr-only): the\n  session looks perfectly healthy (brief renders, turns run) while local AND cross-node delivery is\n  permanently dead. **No self-heal exists**: later boundary attempts present the new sid and refuse\n  too, and spt-core's dead-owner re-pin correctly does NOT cover a live harness pid (squat\n  protection). Observed live: the ENLYZEAM `ball-b` wedge (F-024, root-caused 2026-07-02) —\n  operator clean-room repro: fresh session + `/clear` wedges with zero hook errors.\n- **Invariant:** (1) The boundary branch resolves the endpoint id from `$SPT_ENDPOINT_ID` FIRST\n  (the brief path's proven preference) — never whoami-by-new-sid alone. (2) The hook persists the\n  CURRENT session id at EVERY SessionStart to an adapter-owned state file\n  (`{adapter_dir}/state/session/<endpoint_id>.sid`) and presents it as `--session-id` proof at the\n  next rotation — NOT via `CLAUDE_ENV_FILE`, whose per-session lifetime is the exact failure mode.\n  (3) A rotation that cannot run (no id) or is refused (auth) logs LOUDLY to stderr with the\n  reason; a silent skip is forbidden.\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n  recorded sid; live-proven on ENLYZEAM 2026-07-02).\n- **cite:** F-024 correction arc, `docs/SPT-CORE-FINDINGS.md` (empirics: whoami-by-dead-sid →\n  `self:null`; `AUTH_REFUSED:ball-b (need --token or matching --session-id)` shell-verified); doyle\n  ruling `F024D-BOUNDARY-RULING` 2026-07-02 (adapter-owned state file = the contract-clean carrier;\n  the reference pattern for the harness-contract boundary doc). Binding evidence = the tagged tests\n  under `REQ-HAZARD-SESSION-PIN-WEDGE`. Fix live-validated on-node: E2E-0104 GREEN (doyle verdict\n  2026-07-02 — rotation authenticates with the prior sid, `.sid` rolls forward in lockstep,\n  post-rotation delivery lands; the `[int->REQ-DIST-BOUNDARY-ROTATE]` record in\n  `docs/SPT-CORE-FINDINGS.md` F-024).\n\n### 2.4 The checkpoint wake races the `/clear` and lands in the OLD session\n\n<!-- [doc->REQ-HAZARD-CHECKPOINT-CLEAR-RACE] -->\n\n- **Failure:** The agent-driven checkpoint submits `/clear` then a wake directive. CC's `/clear` is\n  ASYNC — it tears down and rebuilds the session, re-running SessionStart (whoami, psyche-download,\n  brief assembly — network I/O, seconds). The pre-fix macro emitted both in ONE inject sequence with a\n  fixed `500ms` gap between the `/clear` submit and the wake. Two stacked problems: (1) 500ms is a\n  blind guess — a slow clear routinely exceeds it, so the wake is typed/submitted BEFORE the clear\n  settles and lands in the OLD (about-to-be-wiped) session, or the ordering inverts entirely (the\n  wake hits first). (2) A hard ceiling: the whole inject sequence must `{commit}` within the 5s\n  `INJECT_COMMIT_DEADLINE`, so the delay CANNOT be widened to cover a genuinely slow clear — a single\n  inject sequence is architecturally incapable of straddling a `/clear`. Field-observed: the followup\n  wake hit first.\n- **Invariant:** The clear and the wake are SPLIT into two inject sequences, synchronized on CC's own\n  clear-done signal (SessionStart firing with `source == clear`). (1) ARM: a `{\"checkpoint\":\"v1\",…}`\n  envelope stashes the wake in the translation binary's in-memory `pending_wake` and emits CLEAR-ONLY\n  (`ctrl+s · 50ms · /clear · 50ms · enter · commit`); the wake is withheld. (2) FIRE: the SessionStart hook,\n  on a `clear` boundary, UNCONDITIONALLY self-sends `{\"checkpoint_fire\":\"v1\"}`; the binary emits\n  WAKE-ONLY (`ctrl+s · 50ms · wake · 50ms · enter · commit`) iff a wake is armed, else no-ops. State lives\n  ONLY in translate memory (no marker file; the hook is stateless — it fires on every clear and the\n  binary decides). BOTH self-sends use `--force-native` so the signal is delivered through the\n  translation binary's stdin (where the markers parse), never spooled to the active-poll channel\n  (which surfaces the envelope as a plain message the agent reads instead of firing — the ENLYZEAM\n  misdelivery mode). Ordering is guaranteed by construction: the wake can only emit after SessionStart,\n  which only fires after `/clear` completes. Fires ONLY on `clear`, not `compact` (no checkpoint\n  variant leverages `/compact`).\n- **Accepted residual:** if the checkpoint's own clear fails to fire the signal (hook error), the\n  armed wake fires on the NEXT clear — possibly a slightly-wrong context. The every-clear-fire model\n  self-limits it (it won't sit forever); a translate-side generation/timeout could bound staleness if\n  it ever bites. Accepted for v1.\n- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n  `REQ-DIST-CHECKPOINT-COMMUNE`. Binding evidence = the tagged tests under\n  `REQ-HAZARD-CHECKPOINT-CLEAR-RACE` (crate units: `arm_then_fire_orders_clear_before_wake`,\n  `fire_emits_wake_only_and_disarms`, `fire_with_no_armed_wake_is_a_noop`; hook units:\n  `clear_boundary_fires_checkpoint_fire_force_native`, `non_clear_starts_do_not_fire_checkpoint`; int:\n  the ARM-emit block in `ci/idle-translate/translate-proof-int.sh`).\n- **Second leg (v0.15.1, paid 2026-07-06): everything that fires POST-clear must ride ONE inject\n  sequence.** The boundary rename (v0.14.1) originally rode its own sequence immediately before the\n  wake's — two back-to-back sequences at the just-rebuilt boundary RACED (CC input processing lags\n  the PTY stream: the rename's enter registered as a soft newline, the next sequence's ctrl+s failed\n  to stash the residue, and the wake text submitted INSIDE the `/rename` argument — flynn: session\n  titled with the wake, NO wake turn, agent dormant ~9.5h; ~33% observed, 1 of 3 checkpoints).\n  Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s\n  bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename\n  delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:\n  `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).\n\n---\n\n### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch\n\n<!-- [doc->REQ-HAZARD-PSYCHE-IDENTITY-ENV] -->\n\n- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —\n  including `OWL_SESSION_ID` / `SPT_AGENT_ID`, the parent's identity env. The Psyche's own harness\n  hooks then resolve \"self\" to the PARENT (or, worse, an unrelated top-level perch) and stamp/rebind\n  THAT perch's records. Field-observed on HFENDULEAM (F-028 C2 evidence): `f015b-probe-psyche`'s hook\n  rebound `hall-a`'s `info.json.session_id` to the psyche's own session via `authenticate()`'s\n  dead-owner re-pin — cross-perch identity contamination, silent.\n- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare\n  `env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`\n  AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`\n  (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —\n…\n  side, but the adapter must not hand the child a foreign identity in the first place.\n…\n\n…\n\n…\n  evidence = this entry; impl/unit land with the guard slice.\n\n[Showing lines 1-223 of 445. Use :224 to continue]"}],"details":{"truncation":{"content":"# Known Hazards\n\n> A **conformance checklist, not advice.** Each hazard below is a first-class\n> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n> and `traceable-reqs check` will say so once the hazard is activated.\n\nA hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\nincident) or one you have *committed never to introduce*. State it so a test can prove it.\n\n## Entry format\n\nEach entry is one numbered subsection with these fields:\n\n- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n  `REQ-HAZARD-*` requires).\n- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n  reference only — the binding evidence is the tagged test.\n\nMirror each entry as a requirement:\n\n```toml\n[[requirements]]\nid = \"REQ-HAZARD-EXAMPLE\"\ntitle = \"The invariant, stated so a test can prove it\"\nrequired_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n```\n\n---\n\n## 1. Windows / MSYS shell environment\n\n### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments\n\n- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`\n  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n  documents the identical hazard in `new-alarm`.)\n- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  A test asserts the stdin path is honored (no argv `/`-token dependency).\n- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n\n---\n\n## 2. Live-agent perch / CI orchestration\n\n### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n\n<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n\n- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a\n  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the\n  operator as self-inflicted, NOT a legacy-substrate bug.\n- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n  overrides both to a `sptc-ci-` id and never emits a live id.\n- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n  dir is not part of the public surface, so isolation rides on identity.\n- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`\n  loading the spt plugin). Reference only — binding evidence is the tagged test under\n  `REQ-HAZARD-PERCH-COLLISION`.\n\n### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt\n\n<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n\n- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n  discarded): the daemon believes it hosts a working companion that never produces a commune.\n- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n  trust (see `docs/adr/0003-*`).\n- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass\n  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference\n  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.\n\n### 2.3 A missed `/clear` boundary rotation pins the perch to a dead session id — silently and permanently\n\n<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n\n- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart\n  boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**\n  session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the\n  departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two\n  stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22\n  (the new sid is unregistered until the rotation succeeds) that resolved `self:null` and **skipped\n  the rotation with zero diagnostics** — and even when the id resolved it presented **no auth\n  proof**. Either way the perch stays pinned to the dead sid, after which EVERY id-scoped api call\n  (`poll`/`state`/`boundary`) from the live session is silently `AUTH_REFUSED` (stderr-only): the\n  session looks perfectly healthy (brief renders, turns run) while local AND cross-node delivery is\n  permanently dead. **No self-heal exists**: later boundary attempts present the new sid and refuse\n  too, and spt-core's dead-owner re-pin correctly does NOT cover a live harness pid (squat\n  protection). Observed live: the ENLYZEAM `ball-b` wedge (F-024, root-caused 2026-07-02) —\n  operator clean-room repro: fresh session + `/clear` wedges with zero hook errors.\n- **Invariant:** (1) The boundary branch resolves the endpoint id from `$SPT_ENDPOINT_ID` FIRST\n  (the brief path's proven preference) — never whoami-by-new-sid alone. (2) The hook persists the\n  CURRENT session id at EVERY SessionStart to an adapter-owned state file\n  (`{adapter_dir}/state/session/<endpoint_id>.sid`) and presents it as `--session-id` proof at the\n  next rotation — NOT via `CLAUDE_ENV_FILE`, whose per-session lifetime is the exact failure mode.\n  (3) A rotation that cannot run (no id) or is refused (auth) logs LOUDLY to stderr with the\n  reason; a silent skip is forbidden.\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n  recorded sid; live-proven on ENLYZEAM 2026-07-02).\n- **cite:** F-024 correction arc, `docs/SPT-CORE-FINDINGS.md` (empirics: whoami-by-dead-sid →\n  `self:null`; `AUTH_REFUSED:ball-b (need --token or matching --session-id)` shell-verified); doyle\n  ruling `F024D-BOUNDARY-RULING` 2026-07-02 (adapter-owned state file = the contract-clean carrier;\n  the reference pattern for the harness-contract boundary doc). Binding evidence = the tagged tests\n  under `REQ-HAZARD-SESSION-PIN-WEDGE`. Fix live-validated on-node: E2E-0104 GREEN (doyle verdict\n  2026-07-02 — rotation authenticates with the prior sid, `.sid` rolls forward in lockstep,\n  post-rotation delivery lands; the `[int->REQ-DIST-BOUNDARY-ROTATE]` record in\n  `docs/SPT-CORE-FINDINGS.md` F-024).\n\n### 2.4 The checkpoint wake races the `/clear` and lands in the OLD session\n\n<!-- [doc->REQ-HAZARD-CHECKPOINT-CLEAR-RACE] -->\n\n- **Failure:** The agent-driven checkpoint submits `/clear` then a wake directive. CC's `/clear` is\n  ASYNC — it tears down and rebuilds the session, re-running SessionStart (whoami, psyche-download,\n  brief assembly — network I/O, seconds). The pre-fix macro emitted both in ONE inject sequence with a\n  fixed `500ms` gap between the `/clear` submit and the wake. Two stacked problems: (1) 500ms is a\n  blind guess — a slow clear routinely exceeds it, so the wake is typed/submitted BEFORE the clear\n  settles and lands in the OLD (about-to-be-wiped) session, or the ordering inverts entirely (the\n  wake hits first). (2) A hard ceiling: the whole inject sequence must `{commit}` within the 5s\n  `INJECT_COMMIT_DEADLINE`, so the delay CANNOT be widened to cover a genuinely slow clear — a single\n  inject sequence is architecturally incapable of straddling a `/clear`. Field-observed: the followup\n  wake hit first.\n- **Invariant:** The clear and the wake are SPLIT into two inject sequences, synchronized on CC's own\n  clear-done signal (SessionStart firing with `source == clear`). (1) ARM: a `{\"checkpoint\":\"v1\",…}`\n  envelope stashes the wake in the translation binary's in-memory `pending_wake` and emits CLEAR-ONLY\n  (`ctrl+s · 50ms · /clear · 50ms · enter · commit`); the wake is withheld. (2) FIRE: the SessionStart hook,\n  on a `clear` boundary, UNCONDITIONALLY self-sends `{\"checkpoint_fire\":\"v1\"}`; the binary emits\n  WAKE-ONLY (`ctrl+s · 50ms · wake · 50ms · enter · commit`) iff a wake is armed, else no-ops. State lives\n  ONLY in translate memory (no marker file; the hook is stateless — it fires on every clear and the\n  binary decides). BOTH self-sends use `--force-native` so the signal is delivered through the\n  translation binary's stdin (where the markers parse), never spooled to the active-poll channel\n  (which surfaces the envelope as a plain message the agent reads instead of firing — the ENLYZEAM\n  misdelivery mode). Ordering is guaranteed by construction: the wake can only emit after SessionStart,\n  which only fires after `/clear` completes. Fires ONLY on `clear`, not `compact` (no checkpoint\n  variant leverages `/compact`).\n- **Accepted residual:** if the checkpoint's own clear fails to fire the signal (hook error), the\n  armed wake fires on the NEXT clear — possibly a slightly-wrong context. The every-clear-fire model\n  self-limits it (it won't sit forever); a translate-side generation/timeout could bound staleness if\n  it ever bites. Accepted for v1.\n- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n  `REQ-DIST-CHECKPOINT-COMMUNE`. Binding evidence = the tagged tests under\n  `REQ-HAZARD-CHECKPOINT-CLEAR-RACE` (crate units: `arm_then_fire_orders_clear_before_wake`,\n  `fire_emits_wake_only_and_disarms`, `fire_with_no_armed_wake_is_a_noop`; hook units:\n  `clear_boundary_fires_checkpoint_fire_force_native`, `non_clear_starts_do_not_fire_checkpoint`; int:\n  the ARM-emit block in `ci/idle-translate/translate-proof-int.sh`).\n- **Second leg (v0.15.1, paid 2026-07-06): everything that fires POST-clear must ride ONE inject\n  sequence.** The boundary rename (v0.14.1) originally rode its own sequence immediately before the\n  wake's — two back-to-back sequences at the just-rebuilt boundary RACED (CC input processing lags\n  the PTY stream: the rename's enter registered as a soft newline, the next sequence's ctrl+s failed\n  to stash the residue, and the wake text submitted INSIDE the `/rename` argument — flynn: session\n  titled with the wake, NO wake turn, agent dormant ~9.5h; ~33% observed, 1 of 3 checkpoints).\n  Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s\n  bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename\n  delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:\n  `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).\n\n---\n\n### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch\n\n<!-- [doc->REQ-HAZARD-PSYCHE-IDENTITY-ENV] -->\n\n- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —\n  including `OWL_SESSION_ID` / `SPT_AGENT_ID`, the parent's identity env. The Psyche's own harness\n  hooks then resolve \"self\" to the PARENT (or, worse, an unrelated top-level perch) and stamp/rebind\n  THAT perch's records. Field-observed on HFENDULEAM (F-028 C2 evidence): `f015b-probe-psyche`'s hook\n  rebound `hall-a`'s `info.json.session_id` to the psyche's own session via `authenticate()`'s\n  dead-owner re-pin — cross-perch identity contamination, silent.\n- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare\n  `env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`\n  AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`\n  (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —","truncated":true,"truncatedBy":"lines","totalLines":445,"totalBytes":16462,"outputLines":223,"outputBytes":16462,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# Known Hazards\n\n> A **conformance checklist, not advice.** Each hazard below is a first-class\n> `REQ-HAZARD-*` requirement in `traceable-reqs.toml`, and is **not \"covered\" until a test tags\n> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n> and `traceable-reqs check` will say so once the hazard is activated.\n\nA hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\nincident) or one you have *committed never to introduce*. State it so a test can prove it.\n\n## Entry format\n\nEach entry is one numbered subsection with these fields:\n\n- **Failure** — the concrete bad behavior: what goes wrong, under what sequence / timing / input.\n- **Invariant** — the property that MUST hold, phrased so a test can assert it (the thing the\n  `REQ-HAZARD-*` requires).\n- **Mapping / notes** — where this lives in *this* project, and anything that changes the shape\n  of the test (e.g. \"in-process now, so use a lock instead of racing on disk\").\n- **cite** — where the failure / fix is evidenced (an incident, a prior commit, a source path);\n  reference only — the binding evidence is the tagged test.\n\nMirror each entry as a requirement:\n\n```toml\n[[requirements]]\nid = \"REQ-HAZARD-EXAMPLE\"\ntitle = \"The invariant, stated so a test can prove it\"\nrequired_stages = []   # activate ([\"unit\"] or [\"unit\",\"int\"]) when you cover it\n```\n\n---\n\n## 1. Windows / MSYS shell environment\n\n### 1.1 MSYS `/`-prefix path conversion mangles slash-leading arguments\n\n- **Failure:** On Windows under Git-Bash / MSYS, any **command-line argument** beginning with `/`\n  is silently rewritten to an absolute Windows path before the target binary sees it. Observed\n  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n  documents the identical hazard in `new-alarm`.)\n- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  A test asserts the stdin path is honored (no argv `/`-token dependency).\n- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n\n---\n\n## 2. Live-agent perch / CI orchestration\n\n### 2.1 Nested-`claude` perch collision tears down the live agent's poll stream\n\n<!-- [doc->REQ-HAZARD-PERCH-COLLISION] -->\n\n- **Failure:** The CI acceptance harness spawns a real `claude` session as the system-under-test.\n  If that nested session loads the spt plugin (whose SessionStart establishes a perch) and resolves\n  the **same perch id as the live operator agent** (e.g. `perri`), the nested establish **displaces\n  the operator's perch** — perches are name-keyed, last-establish-wins — killing the operator's\n  `api poll` / live stream. Observed 2026-06-15 as the live poll Monitor exiting `exit-1` plus a\n  `sessions log seal failed: git failed` on revive (the collision teardown). Diagnosed by the\n  operator as self-inflicted, NOT a legacy-substrate bug.\n- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n  overrides both to a `sptc-ci-` id and never emits a live id.\n- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n  name-keyed knob (`spt whoami` resolves from `$OWL_SESSION_ID`/`$SPT_AGENT_ID`); a separate data\n  dir is not part of the public surface, so isolation rides on identity.\n- **cite:** Operator diagnosis 2026-06-15 (perch collision, self-inflicted via nested `claude -p`\n  loading the spt plugin). Reference only — binding evidence is the tagged test under\n  `REQ-HAZARD-PERCH-COLLISION`.\n\n### 2.2 A non-interactive spt-spawned CC deadlocks on a permission prompt\n\n<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->\n\n- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n  discarded): the daemon believes it hosts a working companion that never produces a commune.\n- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n  trust (see `docs/adr/0003-*`).\n- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n- **cite:** `claude_skill_owl/src/live/wrapper/claude.rs` (init/resume/final all pass\n  `--dangerously-skip-permissions`); identified 2026-06-18 while reaching psyche parity. Reference\n  only — binding evidence is the tagged tests under `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`.\n\n### 2.3 A missed `/clear` boundary rotation pins the perch to a dead session id — silently and permanently\n\n<!-- [doc->REQ-HAZARD-SESSION-PIN-WEDGE] -->\n\n- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart\n  boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**\n  session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the\n  departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two\n  stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22\n  (the new sid is unregistered until the rotation succeeds) that resolved `self:null` and **skipped\n  the rotation with zero diagnostics** — and even when the id resolved it presented **no auth\n  proof**. Either way the perch stays pinned to the dead sid, after which EVERY id-scoped api call\n  (`poll`/`state`/`boundary`) from the live session is silently `AUTH_REFUSED` (stderr-only): the\n  session looks perfectly healthy (brief renders, turns run) while local AND cross-node delivery is\n  permanently dead. **No self-heal exists**: later boundary attempts present the new sid and refuse\n  too, and spt-core's dead-owner re-pin correctly does NOT cover a live harness pid (squat\n  protection). Observed live: the ENLYZEAM `ball-b` wedge (F-024, root-caused 2026-07-02) —\n  operator clean-room repro: fresh session + `/clear` wedges with zero hook errors.\n- **Invariant:** (1) The boundary branch resolves the endpoint id from `$SPT_ENDPOINT_ID` FIRST\n  (the brief path's proven preference) — never whoami-by-new-sid alone. (2) The hook persists the\n  CURRENT session id at EVERY SessionStart to an adapter-owned state file\n  (`{adapter_dir}/state/session/<endpoint_id>.sid`) and presents it as `--session-id` proof at the\n  next rotation — NOT via `CLAUDE_ENV_FILE`, whose per-session lifetime is the exact failure mode.\n  (3) A rotation that cannot run (no id) or is refused (auth) logs LOUDLY to stderr with the\n  reason; a silent skip is forbidden.\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n  recorded sid; live-proven on ENLYZEAM 2026-07-02).\n- **cite:** F-024 correction arc, `docs/SPT-CORE-FINDINGS.md` (empirics: whoami-by-dead-sid →\n  `self:null`; `AUTH_REFUSED:ball-b (need --token or matching --session-id)` shell-verified); doyle\n  ruling `F024D-BOUNDARY-RULING` 2026-07-02 (adapter-owned state file = the contract-clean carrier;\n  the reference pattern for the harness-contract boundary doc). Binding evidence = the tagged tests\n  under `REQ-HAZARD-SESSION-PIN-WEDGE`. Fix live-validated on-node: E2E-0104 GREEN (doyle verdict\n  2026-07-02 — rotation authenticates with the prior sid, `.sid` rolls forward in lockstep,\n  post-rotation delivery lands; the `[int->REQ-DIST-BOUNDARY-ROTATE]` record in\n  `docs/SPT-CORE-FINDINGS.md` F-024).\n\n### 2.4 The checkpoint wake races the `/clear` and lands in the OLD session\n\n<!-- [doc->REQ-HAZARD-CHECKPOINT-CLEAR-RACE] -->\n\n- **Failure:** The agent-driven checkpoint submits `/clear` then a wake directive. CC's `/clear` is\n  ASYNC — it tears down and rebuilds the session, re-running SessionStart (whoami, psyche-download,\n  brief assembly — network I/O, seconds). The pre-fix macro emitted both in ONE inject sequence with a\n  fixed `500ms` gap between the `/clear` submit and the wake. Two stacked problems: (1) 500ms is a\n  blind guess — a slow clear routinely exceeds it, so the wake is typed/submitted BEFORE the clear\n  settles and lands in the OLD (about-to-be-wiped) session, or the ordering inverts entirely (the\n  wake hits first). (2) A hard ceiling: the whole inject sequence must `{commit}` within the 5s\n  `INJECT_COMMIT_DEADLINE`, so the delay CANNOT be widened to cover a genuinely slow clear — a single\n  inject sequence is architecturally incapable of straddling a `/clear`. Field-observed: the followup\n  wake hit first.\n- **Invariant:** The clear and the wake are SPLIT into two inject sequences, synchronized on CC's own\n  clear-done signal (SessionStart firing with `source == clear`). (1) ARM: a `{\"checkpoint\":\"v1\",…}`\n  envelope stashes the wake in the translation binary's in-memory `pending_wake` and emits CLEAR-ONLY\n  (`ctrl+s · 50ms · /clear · 50ms · enter · commit`); the wake is withheld. (2) FIRE: the SessionStart hook,\n  on a `clear` boundary, UNCONDITIONALLY self-sends `{\"checkpoint_fire\":\"v1\"}`; the binary emits\n  WAKE-ONLY (`ctrl+s · 50ms · wake · 50ms · enter · commit`) iff a wake is armed, else no-ops. State lives\n  ONLY in translate memory (no marker file; the hook is stateless — it fires on every clear and the\n  binary decides). BOTH self-sends use `--force-native` so the signal is delivered through the\n  translation binary's stdin (where the markers parse), never spooled to the active-poll channel\n  (which surfaces the envelope as a plain message the agent reads instead of firing — the ENLYZEAM\n  misdelivery mode). Ordering is guaranteed by construction: the wake can only emit after SessionStart,\n  which only fires after `/clear` completes. Fires ONLY on `clear`, not `compact` (no checkpoint\n  variant leverages `/compact`).\n- **Accepted residual:** if the checkpoint's own clear fails to fire the signal (hook error), the\n  armed wake fires on the NEXT clear — possibly a slightly-wrong context. The every-clear-fire model\n  self-limits it (it won't sit forever); a translate-side generation/timeout could bound staleness if\n  it ever bites. Accepted for v1.\n- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n  `REQ-DIST-CHECKPOINT-COMMUNE`. Binding evidence = the tagged tests under\n  `REQ-HAZARD-CHECKPOINT-CLEAR-RACE` (crate units: `arm_then_fire_orders_clear_before_wake`,\n  `fire_emits_wake_only_and_disarms`, `fire_with_no_armed_wake_is_a_noop`; hook units:\n  `clear_boundary_fires_checkpoint_fire_force_native`, `non_clear_starts_do_not_fire_checkpoint`; int:\n  the ARM-emit block in `ci/idle-translate/translate-proof-int.sh`).\n- **Second leg (v0.15.1, paid 2026-07-06): everything that fires POST-clear must ride ONE inject\n  sequence.** The boundary rename (v0.14.1) originally rode its own sequence immediately before the\n  wake's — two back-to-back sequences at the just-rebuilt boundary RACED (CC input processing lags\n  the PTY stream: the rename's enter registered as a soft newline, the next sequence's ctrl+s failed\n  to stash the residue, and the wake text submitted INSIDE the `/rename` argument — flynn: session\n  titled with the wake, NO wake turn, agent dormant ~9.5h; ~33% observed, 1 of 3 checkpoints).\n  Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s\n  bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename\n  delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:\n  `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).\n\n---\n\n### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch\n\n<!-- [doc->REQ-HAZARD-PSYCHE-IDENTITY-ENV] -->\n\n- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —\n  including `OWL_SESSION_ID` / `SPT_AGENT_ID`, the parent's identity env. The Psyche's own harness\n  hooks then resolve \"self\" to the PARENT (or, worse, an unrelated top-level perch) and stamp/rebind\n  THAT perch's records. Field-observed on HFENDULEAM (F-028 C2 evidence): `f015b-probe-psyche`'s hook\n  rebound `hall-a`'s `info.json.session_id` to the psyche's own session via `authenticate()`'s\n  dead-owner re-pin — cross-perch identity contamination, silent.\n- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare\n  `env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`\n  AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`\n  (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —\n…\n  side, but the adapter must not hand the child a foreign identity in the first place.\n…\n\n…\n\n…\n  evidence = this entry; impl/unit land with the guard slice.","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,167,168,169,170,171,172,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,null,225,null,232,null,370,null,444]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\KNOWN-HAZARDS.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":445,"totalBytes":16462,"outputLines":223,"outputBytes":16462,"shownRange":{"start":1,"end":223},"nextOffset":224}}},"isError":false,"timestamp":1784087152185}}
{"type":"message","id":"764c0089","parentId":"8fa92b70","timestamp":"2026-07-15T03:45:52.193Z","message":{"role":"toolResult","toolCallId":"call_yAVRhbHchohhakARhACzhWeP|fc_0ca4af36969c80fa016a5702700b2c8198bdba49b088840754","toolName":"read","content":[{"type":"text","text":"# Known Hazards\n…\n## 2. Live-agent perch / CI orchestration\n…\n### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch\n…\n- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —\n…\n- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare\n  `env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`\n  AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`\n  (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —\n  spt-core's halves (nested-inclusive self-resolve + cross-perch bind-honesty refuse) guard the daemon\n  side, but the adapter must not hand the child a foreign identity in the first place.\n- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n  child that runs harness hooks, it needs the same scrub.\n- **cite:** doyle's F-028 C2 adapter change brief (2026-07-03); spt-core runtime `env_remove` support\n  is public contract (`manifest.schema.json` `SessionRole.env_remove`).\n\n### 2.6 The psyche host loops silently at ~3 boots/sec when claude dies at spawn\n\n<!-- [doc->REQ-HAZARD-PSYCHE-HOST-THRASH] -->\n\n- **Failure:** The psyche host's resident loop had no pacing: each iteration polls the perch\n  (`spt ready --once`) then maybe runs a claude turn. A healthy iteration BLOCKS (the poll waits for\n  a pulse; a model turn takes seconds) — but when claude dies instantly at spawn (canonically: an\n  untrusted `{psyche_dir}` cwd puts CC at a workspace-trust prompt with null stdin, F-h/F-026) or\n  the poll returns success-instantly-empty, the loop spun ~3 iterations/sec, silently, forever.\n  Field-observed on HFENDULEAM: ordinal 5358 boot rows in ~30min. Doubly invisible to spt-core:\n  its residency pacing keys on wrapper DEATH (the wrapper stays resident), and its\n  `psyche_host_error` surface sees wrapper EXITS only (nothing exits). Even `spt endpoint shutdown`\n  left the looping wrapper running (core-side hardening for that is C3(b)/(c), theirs).\n- **Invariant:** The loop is PACED and TERMINAL under persistent instant cycles: a cycle finishing\n  sub-2s (`INSTANT_CYCLE_MS`) increments a streak — exponential backoff (500ms doubling, 5s cap) —\n  and at 8 consecutive (`GIVE_UP_AFTER`) the host prints `PSYCHE_HOST_GIVE_UP:<id>` to stderr and\n  exits **nonzero** (3), converting the invisible loop into the wrapper death spt-core's existing\n  `psyche_host_error`/residency machinery already handles. Any healthy cycle resets the streak\n  (recovery mid-thrash — e.g. the operator trusts the dir — must not accumulate toward give-up).\n- **Threshold coupling (doyle-ruled, binding):** spt-core's C3(b) backstop trips at ≥10 ledger\n  boundaries per 60s window. For the pure instant-death class the adapter always fires first —\n  ≤8 boundaries, <60s worst-case wall clock, strictly inside the core trip on both axes. Failure\n  modes that dodge the streak (e.g. just-over-threshold cycles resetting it — the reset\n  deliberately forgives) legitimately reach the core backstop: a core trip on a guarded wrapper is\n  CORRECT backstop behavior, not a bug. Do not retune these constants except in step with the\n  ruling (the coupling is asserted by `give_up_stays_inside_the_core_backstop_window`, derived\n  from the real constants).\n- **Second leg (v0.13.4, field 2026-07-04) — a REFUSED poll must die loud, not exit clean:** the\n  runner's per-pulse `spt ready <id> --once` can fail for CONFIG/ENV reasons the runner can never\n  satisfy — canonically the multi-subnet home guard (`READY_FAIL: … pass --subnet`; the daemon\n  fills carry no subnet, a core seam, F-017 sibling). Treating any non-success as \"perch gone\"\n  exited 0 silently and moved the invisible loop ONE LEVEL UP: the daemon rehosted the runner\n  ~every 5s forever (hall-bf-psyche, ordinal 6491, since the node's second subnet joined —\n  invisible to C3(b), see doyle's boot-records-vs-ledger item). Invariant: a poll failure whose\n  stderr carries `READY_FAIL` exits LOUD + NONZERO (`PSYCHE_POLL_FAIL`, exit 4) so\n  psyche_host_error stamps a real fault; a plain non-success (perch closed at session end) stays\n  the quiet lifecycle exit. Asserted by `ready_fail_refusal_is_loud_not_perch_gone` +\n  `plain_poll_death_stays_the_clean_perch_gone_exit`.\n- **RESOLVED-BY-DESIGN (v0.14.0, F-030 psyche-ephemeral redesign):** the resident loop this hazard\n  guards — and its second-leg poll-fail — are DELETED. The shim is run-to-completion (one `claude`\n  spawn per invocation, then exit): there is no loop and no per-pulse `spt ready --once`, so neither\n  the instant-cycle class nor the multi-subnet `READY_FAIL` churn can occur adapter-side. Crashloop is\n  now repeated ephemeral deaths, which core C3(b)'s ledger-rate backstop + the daemon's\n  `psyche_host_error` surface own by design (an ephemeral death is exactly what that surface consumes).\n  The subnet-less nested-resolution seam (a runner could never know its subnet) folds into the F-030\n  core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.\n- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`\n  (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to\n  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n- **cite:** doyle's C3 adapter-half brief (2026-07-03), formalizing our F-h finding; spt-core\n  halves = C3(b) ledger-rate trip @b2351a0 + shutdown tree-kill hardening; second leg = the\n  hall-bf field residual (doyle confirm run, 2026-07-04) + perri's multi-subnet READY_FAIL repro;\n  RESOLVED-BY-DESIGN = the v0.14.0 ephemeral redesign (PSYCHE-EPHEMERAL-PLAN.md, F-030 adapter half).\n\n### 2.7 A delivery answered with ZERO records faults + terminates the translation binary\n\n<!-- [doc->REQ-HAZARD-EMPTY-RESPONSE-COMMIT] -->\n\n- **Failure:** The broker opens an inject sequence for every event delivery it feeds the\n  translation binary and ends it ONLY on an explicit `{commit}` — a delivery the binary answers\n  with zero output lines leaves the sequence unterminated, the 5s `INJECT_COMMIT_DEADLINE`\n  expires, and the broker faults + TERMINATES the binary (permanently until respawn; the\n  published contract's \"raw-inject fallback\" consequence is stale — removed core-side v0.14.3).\n  The adapter had exactly this zero-record path: a `checkpoint_fire` with nothing armed answered\n  `Vec::new()`. Since the SessionStart hook self-sends `checkpoint_fire` on EVERY clear\n  (stateless by design, REQ-HAZARD-CHECKPOINT-CLEAR-RACE), every non-checkpoint `/clear`\n  killed the binary at the boundary — the deterministic half of the \"force-native decay\" ghost\n  (B6). Pinned 2026-07-04: iso captured stderr shows the ARM's commit ACCEPTED and the\n  `TRANSLATION_FAULT` following the unarmed duplicate fire ~5s later; whole-sequence-deadline\n  and parse-drop mechanisms were excluded by measurement (sequence budgets ≤~150ms vs 5s;\n  zero `TRANSLATION_BIN_BAD_LINE`; commit line bytes exact).\n- **Invariant:** EVERY event-typed delivery answers at least a bare `{\"commit\":true}` — a\n  deliberate no-op delivery still terminates its inject sequence. Scope is event-typed responses\n  ONLY: `init`/`input`/unknown stdin lines are protocol lines, not inject deliveries, and must\n  stay silent (an unsolicited `{commit}` outside a sequence is protocol noise; doyle-confirmed\n  scoping).\n- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `dispatch_event`'s unarmed-fire\n  branch and `dispatch_line`'s event-without-envelope branch each emit the bare commit. Binding\n  evidence = the three `REQ-HAZARD-EMPTY-RESPONSE-COMMIT` unit tests (two bare-commit asserts +\n  the no-stray-commit scoping assert). The live boundary-survival int graduates with the\n  wake-survival e2e once spt-core's C-1 (miss≠fault deadline semantics) lands. This was a JOINT\n  contract gap: the published contract nowhere states the empty-response rule and understates\n  the missed-commit penalty — both docs fixes ride core's C-1 chunk.\n- **cite:** doyle 3-discriminant collab + verdict (2026-07-04, work order @2915eb8); iso wtrace\n  captured stderr (4 injects, fault after the duplicate fire); operator's two-commit premise\n  check that reframed the hunt to the receive side.\n\n### 2.8 Raw PreToolUse stdout is DISCARDED by CC — a mid-turn drain becomes a silent message black hole\n\n<!-- [doc->REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE] -->\n\n- **Failure:** CC injects a hook's additionalContext from PLAIN stdout only on\n  `UserPromptSubmit`. On `PreToolUse`, plain stdout is logged and DISCARDED — context reaches\n  the agent only inside the `{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\n  \"additionalContext\":…}}` JSON envelope. The adapter's mid-turn delivery leg (F-021) emitted\n  raw text on both events, so every message drained during a busy turn was TAKEN from the spool\n  (`delivered=1`, honest core-side) and then thrown away by CC — invisible to the agent,\n  invisible to the sender, invisible in the spool. Field 2026-07-07 (doyle RCA): every\n  claude-spt session on the node was a silent message black hole; three doyle→perri messages\n  (including the outage evidence itself) were eaten mid-turn. The busy-path eat also MASKS\n  itself: a busy agent's rows are consumed within seconds by its constant PreToolUse stream,\n  so the idle-path/UPS legs never get a chance to surface them.\n- **Invariant:** Every PreToolUse emission — message drains, nudges, overflow pointers — rides\n  the `hookSpecificOutput` JSON envelope, never raw stdout. The cap decision is made on the\n  WRAPPED byte size (JSON escaping can double newline-heavy drains), and an over-cap drain\n  spills the RAW text with a WRAPPED pointer — a taken message must always surface somewhere\n  the agent can read.\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `wrap_pretool_context` +\n  `emit_capped_pretool`, wired in `handle_pre_tool_use`. UserPromptSubmit keeps raw-stdout\n  emission (that channel is the documented raw-context path and is live-proven).\n  `~/.claude/reference_docs/claude-code-hooks.md` is the per-event contract source — check it\n  BEFORE wiring any new hook output, per event, never by analogy from another event.\n- **cite:** doyle node-wide RCA + self-probe (2026-07-07); perri repro: the installed binary\n  driven directly emits correctly (binary exonerated), spool rows delivered=1 with zero\n  surfaced `<sptc_messages>`; recovered bodies read straight from `spool.db`.\n\n### 2.9 A poll frame truncated mid-frame is silently dropped WHOLE by the drain renderer\n\n<!-- [doc->REQ-HAZARD-DANGLING-FRAME-LOUD] -->\n\n- **Failure:** `render_frames` walks `spt api poll` output frame by frame (`<EVENT …>body</EVENT>`).\n  If the poll output ends mid-frame — opening tag present but no `>`, or a body with no closing\n  `</EVENT>` — the original loop `break`s and the dangling frame vanishes: the message was taken\n  from the spool (`delivered=1`) but nothing surfaces to the agent. Same silent-loss family as\n  §2.8, one seam upstream. Never observed in the field (the 2026-07-07 seed-#9 head-truncations\n  were core-side idle-inject pacing, spool rows complete, and could NOT have produced this shape),\n  but the drop path existed and was invisible by construction — hardened 2026-07-07 (doyle-ACKed)\n  before it could cost a real message.\n- **Invariant:** A dangling frame is never dropped silently. The renderer emits a loud\n  `<sptc_frame_integrity>` marker carrying the surviving partial content and a spool-recovery\n  pointer (`owlery/<id>/spool.db`, `messages` table) — a taken message must always surface\n  somewhere the agent can read (the §2.8 rule, applied to the framing seam).\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — the dangling-frame arm in `render_frames`.\n  Rides both drain paths (UserPromptSubmit + PreToolUse) and the existing capped emits unchanged.\n- **cite:** perri code-walk during the seed-#9 seam exoneration (2026-07-07); doyle \"ship it\" ACK\n  same night.\n\n## 3. Distribution split (adapter × plugin version skew)\n\n### 3.1 A hook_cmd shape change with a stale dispatch.sh bricks the whole CC session\n\n<!-- [doc->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP] -->\n\n- **Failure:** `spt adapter update` refreshes the adapter (manifest + binary) **immediately**, but\n  the cplugs plugin's `dispatch.sh` on disk only refreshes after a plugin reconcile + CC\n  `/reload-plugins`. Observed live 2026-07-01 (perri, the v0.9.0→0.9.2 on-node update): the stale\n  0.1.8 `dispatch.sh` execs `\"$bin\" <event>` (expecting the legacy ` hook`-suffixed hook_cmd) while\n  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n  `hook` token iff missing, strip iff doubled), and (b) `claude-spt` exits **0** (pass-through with\n  a stderr note) on an unrecognized event/subcommand token so a stale dispatch produces a noisy\n  no-op instead of a tool-blocking failure. Equivalently: pin the dispatch↔binary contract so the\n  shape can never change unilaterally.\n- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n  hooks; the degrade-not-brick exit-0 half covers that remainder.\n- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n  binding evidence lands with the tagged tests under `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`\n  (registry entry present, stages `[]` until the guard slice activates).\n\n### 3.2 CC can boot a session on a STALE cached plugin version despite a newer one installed\n\n<!-- [doc->REQ-HAZARD-STALE-PLUGIN-RESOLUTION] -->\n\n- **Failure:** the harness's plugin resolution picked an OLD cached copy at session boot even\n  though the newer version was already installed. Observed live (F-024 domino #1, ENLYZEAM):\n  the node's plugin cache held BOTH sptc 0.1.8 and 0.1.9; `installed_plugins.json` showed 0.1.9\n  installed **24 minutes before** the 2026-06-29 boot of session `22ce8af0…` — yet that session\n  verifiably ran the **0.1.8** dispatch (the unquoted-`SPTC_HOOK_BIN` v0.9.0 regression, fixed in\n  0.1.9). The stale pick then compounds: the buggy dispatch corrupted the per-session\n  `CLAUDE_ENV_FILE`, which **survives every resume of that session** (the env file lives with the\n  session, so one bad boot poisons the session's whole lifetime), hooks died silently, and the\n  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n  stale pick is.\n- **Invariant (defense-in-depth — the adapter stays safe under a stale pick):\n  (a) degrade-not-brick:** any shipped dispatch version × any adapter version must degrade,\n  never brick (§3.1's invariant is the binding half; a stale-RESOLVED plugin is simply another\n  way to arrive at the same skew).\n  **(b) minimize the stale-pickable set:** the post-update plugin reconcile must reach EVERY\n  plugin root the node's CLIs actually read — the `claude` config root AND each ccs instance root\n  (`~/.ccs/instances/<x>/…`, the v0.10.3 follow-up) — and superseded cached copies should be\n  purged (or at minimum flagged) rather than left resolvable; a picker cannot choose a stale copy\n  that no longer exists.\n  **(c) observability:** which plugin version actually ran must be a log line, not an ssh dig —\n  the dispatch (or the hook binary it execs) should surface its plugin version at least once per\n  session so version-of-truth checks can catch a stale pick immediately.\n- **Mapping / notes:** reconcile halves live in `tools/claude-spt/src/post_update.rs` (primary\n  CLI reconcile + the best-effort ccs follow-up, v0.10.2/v0.10.3 — REQ-DIST-CCS-PLUGIN-FOLLOWUP);\n  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n  demonstrates it.\n- **cite:** F-024 auth-wedge arc, `docs/SPT-CORE-FINDINGS.md` (doyle receipt\n  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n  evidence = this entry; impl/unit land with the guard slice.\n"}],"details":{"displayContent":{"text":"# Known Hazards\n…\n## 2. Live-agent perch / CI orchestration\n…\n### 2.5 A spawned Psyche inherits its parent's identity env and rebinds a FOREIGN perch\n…\n- **Failure:** The `[session.psyche_init]` child inherits the spawning session's environment —\n…\n- **Invariant:** A Psyche NEVER carries its parent's identity env. BOTH psyche roles declare\n  `env_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]` (v0.14.0 ephemeral: the gate `[session.psyche_init]`\n  AND the spawned `[session.psyche_resume]`); the spt-core runtime honors role-level `env_remove`\n  (shipped, f028) and strips them before spawn. This is the adapter's half of the f028 guard pair —\n  spt-core's halves (nested-inclusive self-resolve + cross-perch bind-honesty refuse) guard the daemon\n  side, but the adapter must not hand the child a foreign identity in the first place.\n- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n  child that runs harness hooks, it needs the same scrub.\n- **cite:** doyle's F-028 C2 adapter change brief (2026-07-03); spt-core runtime `env_remove` support\n  is public contract (`manifest.schema.json` `SessionRole.env_remove`).\n\n### 2.6 The psyche host loops silently at ~3 boots/sec when claude dies at spawn\n\n<!-- [doc->REQ-HAZARD-PSYCHE-HOST-THRASH] -->\n\n- **Failure:** The psyche host's resident loop had no pacing: each iteration polls the perch\n  (`spt ready --once`) then maybe runs a claude turn. A healthy iteration BLOCKS (the poll waits for\n  a pulse; a model turn takes seconds) — but when claude dies instantly at spawn (canonically: an\n  untrusted `{psyche_dir}` cwd puts CC at a workspace-trust prompt with null stdin, F-h/F-026) or\n  the poll returns success-instantly-empty, the loop spun ~3 iterations/sec, silently, forever.\n  Field-observed on HFENDULEAM: ordinal 5358 boot rows in ~30min. Doubly invisible to spt-core:\n  its residency pacing keys on wrapper DEATH (the wrapper stays resident), and its\n  `psyche_host_error` surface sees wrapper EXITS only (nothing exits). Even `spt endpoint shutdown`\n  left the looping wrapper running (core-side hardening for that is C3(b)/(c), theirs).\n- **Invariant:** The loop is PACED and TERMINAL under persistent instant cycles: a cycle finishing\n  sub-2s (`INSTANT_CYCLE_MS`) increments a streak — exponential backoff (500ms doubling, 5s cap) —\n  and at 8 consecutive (`GIVE_UP_AFTER`) the host prints `PSYCHE_HOST_GIVE_UP:<id>` to stderr and\n  exits **nonzero** (3), converting the invisible loop into the wrapper death spt-core's existing\n  `psyche_host_error`/residency machinery already handles. Any healthy cycle resets the streak\n  (recovery mid-thrash — e.g. the operator trusts the dir — must not accumulate toward give-up).\n- **Threshold coupling (doyle-ruled, binding):** spt-core's C3(b) backstop trips at ≥10 ledger\n  boundaries per 60s window. For the pure instant-death class the adapter always fires first —\n  ≤8 boundaries, <60s worst-case wall clock, strictly inside the core trip on both axes. Failure\n  modes that dodge the streak (e.g. just-over-threshold cycles resetting it — the reset\n  deliberately forgives) legitimately reach the core backstop: a core trip on a guarded wrapper is\n  CORRECT backstop behavior, not a bug. Do not retune these constants except in step with the\n  ruling (the coupling is asserted by `give_up_stays_inside_the_core_backstop_window`, derived\n  from the real constants).\n- **Second leg (v0.13.4, field 2026-07-04) — a REFUSED poll must die loud, not exit clean:** the\n  runner's per-pulse `spt ready <id> --once` can fail for CONFIG/ENV reasons the runner can never\n  satisfy — canonically the multi-subnet home guard (`READY_FAIL: … pass --subnet`; the daemon\n  fills carry no subnet, a core seam, F-017 sibling). Treating any non-success as \"perch gone\"\n  exited 0 silently and moved the invisible loop ONE LEVEL UP: the daemon rehosted the runner\n  ~every 5s forever (hall-bf-psyche, ordinal 6491, since the node's second subnet joined —\n  invisible to C3(b), see doyle's boot-records-vs-ledger item). Invariant: a poll failure whose\n  stderr carries `READY_FAIL` exits LOUD + NONZERO (`PSYCHE_POLL_FAIL`, exit 4) so\n  psyche_host_error stamps a real fault; a plain non-success (perch closed at session end) stays\n  the quiet lifecycle exit. Asserted by `ready_fail_refusal_is_loud_not_perch_gone` +\n  `plain_poll_death_stays_the_clean_perch_gone_exit`.\n- **RESOLVED-BY-DESIGN (v0.14.0, F-030 psyche-ephemeral redesign):** the resident loop this hazard\n  guards — and its second-leg poll-fail — are DELETED. The shim is run-to-completion (one `claude`\n  spawn per invocation, then exit): there is no loop and no per-pulse `spt ready --once`, so neither\n  the instant-cycle class nor the multi-subnet `READY_FAIL` churn can occur adapter-side. Crashloop is\n  now repeated ephemeral deaths, which core C3(b)'s ledger-rate backstop + the daemon's\n  `psyche_host_error` surface own by design (an ephemeral death is exactly what that surface consumes).\n  The subnet-less nested-resolution seam (a runner could never know its subnet) folds into the F-030\n  core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.\n- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`\n  (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to\n  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n- **cite:** doyle's C3 adapter-half brief (2026-07-03), formalizing our F-h finding; spt-core\n  halves = C3(b) ledger-rate trip @b2351a0 + shutdown tree-kill hardening; second leg = the\n  hall-bf field residual (doyle confirm run, 2026-07-04) + perri's multi-subnet READY_FAIL repro;\n  RESOLVED-BY-DESIGN = the v0.14.0 ephemeral redesign (PSYCHE-EPHEMERAL-PLAN.md, F-030 adapter half).\n\n### 2.7 A delivery answered with ZERO records faults + terminates the translation binary\n\n<!-- [doc->REQ-HAZARD-EMPTY-RESPONSE-COMMIT] -->\n\n- **Failure:** The broker opens an inject sequence for every event delivery it feeds the\n  translation binary and ends it ONLY on an explicit `{commit}` — a delivery the binary answers\n  with zero output lines leaves the sequence unterminated, the 5s `INJECT_COMMIT_DEADLINE`\n  expires, and the broker faults + TERMINATES the binary (permanently until respawn; the\n  published contract's \"raw-inject fallback\" consequence is stale — removed core-side v0.14.3).\n  The adapter had exactly this zero-record path: a `checkpoint_fire` with nothing armed answered\n  `Vec::new()`. Since the SessionStart hook self-sends `checkpoint_fire` on EVERY clear\n  (stateless by design, REQ-HAZARD-CHECKPOINT-CLEAR-RACE), every non-checkpoint `/clear`\n  killed the binary at the boundary — the deterministic half of the \"force-native decay\" ghost\n  (B6). Pinned 2026-07-04: iso captured stderr shows the ARM's commit ACCEPTED and the\n  `TRANSLATION_FAULT` following the unarmed duplicate fire ~5s later; whole-sequence-deadline\n  and parse-drop mechanisms were excluded by measurement (sequence budgets ≤~150ms vs 5s;\n  zero `TRANSLATION_BIN_BAD_LINE`; commit line bytes exact).\n- **Invariant:** EVERY event-typed delivery answers at least a bare `{\"commit\":true}` — a\n  deliberate no-op delivery still terminates its inject sequence. Scope is event-typed responses\n  ONLY: `init`/`input`/unknown stdin lines are protocol lines, not inject deliveries, and must\n  stay silent (an unsolicited `{commit}` outside a sequence is protocol noise; doyle-confirmed\n  scoping).\n- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `dispatch_event`'s unarmed-fire\n  branch and `dispatch_line`'s event-without-envelope branch each emit the bare commit. Binding\n  evidence = the three `REQ-HAZARD-EMPTY-RESPONSE-COMMIT` unit tests (two bare-commit asserts +\n  the no-stray-commit scoping assert). The live boundary-survival int graduates with the\n  wake-survival e2e once spt-core's C-1 (miss≠fault deadline semantics) lands. This was a JOINT\n  contract gap: the published contract nowhere states the empty-response rule and understates\n  the missed-commit penalty — both docs fixes ride core's C-1 chunk.\n- **cite:** doyle 3-discriminant collab + verdict (2026-07-04, work order @2915eb8); iso wtrace\n  captured stderr (4 injects, fault after the duplicate fire); operator's two-commit premise\n  check that reframed the hunt to the receive side.\n\n### 2.8 Raw PreToolUse stdout is DISCARDED by CC — a mid-turn drain becomes a silent message black hole\n\n<!-- [doc->REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE] -->\n\n- **Failure:** CC injects a hook's additionalContext from PLAIN stdout only on\n  `UserPromptSubmit`. On `PreToolUse`, plain stdout is logged and DISCARDED — context reaches\n  the agent only inside the `{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\n  \"additionalContext\":…}}` JSON envelope. The adapter's mid-turn delivery leg (F-021) emitted\n  raw text on both events, so every message drained during a busy turn was TAKEN from the spool\n  (`delivered=1`, honest core-side) and then thrown away by CC — invisible to the agent,\n  invisible to the sender, invisible in the spool. Field 2026-07-07 (doyle RCA): every\n  claude-spt session on the node was a silent message black hole; three doyle→perri messages\n  (including the outage evidence itself) were eaten mid-turn. The busy-path eat also MASKS\n  itself: a busy agent's rows are consumed within seconds by its constant PreToolUse stream,\n  so the idle-path/UPS legs never get a chance to surface them.\n- **Invariant:** Every PreToolUse emission — message drains, nudges, overflow pointers — rides\n  the `hookSpecificOutput` JSON envelope, never raw stdout. The cap decision is made on the\n  WRAPPED byte size (JSON escaping can double newline-heavy drains), and an over-cap drain\n  spills the RAW text with a WRAPPED pointer — a taken message must always surface somewhere\n  the agent can read.\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `wrap_pretool_context` +\n  `emit_capped_pretool`, wired in `handle_pre_tool_use`. UserPromptSubmit keeps raw-stdout\n  emission (that channel is the documented raw-context path and is live-proven).\n  `~/.claude/reference_docs/claude-code-hooks.md` is the per-event contract source — check it\n  BEFORE wiring any new hook output, per event, never by analogy from another event.\n- **cite:** doyle node-wide RCA + self-probe (2026-07-07); perri repro: the installed binary\n  driven directly emits correctly (binary exonerated), spool rows delivered=1 with zero\n  surfaced `<sptc_messages>`; recovered bodies read straight from `spool.db`.\n\n### 2.9 A poll frame truncated mid-frame is silently dropped WHOLE by the drain renderer\n\n<!-- [doc->REQ-HAZARD-DANGLING-FRAME-LOUD] -->\n\n- **Failure:** `render_frames` walks `spt api poll` output frame by frame (`<EVENT …>body</EVENT>`).\n  If the poll output ends mid-frame — opening tag present but no `>`, or a body with no closing\n  `</EVENT>` — the original loop `break`s and the dangling frame vanishes: the message was taken\n  from the spool (`delivered=1`) but nothing surfaces to the agent. Same silent-loss family as\n  §2.8, one seam upstream. Never observed in the field (the 2026-07-07 seed-#9 head-truncations\n  were core-side idle-inject pacing, spool rows complete, and could NOT have produced this shape),\n  but the drop path existed and was invisible by construction — hardened 2026-07-07 (doyle-ACKed)\n  before it could cost a real message.\n- **Invariant:** A dangling frame is never dropped silently. The renderer emits a loud\n  `<sptc_frame_integrity>` marker carrying the surviving partial content and a spool-recovery\n  pointer (`owlery/<id>/spool.db`, `messages` table) — a taken message must always surface\n  somewhere the agent can read (the §2.8 rule, applied to the framing seam).\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — the dangling-frame arm in `render_frames`.\n  Rides both drain paths (UserPromptSubmit + PreToolUse) and the existing capped emits unchanged.\n- **cite:** perri code-walk during the seed-#9 seam exoneration (2026-07-07); doyle \"ship it\" ACK\n  same night.\n\n## 3. Distribution split (adapter × plugin version skew)\n\n### 3.1 A hook_cmd shape change with a stale dispatch.sh bricks the whole CC session\n\n<!-- [doc->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP] -->\n\n- **Failure:** `spt adapter update` refreshes the adapter (manifest + binary) **immediately**, but\n  the cplugs plugin's `dispatch.sh` on disk only refreshes after a plugin reconcile + CC\n  `/reload-plugins`. Observed live 2026-07-01 (perri, the v0.9.0→0.9.2 on-node update): the stale\n  0.1.8 `dispatch.sh` execs `\"$bin\" <event>` (expecting the legacy ` hook`-suffixed hook_cmd) while\n  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n  `hook` token iff missing, strip iff doubled), and (b) `claude-spt` exits **0** (pass-through with\n  a stderr note) on an unrecognized event/subcommand token so a stale dispatch produces a noisy\n  no-op instead of a tool-blocking failure. Equivalently: pin the dispatch↔binary contract so the\n  shape can never change unilaterally.\n- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n  hooks; the degrade-not-brick exit-0 half covers that remainder.\n- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n  binding evidence lands with the tagged tests under `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`\n  (registry entry present, stages `[]` until the guard slice activates).\n\n### 3.2 CC can boot a session on a STALE cached plugin version despite a newer one installed\n\n<!-- [doc->REQ-HAZARD-STALE-PLUGIN-RESOLUTION] -->\n\n- **Failure:** the harness's plugin resolution picked an OLD cached copy at session boot even\n  though the newer version was already installed. Observed live (F-024 domino #1, ENLYZEAM):\n  the node's plugin cache held BOTH sptc 0.1.8 and 0.1.9; `installed_plugins.json` showed 0.1.9\n  installed **24 minutes before** the 2026-06-29 boot of session `22ce8af0…` — yet that session\n  verifiably ran the **0.1.8** dispatch (the unquoted-`SPTC_HOOK_BIN` v0.9.0 regression, fixed in\n  0.1.9). The stale pick then compounds: the buggy dispatch corrupted the per-session\n  `CLAUDE_ENV_FILE`, which **survives every resume of that session** (the env file lives with the\n  session, so one bad boot poisons the session's whole lifetime), hooks died silently, and the\n  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n  stale pick is.\n- **Invariant (defense-in-depth — the adapter stays safe under a stale pick):\n  (a) degrade-not-brick:** any shipped dispatch version × any adapter version must degrade,\n  never brick (§3.1's invariant is the binding half; a stale-RESOLVED plugin is simply another\n  way to arrive at the same skew).\n  **(b) minimize the stale-pickable set:** the post-update plugin reconcile must reach EVERY\n  plugin root the node's CLIs actually read — the `claude` config root AND each ccs instance root\n  (`~/.ccs/instances/<x>/…`, the v0.10.3 follow-up) — and superseded cached copies should be\n  purged (or at minimum flagged) rather than left resolvable; a picker cannot choose a stale copy\n  that no longer exists.\n  **(c) observability:** which plugin version actually ran must be a log line, not an ssh dig —\n  the dispatch (or the hook binary it execs) should surface its plugin version at least once per\n  session so version-of-truth checks can catch a stale pick immediately.\n- **Mapping / notes:** reconcile halves live in `tools/claude-spt/src/post_update.rs` (primary\n  CLI reconcile + the best-effort ccs follow-up, v0.10.2/v0.10.3 — REQ-DIST-CCS-PLUGIN-FOLLOWUP);\n  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n  demonstrates it.\n- **cite:** F-024 auth-wedge arc, `docs/SPT-CORE-FINDINGS.md` (doyle receipt\n  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n  evidence = this entry; impl/unit land with the guard slice.\n","startLine":1,"lineNumbers":[1,null,59,null,210,null,214,null,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,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\KNOWN-HAZARDS.md"}}},"isError":false,"timestamp":1784087152193}}
{"type":"message","id":"966e0376","parentId":"764c0089","timestamp":"2026-07-15T03:45:55.286Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_3lXCar1CxIp7RPOj8WXXnYGd|fc_0ca4af36969c80fa016a570272eddc8198957377b9473d2c65","name":"read","arguments":{"i":"Reading final hazards","path":"docs/KNOWN-HAZARDS.md","selector":"345-500"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":33016,"output":50,"cacheRead":33280,"cacheWrite":0,"totalTokens":66346,"reasoningTokens":12,"cost":{"input":0.16508,"output":0.0015,"cacheRead":0.01664,"cacheWrite":0,"total":0.18322}},"stopReason":"toolUse","timestamp":1784087152196,"responseId":"resp_0ca4af36969c80fa016a5702704f608198a201f62b37d2f900","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a5702724ac08198b82dafcedb8c5ce9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwJygeZs227t_T8sb1yQ5krQAsTlRidI2ANqKW62-hyAzg9FGmcyjT4_iST0dhrgw3vgP2iRNoh01eQ_61x6E_X1dVMcOEFSqYWBAqxKkOHdo6h5w3_MXlwiB6l2RIflLYBrqdQ-aiQPk8fX8IcxYRhImOIYAJ3loHV6l0bj9sDhb-P6HDI0XzkEJhjW-SQamJZ8LRBczJbPF-gw3M3TcgPQ_v-yRUZ0qGFBfLwBgLUeHaaA_iPPBoDGmED_pqTfesRaWo9j7_xrBJE_WxjJLpnSwbxa0Rg9vZcTnAa6vBivrYcgZ27wBw-JbTJvadgfQTFVydwdZiLyJQxarFIea5a1VtXQOo2NOVUq56HWt67Meodp6LWwOCmqowGkGsrxjit69ROmgeliAtOmIhGjWVkWinur10W25uS7XNK0lbaW3oSKU-LRng-it68vwMEfgClH-CtxLDyEL6N8GLduoVKOzL7kU_zT45paChj_HtAMvK7u2tlzQnU9_-GhGorhgtqgKo8hSvGVRzeCOzrfR9p615dPwBBtVAUSBTPyEwIPgQyEoOKv5HBydo_8TPekeNtjdvUX1w1lMy3wtEzrPFc0_rOB27dPs7PxCosv2kbYrUfAZ_kEFLTlFVmg2Y8Evm4i1fa8TsngODzivNtY9swAfRdWccsC0OrkaIlR7Nf98yyd5TCSkQAqN8iZykxmXW2fPOPaDzYIybVGuwP_80tr9LW9lXOUKrzj7dRNrEq8uJXlpTPTQgxxVVXQ0oEjUh_Ppa3n_E_kye9rxZvZMRN1pzFvzrpFLKqpUgI7g37MZVKMUV3kK0z00Z30cV89EtlzuvbqMP3fczBFHOIQSYuIpUmeAZF103rq5_WTjhClsA-d8TRrsrnuSG21xAZUFzc1nQCHmQz8-9c4YpJUhxvQUBnFG9AWq0Jg8EsBuLozGTsoCJWVh19_ldfrBZ05sSrW","summary":[]},{"id":"fc_0ca4af36969c80fa016a570272eddc8198957377b9473d2c65","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading final hazards\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"345-500\"}","call_id":"call_3lXCar1CxIp7RPOj8WXXnYGd","name":"read"}]},"duration":3090.3429999999935,"ttft":2112.63890000002,"contextSnapshot":{"promptTokens":66296,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3lXCar1CxIp7RPOj8WXXnYGd|fc_0ca4af36969c80fa016a570272eddc8198957377b9473d2c65","toolName":"read","startedAt":"2026-07-15T03:45:55.287Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading final hazards"},"id":"fa876d3e","parentId":"966e0376","timestamp":"2026-07-15T03:45:55.287Z"}
{"type":"message","id":"e257c97d","parentId":"fa876d3e","timestamp":"2026-07-15T03:45:55.295Z","message":{"role":"toolResult","toolCallId":"call_3lXCar1CxIp7RPOj8WXXnYGd|fc_0ca4af36969c80fa016a570272eddc8198957377b9473d2c65","toolName":"read","content":[{"type":"text","text":"# Known Hazards\n…\n## 2. Live-agent perch / CI orchestration\n…\n### 2.8 Raw PreToolUse stdout is DISCARDED by CC — a mid-turn drain becomes a silent message black hole\n…\n- **Failure:** CC injects a hook's additionalContext from PLAIN stdout only on\n…\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `wrap_pretool_context` +\n…\n  `~/.claude/reference_docs/claude-code-hooks.md` is the per-event contract source — check it\n  BEFORE wiring any new hook output, per event, never by analogy from another event.\n- **cite:** doyle node-wide RCA + self-probe (2026-07-07); perri repro: the installed binary\n  driven directly emits correctly (binary exonerated), spool rows delivered=1 with zero\n  surfaced `<sptc_messages>`; recovered bodies read straight from `spool.db`.\n\n### 2.9 A poll frame truncated mid-frame is silently dropped WHOLE by the drain renderer\n\n<!-- [doc->REQ-HAZARD-DANGLING-FRAME-LOUD] -->\n\n- **Failure:** `render_frames` walks `spt api poll` output frame by frame (`<EVENT …>body</EVENT>`).\n  If the poll output ends mid-frame — opening tag present but no `>`, or a body with no closing\n  `</EVENT>` — the original loop `break`s and the dangling frame vanishes: the message was taken\n  from the spool (`delivered=1`) but nothing surfaces to the agent. Same silent-loss family as\n  §2.8, one seam upstream. Never observed in the field (the 2026-07-07 seed-#9 head-truncations\n  were core-side idle-inject pacing, spool rows complete, and could NOT have produced this shape),\n  but the drop path existed and was invisible by construction — hardened 2026-07-07 (doyle-ACKed)\n  before it could cost a real message.\n- **Invariant:** A dangling frame is never dropped silently. The renderer emits a loud\n  `<sptc_frame_integrity>` marker carrying the surviving partial content and a spool-recovery\n  pointer (`owlery/<id>/spool.db`, `messages` table) — a taken message must always surface\n  somewhere the agent can read (the §2.8 rule, applied to the framing seam).\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — the dangling-frame arm in `render_frames`.\n  Rides both drain paths (UserPromptSubmit + PreToolUse) and the existing capped emits unchanged.\n- **cite:** perri code-walk during the seed-#9 seam exoneration (2026-07-07); doyle \"ship it\" ACK\n  same night.\n\n## 3. Distribution split (adapter × plugin version skew)\n\n### 3.1 A hook_cmd shape change with a stale dispatch.sh bricks the whole CC session\n\n<!-- [doc->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP] -->\n\n- **Failure:** `spt adapter update` refreshes the adapter (manifest + binary) **immediately**, but\n  the cplugs plugin's `dispatch.sh` on disk only refreshes after a plugin reconcile + CC\n  `/reload-plugins`. Observed live 2026-07-01 (perri, the v0.9.0→0.9.2 on-node update): the stale\n  0.1.8 `dispatch.sh` execs `\"$bin\" <event>` (expecting the legacy ` hook`-suffixed hook_cmd) while\n  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n  `hook` token iff missing, strip iff doubled), and (b) `claude-spt` exits **0** (pass-through with\n  a stderr note) on an unrecognized event/subcommand token so a stale dispatch produces a noisy\n  no-op instead of a tool-blocking failure. Equivalently: pin the dispatch↔binary contract so the\n  shape can never change unilaterally.\n- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n  hooks; the degrade-not-brick exit-0 half covers that remainder.\n- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n  binding evidence lands with the tagged tests under `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`\n  (registry entry present, stages `[]` until the guard slice activates).\n\n### 3.2 CC can boot a session on a STALE cached plugin version despite a newer one installed\n\n<!-- [doc->REQ-HAZARD-STALE-PLUGIN-RESOLUTION] -->\n\n- **Failure:** the harness's plugin resolution picked an OLD cached copy at session boot even\n  though the newer version was already installed. Observed live (F-024 domino #1, ENLYZEAM):\n  the node's plugin cache held BOTH sptc 0.1.8 and 0.1.9; `installed_plugins.json` showed 0.1.9\n  installed **24 minutes before** the 2026-06-29 boot of session `22ce8af0…` — yet that session\n  verifiably ran the **0.1.8** dispatch (the unquoted-`SPTC_HOOK_BIN` v0.9.0 regression, fixed in\n  0.1.9). The stale pick then compounds: the buggy dispatch corrupted the per-session\n  `CLAUDE_ENV_FILE`, which **survives every resume of that session** (the env file lives with the\n  session, so one bad boot poisons the session's whole lifetime), hooks died silently, and the\n  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n  stale pick is.\n- **Invariant (defense-in-depth — the adapter stays safe under a stale pick):\n  (a) degrade-not-brick:** any shipped dispatch version × any adapter version must degrade,\n  never brick (§3.1's invariant is the binding half; a stale-RESOLVED plugin is simply another\n  way to arrive at the same skew).\n  **(b) minimize the stale-pickable set:** the post-update plugin reconcile must reach EVERY\n  plugin root the node's CLIs actually read — the `claude` config root AND each ccs instance root\n  (`~/.ccs/instances/<x>/…`, the v0.10.3 follow-up) — and superseded cached copies should be\n  purged (or at minimum flagged) rather than left resolvable; a picker cannot choose a stale copy\n  that no longer exists.\n  **(c) observability:** which plugin version actually ran must be a log line, not an ssh dig —\n  the dispatch (or the hook binary it execs) should surface its plugin version at least once per\n  session so version-of-truth checks can catch a stale pick immediately.\n- **Mapping / notes:** reconcile halves live in `tools/claude-spt/src/post_update.rs` (primary\n  CLI reconcile + the best-effort ccs follow-up, v0.10.2/v0.10.3 — REQ-DIST-CCS-PLUGIN-FOLLOWUP);\n  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n  demonstrates it.\n- **cite:** F-024 auth-wedge arc, `docs/SPT-CORE-FINDINGS.md` (doyle receipt\n  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n  evidence = this entry; impl/unit land with the guard slice.\n"}],"details":{"displayContent":{"text":"# Known Hazards\n…\n## 2. Live-agent perch / CI orchestration\n…\n### 2.8 Raw PreToolUse stdout is DISCARDED by CC — a mid-turn drain becomes a silent message black hole\n…\n- **Failure:** CC injects a hook's additionalContext from PLAIN stdout only on\n…\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `wrap_pretool_context` +\n…\n  `~/.claude/reference_docs/claude-code-hooks.md` is the per-event contract source — check it\n  BEFORE wiring any new hook output, per event, never by analogy from another event.\n- **cite:** doyle node-wide RCA + self-probe (2026-07-07); perri repro: the installed binary\n  driven directly emits correctly (binary exonerated), spool rows delivered=1 with zero\n  surfaced `<sptc_messages>`; recovered bodies read straight from `spool.db`.\n\n### 2.9 A poll frame truncated mid-frame is silently dropped WHOLE by the drain renderer\n\n<!-- [doc->REQ-HAZARD-DANGLING-FRAME-LOUD] -->\n\n- **Failure:** `render_frames` walks `spt api poll` output frame by frame (`<EVENT …>body</EVENT>`).\n  If the poll output ends mid-frame — opening tag present but no `>`, or a body with no closing\n  `</EVENT>` — the original loop `break`s and the dangling frame vanishes: the message was taken\n  from the spool (`delivered=1`) but nothing surfaces to the agent. Same silent-loss family as\n  §2.8, one seam upstream. Never observed in the field (the 2026-07-07 seed-#9 head-truncations\n  were core-side idle-inject pacing, spool rows complete, and could NOT have produced this shape),\n  but the drop path existed and was invisible by construction — hardened 2026-07-07 (doyle-ACKed)\n  before it could cost a real message.\n- **Invariant:** A dangling frame is never dropped silently. The renderer emits a loud\n  `<sptc_frame_integrity>` marker carrying the surviving partial content and a spool-recovery\n  pointer (`owlery/<id>/spool.db`, `messages` table) — a taken message must always surface\n  somewhere the agent can read (the §2.8 rule, applied to the framing seam).\n- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — the dangling-frame arm in `render_frames`.\n  Rides both drain paths (UserPromptSubmit + PreToolUse) and the existing capped emits unchanged.\n- **cite:** perri code-walk during the seed-#9 seam exoneration (2026-07-07); doyle \"ship it\" ACK\n  same night.\n\n## 3. Distribution split (adapter × plugin version skew)\n\n### 3.1 A hook_cmd shape change with a stale dispatch.sh bricks the whole CC session\n\n<!-- [doc->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP] -->\n\n- **Failure:** `spt adapter update` refreshes the adapter (manifest + binary) **immediately**, but\n  the cplugs plugin's `dispatch.sh` on disk only refreshes after a plugin reconcile + CC\n  `/reload-plugins`. Observed live 2026-07-01 (perri, the v0.9.0→0.9.2 on-node update): the stale\n  0.1.8 `dispatch.sh` execs `\"$bin\" <event>` (expecting the legacy ` hook`-suffixed hook_cmd) while\n  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n  `hook` token iff missing, strip iff doubled), and (b) `claude-spt` exits **0** (pass-through with\n  a stderr note) on an unrecognized event/subcommand token so a stale dispatch produces a noisy\n  no-op instead of a tool-blocking failure. Equivalently: pin the dispatch↔binary contract so the\n  shape can never change unilaterally.\n- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n  hooks; the degrade-not-brick exit-0 half covers that remainder.\n- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n  binding evidence lands with the tagged tests under `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`\n  (registry entry present, stages `[]` until the guard slice activates).\n\n### 3.2 CC can boot a session on a STALE cached plugin version despite a newer one installed\n\n<!-- [doc->REQ-HAZARD-STALE-PLUGIN-RESOLUTION] -->\n\n- **Failure:** the harness's plugin resolution picked an OLD cached copy at session boot even\n  though the newer version was already installed. Observed live (F-024 domino #1, ENLYZEAM):\n  the node's plugin cache held BOTH sptc 0.1.8 and 0.1.9; `installed_plugins.json` showed 0.1.9\n  installed **24 minutes before** the 2026-06-29 boot of session `22ce8af0…` — yet that session\n  verifiably ran the **0.1.8** dispatch (the unquoted-`SPTC_HOOK_BIN` v0.9.0 regression, fixed in\n  0.1.9). The stale pick then compounds: the buggy dispatch corrupted the per-session\n  `CLAUDE_ENV_FILE`, which **survives every resume of that session** (the env file lives with the\n  session, so one bad boot poisons the session's whole lifetime), hooks died silently, and the\n  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n  stale pick is.\n- **Invariant (defense-in-depth — the adapter stays safe under a stale pick):\n  (a) degrade-not-brick:** any shipped dispatch version × any adapter version must degrade,\n  never brick (§3.1's invariant is the binding half; a stale-RESOLVED plugin is simply another\n  way to arrive at the same skew).\n  **(b) minimize the stale-pickable set:** the post-update plugin reconcile must reach EVERY\n  plugin root the node's CLIs actually read — the `claude` config root AND each ccs instance root\n  (`~/.ccs/instances/<x>/…`, the v0.10.3 follow-up) — and superseded cached copies should be\n  purged (or at minimum flagged) rather than left resolvable; a picker cannot choose a stale copy\n  that no longer exists.\n  **(c) observability:** which plugin version actually ran must be a log line, not an ssh dig —\n  the dispatch (or the hook binary it execs) should surface its plugin version at least once per\n  session so version-of-truth checks can catch a stale pick immediately.\n- **Mapping / notes:** reconcile halves live in `tools/claude-spt/src/post_update.rs` (primary\n  CLI reconcile + the best-effort ccs follow-up, v0.10.2/v0.10.3 — REQ-DIST-CCS-PLUGIN-FOLLOWUP);\n  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n  demonstrates it.\n- **cite:** F-024 auth-wedge arc, `docs/SPT-CORE-FINDINGS.md` (doyle receipt\n  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n  evidence = this entry; impl/unit land with the guard slice.\n","startLine":1,"lineNumbers":[1,null,59,null,321,null,325,null,341,null,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\docs\\KNOWN-HAZARDS.md"}}},"isError":false,"timestamp":1784087155295}}
{"type":"message","id":"4b180cd7","parentId":"e257c97d","timestamp":"2026-07-15T03:46:07.269Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning OMP requirement traceability audit**"},{"type":"toolCall","id":"call_T4X8SfTuU0Bf4tAzCKkc9QGK|fc_0ca4af36969c80fa016a57027f29d48198993fadf4489d7b54","name":"grep","arguments":{"i":"Finding OMP requirements","pattern":"REQ-OMP|omp-spt|OMP","path":"traceable-reqs.toml","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_xgHWyiVeisXihO7KaLF4EwLs|fc_0ca4af36969c80fa016a57027f29f08198bd369f0b45c27ec7","name":"grep","arguments":{"i":"Finding test requirement tags","pattern":"REQ-(OMP|HAZARD|DIST|SKILL|UPS)","path":"tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_JJ2XPajVa5RaA7B9TOkr1bAv|fc_0ca4af36969c80fa016a57027f2a008198be40cbf106ea59f6","name":"grep","arguments":{"i":"Finding OMP implementation tags","pattern":"REQ-OMP|omp-spt|OMP_SPT|bridge|sendUserMessage|session_start|turn_start|turn_end|session_shutdown","path":"adapter;tools;plugin;ci;tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_6rNn8d9ygzryLHAEiPpFZ4Ln|fc_0ca4af36969c80fa016a57027f2a10819894baf94064aef8a9","name":"grep","arguments":{"i":"Finding stale Claude terms","pattern":"claude-spt|spt-claude-code|sptc|Claude Code|ccs|cc-<id>|cc launcher|CC adapter|CC session","path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_gLL3fb9BxSgC93hybJkcIHyJ|fc_0ca4af36969c80fa016a57027f2a1c8198bf41bbf486388a7e","name":"grep","arguments":{"i":"Finding explicit live questions","pattern":"(?i)\\bOPEN\\b|open question|to-confirm|to confirm|unconfirmed|decide\\b|confirm\\b|defer|deferred|gated|blocked|pending|unbuilt|later slice|future|optional","path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":2531,"output":557,"cacheRead":66048,"cacheWrite":0,"totalTokens":69136,"reasoningTokens":77,"cost":{"input":0.012655000000000001,"output":0.01671,"cacheRead":0.033024,"cacheWrite":0,"total":0.062389}},"stopReason":"toolUse","timestamp":1784087155304,"responseId":"resp_0ca4af36969c80fa016a57027361e48198bd3ce45645011138","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a57027511a48198a6c1c27cd1e2418a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwJ_HGI3nQd51v8NQJ4Rzxbk6n42NxVeSb6hpAWtso4cDiAnZYLzSuGHTMStkRI3XHb5jRWsVUhEuPwysJEBmVtVDMMLn2Xxji9QNLYnrnqbwm3tylfJXpqAS9idopXxXb3DRpW7zwPGEUyvlkdmfH2wXgOAlcW0Pw5ik0zEIXDi6XEGPO88jaheBNWuOJEv56-D3CcOnfXzt7aqx4g37lLi5v9ARJpn_zjXFXRUwaRep0yJF9QNSaZiBexk0t6qZTsoR3RwUAQChUCqLjzugcuO8Ro-1VLFTShSbjI0diRydl07_YUCv53GvvN0M053JJFZcoaS32lc6Oj0fUranMIPXNPIqFhS6VukxoRDSUxV_DhllZ_AsUQV2o9eCQhIBhJJ1ZPM9Wl8iuPeTGZ5zpVPzH3mLivuDakXxPkF6CLFh2ogwXU1IyzUAn9-YCCREWQ-20Z97yYHXM1pEeqy5QuFDLp4K4h8E55Lo2ALIBeN3wFiigofJG6ClpzKE3vtH8zlEs3y-HJnKtBjbaBINKFBDP5qQwCDGcYYxrbY0skrR4vVQjLaMR-XP_PZNXp8tuU421OP6eNj5c6aa2EpnUuEqXth2LbbopvIvNcguhaTGyy3sqEPFDoznXbMEGkBLGvINTsVwgJ_LwTKZF7JwlHNr9zZY5E0LRnJY7o8hgdZtI7gtkYT4IbBeJRn_c9pn31h3JaIa-0D39wGn--vyZhTnlDBzQcuQQ5VPNKIxRG2uqcKqPqlClYrkH0XLaXfD82M9ifbHZgn5mXntnbZMjEUc5TkGrlOsXtg7LSKQDgaD4URAKPjd7EZh1asicNWdVNgZzVQOESY7bRe0bj-ZVCk3DtFh_DgF13OP860KbeIs7CfCQdikuKtmCDy3C3yeF__2LeKjYYrbICYWTlypOhQYb94CR30dgG__5Z2Qyl83DRUMGhpbb_M8OlrKxBQML2u0lOrZS-uOd3zXxpJxODMlYtXb5Hm505S-RV-P1fQz90Eo_3_F7wU4s_NrbRGNqSRFanjR1XX9MfRJx1CGWOoZe0hx9XdsM5PIRUHy-3JMucWywej5RUNRTO6-JoVU_x0Zm6bLd6vdhNlcbOzriYnyiXo8vrV4eBYA2v15M-N8yWDDWXSrmcosE5w4LcXw0lcHaDbWOUHqAV4qo1QfPKDvX3tSK-Jt-PnIv2JngMDSxDROfrGVXlwnswNTynmaZmBpTxVFK9irpxk1Hg0umGJVKzPFgfyIb0v_G90ZKbv4DL_KFlWCDvwuSKqzWpaUupTiN4qk0Nrv1lkFPqjj0HsUeHcNBsBxNYD4pCgvWXL-lT1s5FfzvO6__nko7umvYhjz6CMtklx1DDEEVSyYYlRxw==","summary":[{"type":"summary_text","text":"**Planning OMP requirement traceability audit**"}]},{"id":"fc_0ca4af36969c80fa016a57027f29d48198993fadf4489d7b54","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding OMP requirements\",\"pattern\":\"REQ-OMP|omp-spt|OMP\",\"path\":\"traceable-reqs.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_T4X8SfTuU0Bf4tAzCKkc9QGK","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57027f29f08198bd369f0b45c27ec7","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding test requirement tags\",\"pattern\":\"REQ-(OMP|HAZARD|DIST|SKILL|UPS)\",\"path\":\"tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_xgHWyiVeisXihO7KaLF4EwLs","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57027f2a008198be40cbf106ea59f6","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding OMP implementation tags\",\"pattern\":\"REQ-OMP|omp-spt|OMP_SPT|bridge|sendUserMessage|session_start|turn_start|turn_end|session_shutdown\",\"path\":\"adapter;tools;plugin;ci;tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_JJ2XPajVa5RaA7B9TOkr1bAv","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57027f2a10819894baf94064aef8a9","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding stale Claude terms\",\"pattern\":\"claude-spt|spt-claude-code|sptc|Claude Code|ccs|cc-<id>|cc launcher|CC adapter|CC session\",\"path\":\"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_6rNn8d9ygzryLHAEiPpFZ4Ln","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57027f2a1c8198bf41bbf486388a7e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding explicit live questions\",\"pattern\":\"(?i)\\\\bOPEN\\\\b|open question|to-confirm|to confirm|unconfirmed|decide\\\\b|confirm\\\\b|defer|deferred|gated|blocked|pending|unbuilt|later slice|future|optional\",\"path\":\"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_gLL3fb9BxSgC93hybJkcIHyJ","name":"grep"}]},"duration":11964.29620000004,"ttft":1774.2771000000066,"contextSnapshot":{"promptTokens":68579,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_T4X8SfTuU0Bf4tAzCKkc9QGK|fc_0ca4af36969c80fa016a57027f29d48198993fadf4489d7b54","toolName":"grep","startedAt":"2026-07-15T03:46:07.269Z","args":{"path":"traceable-reqs.toml"},"intent":"Finding OMP requirements"},"id":"1308cf8f","parentId":"4b180cd7","timestamp":"2026-07-15T03:46:07.269Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xgHWyiVeisXihO7KaLF4EwLs|fc_0ca4af36969c80fa016a57027f29f08198bd369f0b45c27ec7","toolName":"grep","startedAt":"2026-07-15T03:46:07.269Z","args":{"path":"tests"},"intent":"Finding test requirement tags"},"id":"9d134177","parentId":"1308cf8f","timestamp":"2026-07-15T03:46:07.269Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JJ2XPajVa5RaA7B9TOkr1bAv|fc_0ca4af36969c80fa016a57027f2a008198be40cbf106ea59f6","toolName":"grep","startedAt":"2026-07-15T03:46:07.269Z","args":{"path":"adapter;tools;plugin;ci;tests"},"intent":"Finding OMP implementation tags"},"id":"73b09e29","parentId":"9d134177","timestamp":"2026-07-15T03:46:07.269Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6rNn8d9ygzryLHAEiPpFZ4Ln|fc_0ca4af36969c80fa016a57027f2a10819894baf94064aef8a9","toolName":"grep","startedAt":"2026-07-15T03:46:07.269Z","args":{"path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests"},"intent":"Finding stale Claude terms"},"id":"6bc00160","parentId":"73b09e29","timestamp":"2026-07-15T03:46:07.269Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_gLL3fb9BxSgC93hybJkcIHyJ|fc_0ca4af36969c80fa016a57027f2a1c8198bf41bbf486388a7e","toolName":"grep","startedAt":"2026-07-15T03:46:07.270Z","args":{"path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml"},"intent":"Finding explicit live questions"},"id":"af75e94a","parentId":"6bc00160","timestamp":"2026-07-15T03:46:07.270Z"}
{"type":"message","id":"b43c761d","parentId":"af75e94a","timestamp":"2026-07-15T03:46:07.272Z","message":{"role":"toolResult","toolCallId":"call_T4X8SfTuU0Bf4tAzCKkc9QGK|fc_0ca4af36969c80fa016a57027f29d48198993fadf4489d7b54","toolName":"grep","content":[{"type":"text","text":" 378|id = \"REQ-DIST-RC-STARTUP\"\n*379|title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n*380|required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381|\n 382|[[requirements]]\n 383|id = \"REQ-DIST-BOUNDARY-ROTATE\"\n...\n 433|id = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\n*434|title = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped)...\n 435|required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-cover...\n 436|\n 437|[[requirements]]\n...\n 477|[[requirements]]\n*478|id = \"REQ-OMP-NATIVE-TUI\"\n*479|title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\traceable-reqs.toml","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":6,"fileCount":1,"files":["traceable-reqs.toml"],"fileMatches":[{"path":"traceable-reqs.toml","count":6}],"truncated":true,"displayContent":" 378│id = \"REQ-DIST-RC-STARTUP\"\n*379│title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n*380│required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381│\n 382│[[requirements]]\n 383│id = \"REQ-DIST-BOUNDARY-ROTATE\"\n    │...\n 433│id = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\n*434│title = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped)...\n 435│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-cover...\n 436│\n 437│[[requirements]]\n    │...\n 477│[[requirements]]\n*478│id = \"REQ-OMP-NATIVE-TUI\"\n*479│title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087167272}}
{"type":"message","id":"166f0a7d","parentId":"b43c761d","timestamp":"2026-07-15T03:46:07.274Z","message":{"role":"toolResult","toolCallId":"call_xgHWyiVeisXihO7KaLF4EwLs|fc_0ca4af36969c80fa016a57027f29f08198bd369f0b45c27ec7","toolName":"grep","content":[{"type":"text","text":"# tests/\n## acceptance-harness.sh\n 7|# ── Perch-collision guard: identity is ALWAYS overridden to a disposable id, never a live name.\n*8|# [unit->REQ-HAZARD-PERCH-COLLISION]\n 9|# Simulate inheriting the operator's identity, then mint — both vars must be displaced.\n 10|SPT_AGENT_ID=perri OWL_SESSION_ID=perri\n 11|sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n## adapter-archive.sh\n 6|# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into\n*7|# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]\n*8|# [unit->REQ-DIST-ADAPTER-PEROS]\n 9|set -u\n 10|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 11|PACKER=\"$ROOT/ci/publish/package-adapter.sh\"\n## bridge-acceptance.sh\n 2|# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n*3|# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n 4|#\n 5|# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n 6|# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n## hooks-dispatch.sh\n 6|# test pins that wiring + the adapter-side declarations it depends on.\n*7|# [unit->REQ-DIST-HOOK-BINARY] [unit->REQ-DIST-HOOKS-API] [unit->REQ-DIST-SKELETON-THIN]\n 8|set -u\n 9|fail=0\n 10|check() { # check <label> <expected> <actual>\n...\n 35|# binary, which is TYPE-AWARE — it LOGS every type (Step-0 instrumentation) but marks idle ONLY on\n*36|# idle_prompt (REQ-HAZARD-STUCK-ACTIVE-NO-IDLE). A permission_prompt must NOT drain the spool mid-turn.\n 37|grep -q '\"matcher\": \"\\*\"' \"$HK/hooks.json\" && r=yes || r=no\n 38|check \"Notification uses the * matcher (type-aware in-binary)\" \"yes\" \"$r\"\n 39|# StopFailure + PostToolUseFailure are matcherless (all error types / all tools route through — the\n## manifest-schema.sh\n 4|# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).\n*5|# [unit->REQ-DIST-MANIFEST-SCHEMA]\n 6|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 7|SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n 8|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n## manifest-shortcut.sh\n 5|# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n*6|# [unit->REQ-DIST-SHORTCUT-BASENAME]\n 7|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 8|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 9|EXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\n...\n 30|# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n*31|# [unit->REQ-OMP-NATIVE-TUI]\n 32|spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\n 33|nspawn=$(printf '%s' \"$spawn\" | grep -c .)\n 34|if [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\n...\n 59|\n*60|# [unit->REQ-HAZARD-PSYCHE-IDENTITY-ENV] — identity-env scrub on BOTH psyche roles.\n 61|for role in session.psyche_init session.psyche_resume; do\n 62|  scrub=$(field_of \"$role\" '^[[:space:]]*env_remove[[:space:]]*=')\n 63|  if [ -z \"$scrub\" ]; then echo \"FAIL [$role] has no env_remove (identity-env scrub missing)\"; fail=1; else\n## msys-hazard.sh\n 1|#!/bin/sh\n*2|# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain\n 3|# /-leading content (e.g. a prompt \"/omps:send doyle\") — must reach the handler via STDIN (immune to\n 4|# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see\n 5|# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,\n 6|# …) + the numeric --host-pid — never message/prompt content, and never /-leading.\n*7|# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]\n 8|ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n 9|HK=\"$ROOT/plugin/omps/hooks\"\n 10|HOOKRS=\"$ROOT/tools/claude-spt/src/hook.rs\"\n## omp-extension.mjs\n 3|\n*4|// [unit->REQ-OMP-NATIVE-TUI]\n 5|assert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\n 6|\n 7|const partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\n## skeleton-validate.sh\n 4|# real breakage, not just green-on-green. Run: sh tests/skeleton-validate.sh  (exit 0 = pass).\n*5|# [unit->REQ-DIST-PLUGIN-SKELETON]\n 6|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 7|VALIDATE=\"$ROOT/ci/publish/validate-skeleton.sh\"\n 8|fail=0"}],"details":{"scopePath":"tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":14,"fileCount":9,"files":["tests/acceptance-harness.sh","tests/adapter-archive.sh","tests/bridge-acceptance.sh","tests/hooks-dispatch.sh","tests/manifest-schema.sh","tests/manifest-shortcut.sh","tests/msys-hazard.sh","tests/omp-extension.mjs","tests/skeleton-validate.sh"],"fileMatches":[{"path":"tests/acceptance-harness.sh","count":1},{"path":"tests/adapter-archive.sh","count":2},{"path":"tests/bridge-acceptance.sh","count":1},{"path":"tests/hooks-dispatch.sh","count":2},{"path":"tests/manifest-schema.sh","count":1},{"path":"tests/manifest-shortcut.sh","count":3},{"path":"tests/msys-hazard.sh","count":2},{"path":"tests/omp-extension.mjs","count":1},{"path":"tests/skeleton-validate.sh","count":1}],"truncated":false,"displayContent":"# tests/\n## acceptance-harness.sh\n  7│# ── Perch-collision guard: identity is ALWAYS overridden to a disposable id, never a live name.\n *8│# [unit->REQ-HAZARD-PERCH-COLLISION]\n  9│# Simulate inheriting the operator's identity, then mint — both vars must be displaced.\n 10│SPT_AGENT_ID=perri OWL_SESSION_ID=perri\n 11│sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n## adapter-archive.sh\n  6│# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into\n *7│# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]\n *8│# [unit->REQ-DIST-ADAPTER-PEROS]\n  9│set -u\n 10│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 11│PACKER=\"$ROOT/ci/publish/package-adapter.sh\"\n## bridge-acceptance.sh\n 2│# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n*3│# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n 4│#\n 5│# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n 6│# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n## hooks-dispatch.sh\n  6│# test pins that wiring + the adapter-side declarations it depends on.\n *7│# [unit->REQ-DIST-HOOK-BINARY] [unit->REQ-DIST-HOOKS-API] [unit->REQ-DIST-SKELETON-THIN]\n  8│set -u\n  9│fail=0\n 10│check() { # check <label> <expected> <actual>\n   │...\n 35│# binary, which is TYPE-AWARE — it LOGS every type (Step-0 instrumentation) but marks idle ONLY on\n*36│# idle_prompt (REQ-HAZARD-STUCK-ACTIVE-NO-IDLE). A permission_prompt must NOT drain the spool mid-turn.\n 37│grep -q '\"matcher\": \"\\*\"' \"$HK/hooks.json\" && r=yes || r=no\n 38│check \"Notification uses the * matcher (type-aware in-binary)\" \"yes\" \"$r\"\n 39│# StopFailure + PostToolUseFailure are matcherless (all error types / all tools route through — the\n## manifest-schema.sh\n 4│# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).\n*5│# [unit->REQ-DIST-MANIFEST-SCHEMA]\n 6│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 7│SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n 8│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n## manifest-shortcut.sh\n  5│# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n *6│# [unit->REQ-DIST-SHORTCUT-BASENAME]\n  7│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n  8│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n  9│EXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\n   │...\n 30│# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n*31│# [unit->REQ-OMP-NATIVE-TUI]\n 32│spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\n 33│nspawn=$(printf '%s' \"$spawn\" | grep -c .)\n 34│if [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\n   │...\n 59│\n*60│# [unit->REQ-HAZARD-PSYCHE-IDENTITY-ENV] — identity-env scrub on BOTH psyche roles.\n 61│for role in session.psyche_init session.psyche_resume; do\n 62│  scrub=$(field_of \"$role\" '^[[:space:]]*env_remove[[:space:]]*=')\n 63│  if [ -z \"$scrub\" ]; then echo \"FAIL [$role] has no env_remove (identity-env scrub missing)\"; fail=1; else\n## msys-hazard.sh\n  1│#!/bin/sh\n *2│# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain\n  3│# /-leading content (e.g. a prompt \"/omps:send doyle\") — must reach the handler via STDIN (immune to\n  4│# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see\n  5│# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,\n  6│# …) + the numeric --host-pid — never message/prompt content, and never /-leading.\n *7│# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]\n  8│ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n  9│HK=\"$ROOT/plugin/omps/hooks\"\n 10│HOOKRS=\"$ROOT/tools/claude-spt/src/hook.rs\"\n## omp-extension.mjs\n 3│\n*4│// [unit->REQ-OMP-NATIVE-TUI]\n 5│assert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\n 6│\n 7│const partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\n## skeleton-validate.sh\n 4│# real breakage, not just green-on-green. Run: sh tests/skeleton-validate.sh  (exit 0 = pass).\n*5│# [unit->REQ-DIST-PLUGIN-SKELETON]\n 6│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n 7│VALIDATE=\"$ROOT/ci/publish/validate-skeleton.sh\"\n 8│fail=0"},"isError":false,"timestamp":1784087167274}}
{"type":"message","id":"1bc7fb13","parentId":"166f0a7d","timestamp":"2026-07-15T03:46:07.302Z","message":{"role":"toolResult","toolCallId":"call_6rNn8d9ygzryLHAEiPpFZ4Ln|fc_0ca4af36969c80fa016a57027f2a10819894baf94064aef8a9","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n*1|# spt-claude-code — glossary\n 2|\n 3|> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n 4|> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n 5|\n*6|**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n 7|adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n 8|parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n 9|installer. Built by maintainer perri from the public spt-releases surface only.\n*10|(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n 11|[[naming-domains]].)\n 12|\n*13|**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n*14|invocation + the manifest). The CC adapter's identity inside spt-core. The\n 15|**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n 16|\n*17|**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n*18|spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n 19|**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n*20|`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n 21|`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n*22|shortcut (`cc-<id>`) is decoupled from both.\n 23|\n 24|**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration\n 25|(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a\n...\n 50|\n*51|**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n*52|that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n*53|NOT its own adapter — ccs is structurally Claude Code.\n 54|\n 55|**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n 56|spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n...\n 60|**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n*61|(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own\n 62|perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a\n 63|*commune*. It never replies or notifies (that is the echo-commune, a different actor) and\n 64|exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65|profile. (Realized here by the `claude-spt-psyche` runner.)\n 66|\n 67|**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68|parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n...\n 97|**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n*98|adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n 99|users: install the plugin, get spt-core for free.\n 100|\n 101|**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102|extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103|traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104|same-repo published-docs. Defined in its own folder, not here.\n 105|\n...\n 109|resulting submit to consume the edited result. The binary-inject leg and the UPS leg are\n*110|decoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive\n 111|edit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s\n 112|additionalContext (which the operator never edits).\n 113|\n\n# SCOPE.md\n*1|# spt-claude-code — scoping decision ledger\n 2|\n 3|> Running source of truth for the scoping session (started 2026-06-13, doyle).\n 4|> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n...\n 9|\n*10|**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n*11|`claude-spt` adapter) — simultaneously:\n 12|1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n 13|   delegating all core to `spt.exe`).\n 14|2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n...\n 30|\n*31|**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n 32|in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n 33|\n 34|| Layer | Contents | Home | Update path | Churn |\n...\n 37|| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n*38|| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n 39|\n 40|- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n 41|  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n...\n 67|\n*68|## ccs integration\n 69|\n*70|**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n*71|`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n 72|model/billing backend differ, which is exactly the profile seam (leaf-replace\n*73|spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n 74|leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n*75|profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n 76|Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n 77|not only bare adapter_name (profile model already supports composite addressing).\n 78|\n...\n 91|6. OS-service registration (always-on daemon)\n*92|7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value\n 93|   prop) if absent\n 94|8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n 95|   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n...\n 99|full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100|`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n 101|flag if verify should fold inline._\n 102|\n 103|### `cc` launcher (capsule-style) — **LOCKED (design)**\n...\n 110|**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\n*111|hosted CC session (not just headless PTY).\n 112|\n 113|### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n 114|\n...\n 129|  the product as its own CI nervous system.\n*130|- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131|  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132|  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133|  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n...\n 136|  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137|    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n 138|    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139|- This CI pattern → reusable, goes in `experimplate`.\n 140|\n...\n 150|**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),\n*151|profiles + strings + hints wiring, ccs profiles.\n 152|\n 153|**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n 154|harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n...\n 173|**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174|spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175|`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176|\n 177|## spt-core upstream deps\n...\n 179|self-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets\n*180|adapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.\n 181|\n 182|## experimplate (template) — resolved content\n 183|- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH\n*184|  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n 185|  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n 186|  (not spt-core's two-key/counter runbook).\n 187|- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n...\n 194|\n*195|- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`\n 196|  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,\n\n# OMP-ADAPTER-PLAN.md\n*1|# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n 2|\n*3|> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n 4|> **oh-my-pi (omp)** — the terminal coding agent at\n 5|> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n 6|> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n...\n 16|\n*17|claude-spt exists because Claude Code has **no control protocol** — the adapter\n 18|reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n 19|`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n 20|digest, and pushes received messages back via `additionalContext`. Most of its\n...\n 49|**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n*50|spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n 51|runs `omp --mode rpc` as a child, and translates in both directions:\n 52|\n 53|```\n...\n 73|\n*74|## Seam map — claude-spt value → omp-spt target\n 75|\n*76|| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77||---|---|---|\n*78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n 79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n 83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n 84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n 86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n 90|### Session/transcript (for the `[digest]` extractor)\n...\n 110|\n*111|## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112|\n*113|- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114|  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115|  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116|- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117|  (bridge command), `[identity]`. Prove with\n 118|  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119|  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120|- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121|  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122|  Acceptance: from another node, message the perch → get an omp answer; assert\n 123|  `spt` state transitions (isolated CI identity per tips doc §Testing).\n...\n 174|5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest\n*175|   `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2\n 176|   actually calls).\n 177|\n 178|## Validation harness (all on the public surface, no live model needed)\n\n# traceable-reqs.toml\n*1|# traceable-reqs manifest for spt-claude-code — SEED.\n 2|#\n 3|# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n 4|# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n...\n 62|id = \"REQ-DIST-PLUGIN-SKELETON\"\n*63|title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n 64|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n 65|\n 66|[[requirements]]\n...\n 68|title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n*69|required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n 70|\n 71|[[requirements]]\n 72|id = \"REQ-DIST-HOOKS-API\"\n...\n 77|id = \"REQ-DIST-HOOK-BINARY\"\n*78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n 82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n*83|title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n*84|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n 85|\n 86|[[requirements]]\n 87|id = \"REQ-DIST-SHORTCUT-BASENAME\"\n*88|title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n*89|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n 90|\n 91|[[requirements]]\n 92|id = \"REQ-CC-LAUNCHER-BIND\"\n*93|title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n*94|required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n 95|\n 96|[[requirements]]\n 97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n*98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n 102|id = \"REQ-DIST-DIGEST-FETCHER\"\n*103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n 107|id = \"REQ-DIST-WHOAMI-JSON\"\n 108|title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109|required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110|\n 111|[[requirements]]\n 112|id = \"REQ-SKILL-LIVE\"\n*113|title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115|\n 116|[[requirements]]\n 117|id = \"REQ-UPS-INJECTION\"\n*118|title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120|\n 121|[[requirements]]\n 122|id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n\n# docs/\n## KNOWN-HAZARDS.md\n 41|  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n*42|  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n*43|  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n 44|  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n 45|  documents the identical hazard in `new-alarm`.)\n*46|- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n 47|  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n 48|  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n 49|  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n*50|- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n*52|  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n 53|  A test asserts the stdin path is honored (no argv `/`-token dependency).\n 54|- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n 55|  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n...\n 72|- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n*73|  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n 74|  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75|  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n*76|  overrides both to a `sptc-ci-` id and never emits a live id.\n*77|- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n 78|  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n 79|  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n 80|  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n...\n 90|\n*91|- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n*92|  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n 93|  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n 94|  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n 95|  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n...\n 97|- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n*98|  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n*99|  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n 100|  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n 101|  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n 102|  trust (see `docs/adr/0003-*`).\n*103|- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n 104|  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n 105|  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n*106|  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n*107|  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n 108|  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n 109|  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n 110|  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n...\n 139|  reason; a silent skip is forbidden.\n*140|- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n 141|  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n 142|  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n 143|  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n...\n 184|  it ever bites. Accepted for v1.\n*185|- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n 186|  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n 187|  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n*188|  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n 189|  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n 190|  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n 191|- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n...\n 225|  side, but the adapter must not hand the child a foreign identity in the first place.\n*226|- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n 227|  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n 228|  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229|  child that runs harness hooks, it needs the same scrub.\n...\n 278|  core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.\n*279|- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`\n 280|  (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to\n 281|  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n 282|  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n## OMP-BRIDGE-FIELD-ISSUES.md\n 5|\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n*6|and could not be switched back to `claude-spt`. This documents the root causes,\n 7|grounded in this repo's code, so the bridge can be hardened.\n 8|\n*9|All line references are against `tools/claude-spt/src/bridge.rs` and\n 10|`adapter/omp-spt.toml` at commit `f0109d1`.\n 11|\n 12|## Incident summary\n...\n 14|1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15|   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16|2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17|   way to tell working vs. wedged.\n 18|3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19|   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20|   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21|   endpoint looked alive but was dead.\n*22|4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23|5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24|\n 25|Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n...\n 30|\n*31|## HIGH-1 — Bridge binary basename collides with `claude-spt`\n 32|\n*33|**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n*34|indistinguishable from a genuine claude-spt process. During the incident this\n*35|made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n 36|todlando's perch was actually the omp bridge. It directly caused the\n 37|misdiagnosis.\n 38|\n...\n 40|```\n*41|command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42|```\n*43|The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44|This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n 45|bridge (S2) is `omp-spt`\"*).\n 46|\n...\n 58|object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n*59|`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n 60|`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n 61|When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62|those lines never run. On Windows, killing a process does not kill its children,\n...\n 149|**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150|restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151|editing spt-core's session ledger by hand.\n 152|\n 153|**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n\n## adr/\n### 0001-distribution-splits-by-volatility.md\n 12|\n*13|`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n*14|its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n 15|invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n*16|inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n 17|plugin author.\n 18|\n 19|Forces:\n...\n 24|   marketplace or letting logic rot.\n*25|2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n 26|   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n 27|   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n 28|   need to ride the marketplace at all.\n...\n 43|| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n*44|| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n 45|\n 46|Concretely:\n 47|\n...\n 77|- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n*78|  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n 79|  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`\n*80|  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but\n 81|  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)\n 82|- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction\n 83|  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.\n...\n 96|  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /\n*97|  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n*98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n 99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n...\n 103|<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n*104|- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105|  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106|  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n*107|  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108|  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109|- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n 110|  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —\n 111|  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112|  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n*113|  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114|  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n*115|  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n 116|- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the\n 117|  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,\n 118|  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 2|<!-- [doc->REQ-UPS-INJECTION] -->\n*3|# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`\n 4|\n 5|## Status\n 6|\n...\n 12|primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n*13|formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n 14|payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n*15|public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n*16|(the adapter_name — distinct from the plugin name `sptc`).\n 17|\n 18|Two hard constraints shape the wiring:\n 19|\n...\n 21|   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,\n*22|   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n 23|2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n 24|   L52/L181. The plugin hand-writes it.\n 25|\n...\n 27|\n*28|**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n*29|`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n 30|(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n 31|substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n 32|\n...\n 34|\n*35|| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n 36||---|---|---|\n 37|| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n 38|| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n...\n 45|\n*46|`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n 47|**not** from a hook.\n 48|\n 49|### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook\n...\n 51|The `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers\n*52|queued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter\n 53|`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is\n 54|ours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.\n 55|\n...\n 75|\n*76|Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\n*77|the `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\n 78|unescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\n 79|gate, Psyche routing), never silently stripped. Covered by\n 80|`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).\n...\n 99|- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n*100|  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n 101|- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n 102|  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n 103|  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n...\n 115|  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n*116|  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n*117|  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n 118|- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n 119|  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n 120|  packaging on Windows (no per-OS branch needed for the hook to run).\n...\n 136|   chars to a file (evicting it from the inline context the agent sees), silently losing messages.\n*137|   The UPS wrapper now pre-empts: `sptc_cap_output` (`_common.sh`) buffers the combined skill-body +\n 138|   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n*139|   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n*140|   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n 141|   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n*142|   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n 143|\n 144|## Consequences\n 145|\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n 11|A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\n*12|hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\n*13|subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\n 14|the runner seeds one headless `claude` turn from the daemon-supplied prompt,\n 15|then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\n 16|on each pulse.\n...\n 37|Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\n*38|legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n 39|`pulse_cmd`:\n 40|\n 41|- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n...\n 48|The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n*49|`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\n 50|the flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\n 51|recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n 52|\n...\n 64|  auto-approve, and never drop the cap while keeping skip-permissions.\n*65|- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66|  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67|  sibling, and surfaces loudly (no commune produced).\n 68|- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n### 0004-checkpoint-via-self-send-loopback.md\n 38|\n*39|1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own\n 40|   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a\n 41|   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**\n 42|   the clear.\n...\n 69|- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n*70|  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n 71|  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n 72|- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n 73|  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n...\n 76|  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.\n*77|- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no\n 78|  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted\n 79|  lifecycle on purpose.\n 80|- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n### 0005-name-unification.md\n 1|<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n*2|# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n 3|\n 4|## Status\n 5|\n*6|accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n*7|`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n 8|\n 9|## Context\n 10|\n...\n 14||---|---|---|\n*15|| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n*16|| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n*17|| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n*18|| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n 19|\n*20|Three names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The ski...\n 21|\n 22|## Decision\n 23|\n...\n 25|\n*26|- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27|- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n*28|- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29|\n*30|End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n 31|\n 32|## Considered and rejected\n 33|\n*34|- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n*35|- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n 36|\n 37|## Consequences\n 38|\n*39|- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n 40|- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41|- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n 6|\n*7|**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n 8|dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n 9|**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n 10|`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\n 11|let the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\n*12|in `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n*13|`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\n*14|hook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\n 15|hook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n 16|<!-- [doc->REQ-DIST-HOOK-BINARY] -->\n 17|See `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n...\n 34|- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n*35|- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n 36|- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37|- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38|\n 39|### Binary consolidation\n 40|\n*41|Collapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n 42|\n*43|- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44|- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45|\n 46|The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47|\n\n# tests/\n## acceptance-harness.sh\n 10|SPT_AGENT_ID=perri OWL_SESSION_ID=perri\n*11|sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n*12|[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n*13|[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n*14|[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n 15|[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n 16|\n 17|# is_disposable: accepts a minted id, rejects any live name.\n*18|sptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\n*19|sptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\n*20|sptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n 21|\n 22|# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.\n 23|work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n...\n 25|proj=\"$work/p\"; digest=\"$work/d.txt\"\n*26|sptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null\n\n[Showing lines 1-671 of 800 (50.0KB limit). Use :672 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, traceable-reqs.toml, tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":203,"fileCount":20,"files":["CONTEXT.md","SCOPE.md","OMP-ADAPTER-PLAN.md","docs/adr/0001-distribution-splits-by-volatility.md","docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","docs/adr/0004-checkpoint-via-self-send-loopback.md","docs/adr/0005-name-unification.md","docs/adr/0006-one-command-update-and-consolidated-binary.md","docs/KNOWN-HAZARDS.md","docs/OMP-BRIDGE-FIELD-ISSUES.md","traceable-reqs.toml","tests/acceptance-harness.sh","tests/adapter-archive.sh","tests/bridge-acceptance.sh","tests/docs-gen.sh","tests/hooks-dispatch.sh","tests/manifest-schema.sh","tests/manifest-shortcut.sh","tests/msys-hazard.sh"],"fileMatches":[{"path":"CONTEXT.md","count":17},{"path":"SCOPE.md","count":20},{"path":"OMP-ADAPTER-PLAN.md","count":14},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","count":15},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","count":19},{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","count":5},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","count":3},{"path":"docs/adr/0005-name-unification.md","count":15},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","count":9},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","count":13},{"path":"traceable-reqs.toml","count":20},{"path":"tests/acceptance-harness.sh","count":10},{"path":"tests/adapter-archive.sh","count":4},{"path":"tests/bridge-acceptance.sh","count":2},{"path":"tests/docs-gen.sh","count":1},{"path":"tests/hooks-dispatch.sh","count":5},{"path":"tests/manifest-schema.sh","count":5},{"path":"tests/manifest-shortcut.sh","count":5},{"path":"tests/msys-hazard.sh","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# CONTEXT.md\n  *1│# spt-claude-code — glossary\n   2│\n   3│> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n   4│> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n   5│\n  *6│**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n   7│adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n   8│parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n   9│installer. Built by maintainer perri from the public spt-releases surface only.\n *10│(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n  11│[[naming-domains]].)\n  12│\n *13│**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n *14│invocation + the manifest). The CC adapter's identity inside spt-core. The\n  15│**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n  16│\n *17│**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n *18│spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n  19│**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n *20│`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n  21│`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n *22│shortcut (`cc-<id>`) is decoupled from both.\n  23│\n  24│**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration\n  25│(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a\n    │...\n  50│\n *51│**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n *52│that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n *53│NOT its own adapter — ccs is structurally Claude Code.\n  54│\n  55│**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n  56│spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n    │...\n  60│**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n *61│(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own\n  62│perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a\n  63│*commune*. It never replies or notifies (that is the echo-commune, a different actor) and\n  64│exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n *65│profile. (Realized here by the `claude-spt-psyche` runner.)\n  66│\n  67│**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n  68│parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n    │...\n  97│**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n *98│adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n  99│users: install the plugin, get spt-core for free.\n 100│\n 101│**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102│extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103│traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104│same-repo published-docs. Defined in its own folder, not here.\n 105│\n    │...\n 109│resulting submit to consume the edited result. The binary-inject leg and the UPS leg are\n*110│decoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive\n 111│edit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s\n 112│additionalContext (which the operator never edits).\n 113│\n\n# SCOPE.md\n  *1│# spt-claude-code — scoping decision ledger\n   2│\n   3│> Running source of truth for the scoping session (started 2026-06-13, doyle).\n   4│> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n    │...\n   9│\n *10│**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n *11│`claude-spt` adapter) — simultaneously:\n  12│1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n  13│   delegating all core to `spt.exe`).\n  14│2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n    │...\n  30│\n *31│**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n  32│in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n  33│\n  34│| Layer | Contents | Home | Update path | Churn |\n    │...\n  37│| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n *38│| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n  39│\n  40│- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n  41│  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n    │...\n  67│\n *68│## ccs integration\n  69│\n *70│**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n *71│`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n  72│model/billing backend differ, which is exactly the profile seam (leaf-replace\n *73│spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n  74│leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n *75│profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n  76│Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n  77│not only bare adapter_name (profile model already supports composite addressing).\n  78│\n    │...\n  91│6. OS-service registration (always-on daemon)\n *92│7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value\n  93│   prop) if absent\n  94│8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n  95│   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n    │...\n  99│full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100│`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n 101│flag if verify should fold inline._\n 102│\n 103│### `cc` launcher (capsule-style) — **LOCKED (design)**\n    │...\n 110│**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\n*111│hosted CC session (not just headless PTY).\n 112│\n 113│### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n 114│\n    │...\n 129│  the product as its own CI nervous system.\n*130│- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131│  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132│  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133│  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n    │...\n 136│  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137│    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n 138│    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139│- This CI pattern → reusable, goes in `experimplate`.\n 140│\n    │...\n 150│**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),\n*151│profiles + strings + hints wiring, ccs profiles.\n 152│\n 153│**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n 154│harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n    │...\n 173│**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174│spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175│`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176│\n 177│## spt-core upstream deps\n    │...\n 179│self-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets\n*180│adapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.\n 181│\n 182│## experimplate (template) — resolved content\n 183│- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH\n*184│  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n 185│  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n 186│  (not spt-core's two-key/counter runbook).\n 187│- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n    │...\n 194│\n*195│- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`\n 196│  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,\n\n# OMP-ADAPTER-PLAN.md\n  *1│# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n   2│\n  *3│> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n   4│> **oh-my-pi (omp)** — the terminal coding agent at\n   5│> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n   6│> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n    │...\n  16│\n *17│claude-spt exists because Claude Code has **no control protocol** — the adapter\n  18│reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n  19│`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n  20│digest, and pushes received messages back via `additionalContext`. Most of its\n    │...\n  49│**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n *50│spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n  51│runs `omp --mode rpc` as a child, and translates in both directions:\n  52│\n  53│```\n    │...\n  73│\n *74│## Seam map — claude-spt value → omp-spt target\n  75│\n *76│| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n  77│|---|---|---|\n *78│| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n  79│| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n  80│| `[adapter].shortcut_basename` | `cc` | `omp` |\n  81│| `[adapter].kind` | `harness` | `harness` (unchanged) |\n *82│| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n  83│| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n  84│| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n  85│| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n  86│| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n *87│| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n  88│| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n  89│\n  90│### Session/transcript (for the `[digest]` extractor)\n    │...\n 110│\n*111│## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112│\n*113│- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114│  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115│  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116│- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117│  (bridge command), `[identity]`. Prove with\n 118│  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119│  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120│- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121│  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122│  Acceptance: from another node, message the perch → get an omp answer; assert\n 123│  `spt` state transitions (isolated CI identity per tips doc §Testing).\n    │...\n 174│5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest\n*175│   `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2\n 176│   actually calls).\n 177│\n 178│## Validation harness (all on the public surface, no live model needed)\n\n# traceable-reqs.toml\n  *1│# traceable-reqs manifest for spt-claude-code — SEED.\n   2│#\n   3│# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n   4│# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n    │...\n  62│id = \"REQ-DIST-PLUGIN-SKELETON\"\n *63│title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n  64│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n  65│\n  66│[[requirements]]\n    │...\n  68│title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n *69│required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n  70│\n  71│[[requirements]]\n  72│id = \"REQ-DIST-HOOKS-API\"\n    │...\n  77│id = \"REQ-DIST-HOOK-BINARY\"\n *78│title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n *79│required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n  80│\n  81│[[requirements]]\n  82│id = \"REQ-DIST-MANIFEST-SCHEMA\"\n *83│title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n *84│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n  85│\n  86│[[requirements]]\n  87│id = \"REQ-DIST-SHORTCUT-BASENAME\"\n *88│title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n *89│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n  90│\n  91│[[requirements]]\n  92│id = \"REQ-CC-LAUNCHER-BIND\"\n *93│title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n *94│required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n  95│\n  96│[[requirements]]\n  97│id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n *98│title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n *99│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100│\n 101│[[requirements]]\n 102│id = \"REQ-DIST-DIGEST-FETCHER\"\n*103│title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105│\n 106│[[requirements]]\n 107│id = \"REQ-DIST-WHOAMI-JSON\"\n 108│title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109│required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110│\n 111│[[requirements]]\n 112│id = \"REQ-SKILL-LIVE\"\n*113│title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115│\n 116│[[requirements]]\n 117│id = \"REQ-UPS-INJECTION\"\n*118│title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120│\n 121│[[requirements]]\n 122│id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n\n# docs/\n## KNOWN-HAZARDS.md\n  41│  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n *42│  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n *43│  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n  44│  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n  45│  documents the identical hazard in `new-alarm`.)\n *46│- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n  47│  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n  48│  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n  49│  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n *50│- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n  51│  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n *52│  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  53│  A test asserts the stdin path is honored (no argv `/`-token dependency).\n  54│- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n  55│  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n    │...\n  72│- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n *73│  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n  74│  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n  75│  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n *76│  overrides both to a `sptc-ci-` id and never emits a live id.\n *77│- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n  78│  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n  79│  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n  80│  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n    │...\n  90│\n *91│- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n *92│  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n  93│  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n  94│  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n  95│  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n    │...\n  97│- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n *98│  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n *99│  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n 100│  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n 101│  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n 102│  trust (see `docs/adr/0003-*`).\n*103│- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n 104│  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n 105│  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n*106│  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n*107│  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n 108│  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n 109│  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n 110│  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n    │...\n 139│  reason; a silent skip is forbidden.\n*140│- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n 141│  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n 142│  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n 143│  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n    │...\n 184│  it ever bites. Accepted for v1.\n*185│- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n 186│  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n 187│  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n*188│  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n 189│  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n 190│  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n 191│- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n    │...\n 225│  side, but the adapter must not hand the child a foreign identity in the first place.\n*226│- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n 227│  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n 228│  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229│  child that runs harness hooks, it needs the same scrub.\n    │...\n 278│  core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.\n*279│- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`\n 280│  (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to\n 281│  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n 282│  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n## OMP-BRIDGE-FIELD-ISSUES.md\n   5│\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n  *6│and could not be switched back to `claude-spt`. This documents the root causes,\n   7│grounded in this repo's code, so the bridge can be hardened.\n   8│\n  *9│All line references are against `tools/claude-spt/src/bridge.rs` and\n  10│`adapter/omp-spt.toml` at commit `f0109d1`.\n  11│\n  12│## Incident summary\n    │...\n  14│1. Operator started `todlando` under omp-spt (`[session.self]` →\n *15│   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n  16│2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n  17│   way to tell working vs. wedged.\n  18│3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n *19│   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n  20│   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n  21│   endpoint looked alive but was dead.\n *22│4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n  23│5. Starting fresh after switching the harness adapter still launched omp-spt.\n  24│\n  25│Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n    │...\n  30│\n *31│## HIGH-1 — Bridge binary basename collides with `claude-spt`\n  32│\n *33│**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n *34│indistinguishable from a genuine claude-spt process. During the incident this\n *35│made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n  36│todlando's perch was actually the omp bridge. It directly caused the\n  37│misdiagnosis.\n  38│\n    │...\n  40│```\n *41│command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n  42│```\n *43│The executable is named `claude-spt`, the exact basename of the upstream adapter.\n  44│This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n  45│bridge (S2) is `omp-spt`\"*).\n  46│\n    │...\n  58│object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n *59│`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n  60│`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n  61│When the broker or operator terminates the bridge process (the normal \"stop\"),\n  62│those lines never run. On Windows, killing a process does not kill its children,\n    │...\n 149│**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150│restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151│editing spt-core's session ledger by hand.\n 152│\n 153│**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n\n## adr/\n### 0001-distribution-splits-by-volatility.md\n  12│\n *13│`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n *14│its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n  15│invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n *16│inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n  17│plugin author.\n  18│\n  19│Forces:\n    │...\n  24│   marketplace or letting logic rot.\n *25│2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n  26│   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n  27│   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n  28│   need to ride the marketplace at all.\n    │...\n  43│| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n *44│| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n  45│\n  46│Concretely:\n  47│\n    │...\n  77│- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n *78│  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n  79│  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`\n *80│  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but\n  81│  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)\n  82│- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction\n  83│  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.\n    │...\n  96│  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /\n *97│  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n *98│  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n  99│  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100│  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101│- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n    │...\n 103│<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n*104│- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105│  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106│  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n*107│  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108│  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109│- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n 110│  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —\n 111│  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112│  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n*113│  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114│  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n*115│  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n 116│- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the\n 117│  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,\n 118│  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n   2│<!-- [doc->REQ-UPS-INJECTION] -->\n  *3│# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`\n   4│\n   5│## Status\n   6│\n    │...\n  12│primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n *13│formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n  14│payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n *15│public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n *16│(the adapter_name — distinct from the plugin name `sptc`).\n  17│\n  18│Two hard constraints shape the wiring:\n  19│\n    │...\n  21│   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,\n *22│   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n  23│2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n  24│   L52/L181. The plugin hand-writes it.\n  25│\n    │...\n  27│\n *28│**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n *29│`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n  30│(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n  31│substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n  32│\n    │...\n  34│\n *35│| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n  36│|---|---|---|\n  37│| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n  38│| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n    │...\n  45│\n *46│`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n  47│**not** from a hook.\n  48│\n  49│### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook\n    │...\n  51│The `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers\n *52│queued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter\n  53│`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is\n  54│ours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.\n  55│\n    │...\n  75│\n *76│Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\n *77│the `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\n  78│unescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\n  79│gate, Psyche routing), never silently stripped. Covered by\n  80│`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).\n    │...\n  99│- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n*100│  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n 101│- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n 102│  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n 103│  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n    │...\n 115│  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n*116│  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n*117│  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n 118│- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n 119│  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n 120│  packaging on Windows (no per-OS branch needed for the hook to run).\n    │...\n 136│   chars to a file (evicting it from the inline context the agent sees), silently losing messages.\n*137│   The UPS wrapper now pre-empts: `sptc_cap_output` (`_common.sh`) buffers the combined skill-body +\n 138│   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n*139│   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n*140│   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n 141│   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n*142│   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n 143│\n 144│## Consequences\n 145│\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n 11│A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\n*12│hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\n*13│subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\n 14│the runner seeds one headless `claude` turn from the daemon-supplied prompt,\n 15│then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\n 16│on each pulse.\n   │...\n 37│Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\n*38│legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n 39│`pulse_cmd`:\n 40│\n 41│- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n   │...\n 48│The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n*49│`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\n 50│the flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\n 51│recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n 52│\n   │...\n 64│  auto-approve, and never drop the cap while keeping skip-permissions.\n*65│- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66│  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67│  sibling, and surfaces loudly (no commune produced).\n 68│- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n### 0004-checkpoint-via-self-send-loopback.md\n 38│\n*39│1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own\n 40│   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a\n 41│   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**\n 42│   the clear.\n   │...\n 69│- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n*70│  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n 71│  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n 72│- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n 73│  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n   │...\n 76│  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.\n*77│- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no\n 78│  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted\n 79│  lifecycle on purpose.\n 80│- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n### 0005-name-unification.md\n  1│<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n *2│# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n  3│\n  4│## Status\n  5│\n *6│accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n *7│`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n  8│\n  9│## Context\n 10│\n   │...\n 14│|---|---|---|\n*15│| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n*16│| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n*17│| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n*18│| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n 19│\n*20│Three names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The ski...\n 21│\n 22│## Decision\n 23│\n   │...\n 25│\n*26│- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27│- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n*28│- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29│\n*30│End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n 31│\n 32│## Considered and rejected\n 33│\n*34│- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n*35│- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n 36│\n 37│## Consequences\n 38│\n*39│- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n 40│- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41│- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n  6│\n *7│**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n  8│dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n  9│**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n 10│`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\n 11│let the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\n*12│in `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n*13│`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\n*14│hook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\n 15│hook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n 16│<!-- [doc->REQ-DIST-HOOK-BINARY] -->\n 17│See `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n   │...\n 34│- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n*35│- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n 36│- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37│- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38│\n 39│### Binary consolidation\n 40│\n*41│Collapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n 42│\n*43│- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44│- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45│\n 46│The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47│\n\n# tests/\n## acceptance-harness.sh\n 10│SPT_AGENT_ID=perri OWL_SESSION_ID=perri\n*11│sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n*12│[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n*13│[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n*14│[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n 15│[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n 16│\n 17│# is_disposable: accepts a minted id, rejects any live name.\n*18│sptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\n*19│sptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\n*20│sptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n 21│\n 22│# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.\n 23│work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n   │...\n 25│proj=\"$work/p\"; digest=\"$work/d.txt\"\n*26│sptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null\n 27│[ -f \"$proj/.claude/settings.json\" ] && echo \"ok   scaffold writes settings.json\" || { echo \"FAIL no settings.json\"; fail=1; }\n 28│[ -f \"$proj/.claude/hooks/ups-digest.sh\" ] && echo \"ok   scaffold writes UPS hook\" || { echo \"FAIL no UPS hook\"; fail=1; }\n 29│grep -q 'UserPromptSubmit' \"$proj/.claude/settings.json\" && echo \"ok   settings wires UserPromptSubmit\" || { echo \"FAIL UPS not wired\"; fail=1; }\n   │...\n 34│printf '%s' '{\"session_id\":\"x\",\"prompt\":\"hello-probe\"}' | sh \"$proj/.claude/hooks/ups-digest.sh\"\n*35│sptc_ci_assert \"fixture emits digest marker\" \"UPS_FIRED:hello-probe\" \"$digest\" >/dev/null \\\n 36│  && echo \"ok   fixture emits digest marker\" || { echo \"FAIL fixture no marker\"; fail=1; }\n 37│\n 38│# ── Assert helper: negative case (missing needle) must FAIL, not false-pass.\n*39│if sptc_ci_assert \"neg\" \"NOPE\" \"$digest\" >/dev/null; then echo \"FAIL assert false-passed\"; fail=1; else echo \"ok   assert fails on missing needle\"; fi\n 40│\n 41│# ── Orchestrator is env-gated: without SPTC_ACCEPTANCE it skips cleanly (rc 0), spawns no claude.\n 42│out=$(SPTC_ACCEPTANCE=0 sh \"$(dirname \"$0\")/../ci/acceptance/run-acceptance.sh\" 2>&1); orc=$?\n## adapter-archive.sh\n 13│LINUX_TRIPLE=x86_64-unknown-linux-gnu\n*14│BINS=\"omp-spt\"  # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)\n 15│rc=0\n 16│fail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n 17│\n   │...\n 20│for b in $BINS; do\n*21│  [ -f \"$ROOT/tools/claude-spt/target/release/$b.exe\" ] || have_all=0\n*22│  [ -f \"$ROOT/tools/claude-spt/target/$LINUX_TRIPLE/release/$b\" ] || have_all=0\n 23│done\n 24│\n 25│if [ \"$have_all\" -ne 1 ]; then\n   │...\n 37│# Both platforms present → build a real fat archive to a temp file and assert its structure.\n*38│TMP=$(mktemp -d \"${TMPDIR:-/tmp}/sptc-archtest.XXXXXX\") || { echo \"FAIL: mktemp\"; exit 1; }\n 39│trap 'rm -rf \"$TMP\"' EXIT\n 40│OUT=\"$TMP/adapter.spt\"\n 41│\n## bridge-acceptance.sh\n  4│#\n *5│# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n  6│# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n  7│# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n  8│#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n   │...\n 21│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n*22│CRATE=\"$ROOT/tools/claude-spt\"\n 23│fail=0\n 24│\n 25│# ── Resolve binaries ─────────────────────────────────────────────────────────\n## docs-gen.sh\n 21│# 3. Shape: starts with the H1 title and the blockquote summary (llms.txt convention).\n*22│printf '%s\\n' \"$a\" | head -n1 | grep -q '^# claude-spt' && echo \"ok   has H1 title\" || { echo \"FAIL missing H1\"; fail=1; }\n 23│printf '%s\\n' \"$a\" | grep -q '^> ' && echo \"ok   has summary blockquote\" || { echo \"FAIL missing summary\"; fail=1; }\n 24│\n 25│# 4. Drift gate is wired and valid shell.\n## hooks-dispatch.sh\n   2│# Unit coverage for the STATIC-FOREVER plugin hook surface (D1). The hook LOGIC now lives in the\n  *3│# `claude-spt` binary and is unit-tested there (tools/claude-spt/src/hook.rs cargo tests — the port of\n   4│# the old tests/hooks-parse.sh helper coverage). What remains in the PLUGIN is static-forever wiring:\n   5│# hooks.json routes every CC event to dispatch.sh, and dispatch.sh resolves+execs the binary. This\n   6│# test pins that wiring + the adapter-side declarations it depends on.\n    │...\n  46│check \"dispatch.sh is valid POSIX sh\" \"ok\" \"$r\"\n *47│grep -q 'get-string claude-spt hook_cmd' \"$HK/dispatch.sh\" && r=yes || r=no\n  48│check \"dispatch resolves the binary via [strings].hook_cmd\" \"yes\" \"$r\"\n  49│grep -q 'SPTC_HOOK_BIN' \"$HK/dispatch.sh\" && r=yes || r=no\n  50│check \"dispatch caches the resolved bin (SPTC_HOOK_BIN)\" \"yes\" \"$r\"\n    │...\n  66│_ef=$(mktemp); _errf=$(mktemp)\n *67│_old=\"/opt/spt/adapters/x/claude-spt hook\"          # the exact v0.9.0 cached value (space before hook)\n *68│_p=\"/c/Program Files/spt dir/claude-spt\"            # worst case: a path WITH spaces\n  69│# v0.9.0 (unquoted) — sourcing runs the bare `hook` token.\n  70│unset SPTC_HOOK_BIN; printf 'SPTC_HOOK_BIN=%s\\n' \"$_old\" > \"$_ef\"; : > \"$_errf\"\n  71│. \"$_ef\" 2>\"$_errf\" || true\n    │...\n  98│# space-before-hook value (the env-file regression).\n *99│grep -qE '^hook_cmd = \"\\{adapter_dir\\}/claude-spt hook\"' \"$MAN\" && r=present || r=absent\n 100│check \"manifest hook_cmd assignment has no embedded ' hook' (env-file fix)\" \"absent\" \"$r\"\n 101│grep -q '^\\[hooks\\.PreToolUse\\]' \"$MAN\" && r=yes || r=no\n 102│check \"manifest declares [hooks.PreToolUse]\" \"yes\" \"$r\"\n## manifest-schema.sh\n  1│#!/bin/sh\n *2│# Unit tests for the manifest-schema validator: it must PASS the real claude-spt manifest and FAIL\n  3│# on tampered copies (missing required field / bad enum / wrong type) — proving the gate catches\n  4│# real breakage, not just green-on-green. Run: sh tests/manifest-schema.sh  (exit 0 = pass).\n  5│# [unit->REQ-DIST-MANIFEST-SCHEMA]\n   │...\n 48│[adapter]\n*49│name = \"claude-spt\"\n 50│version = \"0.1.0\"\n 51│EOF\n 52│expect_reject \"missing required adapter.min_spt_core_version\"\n   │...\n 56│[adapter]\n*57│name = \"claude-spt\"\n 58│kind = \"robot\"\n 59│version = \"0.1.0\"\n 60│min_spt_core_version = \"0.7.0\"\n   │...\n 66│[adapter]\n*67│name = \"claude-spt\"\n 68│version = \"0.1.0\"\n 69│min_spt_core_version = \"0.7.0\"\n 70│[hooks.SessionStart]\n   │...\n 77│[adapter]\n*78│name = \"claude-spt\"\n 79│version = \"0.1.0\"\n 80│min_spt_core_version = \"0.7.0\"\n 81│shortcut_basename = 42\n## manifest-shortcut.sh\n  1│#!/bin/sh\n *2│# Brand-value + manifest-contract regression guard for omp-spt (rewritten from the claude-spt\n *3│# version at the S4 cut — the CC launch-shim/ccs assertions it carried now guard the native OMP\n  4│# hosting seam; see ADR-0007).\n  5│# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n  6│# [unit->REQ-DIST-SHORTCUT-BASENAME]\n   │...\n 83│\n*84│# ── idle-translate binary seam (harness-agnostic; unchanged from claude-spt) ──────────────────────\n 85│if grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"{adapter_dir}/omp-spt translate\"' \"$MANIFEST\"; then echo \"ok   [message-idle-translation-binary].command = translate\"; else echo \"FAIL idle-translate command missing/wrong\"; fail=1; fi\n 86│if grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*\"cc-spt-idle-translate\"' \"$MANIFEST\"; then echo \"FAIL deprecated idle-translate path still present\"; fail=1; else echo \"ok   no deprecated idle-translate path (command-only)\"; fi\n 87│\n*88│# ── no shipped profile overlays (ccs was CC-specific; omp profiles ride `-- --profile`) ───────────\n*89│if grep -Eq '^\\[profiles\\.' \"$MANIFEST\"; then echo \"FAIL a shipped [profiles.*] table lingers (ccs was removed for omp)\"; fail=1; else echo \"ok   no shipped profile overlays\"; fi\n 90│\n 91│# ── [env.SPT_INJECT_VERIFY_ECHO] stays declared (inert on omp; forward-safe) ──────────────────────\n 92│if awk '/^\\[env\\.SPT_INJECT_VERIFY_ECHO\\]/{found=1} END{exit !found}' \"$MANIFEST\"; then echo \"ok   [env.SPT_INJECT_VERIFY_ECHO] declared\"; else echo \"FAIL [env.SPT_INJECT_VERIFY_ECHO] missing\"; fail=1; fi\n## msys-hazard.sh\n  9│HK=\"$ROOT/plugin/omps/hooks\"\n*10│HOOKRS=\"$ROOT/tools/claude-spt/src/hook.rs\"\n 11│fail=0\n 12│yn() { [ \"$1\" -eq 0 ] && echo yes || echo no; }\n 13│","truncation":{"content":"# CONTEXT.md\n*1|# spt-claude-code — glossary\n 2|\n 3|> Authoritative for meaning (grill-with-docs convention). Glossary only — no\n 4|> implementation detail. Decisions/rationale live in `SCOPE.md` and `docs/adr/`.\n 5|\n*6|**spt-claude-code** — the rebuilt Claude Code harness adapter. The `claude-spt`\n 7|adapter in spt-core terms. Simultaneously spt-core's v1 acceptance proof (legacy\n 8|parity), its first casual-end-user entrypoint (a CC plugin), and an invisible spt-core\n 9|installer. Built by maintainer perri from the public spt-releases surface only.\n*10|(Repo being renamed `spt-claude-code` → `claude-spt` per the naming-domain rule; see\n 11|[[naming-domains]].)\n 12|\n*13|**claude-spt** — the adapter_name spt-claude-code registers under (rides every `api`\n*14|invocation + the manifest). The CC adapter's identity inside spt-core. The\n 15|**spt-core-facing** token (repo + adapter + consolidated binary). See [[naming-domains]].\n 16|\n*17|**naming-domains** — the two-token naming rule (ADR-0005): **`claude-spt`** is the\n*18|spt-core-facing identity (repo, adapter, binary — `spt adapter … claude-spt`), and\n 19|**`spt`** is the Claude-Code-facing identity (plugin + skills, `/spt:*`, via the\n*20|`sptc`→`spt` succession). One token can't cover both because the adapter cannot be named\n 21|`spt` (spt-core's own identity) while the skill prefix wants `/spt:*`. The `cc` launcher\n*22|shortcut (`cc-<id>`) is decoupled from both.\n 23|\n 24|**legacy spt / `claude_skill_owl`** — the sister project; today's shipped CC integration\n 25|(owl.exe + `spt` plugin, v1.11.25). The parity *target* (user-facing features), not a\n...\n 50|\n*51|**ccs profile** — a profile under spt-claude-code (`claude-spt:glm`, `claude-spt:kimi`)\n*52|that leaf-replaces the launch command + history/digest log dir to use the `ccs` backend.\n*53|NOT its own adapter — ccs is structurally Claude Code.\n 54|\n 55|**`cc` launcher (capsule-style)** — a generated `cc`/`cc <id>` script at project root that\n 56|spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n...\n 60|**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n*61|(`/sptc:live`), spt-core's daemon hosts a Psyche alongside it: the Psyche owns its own\n 62|perch (`<parent>-psyche`), is woken by daemon *pulses*, and on each pulse authors a\n 63|*commune*. It never replies or notifies (that is the echo-commune, a different actor) and\n 64|exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65|profile. (Realized here by the `claude-spt-psyche` runner.)\n 66|\n 67|**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68|parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n...\n 97|**casual end user** — a user who *uses* an spt-powered system mostly-invisibly, vs the\n*98|adapter/shell *developer* who is spt-core's nominal target. spt-claude-code serves casual\n 99|users: install the plugin, get spt-core for free.\n 100|\n 101|**experimplate** — the standalone reusable project-workflow template (its own sibling repo)\n*102|extracted from spt-core's working style. spt-claude-code is its first consumer. Carries\n 103|traceable-reqs gating, JIT plans, grill-with-docs scaffolding, release/changelog, and\n 104|same-repo published-docs. Defined in its own folder, not here.\n 105|\n...\n 109|resulting submit to consume the edited result. The binary-inject leg and the UPS leg are\n*110|decoupled and *rendezvous on the tags*, not on a shared channel. Backs `/sptc:role`'s no-directive\n 111|edit. Distinct from message injection (which auto-submits) and from an [[identity brief]]'s\n 112|additionalContext (which the operator never edits).\n 113|\n\n# SCOPE.md\n*1|# spt-claude-code — scoping decision ledger\n 2|\n 3|> Running source of truth for the scoping session (started 2026-06-13, doyle).\n 4|> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n...\n 9|\n*10|**LOCKED.** `spt-claude-code` is the rebuilt Claude Code harness adapter (the\n*11|`claude-spt` adapter) — simultaneously:\n 12|1. spt-core's **v1 acceptance proof** (feature parity with legacy `claude_skill_owl`,\n 13|   delegating all core to `spt.exe`).\n 14|2. spt-core's **first casual-end-user entrypoint** — published as a CC plugin on\n...\n 30|\n*31|**LOCKED.** Split by volatility (Claude Code has NO plugin-file integrity check —\n 32|in-place edits don't orphan; only old version dirs orphan on update, GC'd 7d):\n 33|\n 34|| Layer | Contents | Home | Update path | Churn |\n...\n 37|| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n*38|| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n 39|\n 40|- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n 41|  namespace: `~/.claude/skills` → bare `/live`, collision-prone). Rejected global-skills.\n...\n 67|\n*68|## ccs integration\n 69|\n*70|**LOCKED.** ccs = **profile(s) under spt-claude-code** (`claude-spt:glm`,\n*71|`claude-spt:kimi`, …), NOT its own adapter. It's structurally CC — only launch command +\n 72|model/billing backend differ, which is exactly the profile seam (leaf-replace\n*73|spawn/psyche/echo command templates). Per-profile `~/.ccs` log dir → profile also\n 74|leaf-replaces the history/`[digest]` locate-template. **Hybrid** delivery: ship the\n*75|profile *templates* (ccs-invoking command structure), user supplies own ccs config/keys.\n 76|Reconcile spt-core CONTEXT.md: cross-adapter fallback must target `<adapter>:<profile>`,\n 77|not only bare adapter_name (profile model already supports composite addressing).\n 78|\n...\n 91|6. OS-service registration (always-on daemon)\n*92|7. ccs profile wiring — if `~/.ccs` present; **also offer to install ccs** (+1-sentence value\n 93|   prop) if absent\n 94|8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n 95|   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n...\n 99|full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100|`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n 101|flag if verify should fold inline._\n 102|\n 103|### `cc` launcher (capsule-style) — **LOCKED (design)**\n...\n 110|**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\n*111|hosted CC session (not just headless PTY).\n 112|\n 113|### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n 114|\n...\n 129|  the product as its own CI nervous system.\n*130|- Rationale: a stock GH runner physically can't run Claude Code (auth/interactivity);\n 131|  only a real harness on the fleet reaches the acceptance bar. spt-core's GH-runner\n 132|  justification (heavy multi-platform Rust + signed releases + two-host net) doesn't carry\n 133|  — the adapter binary is thin glue and delegates releases/signing to spt.exe.\n...\n 136|  - **NOTE:** the `$OWL send` (legacy spt) binary location must be discovered flexibly — it\n*137|    lives in a per-version `~/.claude/` or `~/.ccs/` plugins folder whose path changes each\n 138|    version. No further legacy-spt changes anticipated, but the CI bus must locate it robustly.\n 139|- This CI pattern → reusable, goes in `experimplate`.\n 140|\n...\n 150|**ADD** (LOCKED): `/spt:setup`, `/spt:version`, subnet skills (create/join/show-code),\n*151|profiles + strings + hints wiring, ccs profiles.\n 152|\n 153|**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n 154|harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n...\n 173|**Gating prerequisite: full-fat M12 (todlando builds it after this grill, BEFORE perri starts\n*174|spt-claude-code).** M12 deliverables incl. `spt endpoint run` (picker — see\n 175|`../spt-core/M12-ENDPOINT-RUN-PICKER.md`) + `spt rc` (cross-node PTY connect) + whoami→alias.\n 176|\n 177|## spt-core upstream deps\n...\n 179|self-elevating window · 3: spt-hosted bringup + PTY attach · 4: fallback targets\n*180|adapter:profile · 5: whoami obsolescence). spt-claude-code blocks on these.\n 181|\n 182|## experimplate (template) — resolved content\n 183|- **Release procedure = generic shape + placeholders** (CHANGELOG · version bump · tag · GH\n*184|  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n 185|  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n 186|  (not spt-core's two-key/counter runbook).\n 187|- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n...\n 194|\n*195|- **`claude-spt:deep` profile REMOVED — LOCKED.** It was a placeholder (digest `window_turns=20`\n 196|  + a label leaf) that forked nothing real; it existed only to demonstrate overlay-observability,\n\n# OMP-ADAPTER-PLAN.md\n*1|# OMP-ADAPTER-PLAN — port claude-spt to oh-my-pi (omp)\n 2|\n*3|> Turn this fork of `claude-spt` into **omp-spt**: an SPT harness adapter for\n 4|> **oh-my-pi (omp)** — the terminal coding agent at\n 5|> [github.com/can1357/oh-my-pi](https://github.com/can1357/oh-my-pi), docs at\n 6|> [omp.sh/docs](https://omp.sh/docs) / `docs/` in that repo. Local binary here:\n...\n 16|\n*17|claude-spt exists because Claude Code has **no control protocol** — the adapter\n 18|reverse-engineers CC's lifecycle through eight hooks (`SessionStart`,\n 19|`UserPromptSubmit`, `PreToolUse`, `Stop`, …), scrapes the transcript for a\n 20|digest, and pushes received messages back via `additionalContext`. Most of its\n...\n 49|**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n*50|spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n 51|runs `omp --mode rpc` as a child, and translates in both directions:\n 52|\n 53|```\n...\n 73|\n*74|## Seam map — claude-spt value → omp-spt target\n 75|\n*76|| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77||---|---|---|\n*78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n 79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n 83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n 84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n 86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n 90|### Session/transcript (for the `[digest]` extractor)\n...\n 110|\n*111|## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n 112|\n*113|- **S0 — Rebrand.** `claude-spt`→`omp-spt` across manifest, `[update].repo`,\n 114|  `host_binaries`, `shortcut_basename`, strings/labels, CI, docs headers. Keep the\n*115|  version history but reset to a `0.1.0` omp-spt line (note the claude-spt lineage).\n 116|- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n 117|  (bridge command), `[identity]`. Prove with\n 118|  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119|  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120|- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121|  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122|  Acceptance: from another node, message the perch → get an omp answer; assert\n 123|  `spt` state transitions (isolated CI identity per tips doc §Testing).\n...\n 174|5. **min spt-core floor** — which `spt api` verbs the bridge uses, and the honest\n*175|   `min_spt_core_version` (claude-spt is at 0.27.0; omp-spt only needs the verbs S2\n 176|   actually calls).\n 177|\n 178|## Validation harness (all on the public surface, no live model needed)\n\n# traceable-reqs.toml\n*1|# traceable-reqs manifest for spt-claude-code — SEED.\n 2|#\n 3|# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n 4|# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n...\n 62|id = \"REQ-DIST-PLUGIN-SKELETON\"\n*63|title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n 64|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n 65|\n 66|[[requirements]]\n...\n 68|title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n*69|required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n 70|\n 71|[[requirements]]\n 72|id = \"REQ-DIST-HOOKS-API\"\n...\n 77|id = \"REQ-DIST-HOOK-BINARY\"\n*78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n 82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n*83|title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n*84|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n 85|\n 86|[[requirements]]\n 87|id = \"REQ-DIST-SHORTCUT-BASENAME\"\n*88|title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n*89|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n 90|\n 91|[[requirements]]\n 92|id = \"REQ-CC-LAUNCHER-BIND\"\n*93|title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n*94|required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n 95|\n 96|[[requirements]]\n 97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n*98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n 102|id = \"REQ-DIST-DIGEST-FETCHER\"\n*103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n 107|id = \"REQ-DIST-WHOAMI-JSON\"\n 108|title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109|required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110|\n 111|[[requirements]]\n 112|id = \"REQ-SKILL-LIVE\"\n*113|title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115|\n 116|[[requirements]]\n 117|id = \"REQ-UPS-INJECTION\"\n*118|title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120|\n 121|[[requirements]]\n 122|id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n\n# docs/\n## KNOWN-HAZARDS.md\n 41|  2026-06-15 during the UPS-fires validation: invoking `claude -p \"/send hi\"` from Git-Bash\n*42|  delivered the prompt to Claude Code as `C:/Program Files/Git/send hi` — the `/send` token was\n*43|  path-converted. Anything that passes a `/sptc:…` (or other `/`-leading) token as a **positional\n 44|  argument** through a Git-Bash layer is corrupted the same way. (Legacy `claude_skill_owl`\n 45|  documents the identical hazard in `new-alarm`.)\n*46|- **Invariant:** sptc adapter glue MUST NOT depend on receiving `/`-leading content as a\n 47|  Git-Bash positional argument. Message/prompt content is read from the **hook stdin JSON**\n 48|  (`prompt`, message bodies), never reconstructed from a `/`-prefixed argv; any helper that must\n 49|  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n*50|- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n*52|  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n 53|  A test asserts the stdin path is honored (no argv `/`-token dependency).\n 54|- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n 55|  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n...\n 72|- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n*73|  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n 74|  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75|  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n*76|  overrides both to a `sptc-ci-` id and never emits a live id.\n*77|- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n 78|  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n 79|  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n 80|  needed — it asserts the env the harness would hand a spawn). Identity is the documented\n...\n 90|\n*91|- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner\n*92|  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup\n 93|  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that\n 94|  requires permission approval, the interactive permission gate has **no operator / no stdin** to\n 95|  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio\n...\n 97|- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate\n*98|  with `--dangerously-skip-permissions`. Concretely: each `claude-spt psyche` turn (seed **and**\n*99|  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`\n 100|  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox\n 101|  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket\n 102|  trust (see `docs/adr/0003-*`).\n*103|- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` `sandbox_flags()` appends the flag (and\n 104|  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test\n 105|  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n*106|  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n*107|  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n 108|  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n 109|  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n 110|  is a *committed-never-to-reintroduce* invariant rather than an observed incident here.\n...\n 139|  reason; a silent skip is forbidden.\n*140|- **Mapping / notes:** `tools/claude-spt/src/hook.rs` — `sid_state_rel` + the boundary branch +\n 141|  the post-registration persistence; `HookEnv::{spt_strict,log,read_adapter_state,write_adapter_state}`\n 142|  seams (the plain `spt` seam discards stderr and exit — the refusal reason NEEDS the strict seam).\n 143|  Recovery for an already-wedged perch: relaunch the endpoint WITHOUT `/clear` (resume re-binds the\n...\n 184|  it ever bites. Accepted for v1.\n*185|- **Mapping / notes:** `tools/claude-spt/src/translate.rs` — `commands_for_clear` (ARM) +\n 186|  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n 187|  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n*188|  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n 189|  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n 190|  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n 191|- **cite:** `CHECKPOINT-CLEAR-RACE-PLAN.md`; supersedes the single-sequence macro described in\n...\n 225|  side, but the adapter must not hand the child a foreign identity in the first place.\n*226|- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n 227|  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n 228|  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229|  child that runs harness hooks, it needs the same scrub.\n...\n 278|  core wave. Kept for history per KH convention; the successor invariant is the shim's pure planner.\n*279|- **Mapping / notes:** `tools/claude-spt/src/psyche.rs` — the ephemeral shim's stateless `plan()`\n 280|  (successor evidence: `plan_is_stateless_one_turn_no_residency` — plans exactly one turn, nothing to\n 281|  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n 282|  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n## OMP-BRIDGE-FIELD-ISSUES.md\n 5|\"only showed `READY:todlando` and nothing else\", could not be stopped cleanly,\n*6|and could not be switched back to `claude-spt`. This documents the root causes,\n 7|grounded in this repo's code, so the bridge can be hardened.\n 8|\n*9|All line references are against `tools/claude-spt/src/bridge.rs` and\n 10|`adapter/omp-spt.toml` at commit `f0109d1`.\n 11|\n 12|## Incident summary\n...\n 14|1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15|   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16|2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17|   way to tell working vs. wedged.\n 18|3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19|   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20|   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21|   endpoint looked alive but was dead.\n*22|4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23|5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24|\n 25|Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n...\n 30|\n*31|## HIGH-1 — Bridge binary basename collides with `claude-spt`\n 32|\n*33|**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n*34|indistinguishable from a genuine claude-spt process. During the incident this\n*35|made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n 36|todlando's perch was actually the omp bridge. It directly caused the\n 37|misdiagnosis.\n 38|\n...\n 40|```\n*41|command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42|```\n*43|The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44|This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n 45|bridge (S2) is `omp-spt`\"*).\n 46|\n...\n 58|object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n*59|`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n 60|`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n 61|When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62|those lines never run. On Windows, killing a process does not kill its children,\n...\n 149|**Symptom.** After the broken omp session, the endpoint could not be resumed or\n*150|restarted under claude-spt — every attempt relaunched omp-spt. Recovery required\n 151|editing spt-core's session ledger by hand.\n 152|\n 153|**Root (interop).** The bridge binds via `spt api --adapter omp-spt bind`\n\n## adr/\n### 0001-distribution-splits-by-volatility.md\n 12|\n*13|`spt-claude-code` is the `claude-spt` adapter — simultaneously spt-core's v1 acceptance proof,\n*14|its first casual-end-user entrypoint (a Claude Code plugin on `SaberMage/cplugs`), and an\n 15|invisible spt-core installer. It must ship as a CC plugin, yet almost all real logic now lives\n*16|inside the `spt` binary and the CC adapter manifest, both **conducted by spt-core**, not by the\n 17|plugin author.\n 18|\n 19|Forces:\n...\n 24|   marketplace or letting logic rot.\n*25|2. **Claude Code has no plugin-file integrity check.** In-place edits to an installed plugin do\n 26|   not orphan it; only *old version directories* orphan on a version bump (GC'd after 7 days).\n 27|   So low-churn structure is safe to ship via the marketplace, and high-churn logic does *not*\n 28|   need to ride the marketplace at all.\n...\n 43|| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n*44|| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n 45|\n 46|Concretely:\n 47|\n...\n 77|- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n*78|  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n 79|  fired, skill ran, on CC 2.1.177). The injection branch is built (`user-prompt-submit.sh`\n*80|  `sptc_skill_key` + `sptc_inject_skill`); the in-SKILL.md fallback is unneeded for injection (but\n 81|  `setup` stays self-contained — it runs when spt may be absent). (`REQ-UPS-INJECTION`.)\n 82|- **File-backed `[strings]` is an M12 spt-core dependency** — until M12 publishes, instruction\n 83|  bodies cannot be externalized; skeleton SKILL.md files may carry interim inline instructions.\n...\n 96|  version-of-truth is the spt-core-tracked manifest/binary version (`spt adapter list` /\n*97|  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n*98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n 99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n...\n 103|<!-- [doc->REQ-DIST-SHORTCUT-BASENAME] -->\n*104|- The manifest brands the `spt endpoint run` launcher shortcut **`cc-<id>`** via\n 105|  `shortcut_basename = \"cc\"` (the M12 `cc` launcher; the harness-agnostic default would be\n*106|  `spt-<id>`, and it matches the schema's own `spt-claude-code → cc` example). This launcher brand\n*107|  is **decoupled** from the `sptc` plugin name — the `s/sptc/spt/` succession (next bullet) renames\n 108|  the plugin, not this shortcut. (Operator-ruled 2026-06-15.)\n 109|- Forces the cplugs **plugin-name** question, now **ruled (operator, 2026-06-14)**: because CC\n 110|  invokes skills as `/<plugin>:<skill>` (the skill prefix is hard-tied to the `plugin.json` name —\n 111|  no override; confirmed against the official plugin docs), the marketplace plugin is named\n*112|  **`sptc`** and skills surface as **`/sptc:*`** for now. **Succession plan:** once `spt-claude-code`\n*113|  proves parity, flip `plugin.json` name `sptc`→`spt` (skills become `/spt:*`) and retire/rename\n 114|  legacy owl's cplugs `spt` plugin in the same coordinated move (two plugins cannot share the\n*115|  `spt` name). `sptc` is the single-substitution seam — the flip is `s/sptc/spt/`.\n 116|- Mints no new REQs beyond those already seeded for the skeleton milestone; this ADR is the\n 117|  `doc`-stage evidence for `REQ-DIST-PLUGIN-SKELETON`, `REQ-DIST-HOOKS-API`,\n 118|  `REQ-DIST-BOOTSTRAP-INSTALL`, and `REQ-UPS-INJECTION`.\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 2|<!-- [doc->REQ-UPS-INJECTION] -->\n*3|# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`\n 4|\n 5|## Status\n 6|\n...\n 12|primitives + their I/O format; the **adapter** authors the harness-specific wiring and output\n*13|formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON\n 14|payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the\n*15|public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`\n*16|(the adapter_name — distinct from the plugin name `sptc`).\n 17|\n 18|Two hard constraints shape the wiring:\n 19|\n...\n 21|   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,\n*22|   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).\n 23|2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating\n 24|   L52/L181. The plugin hand-writes it.\n 25|\n...\n 27|\n*28|**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n*29|`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n 30|(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n 31|substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n 32|\n...\n 34|\n*35|| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |\n 36||---|---|---|\n 37|| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |\n 38|| SessionStart (`clear`/`compact`) | `boundary <clear\\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |\n...\n 45|\n*46|`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,\n 47|**not** from a hook.\n 48|\n 49|### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook\n...\n 51|The `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers\n*52|queued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter\n 53|`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is\n 54|ours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.\n 55|\n...\n 75|\n*76|Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\n*77|the `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\n 78|unescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\n 79|gate, Psyche routing), never silently stripped. Covered by\n 80|`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).\n...\n 99|- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`\n*100|  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)\n 101|- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id=\"$(spt whoami)\"`; if empty\n 102|  (session never readied → no perch), they no-op. Auth: pass `--session-id \"$OWL_SESSION_ID\"`\n 103|  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so\n...\n 115|  received `prompt:\"/send hi\"` (literal) **and** fired **and** the skill ran. Answers the\n*116|  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,\n*117|  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)\n 118|- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n 119|  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n 120|  packaging on Windows (no per-OS branch needed for the hook to run).\n...\n 136|   chars to a file (evicting it from the inline context the agent sees), silently losing messages.\n*137|   The UPS wrapper now pre-empts: `sptc_cap_output` (`_common.sh`) buffers the combined skill-body +\n 138|   drain, and over `SPTC_CTX_CAP` (9000, margin under 10k) spills the FULL text to an agent-readable\n*139|   file (`~/.claude/sptc-drain-<sid>.txt`) and emits only a concise `<sptc_overflow>` pointer — never\n*140|   a head-cut that would split a `<sptc_messages>`/`<EVENT>` block and drop a message. Under the cap\n 141|   it is verbatim passthrough (no behaviour change for normal drains). Unit: `tests/hooks-parse.sh`\n*142|   `sptc_cap_output` cases (passthrough, no-spill, overflow marker, full-body spill, no-inline-leak).\n 143|\n 144|## Consequences\n 145|\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n 11|A live session's *Psyche* (see `CONTEXT.md`) is a second `claude` process the spt-core daemon\n*12|hosts alongside the parent agent. The daemon launches our `claude-spt psyche` runner (the psyche\n*13|subcommand of the consolidated `claude-spt` binary, ADR-0006/U2) **detached, with `Stdio::null`**;\n 14|the runner seeds one headless `claude` turn from the daemon-supplied prompt,\n 15|then drives one `claude --continue` turn per pulse. The Psyche's only job is to author a *commune*\n 16|on each pulse.\n...\n 37|Every Psyche `claude` turn — the seed **and** each pulse — runs inside a fixed sandbox, mirroring\n*38|legacy owl. `claude-spt psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and\n 39|`pulse_cmd`:\n 40|\n 41|- `--tools Read,Edit,Write` — file IO only; **no** Bash, network, messaging, etc.\n...\n 48|The same `--dangerously-skip-permissions` rationale (force 1) applies to the non-interactive\n*49|`[session.self]` bringup commands (base `claude` and the `ccs` profile), which therefore also carry\n 50|the flag. The cross-cutting \"non-interactive spawn must bypass the permission gate\" invariant is\n 51|recorded as a hazard (`REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`, `docs/KNOWN-HAZARDS.md` §2.2).\n 52|\n...\n 64|  auto-approve, and never drop the cap while keeping skip-permissions.\n*65|- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66|  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67|  sibling, and surfaces loudly (no commune produced).\n 68|- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n### 0004-checkpoint-via-self-send-loopback.md\n 38|\n*39|1. The agent runs `/sptc:commune --checkpoint` and authors its commune **inline, during its own\n 40|   turn**, embedding the literal trigger `!!checkpoint!!` (one = default wake; a pair brackets a\n 41|   custom wake directive). The agent *is* the authoring LLM, so the context is captured **before**\n 42|   the clear.\n...\n 69|- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n*70|  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n 71|  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n 72|- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n 73|  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n...\n 76|  *mechanics* (clear+wake) have **zero** spt-core dependency; only the re-seed *richness* waits on W5.\n*77|- **spt-hosted-only, by design:** a user-launched (seed-path) CC session has no broker PTY / no\n 78|  translation binary, so it cannot checkpoint. Accepted — checkpoint leverages spt-core's hosted\n 79|  lifecycle on purpose.\n 80|- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n### 0005-name-unification.md\n 1|<!-- [doc->REQ-DIST-NAME-UNIFY] -->\n*2|# Name unification: `claude-spt` (spt-core side) + `spt` (Claude Code side)\n 3|\n 4|## Status\n 5|\n*6|accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n*7|`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n 8|\n 9|## Context\n 10|\n...\n 14||---|---|---|\n*15|| GitHub repo | `spt-claude-code` | `spt adapter add --release SaberMage/spt-claude-code`, `[update].repo`, install dir derivation |\n*16|| spt-core adapter | `claude-spt` | every `api --adapter claude-spt`, `[adapter].name`, `spt adapter update claude-spt` |\n*17|| cplugs plugin | `sptc` | `/sptc:*` skill prefix, `claude plugin … sptc@cplugs`, marketplace path |\n*18|| launcher shortcut | `cc` | `cc-<id>` (`spt endpoint run` brand) |\n 19|\n*20|Three names for one adapter is friction — a user reads `--release SaberMage/spt-claude-code` to install but `spt adapter update claude-spt` to update, and `/sptc:*` to invoke. The hard constraint that prevents collapsing to **one** token: the adapter **cannot** be named `spt` (that is spt-core's own CLI/identity — `spt adapter update spt` reads as \"spt updates spt\"), yet the user-facing skill prefix **wants** `/spt:*` (the legacy-parity goal, already planned as the `sptc`→`spt` succession). The ski...\n 21|\n 22|## Decision\n 23|\n...\n 25|\n*26|- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27|- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n*28|- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29|\n*30|End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n 31|\n 32|## Considered and rejected\n 33|\n*34|- **One token everywhere = `claude-spt`** (incl. `/claude-spt:*` skills). Maximally consistent and dependency-free, but makes the most-typed surface (the skill prefix) long. Rejected — the skill prefix should head to `/spt:*`.\n*35|- **One token everywhere = `sptc`** (`/sptc:*` unchanged). Shortest and dependency-free, but `sptc` is cryptic to a newcomer and abandons the cleaner `/spt:*` succession the project already committed to. Rejected.\n 36|\n 37|## Consequences\n 38|\n*39|- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n 40|- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41|- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n 6|\n*7|**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n 8|dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n 9|**resolve-not-execute** (spt-core stays a pure resolver): two v0.16.0 primitives — `{adapter_dir}` /\n 10|`{adapter_name}` substitution keys + **lazy `[strings]` substitution at `get-string` read time** —\n 11|let the plugin resolve the binary itself. The hook LOGIC (all eight wrappers + `_common.sh`) now lives\n*12|in `claude-spt hook <event>` and rides `spt adapter update`; the plugin ships only a STATIC-FOREVER\n*13|`hooks.json` + a thin `dispatch.sh` that `get-string`s `[strings].hook_cmd = \"{adapter_dir}/claude-spt\n*14|hook\"` once per session and execs the binary. This is the realized form of the \"Later: `claude-spt\n 15|hook <event>` if generic hook dispatch (ask #1) lands\" line in Binary consolidation below.\n 16|<!-- [doc->REQ-DIST-HOOK-BINARY] -->\n 17|See `UPDATE-NAMING-DOYLE-ASKS.md` Ask 1 (the full resolve-not-execute resolution) and `V09-PLAN.md`.\n...\n 34|- **`avenue = \"gh_release\"`** pulls the adapter `.spt` (manifest + binaries + strings — all the high-churn). Ships against the published surface today.\n*35|- **`[update].message`** carries the residual manual step. This is a real, docs-confirmed field: a markdown-rendered notice `spt adapter update` prints to stdout **only when a new version is actually applied** (never on a no-op), no `{key}` substitution. It tells the user to run **`/reload-plugins`** — which is a **Claude Code TUI action and cannot be automated** — and mentions the `spt` CLI (alias for `spt endpoint run`) as the more powerful way to create an endpoint, alongside `/spt:live`.\n 36|- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37|- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38|\n 39|### Binary consolidation\n 40|\n*41|Collapse the adapter's executables into **one `claude-spt <subcommand>`** binary (name per ADR-0005), **partially now, fully later**:\n 42|\n*43|- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44|- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45|\n 46|The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47|\n\n# tests/\n## acceptance-harness.sh\n 10|SPT_AGENT_ID=perri OWL_SESSION_ID=perri\n*11|sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n*12|[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n*13|[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n*14|[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n 15|[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n 16|\n 17|# is_disposable: accepts a minted id, rejects any live name.\n*18|sptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\n*19|sptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\n*20|sptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n 21|\n 22|# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.\n 23|work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n...\n 25|proj=\"$work/p\"; digest=\"$work/d.txt\"\n*26|sptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null","truncated":true,"truncatedBy":"bytes","totalLines":800,"totalBytes":58593,"outputLines":671,"outputBytes":51178,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":800,"totalBytes":58593,"outputLines":671,"outputBytes":51178,"shownRange":{"start":1,"end":671},"nextOffset":672}}},"isError":false,"timestamp":1784087167302}}
{"type":"message","id":"802643ab","parentId":"1bc7fb13","timestamp":"2026-07-15T03:46:07.313Z","message":{"role":"toolResult","toolCallId":"call_JJ2XPajVa5RaA7B9TOkr1bAv|fc_0ca4af36969c80fa016a57027f2a008198be40cbf106ea59f6","toolName":"grep","content":[{"type":"text","text":"# adapter/\n## omp-spt.toml\n*1|# omp-spt — oh-my-pi (omp) adapter manifest for spt-core.  [fork of SaberMage/claude-spt]\n 2|#\n 3|# Forked from claude-spt (the Claude Code adapter) and retargeted to oh-my-pi\n 4|# (https://github.com/can1357/oh-my-pi). Unlike Claude Code, omp exposes a native\n 5|# host-drives-agent control surface (`omp --mode rpc`), so the omp integration is an RPC\n*6|# bridge, NOT a hook-scraping plugin — see OMP-ADAPTER-PLAN.md (architecture decision: Style A).\n 7|#\n 8|# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile\n 9|# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin plugin skeleton.\n...\n 11|# marketplace `plugin.json` version. The per-version changelog below is INHERITED claude-spt\n*12|# lineage (retained for seam provenance); omp-spt's own history starts at version 0.1.0.\n 13|#\n 14|# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/\n 15|# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,\n...\n 36|[adapter]\n*37|name = \"omp-spt\"\n 38|kind = \"harness\"\n 39|# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n 40|# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves\n...\n 230|# checkpoints). Fix: the rename delivery now only STASHES (bare {commit}); the fire emits the ONE\n*231|# combined sequence — rename half . 150ms enter→ctrl+s bridge . wake half . one commit\n 232|# (commands_for_boundary/BOUNDARY_BRIDGE_MS). No inter-sequence boundary exists to race.\n 233|# Binary-only; floor unchanged. Also cut as v0.14.3 on the 0.25.0-floor line (same patch off the\n 234|# v0.14.2 tag) so pre-0.27.0 boxes can take the fix immediately.\n...\n 377|# to doyle (undocumented in the harness-contract/CLI reference).\n*378|# omp-spt starts fresh at 0.1.0 — the version history above is inherited claude-spt lineage.\n*379|# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n*380|# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n*381|# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n 382|# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n 383|# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n 384|# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n...\n 386|# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n*387|# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n*388|# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n 389|# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n*390|# uses attach-default `spt endpoint run --adapter omp-spt --id <id> --create`, not `--start`, so the\n 391|# operator sees the broker-held harness PTY and its bringup log instead of being instructed into a\n 392|# silent start-only launch. The gh_release update avenue now pins transport=\"gh\", forcing the\n 393|# authenticated GitHub CLI path for release discovery/asset fetch so private or visibility-flipped\n...\n 396|# packaged OMP extension supplies SPT bind/listen/state/reply integration. The prior attach guidance\n*397|# exposed only the headless RPC bridge's logs because `omp --mode rpc` creates no TUI. Floor unchanged.\n 398|version = \"0.1.3\"\n 399|# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS\n 400|# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that\n...\n 454|# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`\n*455|# land. omp's host process basename is `omp`; the RPC bridge (S2) is `omp-spt`. [impl->REQ-SKILL-LIVE]\n*456|host_binaries = [\"omp\", \"omp-spt\"]\n 457|# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is\n 458|# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher\n 459|# brand is DECOUPLED from the plugin name (`omps`, skills `/omps:*`): the s/omps/spt/ succession seam\n...\n 472|# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no\n*473|# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue\n 474|# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI\n 475|# endpoint route alongside /omps:live. (When doyle ask #2 lands, the post-update step can also trigger\n 476|# this print on a plugin-only change; copy stays valid.)\n...\n 479|avenue = \"gh_release\"\n*480|repo = \"BigscreenVR/omp-spt\"\n 481|transport = \"gh\"\n 482|message = \"\"\"\n*483|**omp-spt updated.** The native OMP extension + extractors refreshed in place — no reload step: OMP\n 484|loads the packaged extension fresh on each endpoint bringup.\n 485|\n 486|- Running endpoints keep the OLD hosting path until restarted: `spt endpoint stop <id>` then\n*487|  `spt endpoint run --adapter omp-spt --id <id>` picks up the new one.\n*488|- Bring up a fresh Librarian endpoint: `spt endpoint run --adapter omp-spt --id <id> --create`.\n 489|\"\"\"\n 490|\n 491|# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────\n\n## strings/\n### omp-spt.mjs\n 2|\n*3|const ADAPTER = \"omp-spt\";\n 4|\n 5|export function decodeBody(body) {\n 6|\treturn body\n...\n 59|\treturn new Promise((resolve, reject) => {\n*60|\t\tconst child = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n 61|\t\t\tstdio: [input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n 62|\t\t\twindowsHide: true,\n 63|\t\t});\n...\n 77|\n*78|// [impl->REQ-OMP-NATIVE-TUI]\n 79|export default function ompSpt(pi) {\n 80|\tconst id = process.env.SPT_ENDPOINT_ID?.trim();\n 81|\tif (!id) return;\n...\n 107|\t\ttry {\n*108|\t\t\tpi.sendUserMessage(current.body);\n 109|\t\t} catch (error) {\n*110|\t\t\tlogError(\"omp-spt could not submit an inbound message\", error);\n 111|\t\t\tcurrent = undefined;\n 112|\t\t\tsetTimeout(dispatchNext, 0);\n 113|\t\t}\n...\n 117|\t\tconst args = [\"ready\", id];\n*118|\t\tif (process.env.OMP_SPT_SUBNET) args.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n*119|\t\tlistener = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n 120|\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n 121|\t\t\twindowsHide: true,\n 122|\t\t});\n...\n 131|\t\t});\n*132|\t\tlistener.stderr.on(\"data\", (chunk) => pi.logger.debug(\"omp-spt listener\", { output: chunk.trim() }));\n*133|\t\tlistener.on(\"error\", (error) => logError(\"omp-spt listener failed\", error));\n 134|\t\tlistener.on(\"close\", (code) => {\n 135|\t\t\tif (!stopping) {\n*136|\t\t\t\tui?.setStatus(\"omp-spt\", \"spt offline\");\n*137|\t\t\t\tui?.notify(`omp-spt listener exited (${code})`, \"error\");\n 138|\t\t\t}\n 139|\t\t});\n 140|\t}\n 141|\n*142|\tpi.on(\"session_start\", async (_event, ctx) => {\n 143|\t\tui = ctx.ui;\n 144|\t\tsid = ctx.sessionManager.getSessionId();\n 145|\t\ttry {\n 146|\t\t\tconst bindArgs = [\"api\", \"--adapter\", ADAPTER, \"bind\", id, \"--set-session-id\", sid];\n*147|\t\t\tif (process.env.OMP_SPT_SUBNET) bindArgs.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n 148|\t\t\tconst bound = await runSpt(bindArgs);\n 149|\t\t\ttoken = bound.match(/\\btoken=([^\\s]+)/)?.[1];\n 150|\t\t\tawait setState(\"idle\");\n 151|\t\t\tstartListener();\n*152|\t\t\tctx.ui.setStatus(\"omp-spt\", `spt:${id}`);\n 153|\t\t} catch (error) {\n*154|\t\t\tctx.ui.setStatus(\"omp-spt\", \"spt bind failed\");\n*155|\t\t\tlogError(`omp-spt could not bind ${id}`, error);\n 156|\t\t}\n 157|\t});\n 158|\n...\n 163|\t\t} catch (error) {\n*164|\t\t\tlogError(\"omp-spt could not mark the endpoint busy\", error);\n 165|\t\t}\n 166|\t});\n 167|\n...\n 172|\t\tif (completed?.from) {\n*173|\t\t\tconst reply = extractReply(event.messages) || \"[omp-spt] turn ended without an assistant response.\";\n 174|\t\t\ttry {\n 175|\t\t\t\tawait runSpt([\"send\", completed.from, \"--from\", id], reply);\n 176|\t\t\t} catch (error) {\n*177|\t\t\t\tlogError(`omp-spt could not reply to ${completed.from}`, error);\n 178|\t\t\t}\n 179|\t\t}\n 180|\t\ttry {\n...\n 182|\t\t} catch (error) {\n*183|\t\t\tlogError(\"omp-spt could not mark the endpoint idle\", error);\n 184|\t\t}\n 185|\t\tsetTimeout(dispatchNext, 0);\n 186|\t});\n\n# tools/claude-spt/\n## Cargo.toml\n 21|[[bin]]\n*22|# HIGH-1 (docs/OMP-BRIDGE-FIELD-ISSUES.md): the SHIPPED BINARY is `omp-spt` — a distinct basename\n*23|# from upstream claude-spt, so the bridge process is self-identifying in a task list (the field\n*24|# incident's misdiagnosis was a bridge indistinguishable from a real claude-spt). The crate/package\n 25|# keeps the claude-spt name (dir layout + CI scripts unchanged).\n*26|name = \"omp-spt\"\n 27|path = \"src/main.rs\"\n 28|\n 29|[dependencies]\n\n## examples/\n### fake-omp.rs\n*1|//! fake-omp — a deterministic, model-free stand-in for `omp --mode rpc`, for the omp-spt bridge\n*2|//! acceptance test (tests/bridge-acceptance.sh). A compiled Cargo EXAMPLE (never shipped in the\n*3|//! release adapter.spt), so it is a real cross-platform executable the bridge can spawn via\n 4|//! `Command::new` — unlike a shell script, which Windows cannot exec directly.\n 5|//!\n*6|//! It ignores all argv (the bridge appends `--mode rpc [--session-dir …]`) and speaks just enough of\n*7|//! the verified RPC protocol (OMP-ADAPTER-PLAN.md, confirmed against omp v16.3.11) for the bridge to\n 8|//! complete a full ready → get_state → prompt → agent_end cycle with zero model billing:\n 9|//!\n 10|//!   • on start                : `{\"type\":\"ready\"}`\n...\n 19|//! FAKE_OMP_MUTE=1 (ack + agent_start but NEVER send agent_end — the hung-turn shape that\n*20|//! exercises the bridge's per-turn deadline, HIGH-4). stdout is flushed after every frame so the\n*21|//! bridge's line reader never stalls behind a full pipe buffer.\n 22|\n 23|use std::io::{BufRead, Write};\n 24|\n\n## src/\n### bridge.rs\n*1|//! The omp-spt RPC bridge (S2 ready-agent; hardened at v0.1.1 per docs/OMP-BRIDGE-FIELD-ISSUES.md).\n 2|//!\n 3|//! This is the hosted harness process for the oh-my-pi (omp) adapter: spt-core's broker spawns it as\n 4|//! `[session.self].command`, and it drives a resident `omp --mode rpc` child, translating in both\n*5|//! directions (OMP-ADAPTER-PLAN.md, \"Architecture decision: RPC-bridge (Style A)\"):\n 6|//!\n 7|//! ```text\n*8|//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\n 9|//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\n 10|//!                          └  prompt / get_state                                    (to omp)\n 11|//! ```\n 12|//!\n*13|//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\n*14|//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\n 15|//! compat alias for the same entry.\n 16|//!\n 17|//! ## Protocol (verified against omp v16.3.11 — see OMP-ADAPTER-PLAN.md §\"decisive finding\")\n...\n 23|//! Drive turn: host → `{\"type\":\"prompt\",\"message\":\"…\"}`; omp acks `{\"type\":\"response\",\n*24|//!            \"command\":\"prompt\",\"success\":true}`, then streams `turn_start` → `agent_start` →\n*25|//!            `message_update`… → `turn_end` → `{\"type\":\"agent_end\",\"messages\":[…]}`.\n 26|//!\n 27|//! ## Hardening (the doyle 2026-07-09 field incident — docs/OMP-BRIDGE-FIELD-ISSUES.md)\n 28|//!\n*29|//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\n*30|//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\n*31|//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\n*32|//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\n 33|//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\n 34|//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\n 35|//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\n*36|//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\n*37|//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\n 38|//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\n 39|//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\n 40|//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\n...\n 54|/// namespace `omps`; mirrors hook.rs's `ADAPTER` for the claude-spt path.\n*55|const ADAPTER: &str = \"omp-spt\";\n 56|\n*57|/// Bringup deadline for omp's `ready` frame (override: OMP_SPT_READY_TIMEOUT_S). Generous — a cold\n 58|/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\n 59|/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\n 60|const DEFAULT_READY_TIMEOUT_S: u64 = 120;\n*61|/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\n 62|const DEFAULT_STATE_TIMEOUT_S: u64 = 30;\n*63|/// Per-turn deadline: prompt dispatch → `agent_end` (override: OMP_SPT_TURN_TIMEOUT_S). Long — a\n 64|/// real agent turn can run tools for minutes — but bounded, so a hung turn frees the perch.\n 65|const DEFAULT_TURN_TIMEOUT_S: u64 = 600;\n*66|/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\n 67|const DEFAULT_HEARTBEAT_S: u64 = 300;\n 68|\n 69|/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\n...\n 72|    ($($a:tt)*) => {\n*73|        eprintln!(\"[omp-spt] {}\", format!($($a)*))\n 74|    };\n 75|}\n 76|\n*77|/// Frame-level trace, still gated behind `OMP_SPT_DEBUG` (diagnostics; off by default).\n 78|macro_rules! trace {\n 79|    ($($a:tt)*) => {\n### digest_omp.rs\n*1|//! `claude-spt digest-omp` — the omp-spt `[digest]` extractor (S3).\n 2|//!\n 3|//! Maps oh-my-pi's native session JSONL → the SAME published digest-record contract the CC extractor\n 4|//! (digest.rs) emits: NDJSON, one `{\"role\",\"text\"?,\"tool\"?,\"ts\"?}` object per stdout line, `role` ∈\n...\n 32|//!   dir-encoding rule need not be reproduced — locate-by-id, as CC's digest.rs does).\n*33|//!   Proof: `spt adapter digest-proof omp-spt --sample <file>` passes the file via `--in` (read\n 34|//!   as-is, never located). stdout is UTF-8 (the Windows cp1252 trap from the tips doc).\n 35|\n 36|use crate::digest::{rec, scalar_str};\n### echo_commune_omp.rs\n*1|//! `claude-spt echo-commune-omp` — the omp-spt `[session.echo_commune]` bounded end-of-session\n 2|//! summarizer (S4). Same contract as the CC echo_commune.rs, omp-flavored:\n 3|//!\n 4|//!   1. INPUT — history on stdin when the (future) published wiring feeds it; otherwise SELF-LOCATE\n...\n 131|    let prompt = compose_prompt(&id, bounded_tail(&history, TAIL_CAP_BYTES));\n*132|    let prompt_file = std::env::temp_dir().join(format!(\"omp-spt-echo-{}-{}.md\", id, std::process::id()));\n 133|    if let Err(e) = std::fs::write(&prompt_file, &prompt) {\n 134|        eprintln!(\"{}\", fail_line(&id, &format!(\"cannot write prompt scratch {}: {e}\", prompt_file.display())));\n 135|        return ExitCode::FAILURE;\n 136|    }\n*137|    let mut cmd = Command::new(crate::bridge::omp_bin());\n 138|    cmd.args(turn_cmd());\n 139|    cmd.arg(format!(\"@{}\", prompt_file.display()));\n 140|    cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());\n### history_omp.rs\n*1|//! `claude-spt history-omp` — the omp-spt `[history]` fetcher (S4).\n 2|//!\n 3|//! Contract (same as the CC `history`): locate ONE omp session's JSONL and stream the file to\n 4|//! stdout VERBATIM — opaque bytes, single-session; the only consumer is our own echo-commune\n### hook.rs\n 523|/// the binary cannot exist before spt-core + the adapter are installed. [impl->REQ-DIST-HOOKS-API]\n*524|fn handle_session_start(env: &mut dyn HookEnv, v: &Value) {\n 525|    let sid = field(v, \"session_id\");\n 526|    let src = field(v, \"source\");\n 527|    let endpoint_id = env.env(\"SPT_ENDPOINT_ID\").filter(|s| !s.is_empty());\n...\n 958|/// sid = the payload's `session_id` ELSE the bind-time carrier `state/session/<id>.sid` (written by\n*959|/// `handle_session_start`). This is the v0.16.1 hardening: `PostToolUseFailure` (and possibly\n 960|/// `StopFailure`) payloads may OMIT `session_id`, and a `state idle` with an empty `--session-id` is\n 961|/// AUTH-refused by core — the silent miss that left `lia` deaf after a real-turn interrupt while its\n 962|/// input box sat at \"Interrupted · What should Claude do instead?\" (field 2026-07-08). A live agent\n...\n 1165|    match event {\n*1166|        \"SessionStart\" => handle_session_start(env, v),\n 1167|        \"UserPromptSubmit\" => handle_user_prompt_submit(env, v),\n 1168|        \"PreToolUse\" => handle_pre_tool_use(env, v),\n 1169|        \"Stop\" => handle_stop(env, v),\n...\n 2070|    #[test]\n*2071|    fn session_start_seed_path_uses_host_pid_and_writes_env() {\n 2072|        // No SPT_ENDPOINT_ID, source startup, no peers → seed + env-file writes, no brief.\n 2073|        let mut env = Recorder::new(|args| match args {\n 2074|            [\"subnet\", \"status\"] => Some(\"no subnets\".into()),\n...\n 2076|        });\n*2077|        handle_session_start(&mut env, &json!({\"session_id\":\"s9\",\"source\":\"startup\"}));\n 2078|        assert!(env.call_lines().iter().any(|l| l == \"api seed --pid 4242 --session-id s9\"), \"seed by host pid\");\n 2079|        assert!(env.env_lines.iter().any(|l| l == \"OWL_SESSION_ID=s9\"));\n 2080|        assert!(env.env_lines.iter().any(|l| l == \"SPT_ADAPTER=claude-spt\"));\n...\n 2084|    #[test]\n*2085|    fn session_start_bind_path_binds_and_briefs() {\n 2086|        let mut env = Recorder::new(|args| match args {\n 2087|            [\"adapter\", \"get-string\", _, k] => Some(format!(\"[{k}]\")), // non-empty brief parts\n 2088|            a if a.contains(&\"psyche-download\") => Some(String::new()), // NO-CONTEXT\n...\n 2091|        .with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2092|        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2093|        assert!(env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s1\"), \"bind\");\n 2094|        let out = env.out();\n 2095|        assert!(out.contains(\"\\\"hookEventName\\\":\\\"SessionStart\\\"\"), \"emits SessionStart additionalContext\");\n...\n 2111|        .with_state(\"state/session/cc-1.sid\", \"s-old\");\n*2112|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"resume\"}));\n 2113|        assert!(\n 2114|            env.call_lines()\n 2115|                .iter()\n...\n 2127|        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2128|        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2129|        assert!(\n 2130|            env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s1\"),\n 2131|            \"fresh bind is proofless: {:?}\",\n...\n 2151|            .with_strict_err(\"AUTH_REFUSED:cc-1 (need --token or matching --session-id)\");\n*2152|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"resume\"}));\n 2153|        assert!(\n 2154|            env.logs.iter().any(|l| l.contains(\"re-pin FAILED\") && l.contains(\"AUTH_REFUSED\")),\n 2155|            \"the re-bind refusal must be loud with its reason: {:?}\",\n...\n 2166|            .with_state(\"state/session/cc-1.sid\", \"s-same\");\n*2167|        handle_session_start(&mut env, &json!({\"session_id\":\"s-same\",\"source\":\"resume\"}));\n 2168|        assert!(\n 2169|            env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s-same\"),\n 2170|            \"same-sid re-fire binds without proof: {:?}\",\n...\n 2182|    #[test]\n*2183|    fn session_start_subagent_gets_no_brief() {\n 2184|        let mut env = Recorder::new(|_| Some(\"x\".into())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2185|        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\",\"agent_type\":\"general-purpose\"}));\n 2186|        assert!(env.out().is_empty(), \"subagent sessions get no brief\");\n 2187|    }\n 2188|\n...\n 2209|            .with_state(\"state/session/ball-b.sid\", \"s-old\");\n*2210|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2211|        let lines = env.call_lines();\n 2212|        assert!(\n 2213|            lines.iter().any(|l| l == \"api --adapter claude-spt boundary clear ball-b --to-session-id s-new --session-id s-old\"),\n...\n 2224|        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2225|        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2226|        assert!(\n 2227|            env.state_writes.contains(&(\"state/session/cc-1.sid\".to_string(), \"s1\".to_string())),\n 2228|            \"bind persists the sid: {:?}\",\n...\n 2231|        // Rotation rolls the proof forward to the NEW sid.\n*2232|        handle_session_start(&mut env, &json!({\"session_id\":\"s2\",\"source\":\"clear\"}));\n 2233|        assert!(\n 2234|            env.state_writes.contains(&(\"state/session/cc-1.sid\".to_string(), \"s2\".to_string())),\n 2235|            \"boundary persists the new sid for the next rotation: {:?}\",\n...\n 2248|        });\n*2249|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2250|        assert!(!env.call_lines().iter().any(|l| l.contains(\"boundary\")), \"no unproven boundary call\");\n 2251|        assert!(\n 2252|            env.logs.iter().any(|l| l.contains(\"SKIPPED\") && l.contains(\"strand\")),\n...\n 2265|            .with_strict_err(\"AUTH_REFUSED:ball-b (need --token or matching --session-id)\");\n*2266|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2267|        assert!(\n 2268|            env.logs.iter().any(|l| l.contains(\"FAILED\") && l.contains(\"AUTH_REFUSED\")),\n 2269|            \"the refusal must be loud with its reason: {:?}\",\n...\n 2278|        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"ball-b\");\n*2279|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"compact\"}));\n 2280|        assert!(\n 2281|            env.call_lines()\n 2282|                .iter()\n...\n 2319|        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"ball-b\");\n*2320|        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2321|        let lines = env.call_lines();\n 2322|        assert!(\n 2323|            lines.iter().any(|l| l == \"api --adapter claude-spt state idle ball-b --session-id s-new\"),\n### main.rs\n 21|\n*22|mod bridge;\n 23|mod digest;\n 24|mod digest_omp;\n 25|mod echo_commune;\n...\n 62|    match sub {\n*63|        Some(\"bridge\") => Sub::Bridge,\n 64|        Some(\"digest\") => Sub::Digest,\n 65|        Some(\"digest-omp\") => Sub::DigestOmp,\n 66|        Some(\"echo-commune\") => Sub::EchoCommune,\n...\n 85|         subcommands:\\n\\\n*86|         \\x20 bridge       drive omp via `omp --mode rpc`, relaying spt api seed/poll/state (ready-agent)\\n\\\n 87|         \\x20 digest       map a Claude Code JSONL transcript to digest NDJSON ([digest] extractor)\\n\\\n 88|         \\x20 digest-omp   map an oh-my-pi (omp) session JSONL to digest NDJSON (omp [digest] extractor)\\n\\\n 89|         \\x20 history-omp  stream an omp session JSONL verbatim (omp [history] fetcher)\\n\\\n...\n 104|    match classify(sub.as_deref()) {\n*105|        Sub::Bridge => bridge::run(),\n 106|        Sub::Digest => digest::run(),\n 107|        Sub::DigestOmp => digest_omp::run(),\n 108|        Sub::EchoCommune => echo_commune::run(),\n...\n 152|    fn classifies_each_real_subcommand() {\n*153|        assert_eq!(classify(Some(\"bridge\")), Sub::Bridge);\n 154|        assert_eq!(classify(Some(\"digest\")), Sub::Digest);\n 155|        assert_eq!(classify(Some(\"digest-omp\")), Sub::DigestOmp);\n 156|        assert_eq!(classify(Some(\"history-omp\")), Sub::HistoryOmp);\n### psyche_omp.rs\n*1|//! `claude-spt psyche-omp` — the omp-spt `[session.psyche_resume]` EPHEMERAL shim (S4).\n 2|//!\n 3|//! The omp counterpart of psyche.rs, holding the SAME frozen psyche-turn contract (one spawned\n 4|//! role, two key shapes, event on stdin, stdout captured, exit 95 = the sole reseed discriminator)\n...\n 222|\n*223|    let mut cmd = Command::new(crate::bridge::omp_bin());\n 224|    cmd.args(turn_cmd(&sdir.to_string_lossy(), is_first));\n 225|    cmd.arg(format!(\"@{}\", prompt_file.display()));\n 226|    // cwd = the psyche's nested perch dir (mind-file parent) — commune file-drops land beside it.\n### translate.rs\n 185|/// Settle between the rename's submit and the wake's stash within the ONE combined post-clear\n*186|/// sequence: the enter→next-keystroke bridge the two-sequence design could not govern. Field-paid\n 187|/// 2026-07-06 (flynn): rename and wake rode two back-to-back inject sequences, and with CC's input\n 188|/// processing lagging the PTY stream at the just-rebuilt boundary, the rename's enter registered\n 189|/// as a soft newline, the next sequence's ctrl+s failed to stash the residue, and the wake text\n...\n 212|        if !v.is_empty() {\n*213|            // enter→ctrl+s bridge: let the rename's submit fully register before the wake half\n 214|            // stashes + types (the 2026-07-06 blend guard).\n 215|            v.push(json!({ \"delay_ms\": BOUNDARY_BRIDGE_MS }));\n 216|        }\n...\n 676|        // THE 2026-07-06 REGRESSION (flynn): rename + wake must ride ONE inject sequence — rename\n*677|        // half, enter->ctrl+s bridge, wake half, ONE commit. Two back-to-back sequences raced at the\n 678|        // post-clear boundary and the wake text submitted INSIDE the /rename argument (session\n 679|        // titled with the wake, no wake turn, agent dormant ~9.5h).\n 680|        let mut pending = Pending::default();\n...\n 694|                \"key:enter\".to_string(),  // rename submits...\n*695|                \"delay:150\".to_string(),  // ...then the enter->ctrl+s bridge (BOUNDARY_BRIDGE_MS)\n 696|                \"key:ctrl+s\".to_string(), // stash any residue (incl. a coalesced rename)\n 697|                \"delay:50\".to_string(),\n 698|                \"text:do the thing\".to_string(),\n\n# ci/\n\n## digest/\n### digest-proof-int.sh\n 27|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*28|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 29|SAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\n 30|RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n 31|\n\n## hooks/\n### poll-int.sh\n 57|#    [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]\n*58|HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*59|[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 60|if [ -x \"$HOOKBIN\" ]; then\n 61|  printf 'hello from probe<NL>second <line> & \"stuff\"' | sed 's/<NL>/\\\n 62|/' | spt send \"$BID\" --from probe-int >/dev/null 2>&1\n\n## idle-translate/\n### translate-proof-int.sh\n 22|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*23|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n*24|RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; manifest command = {adapter_dir}/omp-spt translate (D3)\n 25|EVENT='<EVENT type=\"msg\" from=\"ci\">translate-proof int probe</EVENT>'\n 26|\n 27|command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n\n## manifest/\n### check-manifest.sh\n 9|SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n*10|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 11|VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 12|\n 13|for f in \"$SCHEMA\" \"$MANIFEST\" \"$VALIDATOR\"; do\n### registration-int.sh\n 12|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*13|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 14|\n 15|if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 16|command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n...\n 72|#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\n*73|HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*74|[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 75|if [ -x \"$HOOKBIN\" ]; then\n 76|  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 77|  case \"$inj\" in\n\n## psyche/\n### live-relay-int.sh\n 31|A=claude-spt\n*32|MAN=\"$ROOT/adapter/omp-spt.toml\"\n 33|# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity\n 34|# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):\n 35|# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a\n\n## publish/\n### package-adapter.sh\n 2|# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by\n*3|# `spt adapter add --release BigscreenVR/omp-spt` and carried by the `[update] gh_release`\n 4|# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2).\n*5|# The release/install/update repo is `BigscreenVR/omp-spt` (renamed from spt-claude-code, U3/ADR-0005).\n 6|# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS] [impl->REQ-DIST-NAME-UNIFY]\n 7|#\n 8|#   adapter.spt (tar.gz)\n*9|#   ├── manifest.toml                 ← SHARED, at archive root (renamed from omp-spt.toml)\n 10|#   ├── strings/                      ← SHARED, at archive root\n 11|#   ├── x86_64-pc-windows-msvc/       ← this triple's binary, mirroring the flat-root tree\n 12|#   │   ├── claude-spt.exe\n...\n 33|ADAPTER=\"$ROOT/adapter\"\n*34|MANIFEST=\"$ADAPTER/omp-spt.toml\"   # renamed to manifest.toml INSIDE the archive (root-only rule)\n 35|STRINGS=\"$ADAPTER/strings\"\n 36|TOOLS=\"$ROOT/tools\"\n 37|OUT=\"${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}\"   # overridable so the unit test writes to a tmp file\n...\n 50|LINUX_RELSUB=\"${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}\"\n*51|BINS=\"omp-spt\"\n 52|\n 53|# Validate the manifest first — refuse to ship an invalid adapter.\n 54|echo \"== validate manifest ==\"\n...\n 94|  echo \"DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'\"\n*95|  echo \"to a GitHub release on the monorepo. End users: 'spt adapter add --release BigscreenVR/omp-spt'\"\n 96|  echo \"(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.\"\n 97|  exit 0\n 98|fi\n...\n 120|echo \"$listing\" | grep -qx \"manifest.toml\" || { echo \"FATAL: manifest.toml not at archive root\" >&2; fatal=1; }\n*121|echo \"$listing\" | grep -q \"^$WIN_TRIPLE/omp-spt.exe$\" || { echo \"FATAL: missing $WIN_TRIPLE/ binaries\" >&2; fatal=1; }\n*122|echo \"$listing\" | grep -q \"^$LINUX_TRIPLE/omp-spt$\"   || { echo \"FATAL: missing $LINUX_TRIPLE/ binaries\" >&2; fatal=1; }\n 123|# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).\n 124|badtop=$(echo \"$listing\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\n 125|[ -n \"$badtop\" ] && { echo \"FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat\" >&2; fatal=1; }\n...\n 130|echo \"WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release\"\n*131|echo \"on BigscreenVR/omp-spt; end users 'spt adapter add --release BigscreenVR/omp-spt' (spt\"\n 132|echo \"v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.\"\n 133|exit 0\n\n## setup/\n### activate-int.sh\n 1|#!/bin/sh\n*2|# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n 3|# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n 4|# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n 5|# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n...\n 16|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*17|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 18|\n 19|if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 20|command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n...\n 45|\n*46|# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\n 47|out=$(spt adapter add \"$MANIFEST\" 2>&1)\n 48|case \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n 49|\n\nShowing files 1-20 of 26+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"adapter, tools, plugin, ci, tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":130,"fileCount":20,"files":["adapter/omp-spt.toml","adapter/strings/omp-spt.mjs","tools/claude-spt/Cargo.toml","tools/claude-spt/examples/fake-omp.rs","tools/claude-spt/src/bridge.rs","tools/claude-spt/src/digest_omp.rs","tools/claude-spt/src/echo_commune_omp.rs","tools/claude-spt/src/history_omp.rs","tools/claude-spt/src/hook.rs","tools/claude-spt/src/main.rs","tools/claude-spt/src/psyche_omp.rs","tools/claude-spt/src/translate.rs","ci/digest/digest-proof-int.sh","ci/hooks/poll-int.sh","ci/idle-translate/translate-proof-int.sh","ci/manifest/check-manifest.sh","ci/manifest/registration-int.sh","ci/psyche/live-relay-int.sh","ci/publish/package-adapter.sh","ci/setup/activate-int.sh"],"fileMatches":[{"path":"adapter/omp-spt.toml","count":20},{"path":"adapter/strings/omp-spt.mjs","count":20},{"path":"tools/claude-spt/Cargo.toml","count":4},{"path":"tools/claude-spt/examples/fake-omp.rs","count":7},{"path":"tools/claude-spt/src/bridge.rs","count":20},{"path":"tools/claude-spt/src/digest_omp.rs","count":2},{"path":"tools/claude-spt/src/echo_commune_omp.rs","count":3},{"path":"tools/claude-spt/src/history_omp.rs","count":1},{"path":"tools/claude-spt/src/hook.rs","count":20},{"path":"tools/claude-spt/src/main.rs","count":5},{"path":"tools/claude-spt/src/psyche_omp.rs","count":2},{"path":"tools/claude-spt/src/translate.rs","count":4},{"path":"ci/digest/digest-proof-int.sh","count":1},{"path":"ci/hooks/poll-int.sh","count":2},{"path":"ci/idle-translate/translate-proof-int.sh","count":2},{"path":"ci/manifest/check-manifest.sh","count":1},{"path":"ci/manifest/registration-int.sh","count":3},{"path":"ci/psyche/live-relay-int.sh","count":1},{"path":"ci/publish/package-adapter.sh","count":9},{"path":"ci/setup/activate-int.sh","count":3}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# adapter/\n## omp-spt.toml\n  *1│# omp-spt — oh-my-pi (omp) adapter manifest for spt-core.  [fork of SaberMage/claude-spt]\n   2│#\n   3│# Forked from claude-spt (the Claude Code adapter) and retargeted to oh-my-pi\n   4│# (https://github.com/can1357/oh-my-pi). Unlike Claude Code, omp exposes a native\n   5│# host-drives-agent control surface (`omp --mode rpc`), so the omp integration is an RPC\n  *6│# bridge, NOT a hook-scraping plugin — see OMP-ADAPTER-PLAN.md (architecture decision: Style A).\n   7│#\n   8│# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile\n   9│# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin plugin skeleton.\n    │...\n  11│# marketplace `plugin.json` version. The per-version changelog below is INHERITED claude-spt\n *12│# lineage (retained for seam provenance); omp-spt's own history starts at version 0.1.0.\n  13│#\n  14│# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/\n  15│# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,\n    │...\n  36│[adapter]\n *37│name = \"omp-spt\"\n  38│kind = \"harness\"\n  39│# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n  40│# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves\n    │...\n 230│# checkpoints). Fix: the rename delivery now only STASHES (bare {commit}); the fire emits the ONE\n*231│# combined sequence — rename half . 150ms enter→ctrl+s bridge . wake half . one commit\n 232│# (commands_for_boundary/BOUNDARY_BRIDGE_MS). No inter-sequence boundary exists to race.\n 233│# Binary-only; floor unchanged. Also cut as v0.14.3 on the 0.25.0-floor line (same patch off the\n 234│# v0.14.2 tag) so pre-0.27.0 boxes can take the fix immediately.\n    │...\n 377│# to doyle (undocumented in the harness-contract/CLI reference).\n*378│# omp-spt starts fresh at 0.1.0 — the version history above is inherited claude-spt lineage.\n*379│# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n*380│# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n*381│# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n 382│# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n 383│# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n 384│# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n    │...\n 386│# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n*387│# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n*388│# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n 389│# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n*390│# uses attach-default `spt endpoint run --adapter omp-spt --id <id> --create`, not `--start`, so the\n 391│# operator sees the broker-held harness PTY and its bringup log instead of being instructed into a\n 392│# silent start-only launch. The gh_release update avenue now pins transport=\"gh\", forcing the\n 393│# authenticated GitHub CLI path for release discovery/asset fetch so private or visibility-flipped\n    │...\n 396│# packaged OMP extension supplies SPT bind/listen/state/reply integration. The prior attach guidance\n*397│# exposed only the headless RPC bridge's logs because `omp --mode rpc` creates no TUI. Floor unchanged.\n 398│version = \"0.1.3\"\n 399│# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS\n 400│# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that\n    │...\n 454│# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`\n*455│# land. omp's host process basename is `omp`; the RPC bridge (S2) is `omp-spt`. [impl->REQ-SKILL-LIVE]\n*456│host_binaries = [\"omp\", \"omp-spt\"]\n 457│# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is\n 458│# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher\n 459│# brand is DECOUPLED from the plugin name (`omps`, skills `/omps:*`): the s/omps/spt/ succession seam\n    │...\n 472│# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no\n*473│# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue\n 474│# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI\n 475│# endpoint route alongside /omps:live. (When doyle ask #2 lands, the post-update step can also trigger\n 476│# this print on a plugin-only change; copy stays valid.)\n    │...\n 479│avenue = \"gh_release\"\n*480│repo = \"BigscreenVR/omp-spt\"\n 481│transport = \"gh\"\n 482│message = \"\"\"\n*483│**omp-spt updated.** The native OMP extension + extractors refreshed in place — no reload step: OMP\n 484│loads the packaged extension fresh on each endpoint bringup.\n 485│\n 486│- Running endpoints keep the OLD hosting path until restarted: `spt endpoint stop <id>` then\n*487│  `spt endpoint run --adapter omp-spt --id <id>` picks up the new one.\n*488│- Bring up a fresh Librarian endpoint: `spt endpoint run --adapter omp-spt --id <id> --create`.\n 489│\"\"\"\n 490│\n 491│# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────\n\n## strings/\n### omp-spt.mjs\n   2│\n  *3│const ADAPTER = \"omp-spt\";\n   4│\n   5│export function decodeBody(body) {\n   6│\treturn body\n    │...\n  59│\treturn new Promise((resolve, reject) => {\n *60│\t\tconst child = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n  61│\t\t\tstdio: [input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n  62│\t\t\twindowsHide: true,\n  63│\t\t});\n    │...\n  77│\n *78│// [impl->REQ-OMP-NATIVE-TUI]\n  79│export default function ompSpt(pi) {\n  80│\tconst id = process.env.SPT_ENDPOINT_ID?.trim();\n  81│\tif (!id) return;\n    │...\n 107│\t\ttry {\n*108│\t\t\tpi.sendUserMessage(current.body);\n 109│\t\t} catch (error) {\n*110│\t\t\tlogError(\"omp-spt could not submit an inbound message\", error);\n 111│\t\t\tcurrent = undefined;\n 112│\t\t\tsetTimeout(dispatchNext, 0);\n 113│\t\t}\n    │...\n 117│\t\tconst args = [\"ready\", id];\n*118│\t\tif (process.env.OMP_SPT_SUBNET) args.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n*119│\t\tlistener = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n 120│\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n 121│\t\t\twindowsHide: true,\n 122│\t\t});\n    │...\n 131│\t\t});\n*132│\t\tlistener.stderr.on(\"data\", (chunk) => pi.logger.debug(\"omp-spt listener\", { output: chunk.trim() }));\n*133│\t\tlistener.on(\"error\", (error) => logError(\"omp-spt listener failed\", error));\n 134│\t\tlistener.on(\"close\", (code) => {\n 135│\t\t\tif (!stopping) {\n*136│\t\t\t\tui?.setStatus(\"omp-spt\", \"spt offline\");\n*137│\t\t\t\tui?.notify(`omp-spt listener exited (${code})`, \"error\");\n 138│\t\t\t}\n 139│\t\t});\n 140│\t}\n 141│\n*142│\tpi.on(\"session_start\", async (_event, ctx) => {\n 143│\t\tui = ctx.ui;\n 144│\t\tsid = ctx.sessionManager.getSessionId();\n 145│\t\ttry {\n 146│\t\t\tconst bindArgs = [\"api\", \"--adapter\", ADAPTER, \"bind\", id, \"--set-session-id\", sid];\n*147│\t\t\tif (process.env.OMP_SPT_SUBNET) bindArgs.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n 148│\t\t\tconst bound = await runSpt(bindArgs);\n 149│\t\t\ttoken = bound.match(/\\btoken=([^\\s]+)/)?.[1];\n 150│\t\t\tawait setState(\"idle\");\n 151│\t\t\tstartListener();\n*152│\t\t\tctx.ui.setStatus(\"omp-spt\", `spt:${id}`);\n 153│\t\t} catch (error) {\n*154│\t\t\tctx.ui.setStatus(\"omp-spt\", \"spt bind failed\");\n*155│\t\t\tlogError(`omp-spt could not bind ${id}`, error);\n 156│\t\t}\n 157│\t});\n 158│\n    │...\n 163│\t\t} catch (error) {\n*164│\t\t\tlogError(\"omp-spt could not mark the endpoint busy\", error);\n 165│\t\t}\n 166│\t});\n 167│\n    │...\n 172│\t\tif (completed?.from) {\n*173│\t\t\tconst reply = extractReply(event.messages) || \"[omp-spt] turn ended without an assistant response.\";\n 174│\t\t\ttry {\n 175│\t\t\t\tawait runSpt([\"send\", completed.from, \"--from\", id], reply);\n 176│\t\t\t} catch (error) {\n*177│\t\t\t\tlogError(`omp-spt could not reply to ${completed.from}`, error);\n 178│\t\t\t}\n 179│\t\t}\n 180│\t\ttry {\n    │...\n 182│\t\t} catch (error) {\n*183│\t\t\tlogError(\"omp-spt could not mark the endpoint idle\", error);\n 184│\t\t}\n 185│\t\tsetTimeout(dispatchNext, 0);\n 186│\t});\n\n# tools/claude-spt/\n## Cargo.toml\n 21│[[bin]]\n*22│# HIGH-1 (docs/OMP-BRIDGE-FIELD-ISSUES.md): the SHIPPED BINARY is `omp-spt` — a distinct basename\n*23│# from upstream claude-spt, so the bridge process is self-identifying in a task list (the field\n*24│# incident's misdiagnosis was a bridge indistinguishable from a real claude-spt). The crate/package\n 25│# keeps the claude-spt name (dir layout + CI scripts unchanged).\n*26│name = \"omp-spt\"\n 27│path = \"src/main.rs\"\n 28│\n 29│[dependencies]\n\n## examples/\n### fake-omp.rs\n *1│//! fake-omp — a deterministic, model-free stand-in for `omp --mode rpc`, for the omp-spt bridge\n *2│//! acceptance test (tests/bridge-acceptance.sh). A compiled Cargo EXAMPLE (never shipped in the\n *3│//! release adapter.spt), so it is a real cross-platform executable the bridge can spawn via\n  4│//! `Command::new` — unlike a shell script, which Windows cannot exec directly.\n  5│//!\n *6│//! It ignores all argv (the bridge appends `--mode rpc [--session-dir …]`) and speaks just enough of\n *7│//! the verified RPC protocol (OMP-ADAPTER-PLAN.md, confirmed against omp v16.3.11) for the bridge to\n  8│//! complete a full ready → get_state → prompt → agent_end cycle with zero model billing:\n  9│//!\n 10│//!   • on start                : `{\"type\":\"ready\"}`\n   │...\n 19│//! FAKE_OMP_MUTE=1 (ack + agent_start but NEVER send agent_end — the hung-turn shape that\n*20│//! exercises the bridge's per-turn deadline, HIGH-4). stdout is flushed after every frame so the\n*21│//! bridge's line reader never stalls behind a full pipe buffer.\n 22│\n 23│use std::io::{BufRead, Write};\n 24│\n\n## src/\n### bridge.rs\n *1│//! The omp-spt RPC bridge (S2 ready-agent; hardened at v0.1.1 per docs/OMP-BRIDGE-FIELD-ISSUES.md).\n  2│//!\n  3│//! This is the hosted harness process for the oh-my-pi (omp) adapter: spt-core's broker spawns it as\n  4│//! `[session.self].command`, and it drives a resident `omp --mode rpc` child, translating in both\n *5│//! directions (OMP-ADAPTER-PLAN.md, \"Architecture decision: RPC-bridge (Style A)\"):\n  6│//!\n  7│//! ```text\n *8│//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\n  9│//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\n 10│//!                          └  prompt / get_state                                    (to omp)\n 11│//! ```\n 12│//!\n*13│//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\n*14│//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\n 15│//! compat alias for the same entry.\n 16│//!\n 17│//! ## Protocol (verified against omp v16.3.11 — see OMP-ADAPTER-PLAN.md §\"decisive finding\")\n   │...\n 23│//! Drive turn: host → `{\"type\":\"prompt\",\"message\":\"…\"}`; omp acks `{\"type\":\"response\",\n*24│//!            \"command\":\"prompt\",\"success\":true}`, then streams `turn_start` → `agent_start` →\n*25│//!            `message_update`… → `turn_end` → `{\"type\":\"agent_end\",\"messages\":[…]}`.\n 26│//!\n 27│//! ## Hardening (the doyle 2026-07-09 field incident — docs/OMP-BRIDGE-FIELD-ISSUES.md)\n 28│//!\n*29│//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\n*30│//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\n*31│//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\n*32│//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\n 33│//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\n 34│//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\n 35│//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\n*36│//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\n*37│//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\n 38│//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\n 39│//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\n 40│//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\n   │...\n 54│/// namespace `omps`; mirrors hook.rs's `ADAPTER` for the claude-spt path.\n*55│const ADAPTER: &str = \"omp-spt\";\n 56│\n*57│/// Bringup deadline for omp's `ready` frame (override: OMP_SPT_READY_TIMEOUT_S). Generous — a cold\n 58│/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\n 59│/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\n 60│const DEFAULT_READY_TIMEOUT_S: u64 = 120;\n*61│/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\n 62│const DEFAULT_STATE_TIMEOUT_S: u64 = 30;\n*63│/// Per-turn deadline: prompt dispatch → `agent_end` (override: OMP_SPT_TURN_TIMEOUT_S). Long — a\n 64│/// real agent turn can run tools for minutes — but bounded, so a hung turn frees the perch.\n 65│const DEFAULT_TURN_TIMEOUT_S: u64 = 600;\n*66│/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\n 67│const DEFAULT_HEARTBEAT_S: u64 = 300;\n 68│\n 69│/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\n   │...\n 72│    ($($a:tt)*) => {\n*73│        eprintln!(\"[omp-spt] {}\", format!($($a)*))\n 74│    };\n 75│}\n 76│\n*77│/// Frame-level trace, still gated behind `OMP_SPT_DEBUG` (diagnostics; off by default).\n 78│macro_rules! trace {\n 79│    ($($a:tt)*) => {\n### digest_omp.rs\n *1│//! `claude-spt digest-omp` — the omp-spt `[digest]` extractor (S3).\n  2│//!\n  3│//! Maps oh-my-pi's native session JSONL → the SAME published digest-record contract the CC extractor\n  4│//! (digest.rs) emits: NDJSON, one `{\"role\",\"text\"?,\"tool\"?,\"ts\"?}` object per stdout line, `role` ∈\n   │...\n 32│//!   dir-encoding rule need not be reproduced — locate-by-id, as CC's digest.rs does).\n*33│//!   Proof: `spt adapter digest-proof omp-spt --sample <file>` passes the file via `--in` (read\n 34│//!   as-is, never located). stdout is UTF-8 (the Windows cp1252 trap from the tips doc).\n 35│\n 36│use crate::digest::{rec, scalar_str};\n### echo_commune_omp.rs\n  *1│//! `claude-spt echo-commune-omp` — the omp-spt `[session.echo_commune]` bounded end-of-session\n   2│//! summarizer (S4). Same contract as the CC echo_commune.rs, omp-flavored:\n   3│//!\n   4│//!   1. INPUT — history on stdin when the (future) published wiring feeds it; otherwise SELF-LOCATE\n    │...\n 131│    let prompt = compose_prompt(&id, bounded_tail(&history, TAIL_CAP_BYTES));\n*132│    let prompt_file = std::env::temp_dir().join(format!(\"omp-spt-echo-{}-{}.md\", id, std::process::id()));\n 133│    if let Err(e) = std::fs::write(&prompt_file, &prompt) {\n 134│        eprintln!(\"{}\", fail_line(&id, &format!(\"cannot write prompt scratch {}: {e}\", prompt_file.display())));\n 135│        return ExitCode::FAILURE;\n 136│    }\n*137│    let mut cmd = Command::new(crate::bridge::omp_bin());\n 138│    cmd.args(turn_cmd());\n 139│    cmd.arg(format!(\"@{}\", prompt_file.display()));\n 140│    cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());\n### history_omp.rs\n*1│//! `claude-spt history-omp` — the omp-spt `[history]` fetcher (S4).\n 2│//!\n 3│//! Contract (same as the CC `history`): locate ONE omp session's JSONL and stream the file to\n 4│//! stdout VERBATIM — opaque bytes, single-session; the only consumer is our own echo-commune\n### hook.rs\n  523│/// the binary cannot exist before spt-core + the adapter are installed. [impl->REQ-DIST-HOOKS-API]\n *524│fn handle_session_start(env: &mut dyn HookEnv, v: &Value) {\n  525│    let sid = field(v, \"session_id\");\n  526│    let src = field(v, \"source\");\n  527│    let endpoint_id = env.env(\"SPT_ENDPOINT_ID\").filter(|s| !s.is_empty());\n     │...\n  958│/// sid = the payload's `session_id` ELSE the bind-time carrier `state/session/<id>.sid` (written by\n *959│/// `handle_session_start`). This is the v0.16.1 hardening: `PostToolUseFailure` (and possibly\n  960│/// `StopFailure`) payloads may OMIT `session_id`, and a `state idle` with an empty `--session-id` is\n  961│/// AUTH-refused by core — the silent miss that left `lia` deaf after a real-turn interrupt while its\n  962│/// input box sat at \"Interrupted · What should Claude do instead?\" (field 2026-07-08). A live agent\n     │...\n 1165│    match event {\n*1166│        \"SessionStart\" => handle_session_start(env, v),\n 1167│        \"UserPromptSubmit\" => handle_user_prompt_submit(env, v),\n 1168│        \"PreToolUse\" => handle_pre_tool_use(env, v),\n 1169│        \"Stop\" => handle_stop(env, v),\n     │...\n 2070│    #[test]\n*2071│    fn session_start_seed_path_uses_host_pid_and_writes_env() {\n 2072│        // No SPT_ENDPOINT_ID, source startup, no peers → seed + env-file writes, no brief.\n 2073│        let mut env = Recorder::new(|args| match args {\n 2074│            [\"subnet\", \"status\"] => Some(\"no subnets\".into()),\n     │...\n 2076│        });\n*2077│        handle_session_start(&mut env, &json!({\"session_id\":\"s9\",\"source\":\"startup\"}));\n 2078│        assert!(env.call_lines().iter().any(|l| l == \"api seed --pid 4242 --session-id s9\"), \"seed by host pid\");\n 2079│        assert!(env.env_lines.iter().any(|l| l == \"OWL_SESSION_ID=s9\"));\n 2080│        assert!(env.env_lines.iter().any(|l| l == \"SPT_ADAPTER=claude-spt\"));\n     │...\n 2084│    #[test]\n*2085│    fn session_start_bind_path_binds_and_briefs() {\n 2086│        let mut env = Recorder::new(|args| match args {\n 2087│            [\"adapter\", \"get-string\", _, k] => Some(format!(\"[{k}]\")), // non-empty brief parts\n 2088│            a if a.contains(&\"psyche-download\") => Some(String::new()), // NO-CONTEXT\n     │...\n 2091│        .with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2092│        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2093│        assert!(env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s1\"), \"bind\");\n 2094│        let out = env.out();\n 2095│        assert!(out.contains(\"\\\"hookEventName\\\":\\\"SessionStart\\\"\"), \"emits SessionStart additionalContext\");\n     │...\n 2111│        .with_state(\"state/session/cc-1.sid\", \"s-old\");\n*2112│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"resume\"}));\n 2113│        assert!(\n 2114│            env.call_lines()\n 2115│                .iter()\n     │...\n 2127│        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2128│        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2129│        assert!(\n 2130│            env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s1\"),\n 2131│            \"fresh bind is proofless: {:?}\",\n     │...\n 2151│            .with_strict_err(\"AUTH_REFUSED:cc-1 (need --token or matching --session-id)\");\n*2152│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"resume\"}));\n 2153│        assert!(\n 2154│            env.logs.iter().any(|l| l.contains(\"re-pin FAILED\") && l.contains(\"AUTH_REFUSED\")),\n 2155│            \"the re-bind refusal must be loud with its reason: {:?}\",\n     │...\n 2166│            .with_state(\"state/session/cc-1.sid\", \"s-same\");\n*2167│        handle_session_start(&mut env, &json!({\"session_id\":\"s-same\",\"source\":\"resume\"}));\n 2168│        assert!(\n 2169│            env.call_lines().iter().any(|l| l == \"api --adapter claude-spt bind cc-1 --set-session-id s-same\"),\n 2170│            \"same-sid re-fire binds without proof: {:?}\",\n     │...\n 2182│    #[test]\n*2183│    fn session_start_subagent_gets_no_brief() {\n 2184│        let mut env = Recorder::new(|_| Some(\"x\".into())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2185│        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\",\"agent_type\":\"general-purpose\"}));\n 2186│        assert!(env.out().is_empty(), \"subagent sessions get no brief\");\n 2187│    }\n 2188│\n     │...\n 2209│            .with_state(\"state/session/ball-b.sid\", \"s-old\");\n*2210│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2211│        let lines = env.call_lines();\n 2212│        assert!(\n 2213│            lines.iter().any(|l| l == \"api --adapter claude-spt boundary clear ball-b --to-session-id s-new --session-id s-old\"),\n     │...\n 2224│        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"cc-1\");\n*2225│        handle_session_start(&mut env, &json!({\"session_id\":\"s1\",\"source\":\"startup\"}));\n 2226│        assert!(\n 2227│            env.state_writes.contains(&(\"state/session/cc-1.sid\".to_string(), \"s1\".to_string())),\n 2228│            \"bind persists the sid: {:?}\",\n     │...\n 2231│        // Rotation rolls the proof forward to the NEW sid.\n*2232│        handle_session_start(&mut env, &json!({\"session_id\":\"s2\",\"source\":\"clear\"}));\n 2233│        assert!(\n 2234│            env.state_writes.contains(&(\"state/session/cc-1.sid\".to_string(), \"s2\".to_string())),\n 2235│            \"boundary persists the new sid for the next rotation: {:?}\",\n     │...\n 2248│        });\n*2249│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2250│        assert!(!env.call_lines().iter().any(|l| l.contains(\"boundary\")), \"no unproven boundary call\");\n 2251│        assert!(\n 2252│            env.logs.iter().any(|l| l.contains(\"SKIPPED\") && l.contains(\"strand\")),\n     │...\n 2265│            .with_strict_err(\"AUTH_REFUSED:ball-b (need --token or matching --session-id)\");\n*2266│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2267│        assert!(\n 2268│            env.logs.iter().any(|l| l.contains(\"FAILED\") && l.contains(\"AUTH_REFUSED\")),\n 2269│            \"the refusal must be loud with its reason: {:?}\",\n     │...\n 2278│        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"ball-b\");\n*2279│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"compact\"}));\n 2280│        assert!(\n 2281│            env.call_lines()\n 2282│                .iter()\n     │...\n 2319│        let mut env = Recorder::new(|_| Some(String::new())).with_env(\"SPT_ENDPOINT_ID\", \"ball-b\");\n*2320│        handle_session_start(&mut env, &json!({\"session_id\":\"s-new\",\"source\":\"clear\"}));\n 2321│        let lines = env.call_lines();\n 2322│        assert!(\n 2323│            lines.iter().any(|l| l == \"api --adapter claude-spt state idle ball-b --session-id s-new\"),\n### main.rs\n  21│\n *22│mod bridge;\n  23│mod digest;\n  24│mod digest_omp;\n  25│mod echo_commune;\n    │...\n  62│    match sub {\n *63│        Some(\"bridge\") => Sub::Bridge,\n  64│        Some(\"digest\") => Sub::Digest,\n  65│        Some(\"digest-omp\") => Sub::DigestOmp,\n  66│        Some(\"echo-commune\") => Sub::EchoCommune,\n    │...\n  85│         subcommands:\\n\\\n *86│         \\x20 bridge       drive omp via `omp --mode rpc`, relaying spt api seed/poll/state (ready-agent)\\n\\\n  87│         \\x20 digest       map a Claude Code JSONL transcript to digest NDJSON ([digest] extractor)\\n\\\n  88│         \\x20 digest-omp   map an oh-my-pi (omp) session JSONL to digest NDJSON (omp [digest] extractor)\\n\\\n  89│         \\x20 history-omp  stream an omp session JSONL verbatim (omp [history] fetcher)\\n\\\n    │...\n 104│    match classify(sub.as_deref()) {\n*105│        Sub::Bridge => bridge::run(),\n 106│        Sub::Digest => digest::run(),\n 107│        Sub::DigestOmp => digest_omp::run(),\n 108│        Sub::EchoCommune => echo_commune::run(),\n    │...\n 152│    fn classifies_each_real_subcommand() {\n*153│        assert_eq!(classify(Some(\"bridge\")), Sub::Bridge);\n 154│        assert_eq!(classify(Some(\"digest\")), Sub::Digest);\n 155│        assert_eq!(classify(Some(\"digest-omp\")), Sub::DigestOmp);\n 156│        assert_eq!(classify(Some(\"history-omp\")), Sub::HistoryOmp);\n### psyche_omp.rs\n  *1│//! `claude-spt psyche-omp` — the omp-spt `[session.psyche_resume]` EPHEMERAL shim (S4).\n   2│//!\n   3│//! The omp counterpart of psyche.rs, holding the SAME frozen psyche-turn contract (one spawned\n   4│//! role, two key shapes, event on stdin, stdout captured, exit 95 = the sole reseed discriminator)\n    │...\n 222│\n*223│    let mut cmd = Command::new(crate::bridge::omp_bin());\n 224│    cmd.args(turn_cmd(&sdir.to_string_lossy(), is_first));\n 225│    cmd.arg(format!(\"@{}\", prompt_file.display()));\n 226│    // cwd = the psyche's nested perch dir (mind-file parent) — commune file-drops land beside it.\n### translate.rs\n 185│/// Settle between the rename's submit and the wake's stash within the ONE combined post-clear\n*186│/// sequence: the enter→next-keystroke bridge the two-sequence design could not govern. Field-paid\n 187│/// 2026-07-06 (flynn): rename and wake rode two back-to-back inject sequences, and with CC's input\n 188│/// processing lagging the PTY stream at the just-rebuilt boundary, the rename's enter registered\n 189│/// as a soft newline, the next sequence's ctrl+s failed to stash the residue, and the wake text\n    │...\n 212│        if !v.is_empty() {\n*213│            // enter→ctrl+s bridge: let the rename's submit fully register before the wake half\n 214│            // stashes + types (the 2026-07-06 blend guard).\n 215│            v.push(json!({ \"delay_ms\": BOUNDARY_BRIDGE_MS }));\n 216│        }\n    │...\n 676│        // THE 2026-07-06 REGRESSION (flynn): rename + wake must ride ONE inject sequence — rename\n*677│        // half, enter->ctrl+s bridge, wake half, ONE commit. Two back-to-back sequences raced at the\n 678│        // post-clear boundary and the wake text submitted INSIDE the /rename argument (session\n 679│        // titled with the wake, no wake turn, agent dormant ~9.5h).\n 680│        let mut pending = Pending::default();\n    │...\n 694│                \"key:enter\".to_string(),  // rename submits...\n*695│                \"delay:150\".to_string(),  // ...then the enter->ctrl+s bridge (BOUNDARY_BRIDGE_MS)\n 696│                \"key:ctrl+s\".to_string(), // stash any residue (incl. a coalesced rename)\n 697│                \"delay:50\".to_string(),\n 698│                \"text:do the thing\".to_string(),\n\n# ci/\n\n## digest/\n### digest-proof-int.sh\n 27│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*28│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 29│SAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\n 30│RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n 31│\n\n## hooks/\n### poll-int.sh\n 57│#    [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]\n*58│HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*59│[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 60│if [ -x \"$HOOKBIN\" ]; then\n 61│  printf 'hello from probe<NL>second <line> & \"stuff\"' | sed 's/<NL>/\\\n 62│/' | spt send \"$BID\" --from probe-int >/dev/null 2>&1\n\n## idle-translate/\n### translate-proof-int.sh\n 22│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*23│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n*24│RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; manifest command = {adapter_dir}/omp-spt translate (D3)\n 25│EVENT='<EVENT type=\"msg\" from=\"ci\">translate-proof int probe</EVENT>'\n 26│\n 27│command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n\n## manifest/\n### check-manifest.sh\n  9│SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n*10│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 11│VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 12│\n 13│for f in \"$SCHEMA\" \"$MANIFEST\" \"$VALIDATOR\"; do\n### registration-int.sh\n 12│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*13│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 14│\n 15│if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 16│command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n   │...\n 72│#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\n*73│HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*74│[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 75│if [ -x \"$HOOKBIN\" ]; then\n 76│  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 77│  case \"$inj\" in\n\n## psyche/\n### live-relay-int.sh\n 31│A=claude-spt\n*32│MAN=\"$ROOT/adapter/omp-spt.toml\"\n 33│# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity\n 34│# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):\n 35│# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a\n\n## publish/\n### package-adapter.sh\n   2│# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by\n  *3│# `spt adapter add --release BigscreenVR/omp-spt` and carried by the `[update] gh_release`\n   4│# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2).\n  *5│# The release/install/update repo is `BigscreenVR/omp-spt` (renamed from spt-claude-code, U3/ADR-0005).\n   6│# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS] [impl->REQ-DIST-NAME-UNIFY]\n   7│#\n   8│#   adapter.spt (tar.gz)\n  *9│#   ├── manifest.toml                 ← SHARED, at archive root (renamed from omp-spt.toml)\n  10│#   ├── strings/                      ← SHARED, at archive root\n  11│#   ├── x86_64-pc-windows-msvc/       ← this triple's binary, mirroring the flat-root tree\n  12│#   │   ├── claude-spt.exe\n    │...\n  33│ADAPTER=\"$ROOT/adapter\"\n *34│MANIFEST=\"$ADAPTER/omp-spt.toml\"   # renamed to manifest.toml INSIDE the archive (root-only rule)\n  35│STRINGS=\"$ADAPTER/strings\"\n  36│TOOLS=\"$ROOT/tools\"\n  37│OUT=\"${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}\"   # overridable so the unit test writes to a tmp file\n    │...\n  50│LINUX_RELSUB=\"${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}\"\n *51│BINS=\"omp-spt\"\n  52│\n  53│# Validate the manifest first — refuse to ship an invalid adapter.\n  54│echo \"== validate manifest ==\"\n    │...\n  94│  echo \"DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'\"\n *95│  echo \"to a GitHub release on the monorepo. End users: 'spt adapter add --release BigscreenVR/omp-spt'\"\n  96│  echo \"(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.\"\n  97│  exit 0\n  98│fi\n    │...\n 120│echo \"$listing\" | grep -qx \"manifest.toml\" || { echo \"FATAL: manifest.toml not at archive root\" >&2; fatal=1; }\n*121│echo \"$listing\" | grep -q \"^$WIN_TRIPLE/omp-spt.exe$\" || { echo \"FATAL: missing $WIN_TRIPLE/ binaries\" >&2; fatal=1; }\n*122│echo \"$listing\" | grep -q \"^$LINUX_TRIPLE/omp-spt$\"   || { echo \"FATAL: missing $LINUX_TRIPLE/ binaries\" >&2; fatal=1; }\n 123│# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).\n 124│badtop=$(echo \"$listing\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\n 125│[ -n \"$badtop\" ] && { echo \"FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat\" >&2; fatal=1; }\n    │...\n 130│echo \"WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release\"\n*131│echo \"on BigscreenVR/omp-spt; end users 'spt adapter add --release BigscreenVR/omp-spt' (spt\"\n 132│echo \"v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.\"\n 133│exit 0\n\n## setup/\n### activate-int.sh\n  1│#!/bin/sh\n *2│# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n  3│# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n  4│# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n  5│# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n   │...\n 16│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*17│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 18│\n 19│if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 20│command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n   │...\n 45│\n*46│# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\n 47│out=$(spt adapter add \"$MANIFEST\" 2>&1)\n 48│case \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n 49│"},"isError":false,"timestamp":1784087167313}}
{"type":"message","id":"95261415","parentId":"802643ab","timestamp":"2026-07-15T03:46:07.351Z","message":{"role":"toolResult","toolCallId":"call_gLL3fb9BxSgC93hybJkcIHyJ|fc_0ca4af36969c80fa016a57027f2a1c8198bf41bbf486388a7e","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n 41|piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\n*42|flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n 43|**ring brief** (the no-perch sibling).\n 44|\n 45|**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`\n 46|topology): a node-local agent that hasn't readied still learns how to reach other agents via\n*47|`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually\n 48|participates in a subnet (has reachable peers), so a solo casual end user is never told how to\n 49|ring agents that don't exist.\n 50|\n...\n 56|spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n*57|attach). The spt-core realization of legacy's unbuilt \"Capsule\" milestone. _Avoid_: equating\n 58|it with psmux/sendkeys — spt-core's broker is the terminal host.\n 59|\n 60|**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n\n# SCOPE.md\n 3|> Running source of truth for the scoping session (started 2026-06-13, doyle).\n*4|> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n 5|> This project will be re-homed onto the `experimplate` template structure once\n 6|> that template is authored — until then this ledger is the interim record.\n 7|\n...\n 20|INTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy\n*21|concepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**\n 22|from `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the\n 23|spt-core source tree.\n 24|\n...\n 36|| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n*37|| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n 38|| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n 39|\n 40|- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n...\n 53|  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n*54|  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n 55|  empirically at build time**, but design on it.\n 56|- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n 57|  manifest doesn't bloat.\n...\n 64|~static marketplace skeleton version. On update applied: spt-core announces via one-time\n*65|SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n 66|(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n 67|\n 68|## ccs integration\n...\n 94|8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n*95|   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n 96|9. Doctor/verify at end\n 97|\n*98|**LOCKED:** v1 = {1,2,3,4,5,6,7}, defer {8,9}. Item #1 (`cc` launcher) IS in v1 — depends on\n 99|full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100|`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n 101|flag if verify should fold inline._\n 102|\n 103|### `cc` launcher (capsule-style) — **LOCKED (design)**\n...\n 109|Sendkeys hazards = spt-core's inject concern, not the adapter's.\n*110|**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\n 111|hosted CC session (not just headless PTY).\n 112|\n 113|### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n...\n 152|\n*153|**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n 154|harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n 155|\n 156|**KEEP — FINALIZED** (post-investigation 2026-06-14):\n...\n 163|- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to\n*164|  recover); restart = re-`ready`/re-`listen` or `endpoint stop`+bringup. Confirm.\n*165|- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.\n 166|- `/whoami` — **DROP skill**. Core `spt whoami` STAYS in hot-path but → **becomes an alias for\n 167|  `spt endpoint list`** (M12), whose SELF-pin output gains the Self `endpoint description`.\n 168|\n...\n 184|  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n*185|  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n 186|  (not spt-core's two-key/counter runbook).\n*187|- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n 188|  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;\n 189|  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.\n 190|- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.\n...\n 208|\n*209|## Open threads (grill queue)\n 210|\n 211|- [ ] CI trigger mechanism (git-hook signal vs polling routine)\n 212|- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)\n...\n 214|- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md\n*215|- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred\n\n# OMP-ADAPTER-PLAN.md\n 77||---|---|---|\n*78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n 79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n...\n 86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n 90|### Session/transcript (for the `[digest]` extractor)\n...\n 155|\n*156|## Open questions — verify against the live `omp` + `spt` binaries\n 157|\n 158|Per the tips doc discipline (JSON-schema validity ≠ runtime; validate on the live\n*159|binary), confirm before/while building:\n 160|\n 161|1. **RPC session lifecycle across turns** — does a resident `omp --mode rpc` hold\n 162|   one session for many `prompt`s (ready-agent), and can the bridge start it on a\n...\n 165|2. **Daemon hosting a resident bridge** — spt-core's broker spawns\n*166|   `[session.self].command` into a PTY; confirm a bridge that itself holds a child\n 167|   `omp --mode rpc` survives as the hosted process (vs the ephemeral per-event\n*168|   psyche model). Decide ready-resident vs live-ephemeral per role.\n 169|3. **dir-encoding exactness** — reproduce omp's `<dir-encoded>` rule precisely for\n 170|   the digest locate (home-relative, `/ \\ :`→`-`); handle `--session-dir` override.\n 171|4. **Hook fallback need** — anything the RPC stream doesn't expose that we still\n\n# traceable-reqs.toml\n 22|# stays green pre-code. A milestone ACTIVATES its requirements by setting their real\n*23|# `required_stages` when work on them begins. Deferred items stay [] until promoted.\n 24|# Never declare the full end-state stage list up front — activate incrementally as evidence\n 25|# lands (add \"int\" at the final integrating wave).\n 26|\n...\n 73|title = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\n*74|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n 75|\n 76|[[requirements]]\n 77|id = \"REQ-DIST-HOOK-BINARY\"\n 78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n 82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n...\n 88|title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n*89|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n 90|\n 91|[[requirements]]\n 92|id = \"REQ-CC-LAUNCHER-BIND\"\n 93|title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n*94|required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n 95|\n 96|[[requirements]]\n 97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n 98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n 102|id = \"REQ-DIST-DIGEST-FETCHER\"\n 103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n 107|id = \"REQ-DIST-WHOAMI-JSON\"\n 108|title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109|required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110|\n 111|[[requirements]]\n 112|id = \"REQ-SKILL-LIVE\"\n...\n 118|title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120|\n 121|[[requirements]]\n 122|id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n*123|title = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\n*124|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int ...\n 125|\n 126|# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n 127|# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n*128|# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.\n 129|\n 130|[[requirements]]\n 131|id = \"REQ-CI-GATES\"\n...\n 177|title = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_erro...\n*178|required_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger...\n 179|\n 180|[[requirements]]\n 181|id = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\n 182|title = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamin...\n*183|required_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child en...\n 184|\n 185|[[requirements]]\n 186|id = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\n 187|title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189|\n 190|[[requirements]]\n 191|id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n...\n 196|id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n*197|title = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\n*198|required_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance ro...\n 199|\n*200|# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n*201|# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n 202|\n 203|[[requirements]]\n 204|id = \"REQ-DOCS-SITE\"\n...\n 209|id = \"REQ-DOCS-DRIFT\"\n*210|title = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\n 211|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n 212|\n 213|[[requirements]]\n\n# docs/\n## KNOWN-HAZARDS.md\n 5|> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n*6|> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n 7|> and `traceable-reqs check` will say so once the hazard is activated.\n 8|\n 9|A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\n...\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n*52|  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n 53|  A test asserts the stdin path is honored (no argv `/`-token dependency).\n 54|- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n 55|  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n...\n 119|- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart\n*120|  boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**\n 121|  session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the\n 122|  departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two\n 123|  stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22\n...\n 169|  clear-done signal (SessionStart firing with `source == clear`). (1) ARM: a `{\"checkpoint\":\"v1\",…}`\n*170|  envelope stashes the wake in the translation binary's in-memory `pending_wake` and emits CLEAR-ONLY\n 171|  (`ctrl+s · 50ms · /clear · 50ms · enter · commit`); the wake is withheld. (2) FIRE: the SessionStart hook,\n 172|  on a `clear` boundary, UNCONDITIONALLY self-sends `{\"checkpoint_fire\":\"v1\"}`; the binary emits\n 173|  WAKE-ONLY (`ctrl+s · 50ms · wake · 50ms · enter · commit`) iff a wake is armed, else no-ops. State lives\n...\n 186|  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n*187|  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n 188|  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n 189|  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n 190|  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n...\n 227|  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n*228|  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229|  child that runs harness hooks, it needs the same scrub.\n 230|- **cite:** doyle's F-028 C2 adapter change brief (2026-07-03); spt-core runtime `env_remove` support\n 231|  is public contract (`manifest.schema.json` `SessionRole.env_remove`).\n...\n 281|  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n*282|  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n 283|- **cite:** doyle's C3 adapter-half brief (2026-07-03), formalizing our F-h finding; spt-core\n 284|  halves = C3(b) ledger-rate trip @b2351a0 + shutdown tree-kill hardening; second leg = the\n*285|  hall-bf field residual (doyle confirm run, 2026-07-04) + perri's multi-subnet READY_FAIL repro;\n 286|  RESOLVED-BY-DESIGN = the v0.14.0 ephemeral redesign (PSYCHE-EPHEMERAL-PLAN.md, F-030 adapter half).\n 287|\n 288|### 2.7 A delivery answered with ZERO records faults + terminates the translation binary\n...\n 381|  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n*382|  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n 383|  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n 384|  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n*385|  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n 386|  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n 387|- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n 388|  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n...\n 396|  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n*397|  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n 398|  hooks; the degrade-not-brick exit-0 half covers that remainder.\n 399|- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n 400|  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n...\n 416|  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n*417|  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n 418|  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n 419|  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n 420|  stale pick is.\n...\n 435|  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n*436|  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n 437|  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n 438|  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n 439|  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n...\n 442|  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n*443|  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n 444|  evidence = this entry; impl/unit land with the guard slice.\n## OMP-BRIDGE-FIELD-ISSUES.md\n 126|`spt ready` line arrives (`bridge.rs:216`). While the perch is idle the bridge is\n*127|blocked on `ready_out.lines()`; if the omp child dies there, the bridge doesn't\n 128|notice. The perch stays online/idle and only discovers the dead child when the\n 129|next message arrives (then returns an empty reply — see MEDIUM-5).\n 130|\n\n## adr/\n### 0000-template.md\n 20|<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,\n*21|     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->\n 22|\n 23|## Consequences\n 24|\n### 0001-distribution-splits-by-volatility.md\n 42|| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |\n*43|| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n 44|| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n 45|\n 46|Concretely:\n...\n 74|\n*75|### Open / to-confirm\n 76|\n 77|- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n 78|  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n...\n 98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n*99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n 102|  `claude plugin update` (skeleton sync only).\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 90|thin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is\n*91|settled during impl (see Open).\n 92|\n 93|### Hook-side id-resolution — RESOLVED (observed on the 0.6.0 binary)\n 94|\n...\n 118|- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n*119|  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n 120|  packaging on Windows (no per-OS branch needed for the hook to run).\n 121|- ✅ **Hook stdin schema confirmed**: `{session_id, transcript_path, cwd, permission_mode,\n 122|  hook_event_name, prompt}` — `json_str` targets the right fields; `session_id` sourcing confirmed.\n...\n 126|\n*127|## Open / resolved `int`\n 128|\n 129|1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** The canonical poll envelope shipped\n 130|   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed\n 131|   the canonical `<EVENT type=\"msg\" from=…>body</EVENT>\\n` envelope (no `__REPLY_TO__`, no\n*132|   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-\n 133|   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`\n 134|   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.\n 135|2. **Large-drain injection size — RESOLVED (2026-06-15).** CC spills `additionalContext` over ~10k\n...\n 146|- `REQ-DIST-HOOKS-API` / `REQ-UPS-INJECTION` gain a concrete, grounded `doc` design here; their\n*147|  `impl` activates when the validated `hooks.json` + wrappers land (gated on Open #1).\n 148|- Supersedes the `ADR-0001` \"hooks.json delegates to `spt api`\" stub with the full wiring; ADR-0001's\n*149|  UPS-injection open item (UPS-fires empirical) is carried here as Open #2.\n 150|- No spt-core dependency for the wiring itself — only the (non-blocking) residual docs publish of the\n 151|  `api poll` frame format, which we can substitute with observed behavior until it lands.\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n 58|\n*59|- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future\n 60|  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a\n 61|  deliberate change to `sandbox_flags()` + this ADR, not an accident.\n 62|- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination\n...\n 64|  auto-approve, and never drop the cap while keeping skip-permissions.\n*65|- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66|  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67|  sibling, and surfaces loudly (no commune produced).\n 68|- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n### 0004-checkpoint-via-self-send-loopback.md\n 52|   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized\n*53|   `<pending-commune>` (trigger stripped core-side), race-free.\n 54|\n 55|**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never\n*56|defers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale\n*57|gap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.\n 58|\n 59|**Rejected alternatives:**\n 60|- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's\n...\n 64|- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the\n*65|  race-free re-seed (the `psyche-download` `<pending-*>` append).\n 66|\n 67|## Consequences\n 68|\n...\n 80|- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n*81|  clear is non-obvious) so it is not \"simplified\" away by a future reader.\n### 0005-name-unification.md\n 26|- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27|- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n 28|- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29|\n 30|End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n...\n 39|- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n*40|- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41|- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n 4|\n*5|accepted (2026-06-24) — extends ADR-0001 (distribution splits by volatility). Some pieces depend on spt-core capabilities not yet shipped (the \"doyle asks\" below); those parts are decided in shape but gated on delivery.\n 6|\n 7|**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n 8|dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n...\n 36|- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37|- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38|\n 39|### Binary consolidation\n 40|\n...\n 42|\n*43|- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44|- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45|\n 46|The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47|\n*48|### The spt-core asks (findings to doyle — decided in shape, gated on delivery)\n 49|\n 50|1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.\n*51|2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **\"changed\"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.\n 52|3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.\n 53|\n 54|## Consequences\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":85,"fileCount":13,"files":["CONTEXT.md","SCOPE.md","OMP-ADAPTER-PLAN.md","docs/adr/0000-template.md","docs/adr/0001-distribution-splits-by-volatility.md","docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","docs/adr/0004-checkpoint-via-self-send-loopback.md","docs/adr/0005-name-unification.md","docs/adr/0006-one-command-update-and-consolidated-binary.md","docs/KNOWN-HAZARDS.md","docs/OMP-BRIDGE-FIELD-ISSUES.md","traceable-reqs.toml"],"fileMatches":[{"path":"CONTEXT.md","count":3},{"path":"SCOPE.md","count":16},{"path":"OMP-ADAPTER-PLAN.md","count":6},{"path":"docs/adr/0000-template.md","count":1},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","count":3},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","count":6},{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","count":2},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","count":5},{"path":"docs/adr/0005-name-unification.md","count":2},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","count":6},{"path":"docs/KNOWN-HAZARDS.md","count":14},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","count":1},{"path":"traceable-reqs.toml","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# CONTEXT.md\n 41│piece plus shared messaging pieces. Liveness-agnostic: it does not distinguish live-vs-ready (that\n*42│flavor is deferred until spt-core publishes a machine-readable liveness query). Distinct from the\n 43│**ring brief** (the no-perch sibling).\n 44│\n 45│**ring brief** — the SessionStart counterpart for a session with **no** perch (the `seed`\n 46│topology): a node-local agent that hasn't readied still learns how to reach other agents via\n*47│`spt ring` (the no-id messaging path). Peer-gated — emitted only when the node actually\n 48│participates in a subnet (has reachable peers), so a solo casual end user is never told how to\n 49│ring agents that don't exist.\n 50│\n   │...\n 56│spawns-or-attaches a CC endpoint via spt-core's spt-hosted topology (broker PTY + inject +\n*57│attach). The spt-core realization of legacy's unbuilt \"Capsule\" milestone. _Avoid_: equating\n 58│it with psmux/sendkeys — spt-core's broker is the terminal host.\n 59│\n 60│**Psyche** — a LiveAgent's detached *companion* process. When a session goes live\n\n# SCOPE.md\n   3│> Running source of truth for the scoping session (started 2026-06-13, doyle).\n  *4│> Decisions marked **LOCKED** are user-ratified; **OPEN** are still in grill.\n   5│> This project will be re-homed onto the `experimplate` template structure once\n   6│> that template is authored — until then this ledger is the interim record.\n   7│\n    │...\n  20│INTO spt.exe/daemon; the adapter is the CC-specific shell only. Dead/rare legacy\n *21│concepts get dropped (parity-trim → REQ seed, OPEN). Built by maintainer **perri**\n  22│from `SaberMage/spt-releases` + GH Pages docs ONLY (public surface), never the\n  23│spt-core source tree.\n  24│\n    │...\n  36│| Plugin (marketplace) | namespaced `/spt:*` skill *skeletons*, `hooks.json` (call `spt api`), SessionStart bootstrap (installs spt-core if absent), `plugin.json` | `SaberMage/cplugs` | `claude plugin update` (rare structural changes only) | low |\n *37│| spt binary | all logic | spt-core domain | spt-core's own framework (signed, peer-propagated) | high |\n  38│| CC adapter manifest | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (NOT plugin files) | spt-core adapter-update (file-pull) | medium |\n  39│\n  40│- Skills stay **plugin-provided** → keep `/spt:` namespace (user-scope skills can't\n    │...\n  53│  slash-commands (CC highlights slash-commands anywhere in the entry field and isn't strict\n *54│  about trailing text). My fact-check called this undocumented/uncertain — **must confirm\n  55│  empirically at build time**, but design on it.\n  56│- Long instruction bodies → **file-backed adapter strings** (M12 spt-core dep #1) so the\n  57│  manifest doesn't bloat.\n    │...\n  64│~static marketplace skeleton version. On update applied: spt-core announces via one-time\n *65│SessionStart additionalContext (changelog) + optional `notify`. Update path = file-pull\n  66│(real channel) + cautionary `claude plugin update` (skeleton sync) — dual, kept in sync.\n  67│\n  68│## ccs integration\n    │...\n  94│8. Psyche cross-machine sync — **in-subnet sync is AUTOMATIC** (confirmed, M4-D6c); only the\n *95│   optional off-subnet/hub-mode backup remains, retired-as-default → near-zero v1 surface\n  96│9. Doctor/verify at end\n  97│\n *98│**LOCKED:** v1 = {1,2,3,4,5,6,7}, defer {8,9}. Item #1 (`cc` launcher) IS in v1 — depends on\n  99│full-fat M12 (gating prerequisite, todlando-built before perri starts). `cc` wraps\n*100│`spt endpoint run` defaulted to claude-spt. _Operator \"9. agree\" then \"except 9\" — deferred 9;\n 101│flag if verify should fold inline._\n 102│\n 103│### `cc` launcher (capsule-style) — **LOCKED (design)**\n    │...\n 109│Sendkeys hazards = spt-core's inject concern, not the adapter's.\n*110│**OPEN gap:** confirm spt-core \"remote attach\" covers *local user interactive attach* to a\n 111│hosted CC session (not just headless PTY).\n 112│\n 113│### Elevation (subnet create/join/show-totp) — **LOCKED (design)**\n    │...\n 152│\n*153│**DECIDE** (LOCKED): DROP `/spt:fork` + `amend-signoff`. (fork near-unused & only meaningful\n 154│harness-hosted; amend-signoff deprecated, folds into a follow-up commune)\n 155│\n 156│**KEEP — FINALIZED** (post-investigation 2026-06-14):\n    │...\n 163│- `/revive` — **DROP.** Daemon owns psyche-loop + liveness (no orphan/dead-listener states to\n*164│  recover); restart = re-`ready`/re-`listen` or `endpoint stop`+bringup. Confirm.\n*165│- `/clear-psyche` — **DROP.** Equivalent = delete + recreate endpoint. Confirm.\n 166│- `/whoami` — **DROP skill**. Core `spt whoami` STAYS in hot-path but → **becomes an alias for\n 167│  `spt endpoint list`** (M12), whose SELF-pin output gains the Self `endpoint description`.\n 168│\n    │...\n 184│  Release on same repo); project fills publish specifics. spt-claude-code's publish = cplugs\n*185│  marketplace + spt-core adapter-registry; **release is light — signing delegated to spt.exe**\n 186│  (not spt-core's two-key/counter runbook).\n*187│- **Docs = same-repo `docs-site/` → GH Pages, mdBook, CI-gated against drift.** Reuse\n 188│  **spt-core's docs CSS / page-layout / styling as a shared transferable theme** (it's strong;\n 189│  carry it across all consumer projects). DOCS-STRATEGY reframed separate-repo→same-repo.\n 190│- AGENTS.md = source of truth + thin `CLAUDE.md`=`@AGENTS.md` stub.\n    │...\n 208│\n*209│## Open threads (grill queue)\n 210│\n 211│- [ ] CI trigger mechanism (git-hook signal vs polling routine)\n 212│- [ ] Handoff framing to perri (package presentation + public-surface-only constraint)\n    │...\n 214│- [ ] experimplate authoring: finalize skeleton + write INSTANTIATE.md\n*215│- [ ] (downstream) confirm `/spt:setup` #9 verify fold-inline vs deferred\n\n# OMP-ADAPTER-PLAN.md\n  77│|---|---|---|\n *78│| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n  79│| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n  80│| `[adapter].shortcut_basename` | `cc` | `omp` |\n  81│| `[adapter].kind` | `harness` | `harness` (unchanged) |\n    │...\n  86│| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n *87│| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n  88│| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n  89│\n  90│### Session/transcript (for the `[digest]` extractor)\n    │...\n 155│\n*156│## Open questions — verify against the live `omp` + `spt` binaries\n 157│\n 158│Per the tips doc discipline (JSON-schema validity ≠ runtime; validate on the live\n*159│binary), confirm before/while building:\n 160│\n 161│1. **RPC session lifecycle across turns** — does a resident `omp --mode rpc` hold\n 162│   one session for many `prompt`s (ready-agent), and can the bridge start it on a\n    │...\n 165│2. **Daemon hosting a resident bridge** — spt-core's broker spawns\n*166│   `[session.self].command` into a PTY; confirm a bridge that itself holds a child\n 167│   `omp --mode rpc` survives as the hosted process (vs the ephemeral per-event\n*168│   psyche model). Decide ready-resident vs live-ephemeral per role.\n 169│3. **dir-encoding exactness** — reproduce omp's `<dir-encoded>` rule precisely for\n 170│   the digest locate (home-relative, `/ \\ :`→`-`); handle `--session-dir` override.\n 171│4. **Hook fallback need** — anything the RPC stream doesn't expose that we still\n\n# traceable-reqs.toml\n  22│# stays green pre-code. A milestone ACTIVATES its requirements by setting their real\n *23│# `required_stages` when work on them begins. Deferred items stay [] until promoted.\n  24│# Never declare the full end-state stage list up front — activate incrementally as evidence\n  25│# lands (add \"int\" at the final integrating wave).\n  26│\n    │...\n  73│title = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\n *74│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n  75│\n  76│[[requirements]]\n  77│id = \"REQ-DIST-HOOK-BINARY\"\n  78│title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n *79│required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n  80│\n  81│[[requirements]]\n  82│id = \"REQ-DIST-MANIFEST-SCHEMA\"\n    │...\n  88│title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n *89│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n  90│\n  91│[[requirements]]\n  92│id = \"REQ-CC-LAUNCHER-BIND\"\n  93│title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n *94│required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n  95│\n  96│[[requirements]]\n  97│id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n  98│title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n *99│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100│\n 101│[[requirements]]\n 102│id = \"REQ-DIST-DIGEST-FETCHER\"\n 103│title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105│\n 106│[[requirements]]\n 107│id = \"REQ-DIST-WHOAMI-JSON\"\n 108│title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109│required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110│\n 111│[[requirements]]\n 112│id = \"REQ-SKILL-LIVE\"\n    │...\n 118│title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120│\n 121│[[requirements]]\n 122│id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n*123│title = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\n*124│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int ...\n 125│\n 126│# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n 127│# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n*128│# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.\n 129│\n 130│[[requirements]]\n 131│id = \"REQ-CI-GATES\"\n    │...\n 177│title = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_erro...\n*178│required_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger...\n 179│\n 180│[[requirements]]\n 181│id = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\n 182│title = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamin...\n*183│required_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child en...\n 184│\n 185│[[requirements]]\n 186│id = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\n 187│title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189│\n 190│[[requirements]]\n 191│id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n    │...\n 196│id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n*197│title = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\n*198│required_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance ro...\n 199│\n*200│# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n*201│# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n 202│\n 203│[[requirements]]\n 204│id = \"REQ-DOCS-SITE\"\n    │...\n 209│id = \"REQ-DOCS-DRIFT\"\n*210│title = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\n 211│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n 212│\n 213│[[requirements]]\n\n# docs/\n## KNOWN-HAZARDS.md\n   5│> it** (`unit`, plus `int` where the failure is cross-process / cross-node). This file exists to\n  *6│> make \"we won't re-break X\" mechanical: an entry without a passing tagged test is an open risk,\n   7│> and `traceable-reqs check` will say so once the hazard is activated.\n   8│\n   9│A hazard earns a place here when it is an invariant you have *paid for once* (a real bug, an\n    │...\n  51│  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n *52│  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  53│  A test asserts the stdin path is honored (no argv `/`-token dependency).\n  54│- **cite:** UPS-fires validation 2026-06-15 (`ups.log`, run A); legacy `new-alarm` SKILL.md\n  55│  MSYS note. Reference only — binding evidence is the tagged test under `REQ-HAZARD-MSYS-PATHCONV`.\n    │...\n 119│- **Failure:** On `/clear` (or `/compact`) CC rotates to a NEW session id, and the SessionStart\n*120│  boundary hook must re-pin the perch to it. The rotation verb is auth-gated on the **departed**\n 121│  session's proof (`AUTH_REFUSED … (need --token or matching --session-id)` without it), and the\n 122│  departed session's env file is already gone when the hook fires. The pre-v0.10.4 hook had two\n 123│  stacked defects: it resolved the perch id via `whoami --json` keyed by the NEW sid — a catch-22\n    │...\n 169│  clear-done signal (SessionStart firing with `source == clear`). (1) ARM: a `{\"checkpoint\":\"v1\",…}`\n*170│  envelope stashes the wake in the translation binary's in-memory `pending_wake` and emits CLEAR-ONLY\n 171│  (`ctrl+s · 50ms · /clear · 50ms · enter · commit`); the wake is withheld. (2) FIRE: the SessionStart hook,\n 172│  on a `clear` boundary, UNCONDITIONALLY self-sends `{\"checkpoint_fire\":\"v1\"}`; the binary emits\n 173│  WAKE-ONLY (`ctrl+s · 50ms · wake · 50ms · enter · commit`) iff a wake is armed, else no-ops. State lives\n    │...\n 186│  `commands_for_wake` (FIRE) + `is_checkpoint_fire` + the stateful `dispatch_event` threading\n*187│  `pending_wake` (the binary is one persistent process per endpoint, reaped on shutdown NOT on\n 188│  `/clear`, so the armed wake survives the clear). `tools/claude-spt/src/hook.rs` —\n 189│  `handle_post_tool_use` (`--force-native` on the arm self-send) + `handle_session_start` (the\n 190│  `clear`-boundary fire: mark idle + self-send `checkpoint_fire` `--force-native`).\n    │...\n 227│  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n*228│  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229│  child that runs harness hooks, it needs the same scrub.\n 230│- **cite:** doyle's F-028 C2 adapter change brief (2026-07-03); spt-core runtime `env_remove` support\n 231│  is public contract (`manifest.schema.json` `SessionRole.env_remove`).\n    │...\n 281│  thrash), replacing the retired `thrash_guard` module + `poll_refused` classifier (both deleted with\n*282│  the resident loop). The deferred give-up/poll-fail live ints die with the loop (nothing to trigger).\n 283│- **cite:** doyle's C3 adapter-half brief (2026-07-03), formalizing our F-h finding; spt-core\n 284│  halves = C3(b) ledger-rate trip @b2351a0 + shutdown tree-kill hardening; second leg = the\n*285│  hall-bf field residual (doyle confirm run, 2026-07-04) + perri's multi-subnet READY_FAIL repro;\n 286│  RESOLVED-BY-DESIGN = the v0.14.0 ephemeral redesign (PSYCHE-EPHEMERAL-PLAN.md, F-030 adapter half).\n 287│\n 288│### 2.7 A delivery answered with ZERO records faults + terminates the translation binary\n    │...\n 381│  the new manifest's `[strings].hook_cmd` is a **bare** binary path → it ran `claude-spt PreToolUse`\n*382│  → `unknown subcommand` → **nonzero exit on every hook event** → CC blocked EVERY tool call\n 383│  (Bash/Read/Write/Glob/AskUserQuestion) **and looped the Stop hook** (the agent could not even end\n 384│  its turn). Zero self-repair is possible from inside the session — every repair lever is itself a\n*385│  blocked tool call; only the operator's `/reload-plugins` recovered it. The v0.9.1 dispatch fix\n 386│  (strip legacy ` hook` suffix) protects the OTHER direction only (new dispatch × old manifest).\n 387│- **Invariant:** a `hook_cmd` SHAPE change in either layer with the other layer stale MUST\n 388│  **degrade, never brick**: (a) `dispatch.sh` tolerates BOTH shapes in BOTH directions (append the\n    │...\n 396│  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n*397│  unconfirmed) closes the window but cannot cover a mid-session CC that has already snapshotted its\n 398│  hooks; the degrade-not-brick exit-0 half covers that remainder.\n 399│- **cite:** live incident 2026-07-01 (perri session, this node; todlando concurs adapter-side —\n 400│  off the spt-core triage doc). Memory: `v092-onnode-update-skew-brick.md`. Reference only —\n    │...\n 416│  dead-hook era swallowed the `/clear` boundary — the first domino of the ball-b wedge. WHY CC's\n*417│  resolver picked the stale copy is **unconfirmed** (CC-internals: resolution lag, a pin, or an\n 418│  orphaned-dir artifact — cf. the orphaned old install dir left by the v0.9.2 update). The\n 419│  adapter cannot control the harness's picker; it CAN control how survivable and how visible a\n 420│  stale pick is.\n    │...\n 435│  the degrade half is §3.1 / `REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP`; the purge and\n*436│  version-surfacing halves are **unbuilt** (guard-slice work, activate on start). Operator\n 437│  guidance meanwhile: after any adapter/plugin update, purge stale plugin-cache copies of sptc\n 438│  from every plugin root on the node (doyle's F-024 advisory — a latent re-brick hazard while any\n 439│  pre-0.10.2 copy remains), and treat \"version installed\" as unproven until the running session\n    │...\n 442│  `F024-AUTHWEDGE-CONFIRMED`; perri ssh-verified the 0.1.8-dispatch-at-boot on ENLYZEAM). The\n*443│  CC-side residual question (why the resolver lagged/pinned) stays open — perri court. Doc\n 444│  evidence = this entry; impl/unit land with the guard slice.\n## OMP-BRIDGE-FIELD-ISSUES.md\n 126│`spt ready` line arrives (`bridge.rs:216`). While the perch is idle the bridge is\n*127│blocked on `ready_out.lines()`; if the omp child dies there, the bridge doesn't\n 128│notice. The perch stays online/idle and only discovers the dead child when the\n 129│next message arrives (then returns an empty reply — see MEDIUM-5).\n 130│\n\n## adr/\n### 0000-template.md\n 20│<!-- What we decided, stated plainly and actively. The specifics: what is built / changed,\n*21│     and the rejected-but-tempting alternative noted so it isn't re-litigated. -->\n 22│\n 23│## Consequences\n 24│\n### 0001-distribution-splits-by-volatility.md\n  42│| **Skeleton plugin** | namespaced `/spt:*` skill *skeletons*, `hooks.json`, SessionStart bootstrap, `plugin.json` | `SaberMage/cplugs` | low |\n *43│| **spt binary** | all logic | spt-core domain (signed, peer-propagated) | high |\n  44│| **CC adapter manifest** | `[digest]` extractor, profiles, strings, hints | spt-core adapter registry (**not** plugin files) | medium |\n  45│\n  46│Concretely:\n    │...\n  74│\n *75│### Open / to-confirm\n  76│\n  77│- **UPS-fires-on-slash-command — CONFIRMED (2026-06-15, ADR-0002 validation).** `UserPromptSubmit`\n  78│  fires on a `/sptc:X` slash-command with the token intact (`prompt:\"/sptc:send doyle\"` literal, hook\n    │...\n  98│  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n *99│  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100│  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101│- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n 102│  `claude plugin update` (skeleton sync only).\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n  90│thin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is\n *91│settled during impl (see Open).\n  92│\n  93│### Hook-side id-resolution — RESOLVED (observed on the 0.6.0 binary)\n  94│\n    │...\n 118│- ✅ **Windows `shell:\"bash\"` command hooks work** — the hook executed via Git-Bash,\n*119│  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper\n 120│  packaging on Windows (no per-OS branch needed for the hook to run).\n 121│- ✅ **Hook stdin schema confirmed**: `{session_id, transcript_path, cwd, permission_mode,\n 122│  hook_event_name, prompt}` — `json_str` targets the right fields; `session_id` sourcing confirmed.\n    │...\n 126│\n*127│## Open / resolved `int`\n 128│\n 129│1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** The canonical poll envelope shipped\n 130│   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed\n 131│   the canonical `<EVENT type=\"msg\" from=…>body</EVENT>\\n` envelope (no `__REPLY_TO__`, no\n*132│   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-\n 133│   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`\n 134│   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.\n 135│2. **Large-drain injection size — RESOLVED (2026-06-15).** CC spills `additionalContext` over ~10k\n    │...\n 146│- `REQ-DIST-HOOKS-API` / `REQ-UPS-INJECTION` gain a concrete, grounded `doc` design here; their\n*147│  `impl` activates when the validated `hooks.json` + wrappers land (gated on Open #1).\n 148│- Supersedes the `ADR-0001` \"hooks.json delegates to `spt api`\" stub with the full wiring; ADR-0001's\n*149│  UPS-injection open item (UPS-fires empirical) is carried here as Open #2.\n 150│- No spt-core dependency for the wiring itself — only the (non-blocking) residual docs publish of the\n 151│  `api poll` frame format, which we can substitute with observed behavior until it lands.\n### 0003-psyche-is-a-constrained-auto-approving-companion.md\n 58│\n*59│- The Psyche cannot perform non-file actions even if a prompt asks it to — by design. If a future\n 60│  Psyche capability genuinely needs a wider tool (e.g. to `git commit` its own context), that is a\n 61│  deliberate change to `sandbox_flags()` + this ADR, not an accident.\n 62│- `--dangerously-skip-permissions` reads alarming in isolation; it is safe **only** in combination\n   │...\n 64│  auto-approve, and never drop the cap while keeping skip-permissions.\n*65│- These flags mirror legacy owl **verbatim**. If a future Claude Code renames `--tools` /\n 66│  `--disable-slash-commands`, the Psyche turn breaks — but that risk is shared with the proven\n 67│  sibling, and surfaces loudly (no commune produced).\n 68│- `doc`-stage evidence for `REQ-SKILL-LIVE` (the Psyche runner) and `REQ-HAZARD-PSYCHE-PERMS-DEADLOCK`\n### 0004-checkpoint-via-self-send-loopback.md\n 52│   adapter-side drop reading. The verb returns durable context + the freshest un-synthesized\n*53│   `<pending-commune>` (trigger stripped core-side), race-free.\n 54│\n 55│**Load-bearing invariant:** checkpoint authors the commune **inline pre-clear (Shape 1)** and never\n*56│defers authoring to a post-clear resume-Self refresh (Shape 2). Shape 2 reintroduces force #3's stale\n*57│gap. Any future refactor that routes checkpoint through boundary/refresh must re-open this ADR.\n 58│\n 59│**Rejected alternatives:**\n 60│- *Binary watches the commune file* — wrong actor (no FS knowledge), and TOCTOU against the daemon's\n   │...\n 64│- *Adapter-side raw-file SessionStart inject* — TOCTOU against core's pulse-delete; spt-core owns the\n*65│  race-free re-seed (the `psyche-download` `<pending-*>` append).\n 66│\n 67│## Consequences\n 68│\n   │...\n 80│- **The indirection is deliberately documented here** (an agent messaging itself to trigger its own\n*81│  clear is non-obvious) so it is not \"simplified\" away by a future reader.\n### 0005-name-unification.md\n 26│- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n*27│- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n 28│- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29│\n 30│End state: `claude-spt` everywhere spt-core sees it; `spt` everywhere the Claude Code user types a skill; `cc` for the spawned-endpoint launcher.\n   │...\n 39│- **The repo rename changes the adapter install dir** (`adapters/_github/SaberMage-spt-claude-code` → `…SaberMage-claude-spt`, derived from the repo path), so existing `--release` installs must be re-added. Acceptable: one end user today. GitHub redirects old links; `[update].repo`, the README install chains, CI, and the package scripts must be updated in the same move.\n*40│- Two names is the floor, not a failure to unify — it reflects a real constraint (adapter ≠ `spt`), recorded here so a future reader does not \"simplify\" the adapter to `spt` and collide with spt-core.\n 41│- The `spt` plugin succession keeps its existing gate (owl retirement); this ADR does not change that timing, only commits the target token split.\n### 0006-one-command-update-and-consolidated-binary.md\n  4│\n *5│accepted (2026-06-24) — extends ADR-0001 (distribution splits by volatility). Some pieces depend on spt-core capabilities not yet shipped (the \"doyle asks\" below); those parts are decided in shape but gated on delivery.\n  6│\n  7│**D1 realized (v0.9.0, 2026-06-28).** Ask #1 (\"fold in `claude-spt hook <event>` if generic hook\n  8│dispatch lands\") shipped — but NOT as the proposed `spt api run-hook`. doyle RESOLVED it as\n   │...\n 36│- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37│- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38│\n 39│### Binary consolidation\n 40│\n   │...\n 42│\n*43│- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n*44│- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45│\n 46│The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n 47│\n*48│### The spt-core asks (findings to doyle — decided in shape, gated on delivery)\n 49│\n 50│1. **Generic hook dispatch** — `spt api run-hook <adapter> <event>`: spt-core executes the adapter's handler for a hook event (from the manifest), so the plugin's `hooks.json` can pre-wire all events to one generic stub and hook *logic* rides `spt adapter update`. Eliminates the plugin-channel churn that adding this wave's PostToolUse hook forced.\n*51│2. **Composite `[update]`** — `gh_release` (pull the `.spt`) **plus** a delegated post-step in the same `spt adapter update`, so the plugin update is automated (the only manual residual becomes `/reload-plugins`). Two sub-requirements: the post-step must run **unconditionally** (the plugin can change when the adapter version did not), and its return value must be able to flag **\"changed\"** so `spt adapter update` still prints `[update].message` even on an adapter no-op.\n 52│3. **Translation seam takes a command** — `[message-idle-translation-binary]` accept a command/subcommand (or a default-on-no-args convention) instead of a bare `path`, so `translate` can fold into the consolidated binary.\n 53│\n 54│## Consequences","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087167351}}
{"type":"message","id":"940b9213","parentId":"95261415","timestamp":"2026-07-15T03:46:15.836Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing traceable request entries**"},{"type":"toolCall","id":"call_wKjJYQNCNj1t9jIpzpCLo5AX|fc_0ca4af36969c80fa016a570287c04481988ec7ad4a0519f265","name":"read","arguments":{"i":"Reading requirement registry start","path":"traceable-reqs.toml","selector":"1-220"}},{"type":"toolCall","id":"call_i8pUlPiq9GiBOib3EL7lcaIA|fc_0ca4af36969c80fa016a570287c0588198892b2d3143988f57","name":"read","arguments":{"i":"Reading requirement registry end","path":"traceable-reqs.toml","selector":"430-495"}},{"type":"toolCall","id":"call_TGvGV7MWW0LFTksA2F3rFMAK|fc_0ca4af36969c80fa016a570287c060819886ce066ba7d579a1","name":"read","arguments":{"i":"Inspecting manifest architecture","path":"adapter/omp-spt.toml","selector":"1-40,370-510,650-760"}},{"type":"toolCall","id":"call_9hOOvZcTlaLybtHACMVsLz34|fc_0ca4af36969c80fa016a570287c0648198a41226a0346895a8","name":"read","arguments":{"i":"Inspecting native extension","path":"adapter/strings/omp-spt.mjs","selector":"1-230"}},{"type":"toolCall","id":"call_DfYDIz7zdCyhaaRrGc9HbgrC|fc_0ca4af36969c80fa016a570287c06881989c707bb8972a2746","name":"read","arguments":{"i":"Inspecting OMP extension tests","path":"tests/omp-extension.mjs","selector":"1-180"}},{"type":"toolCall","id":"call_StQLIlJtr9R16NTIGHMhtRRv|fc_0ca4af36969c80fa016a570287c0708198b326ac9a97085e37","name":"read","arguments":{"i":"Inspecting manifest tests","path":"tests/manifest-shortcut.sh","selector":"1-180"}},{"type":"toolCall","id":"call_SCnyv2FSHpWaHhM60h6NdFzT|fc_0ca4af36969c80fa016a570287c0748198b45a645bf44f03ee","name":"read","arguments":{"i":"Inspecting bridge acceptance","path":"tests/bridge-acceptance.sh","selector":"1-240"}},{"type":"toolCall","id":"call_oJpBZpbDZMAXXUagtIIJpP8U|fc_0ca4af36969c80fa016a570287c0788198b52b351e1d38cfbc","name":"read","arguments":{"i":"Inspecting bridge hardening implementation","path":"tools/claude-spt/src/bridge.rs","selector":"1-180,180-430,430-620"}},{"type":"toolCall","id":"call_3iQjctgeU7kJTGidUBwaikjJ|fc_0ca4af36969c80fa016a570287c07c8198b39ea41a17bde31f","name":"read","arguments":{"i":"Inspecting OMP binary dispatch","path":"tools/claude-spt/src/main.rs","selector":"1-190"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":39776,"output":361,"cacheRead":68096,"cacheWrite":0,"totalTokens":108233,"reasoningTokens":24,"cost":{"input":0.19888000000000003,"output":0.010830000000000001,"cacheRead":0.034048,"cacheWrite":0,"total":0.24375800000000003}},"stopReason":"toolUse","timestamp":1784087167387,"responseId":"resp_0ca4af36969c80fa016a57027f87108198b4f3931bc728e3f0","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a570281344481989bcf5ac1d2d5cc67","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKHteBD_aHdfEAYTRcftVGyJk2SdxWikUVbc3iaUdUgwBgnvl0mm8mYCGzdl70q7P1xjdeQfdy7dBg3KCDIXI62Qn0hEq5n_z2Uw2VtJhb8G-r1nXBb1qjtWfHTnNdsj-Mh6B2tkXTty1evGklT5VIvkNCQFjMpAR-tpwZiHg6KiKGDRCahTo55AgtNK7G_soPVeNAyh_Ovr6dgDdIA5ZvKW1hz0SlIC8PrI2DPsB9jHJMzCRJ2Gt_qqELyyZvjO085e3v-64B4mwYrqgfLmKH9nOf1nTyv7Agj1i2SzDaMSZNW3dEBSK0IWGaDnP8HowYmhDNIFcmdjo4ceapl6s3Lt8wE9n13npaNk5wBK_2S1zx6UY1JBoQGwwL22KqsqsnPMmevqOe7g4UZDWYNkkZrjdjBmKzCg64P_dgsS6uVSYg52K6BFVtZlqbjUM1-6ZlfxZkYCzsb6D9dpYiwKjKi4OU5QviRC9XmnVKvwT2dm1zhW0LFGKYcfEgVkvzTZGCZGKIAdmTnfAqmQo2ZtKUCRF5JT6_CawZlbyxPKFyz8bLZweZtLxuaVtwChC7cX6to2lvCHUWf62g_YzGfZ-EhABncY5ihS9pmZUFdxs8VifqxSbodDfCIa3C0V29KUmIOqbZhMfqTkxxcI6ojtYMN68z5PtFvwSAZaX4j0Vbksf5wepWscl9U20yKIHiB7GvOKPhC-plRWOT4dvt86jM6kDJiEkBQLLLOOZm6ATAKdYLgKSUR07dmdSWMZvD_M9zUl6Ytet-7iNITHRzNcVVlTXnKBytuG3RT_OkO6QgN0cFtoBYbVDX5M06hgHQkxZ1gB_VbEvq8G_SGTJMx2EthCMI5hGLledJ6dQZkD7mCivkOIZNmpw_ilA_sCA4OWrGz5wIPqfFRjMj7FPYsDvpHp44Rz10l2nC1UXpvx4hjKqUyT6vonFyN6SafBCiiq6xb1q0SZX2O5aYupCNg8P5c9tbH1N6TXpepXaHLScdlDWWazebZ-NLA4_GEuffVnZ7o","summary":[{"type":"summary_text","text":"**Analyzing traceable request entries**"}]},{"id":"fc_0ca4af36969c80fa016a570287c04481988ec7ad4a0519f265","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading requirement registry start\",\"path\":\"traceable-reqs.toml\",\"selector\":\"1-220\"}","call_id":"call_wKjJYQNCNj1t9jIpzpCLo5AX","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c0588198892b2d3143988f57","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading requirement registry end\",\"path\":\"traceable-reqs.toml\",\"selector\":\"430-495\"}","call_id":"call_i8pUlPiq9GiBOib3EL7lcaIA","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c060819886ce066ba7d579a1","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting manifest architecture\",\"path\":\"adapter/omp-spt.toml\",\"selector\":\"1-40,370-510,650-760\"}","call_id":"call_TGvGV7MWW0LFTksA2F3rFMAK","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c0648198a41226a0346895a8","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting native extension\",\"path\":\"adapter/strings/omp-spt.mjs\",\"selector\":\"1-230\"}","call_id":"call_9hOOvZcTlaLybtHACMVsLz34","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c06881989c707bb8972a2746","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting OMP extension tests\",\"path\":\"tests/omp-extension.mjs\",\"selector\":\"1-180\"}","call_id":"call_DfYDIz7zdCyhaaRrGc9HbgrC","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c0708198b326ac9a97085e37","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting manifest tests\",\"path\":\"tests/manifest-shortcut.sh\",\"selector\":\"1-180\"}","call_id":"call_StQLIlJtr9R16NTIGHMhtRRv","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c0748198b45a645bf44f03ee","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting bridge acceptance\",\"path\":\"tests/bridge-acceptance.sh\",\"selector\":\"1-240\"}","call_id":"call_SCnyv2FSHpWaHhM60h6NdFzT","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c0788198b52b351e1d38cfbc","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting bridge hardening implementation\",\"path\":\"tools/claude-spt/src/bridge.rs\",\"selector\":\"1-180,180-430,430-620\"}","call_id":"call_oJpBZpbDZMAXXUagtIIJpP8U","name":"read"},{"id":"fc_0ca4af36969c80fa016a570287c07c8198b39ea41a17bde31f","type":"function_call","status":"completed","arguments":"{\"i\":\"Inspecting OMP binary dispatch\",\"path\":\"tools/claude-spt/src/main.rs\",\"selector\":\"1-190\"}","call_id":"call_3iQjctgeU7kJTGidUBwaikjJ","name":"read"}]},"duration":8447.85890000005,"ttft":1826.915399999998,"contextSnapshot":{"promptTokens":107872,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wKjJYQNCNj1t9jIpzpCLo5AX|fc_0ca4af36969c80fa016a570287c04481988ec7ad4a0519f265","toolName":"read","startedAt":"2026-07-15T03:46:15.836Z","args":{"path":"traceable-reqs.toml"},"intent":"Reading requirement registry start"},"id":"fc65337f","parentId":"940b9213","timestamp":"2026-07-15T03:46:15.836Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_i8pUlPiq9GiBOib3EL7lcaIA|fc_0ca4af36969c80fa016a570287c0588198892b2d3143988f57","toolName":"read","startedAt":"2026-07-15T03:46:15.836Z","args":{"path":"traceable-reqs.toml"},"intent":"Reading requirement registry end"},"id":"8a582d1e","parentId":"fc65337f","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TGvGV7MWW0LFTksA2F3rFMAK|fc_0ca4af36969c80fa016a570287c060819886ce066ba7d579a1","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"adapter/omp-spt.toml"},"intent":"Inspecting manifest architecture"},"id":"66b2a216","parentId":"8a582d1e","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9hOOvZcTlaLybtHACMVsLz34|fc_0ca4af36969c80fa016a570287c0648198a41226a0346895a8","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"adapter/strings/omp-spt.mjs"},"intent":"Inspecting native extension"},"id":"ee508ffb","parentId":"66b2a216","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DfYDIz7zdCyhaaRrGc9HbgrC|fc_0ca4af36969c80fa016a570287c06881989c707bb8972a2746","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"tests/omp-extension.mjs"},"intent":"Inspecting OMP extension tests"},"id":"4e650cbe","parentId":"ee508ffb","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_StQLIlJtr9R16NTIGHMhtRRv|fc_0ca4af36969c80fa016a570287c0708198b326ac9a97085e37","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"tests/manifest-shortcut.sh"},"intent":"Inspecting manifest tests"},"id":"8c6f1c95","parentId":"4e650cbe","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_SCnyv2FSHpWaHhM60h6NdFzT|fc_0ca4af36969c80fa016a570287c0748198b45a645bf44f03ee","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"tests/bridge-acceptance.sh"},"intent":"Inspecting bridge acceptance"},"id":"de33d0b1","parentId":"8c6f1c95","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_oJpBZpbDZMAXXUagtIIJpP8U|fc_0ca4af36969c80fa016a570287c0788198b52b351e1d38cfbc","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"tools/claude-spt/src/bridge.rs"},"intent":"Inspecting bridge hardening implementation"},"id":"f88048fb","parentId":"de33d0b1","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3iQjctgeU7kJTGidUBwaikjJ|fc_0ca4af36969c80fa016a570287c07c8198b39ea41a17bde31f","toolName":"read","startedAt":"2026-07-15T03:46:15.837Z","args":{"path":"tools/claude-spt/src/main.rs"},"intent":"Inspecting OMP binary dispatch"},"id":"4bece707","parentId":"f88048fb","timestamp":"2026-07-15T03:46:15.837Z"}
{"type":"message","id":"ac64c777","parentId":"4bece707","timestamp":"2026-07-15T03:46:15.843Z","message":{"role":"toolResult","toolCallId":"call_TGvGV7MWW0LFTksA2F3rFMAK|fc_0ca4af36969c80fa016a570287c060819886ce066ba7d579a1","toolName":"read","content":[{"type":"text","text":"# omp-spt — oh-my-pi (omp) adapter manifest for spt-core.  [fork of SaberMage/claude-spt]\n#\n# Forked from claude-spt (the Claude Code adapter) and retargeted to oh-my-pi\n# (https://github.com/can1357/oh-my-pi). Unlike Claude Code, omp exposes a native\n# host-drives-agent control surface (`omp --mode rpc`), so the omp integration is an RPC\n# bridge, NOT a hook-scraping plugin — see OMP-ADAPTER-PLAN.md (architecture decision: Style A).\n#\n# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile\n# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin plugin skeleton.\n# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the\n# marketplace `plugin.json` version. The per-version changelog below is INHERITED claude-spt\n# lineage (retained for seam provenance); omp-spt's own history starts at version 0.1.0.\n#\n# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/\n# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,\n# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the\n# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).\n#\n# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not\n# yet built, or needs a capability not on the public surface — see notes inline):\n#   * [session.<role>] for signoff/notif — still-deferred live-agent roles (no field forcing yet).\n#     psyche_init/psyche_resume authored at v0.14.0 (F-030 ephemeral shim); echo_commune + [history]\n#     authored at v0.15.2 (field-forced: core 0.27/0.28 spawns the summarizer and 3-strikes the\n#     psyche host when the role is missing — flynn 2026-07-07). [session.self] IS authored below\n#     (the endpoint-run/bind bringup that hosts a fresh CC session — the M12 `cc` launcher target).\n#   * [update]                      — DECLARED below via the `gh_release` avenue (the published\n#     `--release` machinery, operative on the current min_spt_core floor): spt-core auto-updates the\n#     adapter from THIS repo's GitHub releases on `spt adapter update`, with NO Ed25519 signing infra\n#     (closes the F-005 parked `file_pull`/`delegated` legs). The COMPOSITE `[update.post]` step (the\n#     delegated plugin-reconcile) lands in spt-core v0.16.0 (ADR-0029) — wired at the adapter v0.8.0\n#     cut. (NB: \"v0.8.0\" everywhere in THIS arc = the claude-spt ADAPTER version, NOT an spt-core\n#     version — spt-core's next is v0.16.0; doyle 2026-06-25.)\n#\n# [impl->REQ-DIST-MANIFEST-SCHEMA]\n\n[adapter]\nname = \"omp-spt\"\nkind = \"harness\"\n# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves\n…\n# reported NO_PERCH for EVERY peer send though the message DELIVERED (confirm-back lied; self-send was\n# unaffected as it ignores its result). Root-caused offline via an isolated hook-binary rig (peer send\n# returned raw=\"\" while stderr carried SENT:doyle). Fix: HookEnv::spt_send captures stdout+stderr merged;\n# scan_and_dispatch classifies through it (REQ-HAZARD-SEND-STATUS-STDERR). ALSO folds doyle's two-slice\n# fix: the commune brief + echo-commune summarizer now teach the <project-context>/<live-context>\n# envelope so core routes context per-project instead of dumping all in the live tier\n# (REQ-COMMUNE-CONTEXT-TIER-SLICING). Binary-only; floor unchanged (0.27.0). Send-status-on-stderr filed\n# to doyle (undocumented in the harness-contract/CLI reference).\n# omp-spt starts fresh at 0.1.0 — the version history above is inherited claude-spt lineage.\n# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n# breach exits loud BEFORE bind, so a failed bringup cannot poison the session ledger), and a\n# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n# uses attach-default `spt endpoint run --adapter omp-spt --id <id> --create`, not `--start`, so the\n# operator sees the broker-held harness PTY and its bringup log instead of being instructed into a\n# silent start-only launch. The gh_release update avenue now pins transport=\"gh\", forcing the\n# authenticated GitHub CLI path for release discovery/asset fetch so private or visibility-flipped\n# repos update through the user's existing gh auth. Floor unchanged.\n# 0.1.3 (2026-07-13): issue #2 root fix. Native interactive omp is now the broker PTY leader and a\n# packaged OMP extension supplies SPT bind/listen/state/reply integration. The prior attach guidance\n# exposed only the headless RPC bridge's logs because `omp --mode rpc` creates no TUI. Floor unchanged.\nversion = \"0.1.3\"\n# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS\n# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that\n# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify\n# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name\n# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset\n# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the\n# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)\n# Floor BUMPED to 0.15.0 (2026-06-24, the checkpoint-commune wave / v0.7.0): T2's checkpoint self-send\n# needs `spt send --json-payload` (the opaque-metadata send axis) and T3's resume pull needs the\n# `spt api psyche-download <id>` verb — BOTH land in spt-core v0.15.0 (counter 34, Latest). The\n# multi-line frame (T1) works on any node, but the wave ships as one unit, so the honest floor is the\n# verb floor. An older node can't fire checkpoint or pull resume context, so declaring 0.15.0 is honest.\n# Floor BUMPED to 0.16.0 (2026-06-26, the UNIFY-CONSOLIDATE cut / v0.8.0): D2's [update.post] composite,\n# D3's [message-idle-translation-binary].command, and the {adapter_dir}/{adapter_name} substitution +\n# lazy [strings] subst ALL land in spt-core v0.16.0 (counter 35, Latest; ADR-0029). An older node can't\n# read update.post or the command seam, so declaring 0.16.0 is the honest floor.\n# 0.9.1 (2026-06-28): BUGFIX patch. (1) hook_cmd → bare binary path + dispatch caches it QUOTED, fixing\n# the `hook: command not found` env-file regression that polluted every Bash call + killed the hook\n# cache. (2) post-update now refreshes the cplugs marketplace cache (`plugin marketplace update`)\n# before install — fixes \"Plugin omps not found in marketplace cplugs\" on a stale cache — and prints a\n# reworded, cleaner update notice without leaking CC's output onto the [update.post] arbiter channel.\n# (3) the ccs profile carries the U6 -n/RC flags (session.self + session.resume), which its v0.8.0\n# command override had dropped. All bugfix; no new seam → floor stays 0.16.0.\n# Floor BUMPED to 0.19.0 (2026-07-01, the digest-fetcher cut / v0.10.0): [digest] strategy=\"fetcher\"\n# (REQ-DIGEST-FETCHER-STRATEGY, spt-core side) + the [env] direction=\"read\" capture/fill seam BOTH land\n# in spt-core v0.19.0 (counter 38). An older node can't parse strategy=\"fetcher\" nor fill the\n# {CLAUDE_CONFIG_DIR} read-var, so declaring 0.19.0 is the honest floor (todlando GO, cluster-A\n# consumable; doyle gate-verified).\n# Floor BUMPED to 0.20.0 (2026-07-02, the {node} cut / v0.11.0): the {node} substitution key for\n# [session.*] commands lands in spt-core v0.20.0 (counter 40; doyle release ping). An older node\n# would pass the literal `{node}` (the shim guards it, degrading to hostname detection), but the\n# feature this cut ships IS the advertised-label fill — declaring 0.20.0 is the honest floor.\n# Floor BUMPED to 0.23.0 (2026-07-03, the C3 thrash-guard cut / v0.13.0): the psyche-host thrash\n# guard's loud give-up (PSYCHE_HOST_GIVE_UP, nonzero exit) composes through spt-core's\n# psyche_host_error surface — which exists on older cores — BUT the coupled crashloop story it was\n# ordering-proved against (adapter fires first, core's >=10/60s ledger-rate backstop catches what\n# the streak-reset forgives) only holds on spt-core >= 0.23.0 (counter 43, where C3(b) landed). The\n# floor guarantees the pairing is present; a lone give-up on an older core still degrades safely (it\n# just exits loud, which any residency-managed core handles).\n#\n# Floor BUMPED to 0.25.0 (2026-07-04, the F-030 psyche-ephemeral cut / v0.14.0): the ephemeral-shim\n# driver — per-event [session.psyche_resume] invocation, the core-minted psyche-own sid, the\n# core-written {psyche_context_file} mind file, and the exit-95 reseed contract — ALL land in\n# spt-core v0.25.0 (counter 45, tag v0.25.0, core sha c01fe6ee29c2a19c4a6ebc72f11c471cab7d13d0;\n# doyle GO + deployah publish ping 2026-07-04). On a below-0.25.0 core the shim does one seed turn\n# then STARVES (the daemon never re-invokes) — the silent-degradation class that held this floor as\n# a hard PUBLISH BLOCKER until the core shipped (the v0.13.2 bad-ship lesson). The shim was\n# RC-certified against the exact published v0.25.0 bytes before this pin (windows sha256\n# 0202742115d5bb9109b6abfa336b4d7ec54bb6a99fc1d7561bb83c17016a8491 == the RC-tested anchor).\nmin_spt_core_version = \"0.27.0\"\nhostable_types = [\"LiveAgent\", \"ReadyAgent\", \"Worker\"]\n# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for\n# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking\n# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms\n# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the\n# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`\n# land. omp's host process basename is `omp`; the RPC bridge (S2) is `omp-spt`. [impl->REQ-SKILL-LIVE]\nhost_binaries = [\"omp\", \"omp-spt\"]\n# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is\n# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher\n# brand is DECOUPLED from the plugin name (`omps`, skills `/omps:*`): the s/omps/spt/ succession seam\n# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh\n# (unit->REQ-DIST-SHORTCUT-BASENAME).\nshortcut_basename = \"omp\"\n\n# ── [update] ───────────────────────────────────────────────────────────────────────────────────\n# Adapter self-update via OUR GitHub releases (gh_release avenue). `spt adapter update`\n# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —\n# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default\n# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared\n# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014\n# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a\n# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.\n# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no\n# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue\n# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI\n# endpoint route alongside /omps:live. (When doyle ask #2 lands, the post-update step can also trigger\n# this print on a plugin-only change; copy stays valid.)\n# [impl->REQ-DIST-ADAPTER-RELEASE]\n[update]\navenue = \"gh_release\"\nrepo = \"BigscreenVR/omp-spt\"\ntransport = \"gh\"\nmessage = \"\"\"\n**omp-spt updated.** The native OMP extension + extractors refreshed in place — no reload step: OMP\nloads the packaged extension fresh on each endpoint bringup.\n\n- Running endpoints keep the OLD hosting path until restarted: `spt endpoint stop <id>` then\n  `spt endpoint run --adapter omp-spt --id <id>` picks up the new one.\n- Bring up a fresh Librarian endpoint: `spt endpoint run --adapter omp-spt --id <id> --create`.\n\"\"\"\n\n# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────\n# spt-core runs this AFTER the gh_release pull+re-register, UNCONDITIONALLY (even on an adapter no-op),\n# completing the one lever `spt adapter update claude-spt` = pull the adapter .spt + reconcile the\n# cplugs plugin. `command` resolves the consolidated binary from the install dir via {adapter_dir}\n# (survives updates). spt-core pipes a one-line JSON context on the command's stdin\n# ({adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}; additive —\n# the subcommand ignores unknown keys) and reads its STDOUT to arbitrate the notice: the reserved\n# sentinel `!!update-message!!` (which post-update emits on a successful reconcile) FIRES the\n# [update].message above; custom text would supersede it; empty prints nothing. Exit code is\n# orthogonal and the step is FAIL-ISOLATED (a committed pull is never rolled back on our failure).\n# `self_verifies = false`: attestation-only, gates nothing yet (no signing infra). The reconcile logic\n# rides `spt adapter update` (it lives in the binary), so it stays current without a separate step.\n# [impl->REQ-DIST-UPDATE-MESSAGE]\n[update.post]\ncommand = \"{adapter_dir}/omp-spt post-update\"\nself_verifies = false\n\n# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────\n# CC hook event → the opaque `api …` command it fires (the representative action; some events do\n# more — e.g. UserPromptSubmit also marks busy + injects skills, PostToolUse self-sends a checkpoint).\n…\n\n…\n[session.psyche_init]\n…\n# I/O (W4, contract 9e6863d0): {psyche_context_file} (ALWAYS filled — a path to the core-written mind\n# file in the psyche's nested perch dir) rides argv; the shim READS it to get the mind (W4 kills the\n# win32 ~32k cmdline cap — ~20KB minds used to ride argv). The per-turn EVENT rides the shim's STDIN\n# (core feeds it — a bare pulse = `<pulse/>`); the psyche's reply rides the shim's CAPTURED STDOUT (the\n# daemon parses intents from it). The daemon runs this role NON-detached, stdin-fed, stdout-captured,\n# bounded (turn.rs run_bounded_stdin) — NOT the old Stdio::null detached spawn; hence detach=false.\n# Reseed is signalled by the shim's reserved EXIT CODE 95 (session-gone); every other nonzero keeps\n# custody. The `<parent>-psyche` perch + the psyche-custody.json sid record are CORE-owned — the shim\n# mints nothing, writes nothing to custody. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# NO cwd TEMPLATE (v0.14.2) — same {psyche_dir} retirement as psyche_init above; the shim owns the\n# cwd (context-file parent dir). [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n[session.psyche_resume]\ncommand = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\ndetach = false\n# Identity-env scrub on BOTH psyche roles (F-028 C2). [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\nenv_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]\nkeys = [\"id\", \"session_id\", \"psyche_context_file\"]\n\n# ── [session.self] ─────────────────────────────────────────────────────────────────────────\n# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter\n# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch\n# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /omps:ready|live uses).\n# This is what the `cc-<id>` shortcut (shortcut_basename=\"cc\") bakes into a non-interactive launch.\n#\n# command is the `claude-spt launch` SHIM (0.10.3+, the delivered doyle-ask-#4 stretch): it spawns\n# `claude -n \"<id> @ <node> (<project>/)\" --remote-control <id>--<node>--<project-token> --dangerously-skip-permissions`\n# (unix: exec — claude IS the PTY leader; windows: spawn+wait, stdio inherited). CC mints its OWN\n# session id internally (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded\n# on argv. The endpoint {id} is threaded THREE ways (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the\n# BIND (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see\n# [env.SPT_ENDPOINT_ID] below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY\n# name (`-n \"<id> @ <node> (<project>/)\"`, shown in the prompt box / `/resume`) AND the REMOTE-CONTROL\n# channel (`--remote-control <id>--<node>--<project-token>`) — so a FRESH spt-hosted endpoint is\n# `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a RESUMED one ([session.resume]).\n# WHY A SHIM: the node label arrives via the `--node {node}` fill (spt-core v0.20.0, the adopted\n# doyle-ask-#4 key — the daemon's ADVERTISED label, matching the rest of the spt surface), and the\n# <project> folder is the launch-cwd basename (no {project} fill exists) — both computed ON-NODE; and\n# tokenize-then-fill (v0.8.2+) can never yield a space-carrying single argv element — the shim passes\n# each name as one clean argv element, and falls back to on-node hostname detection when the {node}\n# fill is blank or literally unsubstituted (F-013 gap class). The DISPLAY name uses the raw project\n# folder (spaces/parens/slash fine). RC gets the tokeny `<id>--<node>--<project-token>` form with a\n# CHARACTER-SAFE project token (alphanumeric + `-`/`_`, other runs → `_`; CC's auto-generated RC names\n# are hostname-prefixed tokens; space-safety of an explicit RC name is unverifiable from the public\n# surface). Unknown project ⇒ drop only its suffix; unknown node ⇒ bare `<id>` for both (the pre-0.10.3\n# shape); `spt rc <id>` attaches by ENDPOINT id either way (broker-internal, not CC's RC name).\n# `--dangerously-skip-permissions` stays required for the spt-hosted bringup:\n# the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would deadlock\n# the launch with no operator to approve it (the shim passes it through).\n# [impl->REQ-DIST-SHORTCUT-BASENAME]\n#\n# ── omp-spt: native OMP owns the broker PTY ─────────────────────────────────────────────────────\n# Issue #2 proved that the headless RPC bridge can expose only bridge logs when an operator attaches.\n# Native `omp` is therefore the session process; its packaged extension owns SPT bind/listen/state/\n# reply integration through OMP's public extension API. See ADR-0007. [impl->REQ-OMP-NATIVE-TUI]\n[session.self]\ncommand = \"omp --extension {adapter_dir}/strings/omp-spt.mjs\"\nkeys = []\n\n# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────\n# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run\n# --resume <session>` or the picker's \"Resume from history\") AND this role is declared. OMITTING it\n# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented\n# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by\n# {session_id} + cwd.\n#\n# Command = the same `claude-spt launch` shim with `--resume {session_id}` (0.10.3+): it spawns\n# Claude Code's NATIVE-RESUME verb `claude -r <session_id> -n \"<id> @ <node> (<project>/)\"\n# --remote-control <id>--<node>--<project-token> --dangerously-skip-permissions` (-r/--resume reloads\n# the real transcript by id).\n# The endpoint {id} is threaded TWO ways: as the DISPLAY name (U6 — shown in the prompt box /\n# `/resume`, matching [session.self] so a fresh and a resumed endpoint present identically) AND as\n# the REMOTE-CONTROL name — both node-suffixed by the shim, same shapes + degrade rule as\n# [session.self]. (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id}\n# attaches here; `spt rc <id>` still addresses the broker PTY by ENDPOINT id.)\n# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]\n# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the\n# resumed id) + {id} (the endpoint). Satisfies REQ-DIST-SESSION-RESUME (native-resume seam) +\n# REQ-DIST-RC-STARTUP (display+RC parity with [session.self]); the shim's command shape is unit-\n# tested in tools/claude-spt (launch.rs) and the manifest shape in tests/manifest-shortcut.sh.\n# omp-spt resume keeps the same native TUI and extension, while `-r` asks OMP to reload the real\n# transcript. The extension binds the resumed OMP session id to the current SPT endpoint.\n[session.resume]\ncommand = \"omp -r {session_id} --extension {adapter_dir}/strings/omp-spt.mjs\"\nkeys = [\"session_id\"]\n\n# ── [session.echo_commune] — the bounded end-of-session summarizer (v0.15.2) ───────────────────────\n# Published contract: \"when a session ends without a signoff, spt-core runs a bounded summarizer\n# over the session's history so the context delta is captured anyway\"; on core 0.27/0.28 the\n# commune-sync per-event turn ALSO spawns it, and a MISSING role 3-strikes the whole psyche host\n# (flynn 2026-07-07 field pin — psyche_host_error, checkpoints dead). The shim accepts history on\n# stdin when fed (kept for the future published wiring — doyle field pin 2026-07-07: core does NOT\n# stdin-feed [history] today) and otherwise SELF-LOCATES the transcript, most-authoritative rung\n# first: the perch info.json read_env.CLAUDE_CONFIG_DIR (doyle-sanctioned; the role spawn does not\n# inherit the session env, so ccs-relocated transcripts are invisible to the env rung) → the env →\n# ~/.claude/projects → every ~/.ccs/instances/<account>/projects. Bounded whole-line tail, ONE\n# headless `claude -p` turn (psyche-parity sandbox, Read-only tools — a summarizer never writes),\n# delta on stdout for core to ingest. Locate-miss is GRACEFUL (no-delta marker + exit 0 — a v0.15.2\n# exit-1 here rode the psyche host's 3-strike budget and latched two hosts); only real claude\n# faults stay loud nonzero (ECHO_COMMUNE_FAIL:).\n# recursion_guard_env: core sets SPT_ECHO_COMMUNE on the shim; the claude it spawns inherits it and\n# `claude-spt hook` BAILS (exit 0, zero api calls) under it — the inner session can never\n# seed/bind/rebind a perch (no summarizer-of-summarizer loops). Identity-env scrub rides too\n# (F-028 C2 discipline, same as both psyche roles). [impl->REQ-SESSION-ECHO-COMMUNE]\n# S4 (omp-spt): the summarizer is `echo-commune-omp` — self-locates the OMP session JSONL (the\n# digest-omp locator), bounded whole-line tail, ONE read-only headless `omp -p` turn (the prompt\n# rides an @file), delta on stdout. Locate-miss stays GRACEFUL (no-delta marker + exit 0). The\n# recursion_guard_env is kept for parity, but the omp path cannot loop: an `omp -p` turn runs no\n# spt hook layer at all (--no-extensions; the RPC bridge owns lifecycle).\n[session.echo_commune]\ncommand = \"omp-spt echo-commune-omp --id {id} --session-id {session_id}\"\n…\n# {CLAUDE_CONFIG_DIR} dependency. Missing transcript = HISTORY_FAIL: + exit 1, never silent-empty."}],"details":{"displayContent":{"text":"# omp-spt — oh-my-pi (omp) adapter manifest for spt-core.  [fork of SaberMage/claude-spt]\n#\n# Forked from claude-spt (the Claude Code adapter) and retargeted to oh-my-pi\n# (https://github.com/can1357/oh-my-pi). Unlike Claude Code, omp exposes a native\n# host-drives-agent control surface (`omp --mode rpc`), so the omp integration is an RPC\n# bridge, NOT a hook-scraping plugin — see OMP-ADAPTER-PLAN.md (architecture decision: Style A).\n#\n# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile\n# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin plugin skeleton.\n# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the\n# marketplace `plugin.json` version. The per-version changelog below is INHERITED claude-spt\n# lineage (retained for seam provenance); omp-spt's own history starts at version 0.1.0.\n#\n# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/\n# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,\n# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the\n# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).\n#\n# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not\n# yet built, or needs a capability not on the public surface — see notes inline):\n#   * [session.<role>] for signoff/notif — still-deferred live-agent roles (no field forcing yet).\n#     psyche_init/psyche_resume authored at v0.14.0 (F-030 ephemeral shim); echo_commune + [history]\n#     authored at v0.15.2 (field-forced: core 0.27/0.28 spawns the summarizer and 3-strikes the\n#     psyche host when the role is missing — flynn 2026-07-07). [session.self] IS authored below\n#     (the endpoint-run/bind bringup that hosts a fresh CC session — the M12 `cc` launcher target).\n#   * [update]                      — DECLARED below via the `gh_release` avenue (the published\n#     `--release` machinery, operative on the current min_spt_core floor): spt-core auto-updates the\n#     adapter from THIS repo's GitHub releases on `spt adapter update`, with NO Ed25519 signing infra\n#     (closes the F-005 parked `file_pull`/`delegated` legs). The COMPOSITE `[update.post]` step (the\n#     delegated plugin-reconcile) lands in spt-core v0.16.0 (ADR-0029) — wired at the adapter v0.8.0\n#     cut. (NB: \"v0.8.0\" everywhere in THIS arc = the claude-spt ADAPTER version, NOT an spt-core\n#     version — spt-core's next is v0.16.0; doyle 2026-06-25.)\n#\n# [impl->REQ-DIST-MANIFEST-SCHEMA]\n\n[adapter]\nname = \"omp-spt\"\nkind = \"harness\"\n# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n# .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract, so the floor moves\n…\n# reported NO_PERCH for EVERY peer send though the message DELIVERED (confirm-back lied; self-send was\n# unaffected as it ignores its result). Root-caused offline via an isolated hook-binary rig (peer send\n# returned raw=\"\" while stderr carried SENT:doyle). Fix: HookEnv::spt_send captures stdout+stderr merged;\n# scan_and_dispatch classifies through it (REQ-HAZARD-SEND-STATUS-STDERR). ALSO folds doyle's two-slice\n# fix: the commune brief + echo-commune summarizer now teach the <project-context>/<live-context>\n# envelope so core routes context per-project instead of dumping all in the live tier\n# (REQ-COMMUNE-CONTEXT-TIER-SLICING). Binary-only; floor unchanged (0.27.0). Send-status-on-stderr filed\n# to doyle (undocumented in the harness-contract/CLI reference).\n# omp-spt starts fresh at 0.1.0 — the version history above is inherited claude-spt lineage.\n# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n# breach exits loud BEFORE bind, so a failed bringup cannot poison the session ledger), and a\n# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n# uses attach-default `spt endpoint run --adapter omp-spt --id <id> --create`, not `--start`, so the\n# operator sees the broker-held harness PTY and its bringup log instead of being instructed into a\n# silent start-only launch. The gh_release update avenue now pins transport=\"gh\", forcing the\n# authenticated GitHub CLI path for release discovery/asset fetch so private or visibility-flipped\n# repos update through the user's existing gh auth. Floor unchanged.\n# 0.1.3 (2026-07-13): issue #2 root fix. Native interactive omp is now the broker PTY leader and a\n# packaged OMP extension supplies SPT bind/listen/state/reply integration. The prior attach guidance\n# exposed only the headless RPC bridge's logs because `omp --mode rpc` creates no TUI. Floor unchanged.\nversion = \"0.1.3\"\n# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS\n# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that\n# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify\n# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name\n# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset\n# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the\n# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)\n# Floor BUMPED to 0.15.0 (2026-06-24, the checkpoint-commune wave / v0.7.0): T2's checkpoint self-send\n# needs `spt send --json-payload` (the opaque-metadata send axis) and T3's resume pull needs the\n# `spt api psyche-download <id>` verb — BOTH land in spt-core v0.15.0 (counter 34, Latest). The\n# multi-line frame (T1) works on any node, but the wave ships as one unit, so the honest floor is the\n# verb floor. An older node can't fire checkpoint or pull resume context, so declaring 0.15.0 is honest.\n# Floor BUMPED to 0.16.0 (2026-06-26, the UNIFY-CONSOLIDATE cut / v0.8.0): D2's [update.post] composite,\n# D3's [message-idle-translation-binary].command, and the {adapter_dir}/{adapter_name} substitution +\n# lazy [strings] subst ALL land in spt-core v0.16.0 (counter 35, Latest; ADR-0029). An older node can't\n# read update.post or the command seam, so declaring 0.16.0 is the honest floor.\n# 0.9.1 (2026-06-28): BUGFIX patch. (1) hook_cmd → bare binary path + dispatch caches it QUOTED, fixing\n# the `hook: command not found` env-file regression that polluted every Bash call + killed the hook\n# cache. (2) post-update now refreshes the cplugs marketplace cache (`plugin marketplace update`)\n# before install — fixes \"Plugin omps not found in marketplace cplugs\" on a stale cache — and prints a\n# reworded, cleaner update notice without leaking CC's output onto the [update.post] arbiter channel.\n# (3) the ccs profile carries the U6 -n/RC flags (session.self + session.resume), which its v0.8.0\n# command override had dropped. All bugfix; no new seam → floor stays 0.16.0.\n# Floor BUMPED to 0.19.0 (2026-07-01, the digest-fetcher cut / v0.10.0): [digest] strategy=\"fetcher\"\n# (REQ-DIGEST-FETCHER-STRATEGY, spt-core side) + the [env] direction=\"read\" capture/fill seam BOTH land\n# in spt-core v0.19.0 (counter 38). An older node can't parse strategy=\"fetcher\" nor fill the\n# {CLAUDE_CONFIG_DIR} read-var, so declaring 0.19.0 is the honest floor (todlando GO, cluster-A\n# consumable; doyle gate-verified).\n# Floor BUMPED to 0.20.0 (2026-07-02, the {node} cut / v0.11.0): the {node} substitution key for\n# [session.*] commands lands in spt-core v0.20.0 (counter 40; doyle release ping). An older node\n# would pass the literal `{node}` (the shim guards it, degrading to hostname detection), but the\n# feature this cut ships IS the advertised-label fill — declaring 0.20.0 is the honest floor.\n# Floor BUMPED to 0.23.0 (2026-07-03, the C3 thrash-guard cut / v0.13.0): the psyche-host thrash\n# guard's loud give-up (PSYCHE_HOST_GIVE_UP, nonzero exit) composes through spt-core's\n# psyche_host_error surface — which exists on older cores — BUT the coupled crashloop story it was\n# ordering-proved against (adapter fires first, core's >=10/60s ledger-rate backstop catches what\n# the streak-reset forgives) only holds on spt-core >= 0.23.0 (counter 43, where C3(b) landed). The\n# floor guarantees the pairing is present; a lone give-up on an older core still degrades safely (it\n# just exits loud, which any residency-managed core handles).\n#\n# Floor BUMPED to 0.25.0 (2026-07-04, the F-030 psyche-ephemeral cut / v0.14.0): the ephemeral-shim\n# driver — per-event [session.psyche_resume] invocation, the core-minted psyche-own sid, the\n# core-written {psyche_context_file} mind file, and the exit-95 reseed contract — ALL land in\n# spt-core v0.25.0 (counter 45, tag v0.25.0, core sha c01fe6ee29c2a19c4a6ebc72f11c471cab7d13d0;\n# doyle GO + deployah publish ping 2026-07-04). On a below-0.25.0 core the shim does one seed turn\n# then STARVES (the daemon never re-invokes) — the silent-degradation class that held this floor as\n# a hard PUBLISH BLOCKER until the core shipped (the v0.13.2 bad-ship lesson). The shim was\n# RC-certified against the exact published v0.25.0 bytes before this pin (windows sha256\n# 0202742115d5bb9109b6abfa336b4d7ec54bb6a99fc1d7561bb83c17016a8491 == the RC-tested anchor).\nmin_spt_core_version = \"0.27.0\"\nhostable_types = [\"LiveAgent\", \"ReadyAgent\", \"Worker\"]\n# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for\n# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking\n# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms\n# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the\n# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`\n# land. omp's host process basename is `omp`; the RPC bridge (S2) is `omp-spt`. [impl->REQ-SKILL-LIVE]\nhost_binaries = [\"omp\", \"omp-spt\"]\n# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is\n# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher\n# brand is DECOUPLED from the plugin name (`omps`, skills `/omps:*`): the s/omps/spt/ succession seam\n# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh\n# (unit->REQ-DIST-SHORTCUT-BASENAME).\nshortcut_basename = \"omp\"\n\n# ── [update] ───────────────────────────────────────────────────────────────────────────────────\n# Adapter self-update via OUR GitHub releases (gh_release avenue). `spt adapter update`\n# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —\n# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default\n# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared\n# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014\n# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a\n# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.\n# `message` (U1, REQ-DIST-UPDATE-MESSAGE): markdown spt-core prints ONLY on a real version apply (no\n# `{key}` substitution — docs-confirmed). It bridges the one manual residual the gh_release avenue\n# can't automate — the `/reload-plugins` TUI action — and points at the more-powerful `spt` CLI\n# endpoint route alongside /omps:live. (When doyle ask #2 lands, the post-update step can also trigger\n# this print on a plugin-only change; copy stays valid.)\n# [impl->REQ-DIST-ADAPTER-RELEASE]\n[update]\navenue = \"gh_release\"\nrepo = \"BigscreenVR/omp-spt\"\ntransport = \"gh\"\nmessage = \"\"\"\n**omp-spt updated.** The native OMP extension + extractors refreshed in place — no reload step: OMP\nloads the packaged extension fresh on each endpoint bringup.\n\n- Running endpoints keep the OLD hosting path until restarted: `spt endpoint stop <id>` then\n  `spt endpoint run --adapter omp-spt --id <id>` picks up the new one.\n- Bring up a fresh Librarian endpoint: `spt endpoint run --adapter omp-spt --id <id> --create`.\n\"\"\"\n\n# ── [update.post] — the delegated plugin-reconcile step (D2, spt-core v0.16.0) ──────────────────\n# spt-core runs this AFTER the gh_release pull+re-register, UNCONDITIONALLY (even on an adapter no-op),\n# completing the one lever `spt adapter update claude-spt` = pull the adapter .spt + reconcile the\n# cplugs plugin. `command` resolves the consolidated binary from the install dir via {adapter_dir}\n# (survives updates). spt-core pipes a one-line JSON context on the command's stdin\n# ({adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}; additive —\n# the subcommand ignores unknown keys) and reads its STDOUT to arbitrate the notice: the reserved\n# sentinel `!!update-message!!` (which post-update emits on a successful reconcile) FIRES the\n# [update].message above; custom text would supersede it; empty prints nothing. Exit code is\n# orthogonal and the step is FAIL-ISOLATED (a committed pull is never rolled back on our failure).\n# `self_verifies = false`: attestation-only, gates nothing yet (no signing infra). The reconcile logic\n# rides `spt adapter update` (it lives in the binary), so it stays current without a separate step.\n# [impl->REQ-DIST-UPDATE-MESSAGE]\n[update.post]\ncommand = \"{adapter_dir}/omp-spt post-update\"\nself_verifies = false\n\n# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────\n# CC hook event → the opaque `api …` command it fires (the representative action; some events do\n# more — e.g. UserPromptSubmit also marks busy + injects skills, PostToolUse self-sends a checkpoint).\n…\n\n…\n[session.psyche_init]\n…\n# I/O (W4, contract 9e6863d0): {psyche_context_file} (ALWAYS filled — a path to the core-written mind\n# file in the psyche's nested perch dir) rides argv; the shim READS it to get the mind (W4 kills the\n# win32 ~32k cmdline cap — ~20KB minds used to ride argv). The per-turn EVENT rides the shim's STDIN\n# (core feeds it — a bare pulse = `<pulse/>`); the psyche's reply rides the shim's CAPTURED STDOUT (the\n# daemon parses intents from it). The daemon runs this role NON-detached, stdin-fed, stdout-captured,\n# bounded (turn.rs run_bounded_stdin) — NOT the old Stdio::null detached spawn; hence detach=false.\n# Reseed is signalled by the shim's reserved EXIT CODE 95 (session-gone); every other nonzero keeps\n# custody. The `<parent>-psyche` perch + the psyche-custody.json sid record are CORE-owned — the shim\n# mints nothing, writes nothing to custody. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# NO cwd TEMPLATE (v0.14.2) — same {psyche_dir} retirement as psyche_init above; the shim owns the\n# cwd (context-file parent dir). [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n[session.psyche_resume]\ncommand = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\ndetach = false\n# Identity-env scrub on BOTH psyche roles (F-028 C2). [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\nenv_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]\nkeys = [\"id\", \"session_id\", \"psyche_context_file\"]\n\n# ── [session.self] ─────────────────────────────────────────────────────────────────────────\n# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter\n# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch\n# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /omps:ready|live uses).\n# This is what the `cc-<id>` shortcut (shortcut_basename=\"cc\") bakes into a non-interactive launch.\n#\n# command is the `claude-spt launch` SHIM (0.10.3+, the delivered doyle-ask-#4 stretch): it spawns\n# `claude -n \"<id> @ <node> (<project>/)\" --remote-control <id>--<node>--<project-token> --dangerously-skip-permissions`\n# (unix: exec — claude IS the PTY leader; windows: spawn+wait, stdio inherited). CC mints its OWN\n# session id internally (identity.session_id_source = post_spawn), so spt's {session_id} is not threaded\n# on argv. The endpoint {id} is threaded THREE ways (U6, REQ-DIST-RC-STARTUP): it rides the ENV for the\n# BIND (CC has no id flag, so SessionStart reads $SPT_ENDPOINT_ID and binds — see\n# [env.SPT_ENDPOINT_ID] below, the sanctioned flagless-harness pattern), AND it sets the DISPLAY\n# name (`-n \"<id> @ <node> (<project>/)\"`, shown in the prompt box / `/resume`) AND the REMOTE-CONTROL\n# channel (`--remote-control <id>--<node>--<project-token>`) — so a FRESH spt-hosted endpoint is\n# `spt rc <id>`-attachable + idle/checkpoint-injectable identically to a RESUMED one ([session.resume]).\n# WHY A SHIM: the node label arrives via the `--node {node}` fill (spt-core v0.20.0, the adopted\n# doyle-ask-#4 key — the daemon's ADVERTISED label, matching the rest of the spt surface), and the\n# <project> folder is the launch-cwd basename (no {project} fill exists) — both computed ON-NODE; and\n# tokenize-then-fill (v0.8.2+) can never yield a space-carrying single argv element — the shim passes\n# each name as one clean argv element, and falls back to on-node hostname detection when the {node}\n# fill is blank or literally unsubstituted (F-013 gap class). The DISPLAY name uses the raw project\n# folder (spaces/parens/slash fine). RC gets the tokeny `<id>--<node>--<project-token>` form with a\n# CHARACTER-SAFE project token (alphanumeric + `-`/`_`, other runs → `_`; CC's auto-generated RC names\n# are hostname-prefixed tokens; space-safety of an explicit RC name is unverifiable from the public\n# surface). Unknown project ⇒ drop only its suffix; unknown node ⇒ bare `<id>` for both (the pre-0.10.3\n# shape); `spt rc <id>` attaches by ENDPOINT id either way (broker-internal, not CC's RC name).\n# `--dangerously-skip-permissions` stays required for the spt-hosted bringup:\n# the broker spawns CC non-interactively into a PTY, so the permission-prompt gate would deadlock\n# the launch with no operator to approve it (the shim passes it through).\n# [impl->REQ-DIST-SHORTCUT-BASENAME]\n#\n# ── omp-spt: native OMP owns the broker PTY ─────────────────────────────────────────────────────\n# Issue #2 proved that the headless RPC bridge can expose only bridge logs when an operator attaches.\n# Native `omp` is therefore the session process; its packaged extension owns SPT bind/listen/state/\n# reply integration through OMP's public extension API. See ADR-0007. [impl->REQ-OMP-NATIVE-TUI]\n[session.self]\ncommand = \"omp --extension {adapter_dir}/strings/omp-spt.mjs\"\nkeys = []\n\n# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────\n# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run\n# --resume <session>` or the picker's \"Resume from history\") AND this role is declared. OMITTING it\n# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented\n# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by\n# {session_id} + cwd.\n#\n# Command = the same `claude-spt launch` shim with `--resume {session_id}` (0.10.3+): it spawns\n# Claude Code's NATIVE-RESUME verb `claude -r <session_id> -n \"<id> @ <node> (<project>/)\"\n# --remote-control <id>--<node>--<project-token> --dangerously-skip-permissions` (-r/--resume reloads\n# the real transcript by id).\n# The endpoint {id} is threaded TWO ways: as the DISPLAY name (U6 — shown in the prompt box /\n# `/resume`, matching [session.self] so a fresh and a resumed endpoint present identically) AND as\n# the REMOTE-CONTROL name — both node-suffixed by the shim, same shapes + degrade rule as\n# [session.self]. (A resumed session has no $SPT_ENDPOINT_ID env-bind path — -n/RC are how {id}\n# attaches here; `spt rc <id>` still addresses the broker PTY by ENDPOINT id.)\n# `--dangerously-skip-permissions` for the same non-interactive broker-PTY reason as [session.self]\n# (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills spt-core substitutes: {session_id} (the\n# resumed id) + {id} (the endpoint). Satisfies REQ-DIST-SESSION-RESUME (native-resume seam) +\n# REQ-DIST-RC-STARTUP (display+RC parity with [session.self]); the shim's command shape is unit-\n# tested in tools/claude-spt (launch.rs) and the manifest shape in tests/manifest-shortcut.sh.\n# omp-spt resume keeps the same native TUI and extension, while `-r` asks OMP to reload the real\n# transcript. The extension binds the resumed OMP session id to the current SPT endpoint.\n[session.resume]\ncommand = \"omp -r {session_id} --extension {adapter_dir}/strings/omp-spt.mjs\"\nkeys = [\"session_id\"]\n\n# ── [session.echo_commune] — the bounded end-of-session summarizer (v0.15.2) ───────────────────────\n# Published contract: \"when a session ends without a signoff, spt-core runs a bounded summarizer\n# over the session's history so the context delta is captured anyway\"; on core 0.27/0.28 the\n# commune-sync per-event turn ALSO spawns it, and a MISSING role 3-strikes the whole psyche host\n# (flynn 2026-07-07 field pin — psyche_host_error, checkpoints dead). The shim accepts history on\n# stdin when fed (kept for the future published wiring — doyle field pin 2026-07-07: core does NOT\n# stdin-feed [history] today) and otherwise SELF-LOCATES the transcript, most-authoritative rung\n# first: the perch info.json read_env.CLAUDE_CONFIG_DIR (doyle-sanctioned; the role spawn does not\n# inherit the session env, so ccs-relocated transcripts are invisible to the env rung) → the env →\n# ~/.claude/projects → every ~/.ccs/instances/<account>/projects. Bounded whole-line tail, ONE\n# headless `claude -p` turn (psyche-parity sandbox, Read-only tools — a summarizer never writes),\n# delta on stdout for core to ingest. Locate-miss is GRACEFUL (no-delta marker + exit 0 — a v0.15.2\n# exit-1 here rode the psyche host's 3-strike budget and latched two hosts); only real claude\n# faults stay loud nonzero (ECHO_COMMUNE_FAIL:).\n# recursion_guard_env: core sets SPT_ECHO_COMMUNE on the shim; the claude it spawns inherits it and\n# `claude-spt hook` BAILS (exit 0, zero api calls) under it — the inner session can never\n# seed/bind/rebind a perch (no summarizer-of-summarizer loops). Identity-env scrub rides too\n# (F-028 C2 discipline, same as both psyche roles). [impl->REQ-SESSION-ECHO-COMMUNE]\n# S4 (omp-spt): the summarizer is `echo-commune-omp` — self-locates the OMP session JSONL (the\n# digest-omp locator), bounded whole-line tail, ONE read-only headless `omp -p` turn (the prompt\n# rides an @file), delta on stdout. Locate-miss stays GRACEFUL (no-delta marker + exit 0). The\n# recursion_guard_env is kept for parity, but the omp path cannot loop: an `omp -p` turn runs no\n# spt hook layer at all (--no-extensions; the RPC bridge owns lifecycle).\n[session.echo_commune]\ncommand = \"omp-spt echo-commune-omp --id {id} --session-id {session_id}\"\n…\n# {CLAUDE_CONFIG_DIR} dependency. Missing transcript = HISTORY_FAIL: + exit 1, never silent-empty.","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,null,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,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,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,null,516,null,632,null,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,null,778]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\omp-spt.toml"}}},"isError":false,"timestamp":1784087175843}}
{"type":"message","id":"e4bc2ef1","parentId":"ac64c777","timestamp":"2026-07-15T03:46:15.845Z","message":{"role":"toolResult","toolCallId":"call_9hOOvZcTlaLybtHACMVsLz34|fc_0ca4af36969c80fa016a570287c0648198a41226a0346895a8","toolName":"read","content":[{"type":"text","text":"import { spawn } from \"node:child_process\";\n\nconst ADAPTER = \"omp-spt\";\n\nexport function decodeBody(body) {\n\treturn body\n\t\t.replaceAll(\"<br>\", \"\\n\")\n\t\t.replaceAll(\"&lt;\", \"<\")\n\t\t.replaceAll(\"&gt;\", \">\")\n\t\t.replaceAll(\"&quot;\", '\"')\n\t\t.replaceAll(\"&amp;\", \"&\");\n}\n\nfunction attribute(tag, name) {\n\tconst marker = ` ${name}=\"`;\n\tconst start = tag.indexOf(marker);\n\tif (start < 0) return undefined;\n\tconst valueStart = start + marker.length;\n\tconst end = tag.indexOf('\"', valueStart);\n\treturn end < 0 ? undefined : tag.slice(valueStart, end);\n}\n\nexport function drainEvents(raw) {\n\tconst events = [];\n\tlet cursor = 0;\n\twhile (true) {\n\t\tconst start = raw.indexOf(\"<EVENT\", cursor);\n\t\tif (start < 0) return { events, rest: raw.slice(cursor) };\n\t\tconst openEnd = raw.indexOf(\">\", start);\n\t\tif (openEnd < 0) return { events, rest: raw.slice(start) };\n\t\tconst close = raw.indexOf(\"</EVENT>\", openEnd + 1);\n\t\tif (close < 0) return { events, rest: raw.slice(start) };\n\t\tconst tag = raw.slice(start, openEnd);\n\t\tif (attribute(tag, \"type\") === \"msg\") {\n\t\t\tevents.push({\n\t\t\t\tfrom: attribute(tag, \"from\"),\n\t\t\t\tbody: decodeBody(raw.slice(openEnd + 1, close)),\n\t\t\t});\n\t\t}\n\t\tcursor = close + \"</EVENT>\".length;\n\t}\n}\n\nexport function extractReply(messages) {\n\tconst assistant = [...(messages ?? [])].reverse().find((message) => message?.role === \"assistant\");\n\tif (!assistant) return \"\";\n\tif (typeof assistant.content === \"string\") return assistant.content;\n\treturn (assistant.content ?? [])\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\");\n}\n\nfunction firstLine(text) {\n\treturn text.split(/\\r?\\n/).find((line) => line.trim()) ?? \"\";\n}\n\nfunction runSpt(args, input) {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n\t\t\tstdio: [input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\n\t\t\twindowsHide: true,\n\t\t});\n\t\tlet output = \"\";\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk) => (output += chunk));\n\t\tchild.stderr.on(\"data\", (chunk) => (output += chunk));\n\t\tchild.on(\"error\", reject);\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(output.trim());\n\t\t\telse reject(new Error(`spt exited ${code}: ${firstLine(output)}`));\n\t\t});\n\t\tif (input !== undefined) child.stdin.end(input);\n\t});\n}\n\n// [impl->REQ-OMP-NATIVE-TUI]\nexport default function ompSpt(pi) {\n\tconst id = process.env.SPT_ENDPOINT_ID?.trim();\n\tif (!id) return;\n\n\tlet sid;\n\tlet token;\n\tlet listener;\n\tlet listenerBuffer = \"\";\n\tlet agentActive = false;\n\tlet current;\n\tlet stopping = false;\n\tlet ui;\n\tconst queue = [];\n\n\tconst logError = (message, error) => {\n\t\tpi.logger.error(message, { error: String(error) });\n\t\tui?.notify(`${message}: ${error}`, \"error\");\n\t};\n\n\tasync function setState(state) {\n\t\tif (!sid) return;\n\t\tconst auth = token ? [\"--token\", token] : [\"--session-id\", sid];\n\t\tawait runSpt([\"api\", \"--adapter\", ADAPTER, \"state\", state, id, ...auth]);\n\t}\n\n\tfunction dispatchNext() {\n\t\tif (stopping || agentActive || current || queue.length === 0) return;\n\t\tcurrent = queue.shift();\n\t\ttry {\n\t\t\tpi.sendUserMessage(current.body);\n\t\t} catch (error) {\n\t\t\tlogError(\"omp-spt could not submit an inbound message\", error);\n\t\t\tcurrent = undefined;\n\t\t\tsetTimeout(dispatchNext, 0);\n\t\t}\n\t}\n\n\tfunction startListener() {\n\t\tconst args = [\"ready\", id];\n\t\tif (process.env.OMP_SPT_SUBNET) args.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n\t\tlistener = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\twindowsHide: true,\n\t\t});\n\t\tlistener.stdout.setEncoding(\"utf8\");\n\t\tlistener.stderr.setEncoding(\"utf8\");\n\t\tlistener.stdout.on(\"data\", (chunk) => {\n\t\t\tlistenerBuffer += chunk;\n\t\t\tconst drained = drainEvents(listenerBuffer);\n\t\t\tlistenerBuffer = drained.rest;\n\t\t\tfor (const event of drained.events) queue.push(event);\n\t\t\tdispatchNext();\n\t\t});\n\t\tlistener.stderr.on(\"data\", (chunk) => pi.logger.debug(\"omp-spt listener\", { output: chunk.trim() }));\n\t\tlistener.on(\"error\", (error) => logError(\"omp-spt listener failed\", error));\n\t\tlistener.on(\"close\", (code) => {\n\t\t\tif (!stopping) {\n\t\t\t\tui?.setStatus(\"omp-spt\", \"spt offline\");\n\t\t\t\tui?.notify(`omp-spt listener exited (${code})`, \"error\");\n\t\t\t}\n\t\t});\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tui = ctx.ui;\n\t\tsid = ctx.sessionManager.getSessionId();\n\t\ttry {\n\t\t\tconst bindArgs = [\"api\", \"--adapter\", ADAPTER, \"bind\", id, \"--set-session-id\", sid];\n\t\t\tif (process.env.OMP_SPT_SUBNET) bindArgs.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\n\t\t\tconst bound = await runSpt(bindArgs);\n\t\t\ttoken = bound.match(/\\btoken=([^\\s]+)/)?.[1];\n\t\t\tawait setState(\"idle\");\n\t\t\tstartListener();\n\t\t\tctx.ui.setStatus(\"omp-spt\", `spt:${id}`);\n\t\t} catch (error) {\n\t\t\tctx.ui.setStatus(\"omp-spt\", \"spt bind failed\");\n\t\t\tlogError(`omp-spt could not bind ${id}`, error);\n\t\t}\n\t});\n\n\tpi.on(\"agent_start\", async () => {\n\t\tagentActive = true;\n\t\ttry {\n\t\t\tawait setState(\"busy\");\n\t\t} catch (error) {\n\t\t\tlogError(\"omp-spt could not mark the endpoint busy\", error);\n\t\t}\n\t});\n\n\tpi.on(\"agent_end\", async (event) => {\n\t\tagentActive = false;\n\t\tconst completed = current;\n\t\tcurrent = undefined;\n\t\tif (completed?.from) {\n\t\t\tconst reply = extractReply(event.messages) || \"[omp-spt] turn ended without an assistant response.\";\n\t\t\ttry {\n\t\t\t\tawait runSpt([\"send\", completed.from, \"--from\", id], reply);\n\t\t\t} catch (error) {\n\t\t\t\tlogError(`omp-spt could not reply to ${completed.from}`, error);\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tawait setState(\"idle\");\n\t\t} catch (error) {\n\t\t\tlogError(\"omp-spt could not mark the endpoint idle\", error);\n\t\t}\n\t\tsetTimeout(dispatchNext, 0);\n\t});\n\n\tpi.on(\"session_shutdown\", async () => {\n\t\tstopping = true;\n\t\tlistener?.kill();\n\t\tui?.setStatus(\"omp-spt\", undefined);\n\t\tif (!sid) return;\n\t\tconst auth = token ? [\"--token\", token] : [\"--session-id\", sid];\n\t\ttry {\n\t\t\tawait runSpt([\"api\", \"--adapter\", ADAPTER, \"session-end\", id, ...auth]);\n\t\t} catch (error) {\n\t\t\tpi.logger.error(\"omp-spt session teardown failed\", { error: String(error) });\n\t\t}\n\t});\n}\n"}],"details":{"displayContent":{"text":"import { spawn } from \"node:child_process\";\r\n\r\nconst ADAPTER = \"omp-spt\";\r\n\r\nexport function decodeBody(body) {\r\n\treturn body\r\n\t\t.replaceAll(\"<br>\", \"\\n\")\r\n\t\t.replaceAll(\"&lt;\", \"<\")\r\n\t\t.replaceAll(\"&gt;\", \">\")\r\n\t\t.replaceAll(\"&quot;\", '\"')\r\n\t\t.replaceAll(\"&amp;\", \"&\");\r\n}\r\n\r\nfunction attribute(tag, name) {\r\n\tconst marker = ` ${name}=\"`;\r\n\tconst start = tag.indexOf(marker);\r\n\tif (start < 0) return undefined;\r\n\tconst valueStart = start + marker.length;\r\n\tconst end = tag.indexOf('\"', valueStart);\r\n\treturn end < 0 ? undefined : tag.slice(valueStart, end);\r\n}\r\n\r\nexport function drainEvents(raw) {\r\n\tconst events = [];\r\n\tlet cursor = 0;\r\n\twhile (true) {\r\n\t\tconst start = raw.indexOf(\"<EVENT\", cursor);\r\n\t\tif (start < 0) return { events, rest: raw.slice(cursor) };\r\n\t\tconst openEnd = raw.indexOf(\">\", start);\r\n\t\tif (openEnd < 0) return { events, rest: raw.slice(start) };\r\n\t\tconst close = raw.indexOf(\"</EVENT>\", openEnd + 1);\r\n\t\tif (close < 0) return { events, rest: raw.slice(start) };\r\n\t\tconst tag = raw.slice(start, openEnd);\r\n\t\tif (attribute(tag, \"type\") === \"msg\") {\r\n\t\t\tevents.push({\r\n\t\t\t\tfrom: attribute(tag, \"from\"),\r\n\t\t\t\tbody: decodeBody(raw.slice(openEnd + 1, close)),\r\n\t\t\t});\r\n\t\t}\r\n\t\tcursor = close + \"</EVENT>\".length;\r\n\t}\r\n}\r\n\r\nexport function extractReply(messages) {\r\n\tconst assistant = [...(messages ?? [])].reverse().find((message) => message?.role === \"assistant\");\r\n\tif (!assistant) return \"\";\r\n\tif (typeof assistant.content === \"string\") return assistant.content;\r\n\treturn (assistant.content ?? [])\r\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\r\n\t\t.map((part) => part.text)\r\n\t\t.join(\"\");\r\n}\r\n\r\nfunction firstLine(text) {\r\n\treturn text.split(/\\r?\\n/).find((line) => line.trim()) ?? \"\";\r\n}\r\n\r\nfunction runSpt(args, input) {\r\n\treturn new Promise((resolve, reject) => {\r\n\t\tconst child = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\r\n\t\t\tstdio: [input === undefined ? \"ignore\" : \"pipe\", \"pipe\", \"pipe\"],\r\n\t\t\twindowsHide: true,\r\n\t\t});\r\n\t\tlet output = \"\";\r\n\t\tchild.stdout.setEncoding(\"utf8\");\r\n\t\tchild.stderr.setEncoding(\"utf8\");\r\n\t\tchild.stdout.on(\"data\", (chunk) => (output += chunk));\r\n\t\tchild.stderr.on(\"data\", (chunk) => (output += chunk));\r\n\t\tchild.on(\"error\", reject);\r\n\t\tchild.on(\"close\", (code) => {\r\n\t\t\tif (code === 0) resolve(output.trim());\r\n\t\t\telse reject(new Error(`spt exited ${code}: ${firstLine(output)}`));\r\n\t\t});\r\n\t\tif (input !== undefined) child.stdin.end(input);\r\n\t});\r\n}\r\n\r\n// [impl->REQ-OMP-NATIVE-TUI]\r\nexport default function ompSpt(pi) {\r\n\tconst id = process.env.SPT_ENDPOINT_ID?.trim();\r\n\tif (!id) return;\r\n\r\n\tlet sid;\r\n\tlet token;\r\n\tlet listener;\r\n\tlet listenerBuffer = \"\";\r\n\tlet agentActive = false;\r\n\tlet current;\r\n\tlet stopping = false;\r\n\tlet ui;\r\n\tconst queue = [];\r\n\r\n\tconst logError = (message, error) => {\r\n\t\tpi.logger.error(message, { error: String(error) });\r\n\t\tui?.notify(`${message}: ${error}`, \"error\");\r\n\t};\r\n\r\n\tasync function setState(state) {\r\n\t\tif (!sid) return;\r\n\t\tconst auth = token ? [\"--token\", token] : [\"--session-id\", sid];\r\n\t\tawait runSpt([\"api\", \"--adapter\", ADAPTER, \"state\", state, id, ...auth]);\r\n\t}\r\n\r\n\tfunction dispatchNext() {\r\n\t\tif (stopping || agentActive || current || queue.length === 0) return;\r\n\t\tcurrent = queue.shift();\r\n\t\ttry {\r\n\t\t\tpi.sendUserMessage(current.body);\r\n\t\t} catch (error) {\r\n\t\t\tlogError(\"omp-spt could not submit an inbound message\", error);\r\n\t\t\tcurrent = undefined;\r\n\t\t\tsetTimeout(dispatchNext, 0);\r\n\t\t}\r\n\t}\r\n\r\n\tfunction startListener() {\r\n\t\tconst args = [\"ready\", id];\r\n\t\tif (process.env.OMP_SPT_SUBNET) args.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\r\n\t\tlistener = spawn(process.env.OMP_SPT_SPT_BIN || \"spt\", args, {\r\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\r\n\t\t\twindowsHide: true,\r\n\t\t});\r\n\t\tlistener.stdout.setEncoding(\"utf8\");\r\n\t\tlistener.stderr.setEncoding(\"utf8\");\r\n\t\tlistener.stdout.on(\"data\", (chunk) => {\r\n\t\t\tlistenerBuffer += chunk;\r\n\t\t\tconst drained = drainEvents(listenerBuffer);\r\n\t\t\tlistenerBuffer = drained.rest;\r\n\t\t\tfor (const event of drained.events) queue.push(event);\r\n\t\t\tdispatchNext();\r\n\t\t});\r\n\t\tlistener.stderr.on(\"data\", (chunk) => pi.logger.debug(\"omp-spt listener\", { output: chunk.trim() }));\r\n\t\tlistener.on(\"error\", (error) => logError(\"omp-spt listener failed\", error));\r\n\t\tlistener.on(\"close\", (code) => {\r\n\t\t\tif (!stopping) {\r\n\t\t\t\tui?.setStatus(\"omp-spt\", \"spt offline\");\r\n\t\t\t\tui?.notify(`omp-spt listener exited (${code})`, \"error\");\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\tpi.on(\"session_start\", async (_event, ctx) => {\r\n\t\tui = ctx.ui;\r\n\t\tsid = ctx.sessionManager.getSessionId();\r\n\t\ttry {\r\n\t\t\tconst bindArgs = [\"api\", \"--adapter\", ADAPTER, \"bind\", id, \"--set-session-id\", sid];\r\n\t\t\tif (process.env.OMP_SPT_SUBNET) bindArgs.push(\"--subnet\", process.env.OMP_SPT_SUBNET);\r\n\t\t\tconst bound = await runSpt(bindArgs);\r\n\t\t\ttoken = bound.match(/\\btoken=([^\\s]+)/)?.[1];\r\n\t\t\tawait setState(\"idle\");\r\n\t\t\tstartListener();\r\n\t\t\tctx.ui.setStatus(\"omp-spt\", `spt:${id}`);\r\n\t\t} catch (error) {\r\n\t\t\tctx.ui.setStatus(\"omp-spt\", \"spt bind failed\");\r\n\t\t\tlogError(`omp-spt could not bind ${id}`, error);\r\n\t\t}\r\n\t});\r\n\r\n\tpi.on(\"agent_start\", async () => {\r\n\t\tagentActive = true;\r\n\t\ttry {\r\n\t\t\tawait setState(\"busy\");\r\n\t\t} catch (error) {\r\n\t\t\tlogError(\"omp-spt could not mark the endpoint busy\", error);\r\n\t\t}\r\n\t});\r\n\r\n\tpi.on(\"agent_end\", async (event) => {\r\n\t\tagentActive = false;\r\n\t\tconst completed = current;\r\n\t\tcurrent = undefined;\r\n\t\tif (completed?.from) {\r\n\t\t\tconst reply = extractReply(event.messages) || \"[omp-spt] turn ended without an assistant response.\";\r\n\t\t\ttry {\r\n\t\t\t\tawait runSpt([\"send\", completed.from, \"--from\", id], reply);\r\n\t\t\t} catch (error) {\r\n\t\t\t\tlogError(`omp-spt could not reply to ${completed.from}`, error);\r\n\t\t\t}\r\n\t\t}\r\n\t\ttry {\r\n\t\t\tawait setState(\"idle\");\r\n\t\t} catch (error) {\r\n\t\t\tlogError(\"omp-spt could not mark the endpoint idle\", error);\r\n\t\t}\r\n\t\tsetTimeout(dispatchNext, 0);\r\n\t});\r\n\r\n\tpi.on(\"session_shutdown\", async () => {\r\n\t\tstopping = true;\r\n\t\tlistener?.kill();\r\n\t\tui?.setStatus(\"omp-spt\", undefined);\r\n\t\tif (!sid) return;\r\n\t\tconst auth = token ? [\"--token\", token] : [\"--session-id\", sid];\r\n\t\ttry {\r\n\t\t\tawait runSpt([\"api\", \"--adapter\", ADAPTER, \"session-end\", id, ...auth]);\r\n\t\t} catch (error) {\r\n\t\t\tpi.logger.error(\"omp-spt session teardown failed\", { error: String(error) });\r\n\t\t}\r\n\t});\r\n}\r\n","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,167,168,169,170,171,172,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\strings\\omp-spt.mjs"}}},"isError":false,"timestamp":1784087175845}}
{"type":"message","id":"daa07e28","parentId":"e4bc2ef1","timestamp":"2026-07-15T03:46:15.845Z","message":{"role":"toolResult","toolCallId":"call_DfYDIz7zdCyhaaRrGc9HbgrC|fc_0ca4af36969c80fa016a570287c06881989c707bb8972a2746","toolName":"read","content":[{"type":"text","text":"import assert from \"node:assert/strict\";\nimport { decodeBody, drainEvents, extractReply } from \"../adapter/strings/omp-spt.mjs\";\n\n// [unit->REQ-OMP-NATIVE-TUI]\nassert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\n\nconst partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\nassert.deepEqual(partial.events, []);\nassert.equal(partial.rest, '<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\n\nconst complete = drainEvents(`${partial.rest}rld</EVENT><EVENT type=\"notify\">skip</EVENT>`);\nassert.deepEqual(complete.events, [{ from: \"doyle\", body: \"hello\\nworld\" }]);\nassert.equal(complete.rest, \"\");\n\nassert.equal(\n\textractReply([\n\t\t{ role: \"assistant\", content: [{ type: \"text\", text: \"first\" }] },\n\t\t{ role: \"toolResult\", content: [] },\n\t\t{ role: \"assistant\", content: [{ type: \"text\", text: \"final \" }, { type: \"text\", text: \"answer\" }] },\n\t]),\n\t\"final answer\",\n);\nassert.equal(extractReply([{ role: \"user\", content: \"hello\" }]), \"\");\n\nconsole.log(\"OMP-EXTENSION OK\");\n"}],"details":{"displayContent":{"text":"import assert from \"node:assert/strict\";\r\nimport { decodeBody, drainEvents, extractReply } from \"../adapter/strings/omp-spt.mjs\";\r\n\r\n// [unit->REQ-OMP-NATIVE-TUI]\r\nassert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\r\n\r\nconst partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\r\nassert.deepEqual(partial.events, []);\r\nassert.equal(partial.rest, '<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\r\n\r\nconst complete = drainEvents(`${partial.rest}rld</EVENT><EVENT type=\"notify\">skip</EVENT>`);\r\nassert.deepEqual(complete.events, [{ from: \"doyle\", body: \"hello\\nworld\" }]);\r\nassert.equal(complete.rest, \"\");\r\n\r\nassert.equal(\r\n\textractReply([\r\n\t\t{ role: \"assistant\", content: [{ type: \"text\", text: \"first\" }] },\r\n\t\t{ role: \"toolResult\", content: [] },\r\n\t\t{ role: \"assistant\", content: [{ type: \"text\", text: \"final \" }, { type: \"text\", text: \"answer\" }] },\r\n\t]),\r\n\t\"final answer\",\r\n);\r\nassert.equal(extractReply([{ role: \"user\", content: \"hello\" }]), \"\");\r\n\r\nconsole.log(\"OMP-EXTENSION OK\");\r\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\omp-extension.mjs"}}},"isError":false,"timestamp":1784087175845}}
{"type":"message","id":"1aafc726","parentId":"daa07e28","timestamp":"2026-07-15T03:46:15.845Z","message":{"role":"toolResult","toolCallId":"call_SCnyv2FSHpWaHhM60h6NdFzT|fc_0ca4af36969c80fa016a570287c0748198b45a645bf44f03ee","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n#\n# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n#   2. the bridge sends a reply back to the sender (the bridge trace).\n#\n# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\nset -u\n\nif [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n  exit 0\nfi\n\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nCRATE=\"$ROOT/tools/claude-spt\"\nfail=0\n\n# ── Resolve binaries ─────────────────────────────────────────────────────────\n# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n  || { echo \"FAIL cargo build\"; exit 1; }\nexe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\nBIN=$(exe \"$CRATE/target/release/omp-spt\")\nFAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\nif command -v spt >/dev/null 2>&1; then SPT=spt\nelif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\nelse echo \"FAIL spt binary not found\"; exit 1; fi\n\n# ── Disposable identity + scratch ──────────────────────────────────────────────\n# Never the operator's perch (name-keyed, last-establish-wins). A fixed uniquifier keeps replays clean.\nID=\"omp-spt-acc-1\"\nTX=\"omp-spt-acc-tx\"\nwork=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\nLOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\nSUBNET_ARG=\"\"\n[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n\ncleanup() {\n  [ -n \"${BPID:-}\" ] && kill \"$BPID\" 2>/dev/null\n  \"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n  rm -rf \"$work\"\n}\ntrap cleanup EXIT INT TERM\n\nMARKER=\"PING-MARKER-acc-$$\"\nREPLY=\"pong-acc\"\n\n# ── Run: resident bridge (fake omp) → send a message → assert ──────────────────\n# shellcheck disable=SC2086\nSPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_LOG=\"$LOG\" FAKE_OMP_REPLY=\"$REPLY\" OMP_SPT_DEBUG=1 \\\n  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR\" &\nBPID=$!\nsleep 3   # bind + `spt ready` bringup\n\nprintf '%s' \"$MARKER\" | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\nsleep 3   # deliver → drive fake-omp → reply\n\n# 1. Inbound: the SPT message reached omp as a prompt frame.\nif [ -f \"$LOG\" ] && grep -q \"$MARKER\" \"$LOG\"; then\n  echo \"ok   inbound: spt message forwarded to omp as a prompt ($MARKER)\"\nelse\n  echo \"FAIL inbound: '$MARKER' not in fake-omp prompt log\"; fail=1\n  echo \"---- bridge stderr ----\"; tail -8 \"$ERR\" 2>/dev/null\nfi\n\n# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\nif grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n  echo \"ok   outbound: bridge replied to sender ($TX)\"\nelse\n  echo \"FAIL outbound: no 'reply out: to=$TX' on the operational log\"; fail=1\nfi\n\n# 3. Registration: the perch was bound (oplog breadcrumb).\nif grep -q \"bound: id=$ID\" \"$ERR\" 2>/dev/null; then\n  echo \"ok   bind established the perch\"\nelse\n  echo \"FAIL no 'bound: id=$ID' on the operational log\"; fail=1\nfi\n\n# ── HIGH-4: a hung turn breaches the deadline, sender gets a failure notice ──────\nkill \"$BPID\" 2>/dev/null; sleep 1\n\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\nERR2=\"$work/bridge-mute.err\"\n# shellcheck disable=SC2086\nSPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_MUTE=1 OMP_SPT_TURN_TIMEOUT_S=3 \\\n  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR2\" &\nBPID=$!\nsleep 3\nprintf 'hang me' | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\nsleep 6   # 3s deadline + notice dispatch\n\nif grep -q \"turn FAILED: no answer within 3s\" \"$ERR2\" 2>/dev/null; then\n  echo \"ok   hung turn breached the 3s deadline (perch recovered, not wedged)\"\nelse\n  echo \"FAIL no turn-deadline breach on the log\"; fail=1\n  echo \"---- mute-bridge stderr ----\"; tail -8 \"$ERR2\" 2>/dev/null\nfi\nif grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\nelse\n  echo \"FAIL no failure notice was sent\"; fail=1\nfi\n\n# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\nif command -v powershell >/dev/null 2>&1; then\n  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n  if [ -n \"$WPID\" ]; then\n    powershell -NoProfile -Command \"taskkill /F /PID $WPID\" >/dev/null 2>&1\n    sleep 2\n    SURV=$(powershell -NoProfile -Command \"(Get-Process fake-omp -ErrorAction SilentlyContinue | Measure-Object).Count\" | tr -d '\\r')\n    if [ \"${SURV:-1}\" -eq 0 ]; then\n      echo \"ok   hard kill reaped the child tree (no fake-omp orphan)\"\n    else\n      echo \"FAIL $SURV fake-omp orphan(s) survived a hard kill\"; fail=1\n      powershell -NoProfile -Command \"Stop-Process -Name fake-omp -Force -ErrorAction SilentlyContinue\"\n    fi\n  else\n    echo \"SKIP orphan check (bridge already gone)\"\n  fi\nelse\n  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\nfi\n\n[ \"$fail\" -eq 0 ] && { echo \"BRIDGE-ACCEPTANCE OK\"; exit 0; } || { echo \"BRIDGE-ACCEPTANCE FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n#\n# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n#   2. the bridge sends a reply back to the sender (the bridge trace).\n#\n# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\nset -u\n\nif [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n  exit 0\nfi\n\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nCRATE=\"$ROOT/tools/claude-spt\"\nfail=0\n\n# ── Resolve binaries ─────────────────────────────────────────────────────────\n# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n  || { echo \"FAIL cargo build\"; exit 1; }\nexe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\nBIN=$(exe \"$CRATE/target/release/omp-spt\")\nFAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\nif command -v spt >/dev/null 2>&1; then SPT=spt\nelif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\nelse echo \"FAIL spt binary not found\"; exit 1; fi\n\n# ── Disposable identity + scratch ──────────────────────────────────────────────\n# Never the operator's perch (name-keyed, last-establish-wins). A fixed uniquifier keeps replays clean.\nID=\"omp-spt-acc-1\"\nTX=\"omp-spt-acc-tx\"\nwork=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\nLOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\nSUBNET_ARG=\"\"\n[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n\ncleanup() {\n  [ -n \"${BPID:-}\" ] && kill \"$BPID\" 2>/dev/null\n  \"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n  rm -rf \"$work\"\n}\ntrap cleanup EXIT INT TERM\n\nMARKER=\"PING-MARKER-acc-$$\"\nREPLY=\"pong-acc\"\n\n# ── Run: resident bridge (fake omp) → send a message → assert ──────────────────\n# shellcheck disable=SC2086\nSPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_LOG=\"$LOG\" FAKE_OMP_REPLY=\"$REPLY\" OMP_SPT_DEBUG=1 \\\n  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR\" &\nBPID=$!\nsleep 3   # bind + `spt ready` bringup\n\nprintf '%s' \"$MARKER\" | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\nsleep 3   # deliver → drive fake-omp → reply\n\n# 1. Inbound: the SPT message reached omp as a prompt frame.\nif [ -f \"$LOG\" ] && grep -q \"$MARKER\" \"$LOG\"; then\n  echo \"ok   inbound: spt message forwarded to omp as a prompt ($MARKER)\"\nelse\n  echo \"FAIL inbound: '$MARKER' not in fake-omp prompt log\"; fail=1\n  echo \"---- bridge stderr ----\"; tail -8 \"$ERR\" 2>/dev/null\nfi\n\n# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\nif grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n  echo \"ok   outbound: bridge replied to sender ($TX)\"\nelse\n  echo \"FAIL outbound: no 'reply out: to=$TX' on the operational log\"; fail=1\nfi\n\n# 3. Registration: the perch was bound (oplog breadcrumb).\nif grep -q \"bound: id=$ID\" \"$ERR\" 2>/dev/null; then\n  echo \"ok   bind established the perch\"\nelse\n  echo \"FAIL no 'bound: id=$ID' on the operational log\"; fail=1\nfi\n\n# ── HIGH-4: a hung turn breaches the deadline, sender gets a failure notice ──────\nkill \"$BPID\" 2>/dev/null; sleep 1\n\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\nERR2=\"$work/bridge-mute.err\"\n# shellcheck disable=SC2086\nSPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_MUTE=1 OMP_SPT_TURN_TIMEOUT_S=3 \\\n  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR2\" &\nBPID=$!\nsleep 3\nprintf 'hang me' | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\nsleep 6   # 3s deadline + notice dispatch\n\nif grep -q \"turn FAILED: no answer within 3s\" \"$ERR2\" 2>/dev/null; then\n  echo \"ok   hung turn breached the 3s deadline (perch recovered, not wedged)\"\nelse\n  echo \"FAIL no turn-deadline breach on the log\"; fail=1\n  echo \"---- mute-bridge stderr ----\"; tail -8 \"$ERR2\" 2>/dev/null\nfi\nif grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\nelse\n  echo \"FAIL no failure notice was sent\"; fail=1\nfi\n\n# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\nif command -v powershell >/dev/null 2>&1; then\n  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n  if [ -n \"$WPID\" ]; then\n    powershell -NoProfile -Command \"taskkill /F /PID $WPID\" >/dev/null 2>&1\n    sleep 2\n    SURV=$(powershell -NoProfile -Command \"(Get-Process fake-omp -ErrorAction SilentlyContinue | Measure-Object).Count\" | tr -d '\\r')\n    if [ \"${SURV:-1}\" -eq 0 ]; then\n      echo \"ok   hard kill reaped the child tree (no fake-omp orphan)\"\n    else\n      echo \"FAIL $SURV fake-omp orphan(s) survived a hard kill\"; fail=1\n      powershell -NoProfile -Command \"Stop-Process -Name fake-omp -Force -ErrorAction SilentlyContinue\"\n    fi\n  else\n    echo \"SKIP orphan check (bridge already gone)\"\n  fi\nelse\n  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\nfi\n\n[ \"$fail\" -eq 0 ] && { echo \"BRIDGE-ACCEPTANCE OK\"; exit 0; } || { echo \"BRIDGE-ACCEPTANCE FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\bridge-acceptance.sh"}}},"isError":false,"timestamp":1784087175845}}
{"type":"message","id":"4e1c5d9f","parentId":"1aafc726","timestamp":"2026-07-15T03:46:15.845Z","message":{"role":"toolResult","toolCallId":"call_StQLIlJtr9R16NTIGHMhtRRv|fc_0ca4af36969c80fa016a570287c0708198b326ac9a97085e37","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Brand-value + manifest-contract regression guard for omp-spt (rewritten from the claude-spt\n# version at the S4 cut — the CC launch-shim/ccs assertions it carried now guard the native OMP\n# hosting seam; see ADR-0007).\n# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n# [unit->REQ-DIST-SHORTCUT-BASENAME]\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\nEXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\nfail=0\n\n# ── shortcut brand: `spt endpoint run` picker shortcut is omp-<id> ────────────────────────────────\nline=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\nif [ -z \"$line\" ]; then echo \"FAIL no shortcut_basename assignment in manifest\"; exit 1; fi\ncase \"$line\" in\n  *'\"omp\"'*) echo \"ok   shortcut_basename = \\\"omp\\\" (omp-<id> brand intact)\" ;;\n  *) echo \"FAIL shortcut_basename is not \\\"omp\\\": $line\"; fail=1 ;;\nesac\nn=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\nif [ \"$n\" -eq 1 ]; then echo \"ok   single shortcut_basename assignment\"; else echo \"FAIL $n shortcut_basename assignments (want 1)\"; fail=1; fi\n\n# ── adapter identity ──────────────────────────────────────────────────────────────────────────────\ngrep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*\"omp-spt\"' \"$MANIFEST\" \\\n  && echo 'ok   [adapter].name = \"omp-spt\"' || { echo \"FAIL [adapter].name != omp-spt\"; fail=1; }\ngrep -Eq '^[[:space:]]*host_binaries[[:space:]]*=.*\"omp\"' \"$MANIFEST\" \\\n  && echo 'ok   host_binaries includes \"omp\"' || { echo \"FAIL host_binaries misses omp\"; fail=1; }\n\nfield_of() { awk -v h=\"[$1]\" -v f=\"$2\" '$0 == h {s=1;next} /^\\[/{s=0} s && $0 ~ f' \"$MANIFEST\"; }\n\n# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n# [unit->REQ-OMP-NATIVE-TUI]\nspawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\nnspawn=$(printf '%s' \"$spawn\" | grep -c .)\nif [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\nif [ \"$(printf '%s\\n' \"$spawn\" | grep -c -- '--mode rpc')\" -eq 0 ]; then echo \"ok   hosted sessions never use headless RPC mode\"; else echo \"FAIL a hosted session still uses --mode rpc: $spawn\"; fail=1; fi\nresume=$(printf '%s\\n' \"$spawn\" | grep -- '-r {session_id}')\nnresume=$(printf '%s' \"$resume\" | grep -c .)\nif [ \"$nresume\" -eq 1 ]; then echo \"ok   exactly one native resume command carries -r {session_id}\"; else echo \"FAIL expected 1 -r {session_id} command, found $nresume\"; fail=1; fi\nif [ -f \"$EXTENSION\" ]; then echo \"ok   packaged omp-spt extension exists\"; else echo \"FAIL packaged omp-spt extension missing\"; fail=1; fi\nif command -v node >/dev/null 2>&1; then NODE=node\nelif command -v node.exe >/dev/null 2>&1; then NODE=node.exe\nelse echo \"FAIL node is required for the OMP extension unit test\"; fail=1; NODE=:\nfi\nNODE_TEST=\"$ROOT/tests/omp-extension.mjs\"\ncase \"$NODE\" in *.exe) NODE_TEST=$(wslpath -w \"$NODE_TEST\") ;; esac\n\"$NODE\" \"$NODE_TEST\" || fail=1\n\n# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\ngrep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [digest].extractor = digest-omp --session {session_id}\" || { echo \"FAIL [digest].extractor is not digest-omp\"; fail=1; }\ngrep -Eq '^[[:space:]]*fetcher[[:space:]]*=[[:space:]]*\"omp-spt history-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [history].fetcher = history-omp --session {session_id}\" || { echo \"FAIL [history].fetcher is not history-omp\"; fail=1; }\ngrep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt echo-commune-omp --id \\{id\\} --session-id \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [session.echo_commune].command = echo-commune-omp\" || { echo \"FAIL echo_commune command is not echo-commune-omp\"; fail=1; }\nnpsy=$(grep -cE '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt psyche-omp --id \\{id\\} --session-id \\{session_id\\} --psyche-context-file \\{psyche_context_file\\}\"' \"$MANIFEST\")\nif [ \"$npsy\" -eq 2 ]; then echo \"ok   both psyche roles run psyche-omp with the frozen key shape\"; else echo \"FAIL expected 2 psyche-omp role commands, found $npsy\"; fail=1; fi\n# The retired resident key must stay GONE from both psyche roles.\nif grep -q 'psyche_prompt' \"$MANIFEST\"; then echo \"FAIL a psyche role references the retired {psyche_prompt}\"; fail=1; else echo \"ok   no {psyche_prompt} anywhere (event rides stdin)\"; fi\n\n# [unit->REQ-HAZARD-PSYCHE-IDENTITY-ENV] — identity-env scrub on BOTH psyche roles.\nfor role in session.psyche_init session.psyche_resume; do\n  scrub=$(field_of \"$role\" '^[[:space:]]*env_remove[[:space:]]*=')\n  if [ -z \"$scrub\" ]; then echo \"FAIL [$role] has no env_remove (identity-env scrub missing)\"; fail=1; else\n    case \"$scrub\" in\n      *'\"OWL_SESSION_ID\"'*'\"SPT_AGENT_ID\"'*|*'\"SPT_AGENT_ID\"'*'\"OWL_SESSION_ID\"'*)\n        echo \"ok   [$role] env_remove scrubs OWL_SESSION_ID + SPT_AGENT_ID\" ;;\n      *) echo \"FAIL $role env_remove misses an identity var: $scrub\"; fail=1 ;;\n    esac\n  fi\ndone\nrz_detach=$(field_of session.psyche_resume '^[[:space:]]*detach[[:space:]]*=')\ncase \"$rz_detach\" in *false*) echo \"ok   psyche_resume is captured (detach=false)\" ;; *) echo \"FAIL psyche_resume must be detach=false, got: [$rz_detach]\"; fail=1 ;; esac\nrz_keys=$(field_of session.psyche_resume '^[[:space:]]*keys[[:space:]]*=')\ncase \"$rz_keys\" in *'\"psyche_context_file\"'*) echo \"ok   psyche_resume keys include psyche_context_file\" ;; *) echo \"FAIL psyche_resume keys missing psyche_context_file: [$rz_keys]\"; fail=1 ;; esac\n\n# ── [update] — the gh_release self-update seam points at the fork ─────────────────────────────────\nif grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*\"BigscreenVR/omp-spt\"' \"$MANIFEST\"; then echo 'ok   [update].repo = \"BigscreenVR/omp-spt\"'; else echo \"FAIL [update].repo is not BigscreenVR/omp-spt\"; fail=1; fi\nif grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*\"SaberMage/' \"$MANIFEST\"; then echo \"FAIL an active repo assignment still points at SaberMage/*\"; fail=1; else echo \"ok   no active SaberMage repo assignment\"; fi\nif grep -Eq '^[[:space:]]*transport[[:space:]]*=[[:space:]]*\"gh\"' \"$MANIFEST\"; then echo 'ok   [update].transport = \"gh\"'; else echo \"FAIL [update] transport must use gh for auth-capable release fetch\"; fail=1; fi\nif grep -Eq '^[[:space:]]*message[[:space:]]*=' \"$MANIFEST\"; then echo \"ok   [update].message present\"; else echo \"FAIL [update] has no message field\"; fail=1; fi\nif grep -Eq 'endpoint run --adapter omp-spt --id <id> --create --start' \"$MANIFEST\"; then echo \"FAIL fresh-endpoint update notice still uses --start (hides the harness PTY)\"; fail=1; else echo \"ok   fresh-endpoint update notice omits --start (attach-default PTY)\"; fi\nif grep -Eq '^\\[update\\.post\\]' \"$MANIFEST\"; then echo \"ok   [update.post] table present\"; else echo \"FAIL no [update.post] table\"; fail=1; fi\n\n# ── idle-translate binary seam (harness-agnostic; unchanged from claude-spt) ──────────────────────\nif grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"{adapter_dir}/omp-spt translate\"' \"$MANIFEST\"; then echo \"ok   [message-idle-translation-binary].command = translate\"; else echo \"FAIL idle-translate command missing/wrong\"; fail=1; fi\nif grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*\"cc-spt-idle-translate\"' \"$MANIFEST\"; then echo \"FAIL deprecated idle-translate path still present\"; fail=1; else echo \"ok   no deprecated idle-translate path (command-only)\"; fi\n\n# ── no shipped profile overlays (ccs was CC-specific; omp profiles ride `-- --profile`) ───────────\nif grep -Eq '^\\[profiles\\.' \"$MANIFEST\"; then echo \"FAIL a shipped [profiles.*] table lingers (ccs was removed for omp)\"; fail=1; else echo \"ok   no shipped profile overlays\"; fi\n\n# ── [env.SPT_INJECT_VERIFY_ECHO] stays declared (inert on omp; forward-safe) ──────────────────────\nif awk '/^\\[env\\.SPT_INJECT_VERIFY_ECHO\\]/{found=1} END{exit !found}' \"$MANIFEST\"; then echo \"ok   [env.SPT_INJECT_VERIFY_ECHO] declared\"; else echo \"FAIL [env.SPT_INJECT_VERIFY_ECHO] missing\"; fail=1; fi\n\n[ \"$fail\" -eq 0 ] && { echo \"MANIFEST-SHORTCUT OK\"; exit 0; } || { echo \"MANIFEST-SHORTCUT FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Brand-value + manifest-contract regression guard for omp-spt (rewritten from the claude-spt\n# version at the S4 cut — the CC launch-shim/ccs assertions it carried now guard the native OMP\n# hosting seam; see ADR-0007).\n# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n# [unit->REQ-DIST-SHORTCUT-BASENAME]\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\nEXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\nfail=0\n\n# ── shortcut brand: `spt endpoint run` picker shortcut is omp-<id> ────────────────────────────────\nline=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\nif [ -z \"$line\" ]; then echo \"FAIL no shortcut_basename assignment in manifest\"; exit 1; fi\ncase \"$line\" in\n  *'\"omp\"'*) echo \"ok   shortcut_basename = \\\"omp\\\" (omp-<id> brand intact)\" ;;\n  *) echo \"FAIL shortcut_basename is not \\\"omp\\\": $line\"; fail=1 ;;\nesac\nn=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\nif [ \"$n\" -eq 1 ]; then echo \"ok   single shortcut_basename assignment\"; else echo \"FAIL $n shortcut_basename assignments (want 1)\"; fail=1; fi\n\n# ── adapter identity ──────────────────────────────────────────────────────────────────────────────\ngrep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*\"omp-spt\"' \"$MANIFEST\" \\\n  && echo 'ok   [adapter].name = \"omp-spt\"' || { echo \"FAIL [adapter].name != omp-spt\"; fail=1; }\ngrep -Eq '^[[:space:]]*host_binaries[[:space:]]*=.*\"omp\"' \"$MANIFEST\" \\\n  && echo 'ok   host_binaries includes \"omp\"' || { echo \"FAIL host_binaries misses omp\"; fail=1; }\n\nfield_of() { awk -v h=\"[$1]\" -v f=\"$2\" '$0 == h {s=1;next} /^\\[/{s=0} s && $0 ~ f' \"$MANIFEST\"; }\n\n# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n# [unit->REQ-OMP-NATIVE-TUI]\nspawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\nnspawn=$(printf '%s' \"$spawn\" | grep -c .)\nif [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\nif [ \"$(printf '%s\\n' \"$spawn\" | grep -c -- '--mode rpc')\" -eq 0 ]; then echo \"ok   hosted sessions never use headless RPC mode\"; else echo \"FAIL a hosted session still uses --mode rpc: $spawn\"; fail=1; fi\nresume=$(printf '%s\\n' \"$spawn\" | grep -- '-r {session_id}')\nnresume=$(printf '%s' \"$resume\" | grep -c .)\nif [ \"$nresume\" -eq 1 ]; then echo \"ok   exactly one native resume command carries -r {session_id}\"; else echo \"FAIL expected 1 -r {session_id} command, found $nresume\"; fail=1; fi\nif [ -f \"$EXTENSION\" ]; then echo \"ok   packaged omp-spt extension exists\"; else echo \"FAIL packaged omp-spt extension missing\"; fail=1; fi\nif command -v node >/dev/null 2>&1; then NODE=node\nelif command -v node.exe >/dev/null 2>&1; then NODE=node.exe\nelse echo \"FAIL node is required for the OMP extension unit test\"; fail=1; NODE=:\nfi\nNODE_TEST=\"$ROOT/tests/omp-extension.mjs\"\ncase \"$NODE\" in *.exe) NODE_TEST=$(wslpath -w \"$NODE_TEST\") ;; esac\n\"$NODE\" \"$NODE_TEST\" || fail=1\n\n# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\ngrep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [digest].extractor = digest-omp --session {session_id}\" || { echo \"FAIL [digest].extractor is not digest-omp\"; fail=1; }\ngrep -Eq '^[[:space:]]*fetcher[[:space:]]*=[[:space:]]*\"omp-spt history-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [history].fetcher = history-omp --session {session_id}\" || { echo \"FAIL [history].fetcher is not history-omp\"; fail=1; }\ngrep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt echo-commune-omp --id \\{id\\} --session-id \\{session_id\\}\"' \"$MANIFEST\" \\\n  && echo \"ok   [session.echo_commune].command = echo-commune-omp\" || { echo \"FAIL echo_commune command is not echo-commune-omp\"; fail=1; }\nnpsy=$(grep -cE '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt psyche-omp --id \\{id\\} --session-id \\{session_id\\} --psyche-context-file \\{psyche_context_file\\}\"' \"$MANIFEST\")\nif [ \"$npsy\" -eq 2 ]; then echo \"ok   both psyche roles run psyche-omp with the frozen key shape\"; else echo \"FAIL expected 2 psyche-omp role commands, found $npsy\"; fail=1; fi\n# The retired resident key must stay GONE from both psyche roles.\nif grep -q 'psyche_prompt' \"$MANIFEST\"; then echo \"FAIL a psyche role references the retired {psyche_prompt}\"; fail=1; else echo \"ok   no {psyche_prompt} anywhere (event rides stdin)\"; fi\n\n# [unit->REQ-HAZARD-PSYCHE-IDENTITY-ENV] — identity-env scrub on BOTH psyche roles.\nfor role in session.psyche_init session.psyche_resume; do\n  scrub=$(field_of \"$role\" '^[[:space:]]*env_remove[[:space:]]*=')\n  if [ -z \"$scrub\" ]; then echo \"FAIL [$role] has no env_remove (identity-env scrub missing)\"; fail=1; else\n    case \"$scrub\" in\n      *'\"OWL_SESSION_ID\"'*'\"SPT_AGENT_ID\"'*|*'\"SPT_AGENT_ID\"'*'\"OWL_SESSION_ID\"'*)\n        echo \"ok   [$role] env_remove scrubs OWL_SESSION_ID + SPT_AGENT_ID\" ;;\n      *) echo \"FAIL $role env_remove misses an identity var: $scrub\"; fail=1 ;;\n    esac\n  fi\ndone\nrz_detach=$(field_of session.psyche_resume '^[[:space:]]*detach[[:space:]]*=')\ncase \"$rz_detach\" in *false*) echo \"ok   psyche_resume is captured (detach=false)\" ;; *) echo \"FAIL psyche_resume must be detach=false, got: [$rz_detach]\"; fail=1 ;; esac\nrz_keys=$(field_of session.psyche_resume '^[[:space:]]*keys[[:space:]]*=')\ncase \"$rz_keys\" in *'\"psyche_context_file\"'*) echo \"ok   psyche_resume keys include psyche_context_file\" ;; *) echo \"FAIL psyche_resume keys missing psyche_context_file: [$rz_keys]\"; fail=1 ;; esac\n\n# ── [update] — the gh_release self-update seam points at the fork ─────────────────────────────────\nif grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*\"BigscreenVR/omp-spt\"' \"$MANIFEST\"; then echo 'ok   [update].repo = \"BigscreenVR/omp-spt\"'; else echo \"FAIL [update].repo is not BigscreenVR/omp-spt\"; fail=1; fi\nif grep -Eq '^[[:space:]]*repo[[:space:]]*=[[:space:]]*\"SaberMage/' \"$MANIFEST\"; then echo \"FAIL an active repo assignment still points at SaberMage/*\"; fail=1; else echo \"ok   no active SaberMage repo assignment\"; fi\nif grep -Eq '^[[:space:]]*transport[[:space:]]*=[[:space:]]*\"gh\"' \"$MANIFEST\"; then echo 'ok   [update].transport = \"gh\"'; else echo \"FAIL [update] transport must use gh for auth-capable release fetch\"; fail=1; fi\nif grep -Eq '^[[:space:]]*message[[:space:]]*=' \"$MANIFEST\"; then echo \"ok   [update].message present\"; else echo \"FAIL [update] has no message field\"; fail=1; fi\nif grep -Eq 'endpoint run --adapter omp-spt --id <id> --create --start' \"$MANIFEST\"; then echo \"FAIL fresh-endpoint update notice still uses --start (hides the harness PTY)\"; fail=1; else echo \"ok   fresh-endpoint update notice omits --start (attach-default PTY)\"; fi\nif grep -Eq '^\\[update\\.post\\]' \"$MANIFEST\"; then echo \"ok   [update.post] table present\"; else echo \"FAIL no [update.post] table\"; fail=1; fi\n\n# ── idle-translate binary seam (harness-agnostic; unchanged from claude-spt) ──────────────────────\nif grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"{adapter_dir}/omp-spt translate\"' \"$MANIFEST\"; then echo \"ok   [message-idle-translation-binary].command = translate\"; else echo \"FAIL idle-translate command missing/wrong\"; fail=1; fi\nif grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*\"cc-spt-idle-translate\"' \"$MANIFEST\"; then echo \"FAIL deprecated idle-translate path still present\"; fail=1; else echo \"ok   no deprecated idle-translate path (command-only)\"; fi\n\n# ── no shipped profile overlays (ccs was CC-specific; omp profiles ride `-- --profile`) ───────────\nif grep -Eq '^\\[profiles\\.' \"$MANIFEST\"; then echo \"FAIL a shipped [profiles.*] table lingers (ccs was removed for omp)\"; fail=1; else echo \"ok   no shipped profile overlays\"; fi\n\n# ── [env.SPT_INJECT_VERIFY_ECHO] stays declared (inert on omp; forward-safe) ──────────────────────\nif awk '/^\\[env\\.SPT_INJECT_VERIFY_ECHO\\]/{found=1} END{exit !found}' \"$MANIFEST\"; then echo \"ok   [env.SPT_INJECT_VERIFY_ECHO] declared\"; else echo \"FAIL [env.SPT_INJECT_VERIFY_ECHO] missing\"; fail=1; fi\n\n[ \"$fail\" -eq 0 ] && { echo \"MANIFEST-SHORTCUT OK\"; exit 0; } || { echo \"MANIFEST-SHORTCUT FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\manifest-shortcut.sh"}}},"isError":false,"timestamp":1784087175845}}
{"type":"message","id":"b4f5d0a9","parentId":"4e1c5d9f","timestamp":"2026-07-15T03:46:15.847Z","message":{"role":"toolResult","toolCallId":"call_3iQjctgeU7kJTGidUBwaikjJ|fc_0ca4af36969c80fa016a570287c07c8198b39ea41a17bde31f","toolName":"read","content":[{"type":"text","text":"//! claude-spt — consolidated tool binary for the claude-spt adapter (ADR-0006, U2).\n//!\n//! ONE binary, one set of subcommands (was three crates → fewer artifacts per triple in adapter.spt):\n//!   claude-spt digest      — the [digest] extractor (was claude-spt-digest)\n//!   claude-spt psyche      — the [session.psyche_init] Psyche runner (was claude-spt-psyche)\n//!   claude-spt post-update — reconcile the cplugs plugin after `spt adapter update` (ADR-0006)\n//!   claude-spt translate   — the [message-idle-translation-binary] idle filter (was cc-spt-idle-translate)\n//!   claude-spt hook <ev>   — the CC hook handler (D1: hook logic moved off the plugin shell so it\n//!                            rides `spt adapter update`; was the eight plugin hook .sh wrappers)\n//!   claude-spt launch      — the [session.self]/[session.resume] CC spawn shim (node-named\n//!                            sessions: -n \"<id> @ <node>\" + --remote-control <id>--<node>)\n//!\n//! Dispatch is a bare `argv[1]` match (no clap — keep the dependency-light ethos the predecessor\n//! crates were built on). Each subcommand owns its remaining argv via `std::env::args().skip(2)`\n//! (skip the binary name + the subcommand token) and its own hand-written flag parser. `translate`\n//! reads no argv (pure stdin/stdout protocol).\n//!\n//! `translate` folded in at the v0.8.0 cut once spt-core v0.16.0 gave `[message-idle-translation-\n//! binary]` a `command` field (D3) — so claude-spt is now the SINGLE tool binary (one artifact per\n//! triple in adapter.spt). [impl->REQ-DIST-BINARY-CONSOLIDATE] [impl->REQ-DIST-IDLE-TRANSLATE]\n\nmod bridge;\nmod digest;\nmod digest_omp;\nmod echo_commune;\nmod echo_commune_omp;\nmod history;\nmod history_omp;\nmod hook;\nmod interrupt_watch;\nmod launch;\nmod post_update;\nmod psyche;\nmod psyche_omp;\nmod tag_scan;\nmod translate;\n\nuse std::process::ExitCode;\n\n/// The resolved subcommand. Pure classification of `argv[1]` so dispatch routing is unit-testable\n/// without spawning the binary. [impl->REQ-DIST-BINARY-CONSOLIDATE]\n#[derive(Debug, PartialEq)]\nenum Sub {\n    Bridge,\n    Digest,\n    DigestOmp,\n    EchoCommune,\n    EchoCommuneOmp,\n    History,\n    HistoryOmp,\n    Psyche,\n    PsycheOmp,\n    PostUpdate,\n    Translate,\n    Hook,\n    Launch,\n    Help,\n    Unknown(String),\n}\n\nfn classify(sub: Option<&str>) -> Sub {\n    match sub {\n        Some(\"bridge\") => Sub::Bridge,\n        Some(\"digest\") => Sub::Digest,\n        Some(\"digest-omp\") => Sub::DigestOmp,\n        Some(\"echo-commune\") => Sub::EchoCommune,\n        Some(\"echo-commune-omp\") => Sub::EchoCommuneOmp,\n        Some(\"history\") => Sub::History,\n        Some(\"history-omp\") => Sub::HistoryOmp,\n        Some(\"psyche\") => Sub::Psyche,\n        Some(\"psyche-omp\") => Sub::PsycheOmp,\n        Some(\"post-update\") => Sub::PostUpdate,\n        Some(\"translate\") => Sub::Translate,\n        Some(\"hook\") => Sub::Hook,\n        Some(\"launch\") => Sub::Launch,\n        None | Some(\"-h\") | Some(\"--help\") => Sub::Help,\n        Some(other) => Sub::Unknown(other.to_string()),\n    }\n}\n\nfn usage() {\n    eprintln!(\n        \"claude-spt <subcommand> [args]\\n\\\n         \\n\\\n         subcommands:\\n\\\n         \\x20 bridge       drive omp via `omp --mode rpc`, relaying spt api seed/poll/state (ready-agent)\\n\\\n         \\x20 digest       map a Claude Code JSONL transcript to digest NDJSON ([digest] extractor)\\n\\\n         \\x20 digest-omp   map an oh-my-pi (omp) session JSONL to digest NDJSON (omp [digest] extractor)\\n\\\n         \\x20 history-omp  stream an omp session JSONL verbatim (omp [history] fetcher)\\n\\\n         \\x20 psyche-omp   run one ephemeral omp Psyche turn ([session.psyche_resume], omp-flavored)\\n\\\n         \\x20 echo-commune-omp  bounded end-of-session summarizer over an omp transcript\\n\\\n         \\x20 echo-commune bounded end-of-session summarizer ([session.echo_commune] role)\\n\\\n         \\x20 history      stream a session's CC JSONL transcript verbatim ([history] fetcher)\\n\\\n         \\x20 psyche       run the LiveAgent Psyche companion ([session.psyche_init] runner)\\n\\\n         \\x20 post-update  reconcile the cplugs plugin after `spt adapter update`\\n\\\n         \\x20 translate    idle-message translation filter (stdin->stdout JSON lines)\\n\\\n         \\x20 hook <event> handle a Claude Code hook event (stdin = the CC hook payload)\\n\\\n         \\x20 launch       spawn the CC session with node-named display/RC ([session.self]/[session.resume])\"\n    );\n}\n\nfn main() -> ExitCode {\n    let sub = std::env::args().nth(1);\n    match classify(sub.as_deref()) {\n        Sub::Bridge => bridge::run(),\n        Sub::Digest => digest::run(),\n        Sub::DigestOmp => digest_omp::run(),\n        Sub::EchoCommune => echo_commune::run(),\n        Sub::EchoCommuneOmp => echo_commune_omp::run(),\n        Sub::History => history::run(),\n        Sub::HistoryOmp => history_omp::run(),\n        Sub::Psyche => psyche::run(),\n        Sub::PsycheOmp => psyche_omp::run(),\n        Sub::PostUpdate => post_update::run(),\n        Sub::Translate => translate::run(),\n        Sub::Hook => hook::run(),\n        Sub::Launch => launch::run(),\n        Sub::Help => {\n            usage();\n            ExitCode::SUCCESS\n        }\n        Sub::Unknown(other) => {\n            // DEGRADE, NEVER BRICK (REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP). A stale plugin\n            // dispatch.sh (old 0.1.8 shape) execs `claude-spt <CCEvent>` WITHOUT the `hook` token —\n            // e.g. `claude-spt UserPromptSubmit`. Exiting nonzero here made CC treat every hook as a\n            // blocking failure (all tools blocked + a looping Stop hook) with zero self-repair. So\n            // when the unknown subcommand is actually a CC hook event, route it through as a hook\n            // (the perch keeps working) and emit a NON-blocking stderr note about the skew. A genuine\n            // typo (not a hook event) still exits loud so real misinvocations are not masked.\n            if hook::is_cc_hook_event(&other) {\n                eprintln!(\n                    \"claude-spt: received CC hook event '{other}' as a bare subcommand — the sptc \\\n                     plugin dispatch is stale (dropped the `hook` token). Handling it anyway; run \\\n                     /reload-plugins to refresh the plugin. [REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\"\n                );\n                hook::run_event(&other)\n            } else {\n                eprintln!(\"claude-spt: unknown subcommand: {other}\");\n                usage();\n                ExitCode::from(2)\n            }\n        }\n    }\n}\n\n// [unit->REQ-DIST-BINARY-CONSOLIDATE]\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn classifies_each_real_subcommand() {\n        assert_eq!(classify(Some(\"bridge\")), Sub::Bridge);\n        assert_eq!(classify(Some(\"digest\")), Sub::Digest);\n        assert_eq!(classify(Some(\"digest-omp\")), Sub::DigestOmp);\n        assert_eq!(classify(Some(\"history-omp\")), Sub::HistoryOmp);\n        assert_eq!(classify(Some(\"psyche-omp\")), Sub::PsycheOmp);\n        assert_eq!(classify(Some(\"echo-commune-omp\")), Sub::EchoCommuneOmp);\n        assert_eq!(classify(Some(\"echo-commune\")), Sub::EchoCommune);\n        assert_eq!(classify(Some(\"history\")), Sub::History);\n        assert_eq!(classify(Some(\"psyche\")), Sub::Psyche);\n        assert_eq!(classify(Some(\"post-update\")), Sub::PostUpdate);\n        assert_eq!(classify(Some(\"translate\")), Sub::Translate);\n        assert_eq!(classify(Some(\"hook\")), Sub::Hook);\n        assert_eq!(classify(Some(\"launch\")), Sub::Launch);\n    }\n\n    #[test]\n    fn no_subcommand_and_help_flags_are_help() {\n        assert_eq!(classify(None), Sub::Help);\n        assert_eq!(classify(Some(\"-h\")), Sub::Help);\n        assert_eq!(classify(Some(\"--help\")), Sub::Help);\n    }\n\n    #[test]\n    fn unrecognized_subcommand_is_unknown_not_misrouted() {\n        // A typo must NOT silently fall through to a real subcommand (it exits 2 in main).\n        assert_eq!(classify(Some(\"digset\")), Sub::Unknown(\"digset\".into()));\n    }\n\n    // [unit->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\n    #[test]\n    fn stale_dispatch_cc_event_degrades_typo_stays_loud() {\n        // The stale-dispatch signature: `claude-spt <CCEvent>` (the `hook` token dropped) classifies\n        // as Unknown, but main routes it through hook::run_event (exit 0, pass-through) BECAUSE the\n        // token is a CC hook event. A genuine typo is NOT a hook event → stays the loud exit-2 path.\n        for ev in hook::CC_HOOK_EVENTS {\n            assert_eq!(classify(Some(ev)), Sub::Unknown((*ev).to_string()));\n            assert!(hook::is_cc_hook_event(ev), \"{ev} must be recognised as a CC hook event\");\n        }\n        assert!(!hook::is_cc_hook_event(\"digset\")); // typo → loud exit-2 branch, not degrade\n        assert!(!hook::is_cc_hook_event(\"digest\")); // a real subcommand is never a hook event\n    }\n}\n\n[Showing lines 1-193 of 195. Use :194 to continue]"}],"details":{"truncation":{"content":"//! claude-spt — consolidated tool binary for the claude-spt adapter (ADR-0006, U2).\r\n//!\r\n//! ONE binary, one set of subcommands (was three crates → fewer artifacts per triple in adapter.spt):\r\n//!   claude-spt digest      — the [digest] extractor (was claude-spt-digest)\r\n//!   claude-spt psyche      — the [session.psyche_init] Psyche runner (was claude-spt-psyche)\r\n//!   claude-spt post-update — reconcile the cplugs plugin after `spt adapter update` (ADR-0006)\r\n//!   claude-spt translate   — the [message-idle-translation-binary] idle filter (was cc-spt-idle-translate)\r\n//!   claude-spt hook <ev>   — the CC hook handler (D1: hook logic moved off the plugin shell so it\r\n//!                            rides `spt adapter update`; was the eight plugin hook .sh wrappers)\r\n//!   claude-spt launch      — the [session.self]/[session.resume] CC spawn shim (node-named\r\n//!                            sessions: -n \"<id> @ <node>\" + --remote-control <id>--<node>)\r\n//!\r\n//! Dispatch is a bare `argv[1]` match (no clap — keep the dependency-light ethos the predecessor\r\n//! crates were built on). Each subcommand owns its remaining argv via `std::env::args().skip(2)`\r\n//! (skip the binary name + the subcommand token) and its own hand-written flag parser. `translate`\r\n//! reads no argv (pure stdin/stdout protocol).\r\n//!\r\n//! `translate` folded in at the v0.8.0 cut once spt-core v0.16.0 gave `[message-idle-translation-\r\n//! binary]` a `command` field (D3) — so claude-spt is now the SINGLE tool binary (one artifact per\r\n//! triple in adapter.spt). [impl->REQ-DIST-BINARY-CONSOLIDATE] [impl->REQ-DIST-IDLE-TRANSLATE]\r\n\r\nmod bridge;\r\nmod digest;\r\nmod digest_omp;\r\nmod echo_commune;\r\nmod echo_commune_omp;\r\nmod history;\r\nmod history_omp;\r\nmod hook;\r\nmod interrupt_watch;\r\nmod launch;\r\nmod post_update;\r\nmod psyche;\r\nmod psyche_omp;\r\nmod tag_scan;\r\nmod translate;\r\n\r\nuse std::process::ExitCode;\r\n\r\n/// The resolved subcommand. Pure classification of `argv[1]` so dispatch routing is unit-testable\r\n/// without spawning the binary. [impl->REQ-DIST-BINARY-CONSOLIDATE]\r\n#[derive(Debug, PartialEq)]\r\nenum Sub {\r\n    Bridge,\r\n    Digest,\r\n    DigestOmp,\r\n    EchoCommune,\r\n    EchoCommuneOmp,\r\n    History,\r\n    HistoryOmp,\r\n    Psyche,\r\n    PsycheOmp,\r\n    PostUpdate,\r\n    Translate,\r\n    Hook,\r\n    Launch,\r\n    Help,\r\n    Unknown(String),\r\n}\r\n\r\nfn classify(sub: Option<&str>) -> Sub {\r\n    match sub {\r\n        Some(\"bridge\") => Sub::Bridge,\r\n        Some(\"digest\") => Sub::Digest,\r\n        Some(\"digest-omp\") => Sub::DigestOmp,\r\n        Some(\"echo-commune\") => Sub::EchoCommune,\r\n        Some(\"echo-commune-omp\") => Sub::EchoCommuneOmp,\r\n        Some(\"history\") => Sub::History,\r\n        Some(\"history-omp\") => Sub::HistoryOmp,\r\n        Some(\"psyche\") => Sub::Psyche,\r\n        Some(\"psyche-omp\") => Sub::PsycheOmp,\r\n        Some(\"post-update\") => Sub::PostUpdate,\r\n        Some(\"translate\") => Sub::Translate,\r\n        Some(\"hook\") => Sub::Hook,\r\n        Some(\"launch\") => Sub::Launch,\r\n        None | Some(\"-h\") | Some(\"--help\") => Sub::Help,\r\n        Some(other) => Sub::Unknown(other.to_string()),\r\n    }\r\n}\r\n\r\nfn usage() {\r\n    eprintln!(\r\n        \"claude-spt <subcommand> [args]\\n\\\r\n         \\n\\\r\n         subcommands:\\n\\\r\n         \\x20 bridge       drive omp via `omp --mode rpc`, relaying spt api seed/poll/state (ready-agent)\\n\\\r\n         \\x20 digest       map a Claude Code JSONL transcript to digest NDJSON ([digest] extractor)\\n\\\r\n         \\x20 digest-omp   map an oh-my-pi (omp) session JSONL to digest NDJSON (omp [digest] extractor)\\n\\\r\n         \\x20 history-omp  stream an omp session JSONL verbatim (omp [history] fetcher)\\n\\\r\n         \\x20 psyche-omp   run one ephemeral omp Psyche turn ([session.psyche_resume], omp-flavored)\\n\\\r\n         \\x20 echo-commune-omp  bounded end-of-session summarizer over an omp transcript\\n\\\r\n         \\x20 echo-commune bounded end-of-session summarizer ([session.echo_commune] role)\\n\\\r\n         \\x20 history      stream a session's CC JSONL transcript verbatim ([history] fetcher)\\n\\\r\n         \\x20 psyche       run the LiveAgent Psyche companion ([session.psyche_init] runner)\\n\\\r\n         \\x20 post-update  reconcile the cplugs plugin after `spt adapter update`\\n\\\r\n         \\x20 translate    idle-message translation filter (stdin->stdout JSON lines)\\n\\\r\n         \\x20 hook <event> handle a Claude Code hook event (stdin = the CC hook payload)\\n\\\r\n         \\x20 launch       spawn the CC session with node-named display/RC ([session.self]/[session.resume])\"\r\n    );\r\n}\r\n\r\nfn main() -> ExitCode {\r\n    let sub = std::env::args().nth(1);\r\n    match classify(sub.as_deref()) {\r\n        Sub::Bridge => bridge::run(),\r\n        Sub::Digest => digest::run(),\r\n        Sub::DigestOmp => digest_omp::run(),\r\n        Sub::EchoCommune => echo_commune::run(),\r\n        Sub::EchoCommuneOmp => echo_commune_omp::run(),\r\n        Sub::History => history::run(),\r\n        Sub::HistoryOmp => history_omp::run(),\r\n        Sub::Psyche => psyche::run(),\r\n        Sub::PsycheOmp => psyche_omp::run(),\r\n        Sub::PostUpdate => post_update::run(),\r\n        Sub::Translate => translate::run(),\r\n        Sub::Hook => hook::run(),\r\n        Sub::Launch => launch::run(),\r\n        Sub::Help => {\r\n            usage();\r\n            ExitCode::SUCCESS\r\n        }\r\n        Sub::Unknown(other) => {\r\n            // DEGRADE, NEVER BRICK (REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP). A stale plugin\r\n            // dispatch.sh (old 0.1.8 shape) execs `claude-spt <CCEvent>` WITHOUT the `hook` token —\r\n            // e.g. `claude-spt UserPromptSubmit`. Exiting nonzero here made CC treat every hook as a\r\n            // blocking failure (all tools blocked + a looping Stop hook) with zero self-repair. So\r\n            // when the unknown subcommand is actually a CC hook event, route it through as a hook\r\n            // (the perch keeps working) and emit a NON-blocking stderr note about the skew. A genuine\r\n            // typo (not a hook event) still exits loud so real misinvocations are not masked.\r\n            if hook::is_cc_hook_event(&other) {\r\n                eprintln!(\r\n                    \"claude-spt: received CC hook event '{other}' as a bare subcommand — the sptc \\\r\n                     plugin dispatch is stale (dropped the `hook` token). Handling it anyway; run \\\r\n                     /reload-plugins to refresh the plugin. [REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\"\r\n                );\r\n                hook::run_event(&other)\r\n            } else {\r\n                eprintln!(\"claude-spt: unknown subcommand: {other}\");\r\n                usage();\r\n                ExitCode::from(2)\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n// [unit->REQ-DIST-BINARY-CONSOLIDATE]\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    #[test]\r\n    fn classifies_each_real_subcommand() {\r\n        assert_eq!(classify(Some(\"bridge\")), Sub::Bridge);\r\n        assert_eq!(classify(Some(\"digest\")), Sub::Digest);\r\n        assert_eq!(classify(Some(\"digest-omp\")), Sub::DigestOmp);\r\n        assert_eq!(classify(Some(\"history-omp\")), Sub::HistoryOmp);\r\n        assert_eq!(classify(Some(\"psyche-omp\")), Sub::PsycheOmp);\r\n        assert_eq!(classify(Some(\"echo-commune-omp\")), Sub::EchoCommuneOmp);\r\n        assert_eq!(classify(Some(\"echo-commune\")), Sub::EchoCommune);\r\n        assert_eq!(classify(Some(\"history\")), Sub::History);\r\n        assert_eq!(classify(Some(\"psyche\")), Sub::Psyche);\r\n        assert_eq!(classify(Some(\"post-update\")), Sub::PostUpdate);\r\n        assert_eq!(classify(Some(\"translate\")), Sub::Translate);\r\n        assert_eq!(classify(Some(\"hook\")), Sub::Hook);\r\n        assert_eq!(classify(Some(\"launch\")), Sub::Launch);\r\n    }\r\n\r\n    #[test]\r\n    fn no_subcommand_and_help_flags_are_help() {\r\n        assert_eq!(classify(None), Sub::Help);\r\n        assert_eq!(classify(Some(\"-h\")), Sub::Help);\r\n        assert_eq!(classify(Some(\"--help\")), Sub::Help);\r\n    }\r\n\r\n    #[test]\r\n    fn unrecognized_subcommand_is_unknown_not_misrouted() {\r\n        // A typo must NOT silently fall through to a real subcommand (it exits 2 in main).\r\n        assert_eq!(classify(Some(\"digset\")), Sub::Unknown(\"digset\".into()));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\r\n    #[test]\r\n    fn stale_dispatch_cc_event_degrades_typo_stays_loud() {\r\n        // The stale-dispatch signature: `claude-spt <CCEvent>` (the `hook` token dropped) classifies\r\n        // as Unknown, but main routes it through hook::run_event (exit 0, pass-through) BECAUSE the\r\n        // token is a CC hook event. A genuine typo is NOT a hook event → stays the loud exit-2 path.\r\n        for ev in hook::CC_HOOK_EVENTS {\r\n            assert_eq!(classify(Some(ev)), Sub::Unknown((*ev).to_string()));\r\n            assert!(hook::is_cc_hook_event(ev), \"{ev} must be recognised as a CC hook event\");\r\n        }\r\n        assert!(!hook::is_cc_hook_event(\"digset\")); // typo → loud exit-2 branch, not degrade\r\n        assert!(!hook::is_cc_hook_event(\"digest\")); // a real subcommand is never a hook event\r\n    }\r","truncated":true,"truncatedBy":"lines","totalLines":195,"totalBytes":9028,"outputLines":193,"outputBytes":9028,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! claude-spt — consolidated tool binary for the claude-spt adapter (ADR-0006, U2).\r\n//!\r\n//! ONE binary, one set of subcommands (was three crates → fewer artifacts per triple in adapter.spt):\r\n//!   claude-spt digest      — the [digest] extractor (was claude-spt-digest)\r\n//!   claude-spt psyche      — the [session.psyche_init] Psyche runner (was claude-spt-psyche)\r\n//!   claude-spt post-update — reconcile the cplugs plugin after `spt adapter update` (ADR-0006)\r\n//!   claude-spt translate   — the [message-idle-translation-binary] idle filter (was cc-spt-idle-translate)\r\n//!   claude-spt hook <ev>   — the CC hook handler (D1: hook logic moved off the plugin shell so it\r\n//!                            rides `spt adapter update`; was the eight plugin hook .sh wrappers)\r\n//!   claude-spt launch      — the [session.self]/[session.resume] CC spawn shim (node-named\r\n//!                            sessions: -n \"<id> @ <node>\" + --remote-control <id>--<node>)\r\n//!\r\n//! Dispatch is a bare `argv[1]` match (no clap — keep the dependency-light ethos the predecessor\r\n//! crates were built on). Each subcommand owns its remaining argv via `std::env::args().skip(2)`\r\n//! (skip the binary name + the subcommand token) and its own hand-written flag parser. `translate`\r\n//! reads no argv (pure stdin/stdout protocol).\r\n//!\r\n//! `translate` folded in at the v0.8.0 cut once spt-core v0.16.0 gave `[message-idle-translation-\r\n//! binary]` a `command` field (D3) — so claude-spt is now the SINGLE tool binary (one artifact per\r\n//! triple in adapter.spt). [impl->REQ-DIST-BINARY-CONSOLIDATE] [impl->REQ-DIST-IDLE-TRANSLATE]\r\n\r\nmod bridge;\r\nmod digest;\r\nmod digest_omp;\r\nmod echo_commune;\r\nmod echo_commune_omp;\r\nmod history;\r\nmod history_omp;\r\nmod hook;\r\nmod interrupt_watch;\r\nmod launch;\r\nmod post_update;\r\nmod psyche;\r\nmod psyche_omp;\r\nmod tag_scan;\r\nmod translate;\r\n\r\nuse std::process::ExitCode;\r\n\r\n/// The resolved subcommand. Pure classification of `argv[1]` so dispatch routing is unit-testable\r\n/// without spawning the binary. [impl->REQ-DIST-BINARY-CONSOLIDATE]\r\n#[derive(Debug, PartialEq)]\r\nenum Sub {\r\n    Bridge,\r\n    Digest,\r\n    DigestOmp,\r\n    EchoCommune,\r\n    EchoCommuneOmp,\r\n    History,\r\n    HistoryOmp,\r\n    Psyche,\r\n    PsycheOmp,\r\n    PostUpdate,\r\n    Translate,\r\n    Hook,\r\n    Launch,\r\n    Help,\r\n    Unknown(String),\r\n}\r\n\r\nfn classify(sub: Option<&str>) -> Sub {\r\n    match sub {\r\n        Some(\"bridge\") => Sub::Bridge,\r\n        Some(\"digest\") => Sub::Digest,\r\n        Some(\"digest-omp\") => Sub::DigestOmp,\r\n        Some(\"echo-commune\") => Sub::EchoCommune,\r\n        Some(\"echo-commune-omp\") => Sub::EchoCommuneOmp,\r\n        Some(\"history\") => Sub::History,\r\n        Some(\"history-omp\") => Sub::HistoryOmp,\r\n        Some(\"psyche\") => Sub::Psyche,\r\n        Some(\"psyche-omp\") => Sub::PsycheOmp,\r\n        Some(\"post-update\") => Sub::PostUpdate,\r\n        Some(\"translate\") => Sub::Translate,\r\n        Some(\"hook\") => Sub::Hook,\r\n        Some(\"launch\") => Sub::Launch,\r\n        None | Some(\"-h\") | Some(\"--help\") => Sub::Help,\r\n        Some(other) => Sub::Unknown(other.to_string()),\r\n    }\r\n}\r\n\r\nfn usage() {\r\n    eprintln!(\r\n        \"claude-spt <subcommand> [args]\\n\\\r\n         \\n\\\r\n         subcommands:\\n\\\r\n         \\x20 bridge       drive omp via `omp --mode rpc`, relaying spt api seed/poll/state (ready-agent)\\n\\\r\n         \\x20 digest       map a Claude Code JSONL transcript to digest NDJSON ([digest] extractor)\\n\\\r\n         \\x20 digest-omp   map an oh-my-pi (omp) session JSONL to digest NDJSON (omp [digest] extractor)\\n\\\r\n         \\x20 history-omp  stream an omp session JSONL verbatim (omp [history] fetcher)\\n\\\r\n         \\x20 psyche-omp   run one ephemeral omp Psyche turn ([session.psyche_resume], omp-flavored)\\n\\\r\n         \\x20 echo-commune-omp  bounded end-of-session summarizer over an omp transcript\\n\\\r\n         \\x20 echo-commune bounded end-of-session summarizer ([session.echo_commune] role)\\n\\\r\n         \\x20 history      stream a session's CC JSONL transcript verbatim ([history] fetcher)\\n\\\r\n         \\x20 psyche       run the LiveAgent Psyche companion ([session.psyche_init] runner)\\n\\\r\n         \\x20 post-update  reconcile the cplugs plugin after `spt adapter update`\\n\\\r\n         \\x20 translate    idle-message translation filter (stdin->stdout JSON lines)\\n\\\r\n         \\x20 hook <event> handle a Claude Code hook event (stdin = the CC hook payload)\\n\\\r\n         \\x20 launch       spawn the CC session with node-named display/RC ([session.self]/[session.resume])\"\r\n    );\r\n}\r\n\r\nfn main() -> ExitCode {\r\n    let sub = std::env::args().nth(1);\r\n    match classify(sub.as_deref()) {\r\n        Sub::Bridge => bridge::run(),\r\n        Sub::Digest => digest::run(),\r\n        Sub::DigestOmp => digest_omp::run(),\r\n        Sub::EchoCommune => echo_commune::run(),\r\n        Sub::EchoCommuneOmp => echo_commune_omp::run(),\r\n        Sub::History => history::run(),\r\n        Sub::HistoryOmp => history_omp::run(),\r\n        Sub::Psyche => psyche::run(),\r\n        Sub::PsycheOmp => psyche_omp::run(),\r\n        Sub::PostUpdate => post_update::run(),\r\n        Sub::Translate => translate::run(),\r\n        Sub::Hook => hook::run(),\r\n        Sub::Launch => launch::run(),\r\n        Sub::Help => {\r\n            usage();\r\n            ExitCode::SUCCESS\r\n        }\r\n        Sub::Unknown(other) => {\r\n            // DEGRADE, NEVER BRICK (REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP). A stale plugin\r\n            // dispatch.sh (old 0.1.8 shape) execs `claude-spt <CCEvent>` WITHOUT the `hook` token —\r\n            // e.g. `claude-spt UserPromptSubmit`. Exiting nonzero here made CC treat every hook as a\r\n            // blocking failure (all tools blocked + a looping Stop hook) with zero self-repair. So\r\n            // when the unknown subcommand is actually a CC hook event, route it through as a hook\r\n            // (the perch keeps working) and emit a NON-blocking stderr note about the skew. A genuine\r\n            // typo (not a hook event) still exits loud so real misinvocations are not masked.\r\n            if hook::is_cc_hook_event(&other) {\r\n                eprintln!(\r\n                    \"claude-spt: received CC hook event '{other}' as a bare subcommand — the sptc \\\r\n                     plugin dispatch is stale (dropped the `hook` token). Handling it anyway; run \\\r\n                     /reload-plugins to refresh the plugin. [REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\"\r\n                );\r\n                hook::run_event(&other)\r\n            } else {\r\n                eprintln!(\"claude-spt: unknown subcommand: {other}\");\r\n                usage();\r\n                ExitCode::from(2)\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n// [unit->REQ-DIST-BINARY-CONSOLIDATE]\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    #[test]\r\n    fn classifies_each_real_subcommand() {\r\n        assert_eq!(classify(Some(\"bridge\")), Sub::Bridge);\r\n        assert_eq!(classify(Some(\"digest\")), Sub::Digest);\r\n        assert_eq!(classify(Some(\"digest-omp\")), Sub::DigestOmp);\r\n        assert_eq!(classify(Some(\"history-omp\")), Sub::HistoryOmp);\r\n        assert_eq!(classify(Some(\"psyche-omp\")), Sub::PsycheOmp);\r\n        assert_eq!(classify(Some(\"echo-commune-omp\")), Sub::EchoCommuneOmp);\r\n        assert_eq!(classify(Some(\"echo-commune\")), Sub::EchoCommune);\r\n        assert_eq!(classify(Some(\"history\")), Sub::History);\r\n        assert_eq!(classify(Some(\"psyche\")), Sub::Psyche);\r\n        assert_eq!(classify(Some(\"post-update\")), Sub::PostUpdate);\r\n        assert_eq!(classify(Some(\"translate\")), Sub::Translate);\r\n        assert_eq!(classify(Some(\"hook\")), Sub::Hook);\r\n        assert_eq!(classify(Some(\"launch\")), Sub::Launch);\r\n    }\r\n\r\n    #[test]\r\n    fn no_subcommand_and_help_flags_are_help() {\r\n        assert_eq!(classify(None), Sub::Help);\r\n        assert_eq!(classify(Some(\"-h\")), Sub::Help);\r\n        assert_eq!(classify(Some(\"--help\")), Sub::Help);\r\n    }\r\n\r\n    #[test]\r\n    fn unrecognized_subcommand_is_unknown_not_misrouted() {\r\n        // A typo must NOT silently fall through to a real subcommand (it exits 2 in main).\r\n        assert_eq!(classify(Some(\"digset\")), Sub::Unknown(\"digset\".into()));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP]\r\n    #[test]\r\n    fn stale_dispatch_cc_event_degrades_typo_stays_loud() {\r\n        // The stale-dispatch signature: `claude-spt <CCEvent>` (the `hook` token dropped) classifies\r\n        // as Unknown, but main routes it through hook::run_event (exit 0, pass-through) BECAUSE the\r\n        // token is a CC hook event. A genuine typo is NOT a hook event → stays the loud exit-2 path.\r\n        for ev in hook::CC_HOOK_EVENTS {\r\n            assert_eq!(classify(Some(ev)), Sub::Unknown((*ev).to_string()));\r\n            assert!(hook::is_cc_hook_event(ev), \"{ev} must be recognised as a CC hook event\");\r\n        }\r\n        assert!(!hook::is_cc_hook_event(\"digset\")); // typo → loud exit-2 branch, not degrade\r\n        assert!(!hook::is_cc_hook_event(\"digest\")); // a real subcommand is never a hook event\r\n    }\r\n}","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,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tools\\claude-spt\\src\\main.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":195,"totalBytes":9028,"outputLines":193,"outputBytes":9028,"shownRange":{"start":1,"end":193},"nextOffset":194}}},"isError":false,"timestamp":1784087175847}}
{"type":"message","id":"a7bb7414","parentId":"b4f5d0a9","timestamp":"2026-07-15T03:46:15.855Z","message":{"role":"toolResult","toolCallId":"call_oJpBZpbDZMAXXUagtIIJpP8U|fc_0ca4af36969c80fa016a570287c0788198b52b351e1d38cfbc","toolName":"read","content":[{"type":"text","text":"//! The omp-spt RPC bridge (S2 ready-agent; hardened at v0.1.1 per docs/OMP-BRIDGE-FIELD-ISSUES.md).\n//!\n//! This is the hosted harness process for the oh-my-pi (omp) adapter: spt-core's broker spawns it as\n//! `[session.self].command`, and it drives a resident `omp --mode rpc` child, translating in both\n//! directions (OMP-ADAPTER-PLAN.md, \"Architecture decision: RPC-bridge (Style A)\"):\n//!\n//! ```text\n//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\n//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\n//!                          └  prompt / get_state                                    (to omp)\n//! ```\n//!\n//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\n//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\n//! compat alias for the same entry.\n//!\n//! ## Protocol (verified against omp v16.3.11 — see OMP-ADAPTER-PLAN.md §\"decisive finding\")\n//!\n//! Startup:   omp → `{\"type\":\"ready\"}` (then unsolicited `extension_ui_request` /\n//!            `available_commands_update`, which `--mode rpc` — unlike `rpc-ui` — does NOT block on).\n//! Introspect: host → `{\"type\":\"get_state\"}`; omp → `{\"type\":\"response\",\"command\":\"get_state\",\n//!            \"success\":true,\"data\":{\"sessionId\":\"<uuid>\",\"sessionFile\":\"…\",\"model\":…}}`.\n//! Drive turn: host → `{\"type\":\"prompt\",\"message\":\"…\"}`; omp acks `{\"type\":\"response\",\n//!            \"command\":\"prompt\",\"success\":true}`, then streams `turn_start` → `agent_start` →\n//!            `message_update`… → `turn_end` → `{\"type\":\"agent_end\",\"messages\":[…]}`.\n//!\n//! ## Hardening (the doyle 2026-07-09 field incident — docs/OMP-BRIDGE-FIELD-ISSUES.md)\n//!\n//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\n//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\n//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\n//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\n//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\n//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\n//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\n//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\n//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\n//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\n//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\n//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\n//!   before-handshake); a turn breach recovers the perch to idle and tells the sender.\n//! * **MED-2**: the event loop notices a dead omp/listener child immediately (reader-thread EOF),\n//!   even while idle — not on the next message.\n//! * **LOW-1/2**: spt bind/state/send failures land on the operational log, and a failed turn sends\n//!   the sender an explicit failure notice, never a silent empty reply.\n\nuse serde_json::{json, Value};\nuse std::io::{BufRead, BufReader, Write};\nuse std::process::{ChildStdin, Command, ExitCode, Stdio};\nuse std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};\nuse std::time::{Duration, Instant};\n\n/// The adapter_name every targeted `spt api` verb carries (`--adapter`). Distinct from the plugin\n/// namespace `omps`; mirrors hook.rs's `ADAPTER` for the claude-spt path.\nconst ADAPTER: &str = \"omp-spt\";\n\n/// Bringup deadline for omp's `ready` frame (override: OMP_SPT_READY_TIMEOUT_S). Generous — a cold\n/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\n/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\nconst DEFAULT_READY_TIMEOUT_S: u64 = 120;\n/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\nconst DEFAULT_STATE_TIMEOUT_S: u64 = 30;\n/// Per-turn deadline: prompt dispatch → `agent_end` (override: OMP_SPT_TURN_TIMEOUT_S). Long — a\n/// real agent turn can run tools for minutes — but bounded, so a hung turn frees the perch.\nconst DEFAULT_TURN_TIMEOUT_S: u64 = 600;\n/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\nconst DEFAULT_HEARTBEAT_S: u64 = 300;\n\n/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\n/// message/reply/error/heartbeat lines without any env flag.\nmacro_rules! oplog {\n    ($($a:tt)*) => {\n        eprintln!(\"[omp-spt] {}\", format!($($a)*))\n    };\n}\n\n/// Frame-level trace, still gated behind `OMP_SPT_DEBUG` (diagnostics; off by default).\nmacro_rules! trace {\n    ($($a:tt)*) => {\n        if std::env::var_os(\"OMP_SPT_DEBUG\").is_some() {\n            eprintln!(\"[omp-spt::trace] {}\", format!($($a)*));\n        }\n    };\n}\n\nfn env_secs(name: &str, default: u64) -> u64 {\n    std::env::var(name).ok().and_then(|s| s.parse().ok()).unwrap_or(default)\n}\n\n/// Parsed bridge argv. `omp_args` is everything after a `--` separator, forwarded to omp verbatim.\nstruct Args {\n    id: Option<String>,\n    subnet: Option<String>,\n    session_dir: Option<String>,\n    resume: Option<String>,\n    omp_args: Vec<String>,\n}\n\nfn parse_args<I: Iterator<Item = String>>(mut it: I) -> Args {\n    let mut a = Args {\n        id: None,\n        subnet: None,\n        session_dir: None,\n        resume: None,\n        omp_args: Vec::new(),\n    };\n    while let Some(arg) = it.next() {\n        match arg.as_str() {\n            \"--id\" => a.id = it.next(),\n            \"--subnet\" => a.subnet = it.next(),\n            \"--session-dir\" => a.session_dir = it.next(),\n            \"--resume\" => a.resume = it.next(),\n            \"--node\" => {\n                let _ = it.next(); // accepted for manifest {node} parity; the bridge does not use it\n            }\n            \"--\" => {\n                a.omp_args.extend(it.by_ref());\n                break;\n            }\n            other => oplog!(\"ignoring unrecognized flag: {other}\"),\n        }\n    }\n    // Home subnet: required by `spt ready`/`bind` for a NEW endpoint on a multi-subnet node;\n    // harmless otherwise. Broker-hosted perches are pre-homed so the broker path can omit it.\n    if a.subnet.is_none() {\n        a.subnet = std::env::var(\"OMP_SPT_SUBNET\").ok().filter(|s| !s.is_empty());\n    }\n    a\n}\n\n/// Entry for `claude-spt bridge …` (compat alias — argv starts after the subcommand token).\npub fn run() -> ExitCode {\n    run_argv(std::env::args().skip(2))\n}\n\n/// Entry for the `omp-spt` binary (HIGH-1 — argv starts right after the binary name).\npub fn run_argv<I: Iterator<Item = String>>(argv: I) -> ExitCode {\n    let args = parse_args(argv);\n\n    // Endpoint id: --id wins, else the broker-injected $SPT_ENDPOINT_ID ([env.SPT_ENDPOINT_ID],\n    // direction=\"inject\", value=\"{id}\"). Without it we cannot address the perch — fail loud.\n    let id = args\n        .id\n        .clone()\n        .or_else(|| std::env::var(\"SPT_ENDPOINT_ID\").ok())\n        .filter(|s| !s.is_empty());\n    let id = match id {\n        Some(id) => id,\n        None => {\n            oplog!(\"FATAL: no endpoint id (pass --id or set SPT_ENDPOINT_ID)\");\n            return ExitCode::from(2);\n        }\n    };\n\n    // HIGH-2: put OURSELVES in a kill-on-close job object BEFORE spawning any child, so both\n    // children inherit membership and the OS reaps the whole tree when the bridge dies — however it\n    // dies. Best-effort: a failure is logged, never fatal (explicit kills still cover the graceful\n    // path). The returned handle is deliberately held (leaked) for the process lifetime; the OS\n    // closes it at process exit, which is exactly the kill trigger.\n    match teardown::arm_kill_on_close() {\n        Ok(()) => trace!(\"job-object armed (children die with the bridge)\"),\n        Err(e) => oplog!(\"WARN: could not arm child teardown ({e}) — orphan risk on hard kill\"),\n    }\n\n    let spt = spt_bin();\n    let omp = omp_bin();\n    oplog!(\"bridge starting: id={id} omp={omp} spt={spt}\");\n\n    // Spawn the resident omp RPC child. stderr is inherited so omp's own diagnostics (auth, model)\n    // surface in the broker PTY; stdin/stdout carry the NDJSON protocol.\n    let mut cmd = Command::new(&omp);\n    cmd.arg(\"--mode\").arg(\"rpc\");\n    // [session.resume]: reload a prior omp session by id — omp's native resume (`-r <id-prefix>`).\n    // get_state after startup still reports the (resumed) sessionId, so the bind path is identical.\n    if let Some(sid) = &args.resume {\n        cmd.arg(\"-r\").arg(sid);\n    }\n    if let Some(dir) = &args.session_dir {\n        cmd.arg(\"--session-dir\").arg(dir);\n    }\n    cmd.args(&args.omp_args);\n    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit());\n    let mut omp_child = match cmd.spawn() {\n        Ok(c) => c,\n        Err(e) => {\n            oplog!(\"FATAL: failed to spawn omp ({omp}): {e}\");\n            return ExitCode::FAILURE;\n        }\n    };\n    let mut omp_stdin = match omp_child.stdin.take() {\n        Some(s) => s,\n        None => {\n            oplog!(\"FATAL: omp child missing stdin handle\");\n            let _ = omp_child.kill();\n            return ExitCode::FAILURE;\n        }\n    };\n\n    // HIGH-4/MED-2: one event channel, fed by dedicated reader threads — every wait below is a\n    // bounded recv, and a child dying is an Event even while the bridge is idle.\n    let (tx, rx) = std::sync::mpsc::channel::<Event>();\n    spawn_line_reader(omp_child.stdout.take(), tx.clone(), Event::OmpLine, Event::OmpEof);\n\n    // Startup gate: omp's `ready` frame within the deadline. Breaching exits loud BEFORE any bind,\n    // so a failed bringup never poisons the endpoint's session ledger (MED-4 discipline).\n    let ready_deadline = Duration::from_secs(env_secs(\"OMP_SPT_READY_TIMEOUT_S\", DEFAULT_READY_TIMEOUT_S));\n    match await_frame(&rx, ready_deadline, |f| (f[\"type\"].as_str() == Some(\"ready\")).then_some(())) {\n        Ok(()) => oplog!(\"omp ready\"),\n        Err(e) => {\n            oplog!(\"FATAL: omp never became ready within {}s ({e}) — is {omp} really oh-my-pi? (set OMP_SPT_OMP_BIN)\", ready_deadline.as_secs());\n            let _ = omp_child.kill();\n            return ExitCode::FAILURE;\n        }\n    }\n\n    // Identity binding: omp mints the session id, we read it and attach it to the perch. omp has no\n    // \"inject an endpoint id\" flag, so the id flows the other way — this bind is how the perch\n    // learns which omp session backs it, and it returns the capability TOKEN (`BOUND:<id>\n    // token=<hex>`) that authenticates later mutating calls.\n    if !send_frame(&mut omp_stdin, &json!({\"type\": \"get_state\"})) {\n        oplog!(\"FATAL: could not write get_state to omp\");\n        let _ = omp_child.kill();\n        return ExitCode::FAILURE;\n    }\n    let state_deadline = Duration::from_secs(env_secs(\"OMP_SPT_STATE_TIMEOUT_S\", DEFAULT_STATE_TIMEOUT_S));\n    let sid = match await_frame(&rx, state_deadline, |f| {\n        (f[\"type\"].as_str() == Some(\"response\") && f[\"command\"].as_str() == Some(\"get_state\"))\n            .then(|| f[\"data\"][\"sessionId\"].as_str().map(str::to_string))\n            .flatten()\n    }) {\n        Ok(s) => s,\n        Err(e) => {\n            oplog!(\"FATAL: no get_state response within {}s ({e})\", state_deadline.as_secs());\n            let _ = omp_child.kill();\n            return ExitCode::FAILURE;\n        }\n    };\n\n    let mut bind_args = vec![\"api\", \"--adapter\", ADAPTER, \"bind\", &id, \"--set-session-id\", &sid];\n    if let Some(sub) = &args.subnet {\n        bind_args.push(\"--subnet\");\n        bind_args.push(sub);\n    }\n    let bind_out = spt_call(&spt, &bind_args, None).unwrap_or_default();\n    let token = parse_bound_token(&bind_out);\n    if token.is_none() {\n        // Broker-hosted revive may not re-mint a token; state calls then fall back to --session-id.\n        oplog!(\"bind returned no token ({}) — state auth falls back to session id\", first_line(&bind_out));\n    }\n    oplog!(\"bound: id={id} omp-session={sid}\");\n    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\n\n    // ── Receive relay ────────────────────────────────────────────────────────────────────────────\n    // `spt ready <id>` registers the perch as a listener and BLOCKS, printing one\n    // `<EVENT type=\"msg\" from=\"…\">body</EVENT>` line per delivery (verified live). Its stdout feeds\n    // the same event channel; stderr is inherited so its `READY:<id>` banner reaches the PTY.\n    let mut ready_cmd = Command::new(&spt);\n    ready_cmd.arg(\"ready\").arg(&id);\n    if let Some(sub) = &args.subnet {\n        ready_cmd.arg(\"--subnet\").arg(sub);\n    }\n    ready_cmd.stdout(Stdio::piped()).stderr(Stdio::inherit()).stdin(Stdio::null());\n    let mut ready_child = match ready_cmd.spawn() {\n        Ok(c) => c,\n        Err(e) => {\n            oplog!(\"FATAL: failed to spawn `spt ready`: {e}\");\n            let _ = omp_child.kill();\n            return ExitCode::FAILURE;\n        }\n    };\n    spawn_line_reader(ready_child.stdout.take(), tx.clone(), Event::ReadyLine, Event::ReadyEof);\n    oplog!(\"listening: relay is `spt ready {id}` (messages will log here as they arrive)\");\n\n    // ── Event loop ───────────────────────────────────────────────────────────────────────────────\n    let turn_deadline = Duration::from_secs(env_secs(\"OMP_SPT_TURN_TIMEOUT_S\", DEFAULT_TURN_TIMEOUT_S));\n    let heartbeat_s = env_secs(\"OMP_SPT_HEARTBEAT_S\", DEFAULT_HEARTBEAT_S);\n    let heartbeat = Duration::from_secs(if heartbeat_s == 0 { 3600 } else { heartbeat_s });\n    let started = Instant::now();\n    let mut turns: u64 = 0;\n    let mut stale_turns: u32 = 0; // timed-out turns whose late agent_end should be discarded\n    let mut pending: std::collections::VecDeque<String> = std::collections::VecDeque::new();\n    let exit_reason: String;\n\n    'main: loop {\n        // Deliveries queued behind a long turn are served before blocking on the channel again.\n        let event = if let Some(line) = pending.pop_front() {\n            Ok(Event::ReadyLine(line))\n        } else {\n            rx.recv_timeout(heartbeat)\n        };\n        match event {\n            Ok(Event::ReadyLine(line)) => {\n                for (from, body) in parse_events(&line) {\n                    let sender = from.as_deref().unwrap_or(\"<unknown>\");\n                    oplog!(\"msg in: from={sender} bytes={}\", body.len());\n                    // Busy for the whole handling window (pickup → agent_end) so a concurrent\n                    // status read never sees an idle perch mid-turn.\n                    set_state(&spt, &id, &sid, token.as_deref(), \"busy\");\n                    let turn = drive_turn(&mut omp_stdin, &rx, turn_deadline, &body, &mut pending, &mut stale_turns);\n                    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\n                    turns += 1;\n                    let mut omp_gone = false;\n                    match (&from, turn) {\n                        (Some(target), Ok(reply)) if !target.is_empty() => {\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&reply))\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\n                            oplog!(\"reply out: to={target} bytes={} ({})\", reply.len(), first_line(&status));\n                        }\n                        (Some(target), Err(fail)) if !target.is_empty() => {\n                            // LOW-2: a failed turn is reported, never a silent empty reply.\n                            let cause = fail.cause();\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\n                            oplog!(\"turn FAILED: {cause}\");\n                            let notice = format!(\n                                \"[omp-spt] turn failed ({cause}) — no answer was produced.{}\",\n                                if omp_gone {\n                                    \" The endpoint is going down.\"\n                                } else {\n                                    \" The endpoint is still up; retry, or an operator can inspect it with `spt rc`.\"\n                                }\n                            );\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&notice))\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\n                            oplog!(\"failure notice out: to={target} ({})\", first_line(&status));\n                        }\n                        (_, Ok(_)) => oplog!(\"msg had no `from` — reply not routed (kept in omp session)\"),\n                        (_, Err(fail)) => {\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\n                            oplog!(\"turn FAILED on sender-less msg: {}\", fail.cause());\n                        }\n                    }\n                    if omp_gone {\n                        exit_reason = \"omp child exited mid-turn\".into();\n                        break 'main;\n                    }\n                }\n            }\n            Ok(Event::OmpLine(line)) => {\n                // A frame outside a turn: either unsolicited chatter or a STALE agent_end from a\n                // timed-out turn. Log the latter so a late completion is visible, then discard.\n                if line.contains(\"\\\"agent_end\\\"\") && stale_turns > 0 {\n                    stale_turns -= 1;\n                    oplog!(\"stale agent_end from a timed-out turn discarded (late completion)\");\n                } else {\n                    trace!(\"omp frame outside turn: {}\", &line[..line.len().min(120)]);\n                }\n            }\n            Ok(Event::OmpEof) => {\n                exit_reason = \"omp child exited\".into();\n                break 'main;\n            }\n            Ok(Event::ReadyEof) => {\n                exit_reason = \"`spt ready` listener exited (delivery pipe closed)\".into();\n                break 'main;\n            }\n            Err(RecvTimeoutError::Timeout) => {\n                // Liveness on every tick; the visible heartbeat only when enabled.\n                if let Ok(Some(st)) = omp_child.try_wait() {\n                    exit_reason = format!(\"omp child exited ({st})\");\n                    break 'main;\n                }\n                if let Ok(Some(st)) = ready_child.try_wait() {\n                    exit_reason = format!(\"`spt ready` listener exited ({st})\");\n                    break 'main;\n                }\n                if heartbeat_s != 0 {\n                    oplog!(\"heartbeat: idle, up {}m, {turns} turn(s) served\", started.elapsed().as_secs() / 60);\n                }\n            }\n            Err(RecvTimeoutError::Disconnected) => {\n                exit_reason = \"internal event channel closed\".into();\n                break 'main;\n            }\n        }\n    }\n\n    // ── Teardown (graceful path; the job object backstops every other path) ─────────────────────\n    oplog!(\"shutting down: {exit_reason}\");\n    let _ = ready_child.kill();\n    let _ = omp_child.kill();\n    // Soft-tear the perch so a dead endpoint never keeps advertising as listening (field incident\n    // symptom 3). Best-effort: auth via the bind token, else the session id.\n    let mut end_args = vec![\"api\", \"--adapter\", ADAPTER, \"session-end\", &id];\n    match token.as_deref() {\n        Some(t) => {\n            end_args.push(\"--token\");\n            end_args.push(t);\n        }\n        None => {\n            end_args.push(\"--session-id\");\n            end_args.push(&sid);\n        }\n    }\n    match spt_call(&spt, &end_args, None) {\n        Some(out) => oplog!(\"perch soft-teardown: {}\", first_line(&out)),\n        None => oplog!(\"perch soft-teardown: spt could not be spawned\"),\n    }\n    ExitCode::FAILURE // reaching here means a child died — loud, so the broker records a failure\n}\n\n/// The unified event stream (HIGH-4/MED-2): omp frames, listener deliveries, and child EOFs all\n/// arrive on one channel, so every wait is a bounded `recv_timeout`.\nenum Event {\n    OmpLine(String),\n    OmpEof,\n    ReadyLine(String),\n    ReadyEof,\n}\n\n/// Read `stream` line-by-line into the event channel from a dedicated thread; emit `eof` when the\n/// stream closes (child exit). Generic over the two children.\nfn spawn_line_reader<R: std::io::Read + Send + 'static>(\n    stream: Option<R>,\n    tx: Sender<Event>,\n    wrap: fn(String) -> Event,\n    eof: Event,\n) {\n    let Some(stream) = stream else { return };\n    std::thread::spawn(move || {\n        let reader = BufReader::new(stream);\n        for line in reader.lines() {\n            match line {\n                Ok(l) => {\n                    if tx.send(wrap(l)).is_err() {\n                        return; // main loop gone\n                    }\n                }\n                Err(_) => break,\n            }\n        }\n        let _ = tx.send(eof);\n    });\n}\n\n/// Await an omp frame matching `pick` within `deadline`. Non-matching omp frames are skipped;\n/// listener lines arriving early are DROPPED (nothing should be inbound before the perch exists).\nfn await_frame<T>(\n    rx: &Receiver<Event>,\n    deadline: Duration,\n    pick: impl Fn(&Value) -> Option<T>,\n) -> Result<T, String> {\n    let until = Instant::now() + deadline;\n    loop {\n        let left = until.saturating_duration_since(Instant::now());\n        if left.is_zero() {\n            return Err(\"deadline\".into());\n        }\n        match rx.recv_timeout(left) {\n            Ok(Event::OmpLine(line)) => {\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\n                    if let Some(t) = pick(&v) {\n                        return Ok(t);\n                    }\n                }\n            }\n            Ok(Event::OmpEof) => return Err(\"omp exited\".into()),\n            Ok(_) => {} // listener events before the relay exists — ignore\n            Err(RecvTimeoutError::Timeout) => return Err(\"deadline\".into()),\n            Err(RecvTimeoutError::Disconnected) => return Err(\"event channel closed\".into()),\n        }\n    }\n}\n\n/// Write one host→omp frame (newline-delimited, flushed).\nfn send_frame(stdin: &mut ChildStdin, frame: &Value) -> bool {\n    let mut line = frame.to_string();\n    line.push('\\n');\n    stdin.write_all(line.as_bytes()).is_ok() && stdin.flush().is_ok()\n}\n\n/// How a driven turn ended (HIGH-4). `OmpGone` tells the main loop the resident child is dead —\n/// the endpoint cannot serve again and must tear down; the other failures leave it serving.\nenum TurnFail {\n    /// The per-turn deadline passed with no `agent_end`; the late completion is marked stale.\n    Timeout(u64),\n    /// omp's stdout closed mid-turn (the child died).\n    OmpGone,\n    /// The prompt could not be written / the channel broke.\n    Io(String),\n}\n\nimpl TurnFail {\n    fn cause(&self) -> String {\n        match self {\n            TurnFail::Timeout(s) => format!(\"no answer within {s}s\"),\n            TurnFail::OmpGone => \"the omp process died mid-turn\".into(),\n            TurnFail::Io(e) => e.clone(),\n        }\n    }\n}\n\n/// Drive one turn: send the prompt, consume events until `agent_end`, within `deadline` (HIGH-4).\n/// Listener deliveries that arrive MID-TURN are not lost or reordered — they are pushed onto\n/// `pending`, which the main loop drains after this turn completes. On deadline the turn is\n/// abandoned (`stale_turns` marks the late agent_end for discard on arrival).\nfn drive_turn(\n    omp_stdin: &mut ChildStdin,\n    rx: &Receiver<Event>,\n    deadline: Duration,\n    prompt: &str,\n    pending: &mut std::collections::VecDeque<String>,\n    stale_turns: &mut u32,\n) -> Result<String, TurnFail> {\n    if !send_frame(omp_stdin, &json!({\"type\": \"prompt\", \"message\": prompt})) {\n        return Err(TurnFail::Io(\"could not write the prompt frame to omp\".into()));\n    }\n    let until = Instant::now() + deadline;\n    loop {\n        let left = until.saturating_duration_since(Instant::now());\n        if left.is_zero() {\n            *stale_turns += 1;\n            return Err(TurnFail::Timeout(deadline.as_secs()));\n        }\n        match rx.recv_timeout(left) {\n            Ok(Event::OmpLine(line)) => {\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\n                    if v[\"type\"].as_str() == Some(\"agent_end\") {\n                        return Ok(extract_reply(&v));\n                    }\n                }\n                // prompt ack / agent_start / message_update — the turn stream; not the end.\n            }\n            Ok(Event::OmpEof) => return Err(TurnFail::OmpGone),\n            Ok(Event::ReadyLine(line)) => pending.push_back(line), // deliver after this turn\n            Ok(Event::ReadyEof) => {\n                // The delivery pipe died mid-turn; finish the turn (the answer may still be\n                // routable) — the main loop notices the EOF on its own next receive... except we\n                // just consumed it. Re-queue the fact as a poisoned pending marker instead.\n                pending.push_back(String::new()); // sentinel: empty line parses to no events\n                // The listener child's exit is re-detected by the heartbeat try_wait.\n            }\n            Err(RecvTimeoutError::Timeout) => {\n                *stale_turns += 1;\n                return Err(TurnFail::Timeout(deadline.as_secs()));\n            }\n            Err(RecvTimeoutError::Disconnected) => {\n                return Err(TurnFail::Io(\"event channel closed\".into()))\n            }\n        }\n    }\n}\n\n/// Pull the capability token from `spt api bind` stdout. The line is `BOUND:<id> token=<hex>`; the\n/// token authenticates later `state` calls (`--token`). `None` if absent (e.g. a token-less revive).\nfn parse_bound_token(bind_out: &str) -> Option<String> {\n    for line in bind_out.lines() {\n        if let Some(i) = line.find(\"token=\") {\n            let tok = line[i + \"token=\".len()..]\n                .split_whitespace()\n                .next()\n                .unwrap_or(\"\");\n            if !tok.is_empty() {\n                return Some(tok.to_string());\n            }\n        }\n    }\n    None\n}\n\n/// First non-empty line of a multi-line string (for terse diagnostics).\nfn first_line(s: &str) -> &str {\n    s.lines().find(|l| !l.trim().is_empty()).unwrap_or(\"\")\n}\n\n/// Pull the assistant's answer out of an `agent_end` frame: the LAST `role:\"assistant\"` message,\n/// concatenating its `content[]` entries of `type:\"text\"`. Empty string if none.\nfn extract_reply(agent_end: &Value) -> String {\n    let msgs = match agent_end[\"messages\"].as_array() {\n        Some(m) => m,\n        None => return String::new(),\n    };\n    let assistant = msgs\n        .iter()\n        .rev()\n        .find(|m| m[\"role\"].as_str() == Some(\"assistant\"));\n    let assistant = match assistant {\n        Some(a) => a,\n        None => return String::new(),\n    };\n    let mut out = String::new();\n    if let Some(parts) = assistant[\"content\"].as_array() {\n        for p in parts {\n            if p[\"type\"].as_str() == Some(\"text\") {\n                if let Some(t) = p[\"text\"].as_str() {\n                    out.push_str(t);\n                }\n            }\n        }\n    }\n    out\n}\n\n/// Parse `spt ready`/`spt api poll` stdout into `(from, body)` messages. The wire grammar\n/// (spt-proto::event, per hook.rs and SPT_HARNESS_ADAPTER_TIPS.md §\"Message delivery is stdout\n/// framing\") is a stream of `<EVENT type=\"…\" from=\"<sender>\">body</EVENT>` envelopes. Only\n/// `type=\"msg\"` envelopes are returned — `notify`/`alarm`/`echo_commune`/`init_signoff` are dropped\n/// (they must NOT be forwarded to omp as a prompt). `from` is `None` when the envelope carries no\n/// sender attribute. Malformed/truncated tails are skipped (best-effort drain).\nfn parse_events(raw: &str) -> Vec<(Option<String>, String)> {\n    let mut out = Vec::new();\n    let mut rest = raw;\n    while let Some(start) = rest.find(\"<EVENT\") {\n        let after = &rest[start..];\n        let gt = match after.find('>') {\n            Some(g) => g,\n            None => break, // opening tag never closes — truncated tail\n        };\n        let open_tag = &after[..gt]; // \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\"\"\n        let etype = attr_value(open_tag, \"type\");\n        let from = attr_value(open_tag, \"from\");\n        let body_start = gt + 1;\n        let close = match after[body_start..].find(\"</EVENT>\") {\n            Some(c) => c,\n            None => break, // body never closes — truncated tail\n        };\n        if etype.as_deref() == Some(\"msg\") {\n            let body = after[body_start..body_start + close].to_string();\n            out.push((from, body));\n        }\n        let consumed = start + body_start + close + \"</EVENT>\".len();\n        rest = &rest[consumed..];\n    }\n    out\n}\n\n[Showing lines 1-623 of 1017. Use :624 to continue]"}],"details":{"truncation":{"content":"//! The omp-spt RPC bridge (S2 ready-agent; hardened at v0.1.1 per docs/OMP-BRIDGE-FIELD-ISSUES.md).\r\n//!\r\n//! This is the hosted harness process for the oh-my-pi (omp) adapter: spt-core's broker spawns it as\r\n//! `[session.self].command`, and it drives a resident `omp --mode rpc` child, translating in both\r\n//! directions (OMP-ADAPTER-PLAN.md, \"Architecture decision: RPC-bridge (Style A)\"):\r\n//!\r\n//! ```text\r\n//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\r\n//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\r\n//!                          └  prompt / get_state                                    (to omp)\r\n//! ```\r\n//!\r\n//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\r\n//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\r\n//! compat alias for the same entry.\r\n//!\r\n//! ## Protocol (verified against omp v16.3.11 — see OMP-ADAPTER-PLAN.md §\"decisive finding\")\r\n//!\r\n//! Startup:   omp → `{\"type\":\"ready\"}` (then unsolicited `extension_ui_request` /\r\n//!            `available_commands_update`, which `--mode rpc` — unlike `rpc-ui` — does NOT block on).\r\n//! Introspect: host → `{\"type\":\"get_state\"}`; omp → `{\"type\":\"response\",\"command\":\"get_state\",\r\n//!            \"success\":true,\"data\":{\"sessionId\":\"<uuid>\",\"sessionFile\":\"…\",\"model\":…}}`.\r\n//! Drive turn: host → `{\"type\":\"prompt\",\"message\":\"…\"}`; omp acks `{\"type\":\"response\",\r\n//!            \"command\":\"prompt\",\"success\":true}`, then streams `turn_start` → `agent_start` →\r\n//!            `message_update`… → `turn_end` → `{\"type\":\"agent_end\",\"messages\":[…]}`.\r\n//!\r\n//! ## Hardening (the doyle 2026-07-09 field incident — docs/OMP-BRIDGE-FIELD-ISSUES.md)\r\n//!\r\n//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\r\n//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\r\n//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\r\n//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\r\n//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\r\n//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\r\n//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\r\n//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\r\n//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\r\n//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\r\n//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\r\n//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\r\n//!   before-handshake); a turn breach recovers the perch to idle and tells the sender.\r\n//! * **MED-2**: the event loop notices a dead omp/listener child immediately (reader-thread EOF),\r\n//!   even while idle — not on the next message.\r\n//! * **LOW-1/2**: spt bind/state/send failures land on the operational log, and a failed turn sends\r\n//!   the sender an explicit failure notice, never a silent empty reply.\r\n\r\nuse serde_json::{json, Value};\r\nuse std::io::{BufRead, BufReader, Write};\r\nuse std::process::{ChildStdin, Command, ExitCode, Stdio};\r\nuse std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};\r\nuse std::time::{Duration, Instant};\r\n\r\n/// The adapter_name every targeted `spt api` verb carries (`--adapter`). Distinct from the plugin\r\n/// namespace `omps`; mirrors hook.rs's `ADAPTER` for the claude-spt path.\r\nconst ADAPTER: &str = \"omp-spt\";\r\n\r\n/// Bringup deadline for omp's `ready` frame (override: OMP_SPT_READY_TIMEOUT_S). Generous — a cold\r\n/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\r\n/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\r\nconst DEFAULT_READY_TIMEOUT_S: u64 = 120;\r\n/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\r\nconst DEFAULT_STATE_TIMEOUT_S: u64 = 30;\r\n/// Per-turn deadline: prompt dispatch → `agent_end` (override: OMP_SPT_TURN_TIMEOUT_S). Long — a\r\n/// real agent turn can run tools for minutes — but bounded, so a hung turn frees the perch.\r\nconst DEFAULT_TURN_TIMEOUT_S: u64 = 600;\r\n/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\r\nconst DEFAULT_HEARTBEAT_S: u64 = 300;\r\n\r\n/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\r\n/// message/reply/error/heartbeat lines without any env flag.\r\nmacro_rules! oplog {\r\n    ($($a:tt)*) => {\r\n        eprintln!(\"[omp-spt] {}\", format!($($a)*))\r\n    };\r\n}\r\n\r\n/// Frame-level trace, still gated behind `OMP_SPT_DEBUG` (diagnostics; off by default).\r\nmacro_rules! trace {\r\n    ($($a:tt)*) => {\r\n        if std::env::var_os(\"OMP_SPT_DEBUG\").is_some() {\r\n            eprintln!(\"[omp-spt::trace] {}\", format!($($a)*));\r\n        }\r\n    };\r\n}\r\n\r\nfn env_secs(name: &str, default: u64) -> u64 {\r\n    std::env::var(name).ok().and_then(|s| s.parse().ok()).unwrap_or(default)\r\n}\r\n\r\n/// Parsed bridge argv. `omp_args` is everything after a `--` separator, forwarded to omp verbatim.\r\nstruct Args {\r\n    id: Option<String>,\r\n    subnet: Option<String>,\r\n    session_dir: Option<String>,\r\n    resume: Option<String>,\r\n    omp_args: Vec<String>,\r\n}\r\n\r\nfn parse_args<I: Iterator<Item = String>>(mut it: I) -> Args {\r\n    let mut a = Args {\r\n        id: None,\r\n        subnet: None,\r\n        session_dir: None,\r\n        resume: None,\r\n        omp_args: Vec::new(),\r\n    };\r\n    while let Some(arg) = it.next() {\r\n        match arg.as_str() {\r\n            \"--id\" => a.id = it.next(),\r\n            \"--subnet\" => a.subnet = it.next(),\r\n            \"--session-dir\" => a.session_dir = it.next(),\r\n            \"--resume\" => a.resume = it.next(),\r\n            \"--node\" => {\r\n                let _ = it.next(); // accepted for manifest {node} parity; the bridge does not use it\r\n            }\r\n            \"--\" => {\r\n                a.omp_args.extend(it.by_ref());\r\n                break;\r\n            }\r\n            other => oplog!(\"ignoring unrecognized flag: {other}\"),\r\n        }\r\n    }\r\n    // Home subnet: required by `spt ready`/`bind` for a NEW endpoint on a multi-subnet node;\r\n    // harmless otherwise. Broker-hosted perches are pre-homed so the broker path can omit it.\r\n    if a.subnet.is_none() {\r\n        a.subnet = std::env::var(\"OMP_SPT_SUBNET\").ok().filter(|s| !s.is_empty());\r\n    }\r\n    a\r\n}\r\n\r\n/// Entry for `claude-spt bridge …` (compat alias — argv starts after the subcommand token).\r\npub fn run() -> ExitCode {\r\n    run_argv(std::env::args().skip(2))\r\n}\r\n\r\n/// Entry for the `omp-spt` binary (HIGH-1 — argv starts right after the binary name).\r\npub fn run_argv<I: Iterator<Item = String>>(argv: I) -> ExitCode {\r\n    let args = parse_args(argv);\r\n\r\n    // Endpoint id: --id wins, else the broker-injected $SPT_ENDPOINT_ID ([env.SPT_ENDPOINT_ID],\r\n    // direction=\"inject\", value=\"{id}\"). Without it we cannot address the perch — fail loud.\r\n    let id = args\r\n        .id\r\n        .clone()\r\n        .or_else(|| std::env::var(\"SPT_ENDPOINT_ID\").ok())\r\n        .filter(|s| !s.is_empty());\r\n    let id = match id {\r\n        Some(id) => id,\r\n        None => {\r\n            oplog!(\"FATAL: no endpoint id (pass --id or set SPT_ENDPOINT_ID)\");\r\n            return ExitCode::from(2);\r\n        }\r\n    };\r\n\r\n    // HIGH-2: put OURSELVES in a kill-on-close job object BEFORE spawning any child, so both\r\n    // children inherit membership and the OS reaps the whole tree when the bridge dies — however it\r\n    // dies. Best-effort: a failure is logged, never fatal (explicit kills still cover the graceful\r\n    // path). The returned handle is deliberately held (leaked) for the process lifetime; the OS\r\n    // closes it at process exit, which is exactly the kill trigger.\r\n    match teardown::arm_kill_on_close() {\r\n        Ok(()) => trace!(\"job-object armed (children die with the bridge)\"),\r\n        Err(e) => oplog!(\"WARN: could not arm child teardown ({e}) — orphan risk on hard kill\"),\r\n    }\r\n\r\n    let spt = spt_bin();\r\n    let omp = omp_bin();\r\n    oplog!(\"bridge starting: id={id} omp={omp} spt={spt}\");\r\n\r\n    // Spawn the resident omp RPC child. stderr is inherited so omp's own diagnostics (auth, model)\r\n    // surface in the broker PTY; stdin/stdout carry the NDJSON protocol.\r\n    let mut cmd = Command::new(&omp);\r\n    cmd.arg(\"--mode\").arg(\"rpc\");\r\n    // [session.resume]: reload a prior omp session by id — omp's native resume (`-r <id-prefix>`).\r\n    // get_state after startup still reports the (resumed) sessionId, so the bind path is identical.\r\n    if let Some(sid) = &args.resume {\r\n        cmd.arg(\"-r\").arg(sid);\r\n    }\r\n    if let Some(dir) = &args.session_dir {\r\n        cmd.arg(\"--session-dir\").arg(dir);\r\n    }\r\n    cmd.args(&args.omp_args);\r\n    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit());\r\n    let mut omp_child = match cmd.spawn() {\r\n        Ok(c) => c,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: failed to spawn omp ({omp}): {e}\");\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n    let mut omp_stdin = match omp_child.stdin.take() {\r\n        Some(s) => s,\r\n        None => {\r\n            oplog!(\"FATAL: omp child missing stdin handle\");\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n\r\n    // HIGH-4/MED-2: one event channel, fed by dedicated reader threads — every wait below is a\r\n    // bounded recv, and a child dying is an Event even while the bridge is idle.\r\n    let (tx, rx) = std::sync::mpsc::channel::<Event>();\r\n    spawn_line_reader(omp_child.stdout.take(), tx.clone(), Event::OmpLine, Event::OmpEof);\r\n\r\n    // Startup gate: omp's `ready` frame within the deadline. Breaching exits loud BEFORE any bind,\r\n    // so a failed bringup never poisons the endpoint's session ledger (MED-4 discipline).\r\n    let ready_deadline = Duration::from_secs(env_secs(\"OMP_SPT_READY_TIMEOUT_S\", DEFAULT_READY_TIMEOUT_S));\r\n    match await_frame(&rx, ready_deadline, |f| (f[\"type\"].as_str() == Some(\"ready\")).then_some(())) {\r\n        Ok(()) => oplog!(\"omp ready\"),\r\n        Err(e) => {\r\n            oplog!(\"FATAL: omp never became ready within {}s ({e}) — is {omp} really oh-my-pi? (set OMP_SPT_OMP_BIN)\", ready_deadline.as_secs());\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    }\r\n\r\n    // Identity binding: omp mints the session id, we read it and attach it to the perch. omp has no\r\n    // \"inject an endpoint id\" flag, so the id flows the other way — this bind is how the perch\r\n    // learns which omp session backs it, and it returns the capability TOKEN (`BOUND:<id>\r\n    // token=<hex>`) that authenticates later mutating calls.\r\n    if !send_frame(&mut omp_stdin, &json!({\"type\": \"get_state\"})) {\r\n        oplog!(\"FATAL: could not write get_state to omp\");\r\n        let _ = omp_child.kill();\r\n        return ExitCode::FAILURE;\r\n    }\r\n    let state_deadline = Duration::from_secs(env_secs(\"OMP_SPT_STATE_TIMEOUT_S\", DEFAULT_STATE_TIMEOUT_S));\r\n    let sid = match await_frame(&rx, state_deadline, |f| {\r\n        (f[\"type\"].as_str() == Some(\"response\") && f[\"command\"].as_str() == Some(\"get_state\"))\r\n            .then(|| f[\"data\"][\"sessionId\"].as_str().map(str::to_string))\r\n            .flatten()\r\n    }) {\r\n        Ok(s) => s,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: no get_state response within {}s ({e})\", state_deadline.as_secs());\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n\r\n    let mut bind_args = vec![\"api\", \"--adapter\", ADAPTER, \"bind\", &id, \"--set-session-id\", &sid];\r\n    if let Some(sub) = &args.subnet {\r\n        bind_args.push(\"--subnet\");\r\n        bind_args.push(sub);\r\n    }\r\n    let bind_out = spt_call(&spt, &bind_args, None).unwrap_or_default();\r\n    let token = parse_bound_token(&bind_out);\r\n    if token.is_none() {\r\n        // Broker-hosted revive may not re-mint a token; state calls then fall back to --session-id.\r\n        oplog!(\"bind returned no token ({}) — state auth falls back to session id\", first_line(&bind_out));\r\n    }\r\n    oplog!(\"bound: id={id} omp-session={sid}\");\r\n    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\r\n\r\n    // ── Receive relay ────────────────────────────────────────────────────────────────────────────\r\n    // `spt ready <id>` registers the perch as a listener and BLOCKS, printing one\r\n    // `<EVENT type=\"msg\" from=\"…\">body</EVENT>` line per delivery (verified live). Its stdout feeds\r\n    // the same event channel; stderr is inherited so its `READY:<id>` banner reaches the PTY.\r\n    let mut ready_cmd = Command::new(&spt);\r\n    ready_cmd.arg(\"ready\").arg(&id);\r\n    if let Some(sub) = &args.subnet {\r\n        ready_cmd.arg(\"--subnet\").arg(sub);\r\n    }\r\n    ready_cmd.stdout(Stdio::piped()).stderr(Stdio::inherit()).stdin(Stdio::null());\r\n    let mut ready_child = match ready_cmd.spawn() {\r\n        Ok(c) => c,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: failed to spawn `spt ready`: {e}\");\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n    spawn_line_reader(ready_child.stdout.take(), tx.clone(), Event::ReadyLine, Event::ReadyEof);\r\n    oplog!(\"listening: relay is `spt ready {id}` (messages will log here as they arrive)\");\r\n\r\n    // ── Event loop ───────────────────────────────────────────────────────────────────────────────\r\n    let turn_deadline = Duration::from_secs(env_secs(\"OMP_SPT_TURN_TIMEOUT_S\", DEFAULT_TURN_TIMEOUT_S));\r\n    let heartbeat_s = env_secs(\"OMP_SPT_HEARTBEAT_S\", DEFAULT_HEARTBEAT_S);\r\n    let heartbeat = Duration::from_secs(if heartbeat_s == 0 { 3600 } else { heartbeat_s });\r\n    let started = Instant::now();\r\n    let mut turns: u64 = 0;\r\n    let mut stale_turns: u32 = 0; // timed-out turns whose late agent_end should be discarded\r\n    let mut pending: std::collections::VecDeque<String> = std::collections::VecDeque::new();\r\n    let exit_reason: String;\r\n\r\n    'main: loop {\r\n        // Deliveries queued behind a long turn are served before blocking on the channel again.\r\n        let event = if let Some(line) = pending.pop_front() {\r\n            Ok(Event::ReadyLine(line))\r\n        } else {\r\n            rx.recv_timeout(heartbeat)\r\n        };\r\n        match event {\r\n            Ok(Event::ReadyLine(line)) => {\r\n                for (from, body) in parse_events(&line) {\r\n                    let sender = from.as_deref().unwrap_or(\"<unknown>\");\r\n                    oplog!(\"msg in: from={sender} bytes={}\", body.len());\r\n                    // Busy for the whole handling window (pickup → agent_end) so a concurrent\r\n                    // status read never sees an idle perch mid-turn.\r\n                    set_state(&spt, &id, &sid, token.as_deref(), \"busy\");\r\n                    let turn = drive_turn(&mut omp_stdin, &rx, turn_deadline, &body, &mut pending, &mut stale_turns);\r\n                    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\r\n                    turns += 1;\r\n                    let mut omp_gone = false;\r\n                    match (&from, turn) {\r\n                        (Some(target), Ok(reply)) if !target.is_empty() => {\r\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&reply))\r\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\r\n                            oplog!(\"reply out: to={target} bytes={} ({})\", reply.len(), first_line(&status));\r\n                        }\r\n                        (Some(target), Err(fail)) if !target.is_empty() => {\r\n                            // LOW-2: a failed turn is reported, never a silent empty reply.\r\n                            let cause = fail.cause();\r\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\r\n                            oplog!(\"turn FAILED: {cause}\");\r\n                            let notice = format!(\r\n                                \"[omp-spt] turn failed ({cause}) — no answer was produced.{}\",\r\n                                if omp_gone {\r\n                                    \" The endpoint is going down.\"\r\n                                } else {\r\n                                    \" The endpoint is still up; retry, or an operator can inspect it with `spt rc`.\"\r\n                                }\r\n                            );\r\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&notice))\r\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\r\n                            oplog!(\"failure notice out: to={target} ({})\", first_line(&status));\r\n                        }\r\n                        (_, Ok(_)) => oplog!(\"msg had no `from` — reply not routed (kept in omp session)\"),\r\n                        (_, Err(fail)) => {\r\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\r\n                            oplog!(\"turn FAILED on sender-less msg: {}\", fail.cause());\r\n                        }\r\n                    }\r\n                    if omp_gone {\r\n                        exit_reason = \"omp child exited mid-turn\".into();\r\n                        break 'main;\r\n                    }\r\n                }\r\n            }\r\n            Ok(Event::OmpLine(line)) => {\r\n                // A frame outside a turn: either unsolicited chatter or a STALE agent_end from a\r\n                // timed-out turn. Log the latter so a late completion is visible, then discard.\r\n                if line.contains(\"\\\"agent_end\\\"\") && stale_turns > 0 {\r\n                    stale_turns -= 1;\r\n                    oplog!(\"stale agent_end from a timed-out turn discarded (late completion)\");\r\n                } else {\r\n                    trace!(\"omp frame outside turn: {}\", &line[..line.len().min(120)]);\r\n                }\r\n            }\r\n            Ok(Event::OmpEof) => {\r\n                exit_reason = \"omp child exited\".into();\r\n                break 'main;\r\n            }\r\n            Ok(Event::ReadyEof) => {\r\n                exit_reason = \"`spt ready` listener exited (delivery pipe closed)\".into();\r\n                break 'main;\r\n            }\r\n            Err(RecvTimeoutError::Timeout) => {\r\n                // Liveness on every tick; the visible heartbeat only when enabled.\r\n                if let Ok(Some(st)) = omp_child.try_wait() {\r\n                    exit_reason = format!(\"omp child exited ({st})\");\r\n                    break 'main;\r\n                }\r\n                if let Ok(Some(st)) = ready_child.try_wait() {\r\n                    exit_reason = format!(\"`spt ready` listener exited ({st})\");\r\n                    break 'main;\r\n                }\r\n                if heartbeat_s != 0 {\r\n                    oplog!(\"heartbeat: idle, up {}m, {turns} turn(s) served\", started.elapsed().as_secs() / 60);\r\n                }\r\n            }\r\n            Err(RecvTimeoutError::Disconnected) => {\r\n                exit_reason = \"internal event channel closed\".into();\r\n                break 'main;\r\n            }\r\n        }\r\n    }\r\n\r\n    // ── Teardown (graceful path; the job object backstops every other path) ─────────────────────\r\n    oplog!(\"shutting down: {exit_reason}\");\r\n    let _ = ready_child.kill();\r\n    let _ = omp_child.kill();\r\n    // Soft-tear the perch so a dead endpoint never keeps advertising as listening (field incident\r\n    // symptom 3). Best-effort: auth via the bind token, else the session id.\r\n    let mut end_args = vec![\"api\", \"--adapter\", ADAPTER, \"session-end\", &id];\r\n    match token.as_deref() {\r\n        Some(t) => {\r\n            end_args.push(\"--token\");\r\n            end_args.push(t);\r\n        }\r\n        None => {\r\n            end_args.push(\"--session-id\");\r\n            end_args.push(&sid);\r\n        }\r\n    }\r\n    match spt_call(&spt, &end_args, None) {\r\n        Some(out) => oplog!(\"perch soft-teardown: {}\", first_line(&out)),\r\n        None => oplog!(\"perch soft-teardown: spt could not be spawned\"),\r\n    }\r\n    ExitCode::FAILURE // reaching here means a child died — loud, so the broker records a failure\r\n}\r\n\r\n/// The unified event stream (HIGH-4/MED-2): omp frames, listener deliveries, and child EOFs all\r\n/// arrive on one channel, so every wait is a bounded `recv_timeout`.\r\nenum Event {\r\n    OmpLine(String),\r\n    OmpEof,\r\n    ReadyLine(String),\r\n    ReadyEof,\r\n}\r\n\r\n/// Read `stream` line-by-line into the event channel from a dedicated thread; emit `eof` when the\r\n/// stream closes (child exit). Generic over the two children.\r\nfn spawn_line_reader<R: std::io::Read + Send + 'static>(\r\n    stream: Option<R>,\r\n    tx: Sender<Event>,\r\n    wrap: fn(String) -> Event,\r\n    eof: Event,\r\n) {\r\n    let Some(stream) = stream else { return };\r\n    std::thread::spawn(move || {\r\n        let reader = BufReader::new(stream);\r\n        for line in reader.lines() {\r\n            match line {\r\n                Ok(l) => {\r\n                    if tx.send(wrap(l)).is_err() {\r\n                        return; // main loop gone\r\n                    }\r\n                }\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = tx.send(eof);\r\n    });\r\n}\r\n\r\n/// Await an omp frame matching `pick` within `deadline`. Non-matching omp frames are skipped;\r\n/// listener lines arriving early are DROPPED (nothing should be inbound before the perch exists).\r\nfn await_frame<T>(\r\n    rx: &Receiver<Event>,\r\n    deadline: Duration,\r\n    pick: impl Fn(&Value) -> Option<T>,\r\n) -> Result<T, String> {\r\n    let until = Instant::now() + deadline;\r\n    loop {\r\n        let left = until.saturating_duration_since(Instant::now());\r\n        if left.is_zero() {\r\n            return Err(\"deadline\".into());\r\n        }\r\n        match rx.recv_timeout(left) {\r\n            Ok(Event::OmpLine(line)) => {\r\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\r\n                    if let Some(t) = pick(&v) {\r\n                        return Ok(t);\r\n                    }\r\n                }\r\n            }\r\n            Ok(Event::OmpEof) => return Err(\"omp exited\".into()),\r\n            Ok(_) => {} // listener events before the relay exists — ignore\r\n            Err(RecvTimeoutError::Timeout) => return Err(\"deadline\".into()),\r\n            Err(RecvTimeoutError::Disconnected) => return Err(\"event channel closed\".into()),\r\n        }\r\n    }\r\n}\r\n\r\n/// Write one host→omp frame (newline-delimited, flushed).\r\nfn send_frame(stdin: &mut ChildStdin, frame: &Value) -> bool {\r\n    let mut line = frame.to_string();\r\n    line.push('\\n');\r\n    stdin.write_all(line.as_bytes()).is_ok() && stdin.flush().is_ok()\r\n}\r\n\r\n/// How a driven turn ended (HIGH-4). `OmpGone` tells the main loop the resident child is dead —\r\n/// the endpoint cannot serve again and must tear down; the other failures leave it serving.\r\nenum TurnFail {\r\n    /// The per-turn deadline passed with no `agent_end`; the late completion is marked stale.\r\n    Timeout(u64),\r\n    /// omp's stdout closed mid-turn (the child died).\r\n    OmpGone,\r\n    /// The prompt could not be written / the channel broke.\r\n    Io(String),\r\n}\r\n\r\nimpl TurnFail {\r\n    fn cause(&self) -> String {\r\n        match self {\r\n            TurnFail::Timeout(s) => format!(\"no answer within {s}s\"),\r\n            TurnFail::OmpGone => \"the omp process died mid-turn\".into(),\r\n            TurnFail::Io(e) => e.clone(),\r\n        }\r\n    }\r\n}\r\n\r\n/// Drive one turn: send the prompt, consume events until `agent_end`, within `deadline` (HIGH-4).\r\n/// Listener deliveries that arrive MID-TURN are not lost or reordered — they are pushed onto\r\n/// `pending`, which the main loop drains after this turn completes. On deadline the turn is\r\n/// abandoned (`stale_turns` marks the late agent_end for discard on arrival).\r\nfn drive_turn(\r\n    omp_stdin: &mut ChildStdin,\r\n    rx: &Receiver<Event>,\r\n    deadline: Duration,\r\n    prompt: &str,\r\n    pending: &mut std::collections::VecDeque<String>,\r\n    stale_turns: &mut u32,\r\n) -> Result<String, TurnFail> {\r\n    if !send_frame(omp_stdin, &json!({\"type\": \"prompt\", \"message\": prompt})) {\r\n        return Err(TurnFail::Io(\"could not write the prompt frame to omp\".into()));\r\n    }\r\n    let until = Instant::now() + deadline;\r\n    loop {\r\n        let left = until.saturating_duration_since(Instant::now());\r\n        if left.is_zero() {\r\n            *stale_turns += 1;\r\n            return Err(TurnFail::Timeout(deadline.as_secs()));\r\n        }\r\n        match rx.recv_timeout(left) {\r\n            Ok(Event::OmpLine(line)) => {\r\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\r\n                    if v[\"type\"].as_str() == Some(\"agent_end\") {\r\n                        return Ok(extract_reply(&v));\r\n                    }\r\n                }\r\n                // prompt ack / agent_start / message_update — the turn stream; not the end.\r\n            }\r\n            Ok(Event::OmpEof) => return Err(TurnFail::OmpGone),\r\n            Ok(Event::ReadyLine(line)) => pending.push_back(line), // deliver after this turn\r\n            Ok(Event::ReadyEof) => {\r\n                // The delivery pipe died mid-turn; finish the turn (the answer may still be\r\n                // routable) — the main loop notices the EOF on its own next receive... except we\r\n                // just consumed it. Re-queue the fact as a poisoned pending marker instead.\r\n                pending.push_back(String::new()); // sentinel: empty line parses to no events\r\n                // The listener child's exit is re-detected by the heartbeat try_wait.\r\n            }\r\n            Err(RecvTimeoutError::Timeout) => {\r\n                *stale_turns += 1;\r\n                return Err(TurnFail::Timeout(deadline.as_secs()));\r\n            }\r\n            Err(RecvTimeoutError::Disconnected) => {\r\n                return Err(TurnFail::Io(\"event channel closed\".into()))\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Pull the capability token from `spt api bind` stdout. The line is `BOUND:<id> token=<hex>`; the\r\n/// token authenticates later `state` calls (`--token`). `None` if absent (e.g. a token-less revive).\r\nfn parse_bound_token(bind_out: &str) -> Option<String> {\r\n    for line in bind_out.lines() {\r\n        if let Some(i) = line.find(\"token=\") {\r\n            let tok = line[i + \"token=\".len()..]\r\n                .split_whitespace()\r\n                .next()\r\n                .unwrap_or(\"\");\r\n            if !tok.is_empty() {\r\n                return Some(tok.to_string());\r\n            }\r\n        }\r\n    }\r\n    None\r\n}\r\n\r\n/// First non-empty line of a multi-line string (for terse diagnostics).\r\nfn first_line(s: &str) -> &str {\r\n    s.lines().find(|l| !l.trim().is_empty()).unwrap_or(\"\")\r\n}\r\n\r\n/// Pull the assistant's answer out of an `agent_end` frame: the LAST `role:\"assistant\"` message,\r\n/// concatenating its `content[]` entries of `type:\"text\"`. Empty string if none.\r\nfn extract_reply(agent_end: &Value) -> String {\r\n    let msgs = match agent_end[\"messages\"].as_array() {\r\n        Some(m) => m,\r\n        None => return String::new(),\r\n    };\r\n    let assistant = msgs\r\n        .iter()\r\n        .rev()\r\n        .find(|m| m[\"role\"].as_str() == Some(\"assistant\"));\r\n    let assistant = match assistant {\r\n        Some(a) => a,\r\n        None => return String::new(),\r\n    };\r\n    let mut out = String::new();\r\n    if let Some(parts) = assistant[\"content\"].as_array() {\r\n        for p in parts {\r\n            if p[\"type\"].as_str() == Some(\"text\") {\r\n                if let Some(t) = p[\"text\"].as_str() {\r\n                    out.push_str(t);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// Parse `spt ready`/`spt api poll` stdout into `(from, body)` messages. The wire grammar\r\n/// (spt-proto::event, per hook.rs and SPT_HARNESS_ADAPTER_TIPS.md §\"Message delivery is stdout\r\n/// framing\") is a stream of `<EVENT type=\"…\" from=\"<sender>\">body</EVENT>` envelopes. Only\r\n/// `type=\"msg\"` envelopes are returned — `notify`/`alarm`/`echo_commune`/`init_signoff` are dropped\r\n/// (they must NOT be forwarded to omp as a prompt). `from` is `None` when the envelope carries no\r\n/// sender attribute. Malformed/truncated tails are skipped (best-effort drain).\r\nfn parse_events(raw: &str) -> Vec<(Option<String>, String)> {\r\n    let mut out = Vec::new();\r\n    let mut rest = raw;\r\n    while let Some(start) = rest.find(\"<EVENT\") {\r\n        let after = &rest[start..];\r\n        let gt = match after.find('>') {\r\n            Some(g) => g,\r\n            None => break, // opening tag never closes — truncated tail\r\n        };\r\n        let open_tag = &after[..gt]; // \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\"\"\r\n        let etype = attr_value(open_tag, \"type\");\r\n        let from = attr_value(open_tag, \"from\");\r\n        let body_start = gt + 1;\r\n        let close = match after[body_start..].find(\"</EVENT>\") {\r\n            Some(c) => c,\r\n            None => break, // body never closes — truncated tail\r\n        };\r\n        if etype.as_deref() == Some(\"msg\") {\r\n            let body = after[body_start..body_start + close].to_string();\r\n            out.push((from, body));\r\n        }\r\n        let consumed = start + body_start + close + \"</EVENT>\".len();\r\n        rest = &rest[consumed..];\r\n    }\r\n    out\r","truncated":true,"truncatedBy":"lines","totalLines":1017,"totalBytes":29933,"outputLines":623,"outputBytes":29933,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! The omp-spt RPC bridge (S2 ready-agent; hardened at v0.1.1 per docs/OMP-BRIDGE-FIELD-ISSUES.md).\r\n//!\r\n//! This is the hosted harness process for the oh-my-pi (omp) adapter: spt-core's broker spawns it as\r\n//! `[session.self].command`, and it drives a resident `omp --mode rpc` child, translating in both\r\n//! directions (OMP-ADAPTER-PLAN.md, \"Architecture decision: RPC-bridge (Style A)\"):\r\n//!\r\n//! ```text\r\n//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\r\n//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\r\n//!                          └  prompt / get_state                                    (to omp)\r\n//! ```\r\n//!\r\n//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\r\n//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\r\n//! compat alias for the same entry.\r\n//!\r\n//! ## Protocol (verified against omp v16.3.11 — see OMP-ADAPTER-PLAN.md §\"decisive finding\")\r\n//!\r\n//! Startup:   omp → `{\"type\":\"ready\"}` (then unsolicited `extension_ui_request` /\r\n//!            `available_commands_update`, which `--mode rpc` — unlike `rpc-ui` — does NOT block on).\r\n//! Introspect: host → `{\"type\":\"get_state\"}`; omp → `{\"type\":\"response\",\"command\":\"get_state\",\r\n//!            \"success\":true,\"data\":{\"sessionId\":\"<uuid>\",\"sessionFile\":\"…\",\"model\":…}}`.\r\n//! Drive turn: host → `{\"type\":\"prompt\",\"message\":\"…\"}`; omp acks `{\"type\":\"response\",\r\n//!            \"command\":\"prompt\",\"success\":true}`, then streams `turn_start` → `agent_start` →\r\n//!            `message_update`… → `turn_end` → `{\"type\":\"agent_end\",\"messages\":[…]}`.\r\n//!\r\n//! ## Hardening (the doyle 2026-07-09 field incident — docs/OMP-BRIDGE-FIELD-ISSUES.md)\r\n//!\r\n//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\r\n//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\r\n//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\r\n//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\r\n//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\r\n//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\r\n//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\r\n//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\r\n//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\r\n//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\r\n//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\r\n//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\r\n//!   before-handshake); a turn breach recovers the perch to idle and tells the sender.\r\n//! * **MED-2**: the event loop notices a dead omp/listener child immediately (reader-thread EOF),\r\n//!   even while idle — not on the next message.\r\n//! * **LOW-1/2**: spt bind/state/send failures land on the operational log, and a failed turn sends\r\n//!   the sender an explicit failure notice, never a silent empty reply.\r\n\r\nuse serde_json::{json, Value};\r\nuse std::io::{BufRead, BufReader, Write};\r\nuse std::process::{ChildStdin, Command, ExitCode, Stdio};\r\nuse std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};\r\nuse std::time::{Duration, Instant};\r\n\r\n/// The adapter_name every targeted `spt api` verb carries (`--adapter`). Distinct from the plugin\r\n/// namespace `omps`; mirrors hook.rs's `ADAPTER` for the claude-spt path.\r\nconst ADAPTER: &str = \"omp-spt\";\r\n\r\n/// Bringup deadline for omp's `ready` frame (override: OMP_SPT_READY_TIMEOUT_S). Generous — a cold\r\n/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\r\n/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\r\nconst DEFAULT_READY_TIMEOUT_S: u64 = 120;\r\n/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\r\nconst DEFAULT_STATE_TIMEOUT_S: u64 = 30;\r\n/// Per-turn deadline: prompt dispatch → `agent_end` (override: OMP_SPT_TURN_TIMEOUT_S). Long — a\r\n/// real agent turn can run tools for minutes — but bounded, so a hung turn frees the perch.\r\nconst DEFAULT_TURN_TIMEOUT_S: u64 = 600;\r\n/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\r\nconst DEFAULT_HEARTBEAT_S: u64 = 300;\r\n\r\n/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\r\n/// message/reply/error/heartbeat lines without any env flag.\r\nmacro_rules! oplog {\r\n    ($($a:tt)*) => {\r\n        eprintln!(\"[omp-spt] {}\", format!($($a)*))\r\n    };\r\n}\r\n\r\n/// Frame-level trace, still gated behind `OMP_SPT_DEBUG` (diagnostics; off by default).\r\nmacro_rules! trace {\r\n    ($($a:tt)*) => {\r\n        if std::env::var_os(\"OMP_SPT_DEBUG\").is_some() {\r\n            eprintln!(\"[omp-spt::trace] {}\", format!($($a)*));\r\n        }\r\n    };\r\n}\r\n\r\nfn env_secs(name: &str, default: u64) -> u64 {\r\n    std::env::var(name).ok().and_then(|s| s.parse().ok()).unwrap_or(default)\r\n}\r\n\r\n/// Parsed bridge argv. `omp_args` is everything after a `--` separator, forwarded to omp verbatim.\r\nstruct Args {\r\n    id: Option<String>,\r\n    subnet: Option<String>,\r\n    session_dir: Option<String>,\r\n    resume: Option<String>,\r\n    omp_args: Vec<String>,\r\n}\r\n\r\nfn parse_args<I: Iterator<Item = String>>(mut it: I) -> Args {\r\n    let mut a = Args {\r\n        id: None,\r\n        subnet: None,\r\n        session_dir: None,\r\n        resume: None,\r\n        omp_args: Vec::new(),\r\n    };\r\n    while let Some(arg) = it.next() {\r\n        match arg.as_str() {\r\n            \"--id\" => a.id = it.next(),\r\n            \"--subnet\" => a.subnet = it.next(),\r\n            \"--session-dir\" => a.session_dir = it.next(),\r\n            \"--resume\" => a.resume = it.next(),\r\n            \"--node\" => {\r\n                let _ = it.next(); // accepted for manifest {node} parity; the bridge does not use it\r\n            }\r\n            \"--\" => {\r\n                a.omp_args.extend(it.by_ref());\r\n                break;\r\n            }\r\n            other => oplog!(\"ignoring unrecognized flag: {other}\"),\r\n        }\r\n    }\r\n    // Home subnet: required by `spt ready`/`bind` for a NEW endpoint on a multi-subnet node;\r\n    // harmless otherwise. Broker-hosted perches are pre-homed so the broker path can omit it.\r\n    if a.subnet.is_none() {\r\n        a.subnet = std::env::var(\"OMP_SPT_SUBNET\").ok().filter(|s| !s.is_empty());\r\n    }\r\n    a\r\n}\r\n\r\n/// Entry for `claude-spt bridge …` (compat alias — argv starts after the subcommand token).\r\npub fn run() -> ExitCode {\r\n    run_argv(std::env::args().skip(2))\r\n}\r\n\r\n/// Entry for the `omp-spt` binary (HIGH-1 — argv starts right after the binary name).\r\npub fn run_argv<I: Iterator<Item = String>>(argv: I) -> ExitCode {\r\n    let args = parse_args(argv);\r\n\r\n    // Endpoint id: --id wins, else the broker-injected $SPT_ENDPOINT_ID ([env.SPT_ENDPOINT_ID],\r\n    // direction=\"inject\", value=\"{id}\"). Without it we cannot address the perch — fail loud.\r\n    let id = args\r\n        .id\r\n        .clone()\r\n        .or_else(|| std::env::var(\"SPT_ENDPOINT_ID\").ok())\r\n        .filter(|s| !s.is_empty());\r\n    let id = match id {\r\n        Some(id) => id,\r\n        None => {\r\n            oplog!(\"FATAL: no endpoint id (pass --id or set SPT_ENDPOINT_ID)\");\r\n            return ExitCode::from(2);\r\n        }\r\n    };\r\n\r\n    // HIGH-2: put OURSELVES in a kill-on-close job object BEFORE spawning any child, so both\r\n    // children inherit membership and the OS reaps the whole tree when the bridge dies — however it\r\n    // dies. Best-effort: a failure is logged, never fatal (explicit kills still cover the graceful\r\n    // path). The returned handle is deliberately held (leaked) for the process lifetime; the OS\r\n    // closes it at process exit, which is exactly the kill trigger.\r\n    match teardown::arm_kill_on_close() {\r\n        Ok(()) => trace!(\"job-object armed (children die with the bridge)\"),\r\n        Err(e) => oplog!(\"WARN: could not arm child teardown ({e}) — orphan risk on hard kill\"),\r\n    }\r\n\r\n    let spt = spt_bin();\r\n    let omp = omp_bin();\r\n    oplog!(\"bridge starting: id={id} omp={omp} spt={spt}\");\r\n\r\n    // Spawn the resident omp RPC child. stderr is inherited so omp's own diagnostics (auth, model)\r\n    // surface in the broker PTY; stdin/stdout carry the NDJSON protocol.\r\n    let mut cmd = Command::new(&omp);\r\n    cmd.arg(\"--mode\").arg(\"rpc\");\r\n    // [session.resume]: reload a prior omp session by id — omp's native resume (`-r <id-prefix>`).\r\n    // get_state after startup still reports the (resumed) sessionId, so the bind path is identical.\r\n    if let Some(sid) = &args.resume {\r\n        cmd.arg(\"-r\").arg(sid);\r\n    }\r\n    if let Some(dir) = &args.session_dir {\r\n        cmd.arg(\"--session-dir\").arg(dir);\r\n    }\r\n    cmd.args(&args.omp_args);\r\n    cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::inherit());\r\n    let mut omp_child = match cmd.spawn() {\r\n        Ok(c) => c,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: failed to spawn omp ({omp}): {e}\");\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n    let mut omp_stdin = match omp_child.stdin.take() {\r\n        Some(s) => s,\r\n        None => {\r\n            oplog!(\"FATAL: omp child missing stdin handle\");\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n\r\n    // HIGH-4/MED-2: one event channel, fed by dedicated reader threads — every wait below is a\r\n    // bounded recv, and a child dying is an Event even while the bridge is idle.\r\n    let (tx, rx) = std::sync::mpsc::channel::<Event>();\r\n    spawn_line_reader(omp_child.stdout.take(), tx.clone(), Event::OmpLine, Event::OmpEof);\r\n\r\n    // Startup gate: omp's `ready` frame within the deadline. Breaching exits loud BEFORE any bind,\r\n    // so a failed bringup never poisons the endpoint's session ledger (MED-4 discipline).\r\n    let ready_deadline = Duration::from_secs(env_secs(\"OMP_SPT_READY_TIMEOUT_S\", DEFAULT_READY_TIMEOUT_S));\r\n    match await_frame(&rx, ready_deadline, |f| (f[\"type\"].as_str() == Some(\"ready\")).then_some(())) {\r\n        Ok(()) => oplog!(\"omp ready\"),\r\n        Err(e) => {\r\n            oplog!(\"FATAL: omp never became ready within {}s ({e}) — is {omp} really oh-my-pi? (set OMP_SPT_OMP_BIN)\", ready_deadline.as_secs());\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    }\r\n\r\n    // Identity binding: omp mints the session id, we read it and attach it to the perch. omp has no\r\n    // \"inject an endpoint id\" flag, so the id flows the other way — this bind is how the perch\r\n    // learns which omp session backs it, and it returns the capability TOKEN (`BOUND:<id>\r\n    // token=<hex>`) that authenticates later mutating calls.\r\n    if !send_frame(&mut omp_stdin, &json!({\"type\": \"get_state\"})) {\r\n        oplog!(\"FATAL: could not write get_state to omp\");\r\n        let _ = omp_child.kill();\r\n        return ExitCode::FAILURE;\r\n    }\r\n    let state_deadline = Duration::from_secs(env_secs(\"OMP_SPT_STATE_TIMEOUT_S\", DEFAULT_STATE_TIMEOUT_S));\r\n    let sid = match await_frame(&rx, state_deadline, |f| {\r\n        (f[\"type\"].as_str() == Some(\"response\") && f[\"command\"].as_str() == Some(\"get_state\"))\r\n            .then(|| f[\"data\"][\"sessionId\"].as_str().map(str::to_string))\r\n            .flatten()\r\n    }) {\r\n        Ok(s) => s,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: no get_state response within {}s ({e})\", state_deadline.as_secs());\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n\r\n    let mut bind_args = vec![\"api\", \"--adapter\", ADAPTER, \"bind\", &id, \"--set-session-id\", &sid];\r\n    if let Some(sub) = &args.subnet {\r\n        bind_args.push(\"--subnet\");\r\n        bind_args.push(sub);\r\n    }\r\n    let bind_out = spt_call(&spt, &bind_args, None).unwrap_or_default();\r\n    let token = parse_bound_token(&bind_out);\r\n    if token.is_none() {\r\n        // Broker-hosted revive may not re-mint a token; state calls then fall back to --session-id.\r\n        oplog!(\"bind returned no token ({}) — state auth falls back to session id\", first_line(&bind_out));\r\n    }\r\n    oplog!(\"bound: id={id} omp-session={sid}\");\r\n    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\r\n\r\n    // ── Receive relay ────────────────────────────────────────────────────────────────────────────\r\n    // `spt ready <id>` registers the perch as a listener and BLOCKS, printing one\r\n    // `<EVENT type=\"msg\" from=\"…\">body</EVENT>` line per delivery (verified live). Its stdout feeds\r\n    // the same event channel; stderr is inherited so its `READY:<id>` banner reaches the PTY.\r\n    let mut ready_cmd = Command::new(&spt);\r\n    ready_cmd.arg(\"ready\").arg(&id);\r\n    if let Some(sub) = &args.subnet {\r\n        ready_cmd.arg(\"--subnet\").arg(sub);\r\n    }\r\n    ready_cmd.stdout(Stdio::piped()).stderr(Stdio::inherit()).stdin(Stdio::null());\r\n    let mut ready_child = match ready_cmd.spawn() {\r\n        Ok(c) => c,\r\n        Err(e) => {\r\n            oplog!(\"FATAL: failed to spawn `spt ready`: {e}\");\r\n            let _ = omp_child.kill();\r\n            return ExitCode::FAILURE;\r\n        }\r\n    };\r\n    spawn_line_reader(ready_child.stdout.take(), tx.clone(), Event::ReadyLine, Event::ReadyEof);\r\n    oplog!(\"listening: relay is `spt ready {id}` (messages will log here as they arrive)\");\r\n\r\n    // ── Event loop ───────────────────────────────────────────────────────────────────────────────\r\n    let turn_deadline = Duration::from_secs(env_secs(\"OMP_SPT_TURN_TIMEOUT_S\", DEFAULT_TURN_TIMEOUT_S));\r\n    let heartbeat_s = env_secs(\"OMP_SPT_HEARTBEAT_S\", DEFAULT_HEARTBEAT_S);\r\n    let heartbeat = Duration::from_secs(if heartbeat_s == 0 { 3600 } else { heartbeat_s });\r\n    let started = Instant::now();\r\n    let mut turns: u64 = 0;\r\n    let mut stale_turns: u32 = 0; // timed-out turns whose late agent_end should be discarded\r\n    let mut pending: std::collections::VecDeque<String> = std::collections::VecDeque::new();\r\n    let exit_reason: String;\r\n\r\n    'main: loop {\r\n        // Deliveries queued behind a long turn are served before blocking on the channel again.\r\n        let event = if let Some(line) = pending.pop_front() {\r\n            Ok(Event::ReadyLine(line))\r\n        } else {\r\n            rx.recv_timeout(heartbeat)\r\n        };\r\n        match event {\r\n            Ok(Event::ReadyLine(line)) => {\r\n                for (from, body) in parse_events(&line) {\r\n                    let sender = from.as_deref().unwrap_or(\"<unknown>\");\r\n                    oplog!(\"msg in: from={sender} bytes={}\", body.len());\r\n                    // Busy for the whole handling window (pickup → agent_end) so a concurrent\r\n                    // status read never sees an idle perch mid-turn.\r\n                    set_state(&spt, &id, &sid, token.as_deref(), \"busy\");\r\n                    let turn = drive_turn(&mut omp_stdin, &rx, turn_deadline, &body, &mut pending, &mut stale_turns);\r\n                    set_state(&spt, &id, &sid, token.as_deref(), \"idle\");\r\n                    turns += 1;\r\n                    let mut omp_gone = false;\r\n                    match (&from, turn) {\r\n                        (Some(target), Ok(reply)) if !target.is_empty() => {\r\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&reply))\r\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\r\n                            oplog!(\"reply out: to={target} bytes={} ({})\", reply.len(), first_line(&status));\r\n                        }\r\n                        (Some(target), Err(fail)) if !target.is_empty() => {\r\n                            // LOW-2: a failed turn is reported, never a silent empty reply.\r\n                            let cause = fail.cause();\r\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\r\n                            oplog!(\"turn FAILED: {cause}\");\r\n                            let notice = format!(\r\n                                \"[omp-spt] turn failed ({cause}) — no answer was produced.{}\",\r\n                                if omp_gone {\r\n                                    \" The endpoint is going down.\"\r\n                                } else {\r\n                                    \" The endpoint is still up; retry, or an operator can inspect it with `spt rc`.\"\r\n                                }\r\n                            );\r\n                            let status = spt_call(&spt, &[\"send\", target, \"--from\", &id], Some(&notice))\r\n                                .unwrap_or_else(|| \"SEND_SPAWN_FAIL\".into());\r\n                            oplog!(\"failure notice out: to={target} ({})\", first_line(&status));\r\n                        }\r\n                        (_, Ok(_)) => oplog!(\"msg had no `from` — reply not routed (kept in omp session)\"),\r\n                        (_, Err(fail)) => {\r\n                            omp_gone = matches!(fail, TurnFail::OmpGone);\r\n                            oplog!(\"turn FAILED on sender-less msg: {}\", fail.cause());\r\n                        }\r\n                    }\r\n                    if omp_gone {\r\n                        exit_reason = \"omp child exited mid-turn\".into();\r\n                        break 'main;\r\n                    }\r\n                }\r\n            }\r\n            Ok(Event::OmpLine(line)) => {\r\n                // A frame outside a turn: either unsolicited chatter or a STALE agent_end from a\r\n                // timed-out turn. Log the latter so a late completion is visible, then discard.\r\n                if line.contains(\"\\\"agent_end\\\"\") && stale_turns > 0 {\r\n                    stale_turns -= 1;\r\n                    oplog!(\"stale agent_end from a timed-out turn discarded (late completion)\");\r\n                } else {\r\n                    trace!(\"omp frame outside turn: {}\", &line[..line.len().min(120)]);\r\n                }\r\n            }\r\n            Ok(Event::OmpEof) => {\r\n                exit_reason = \"omp child exited\".into();\r\n                break 'main;\r\n            }\r\n            Ok(Event::ReadyEof) => {\r\n                exit_reason = \"`spt ready` listener exited (delivery pipe closed)\".into();\r\n                break 'main;\r\n            }\r\n            Err(RecvTimeoutError::Timeout) => {\r\n                // Liveness on every tick; the visible heartbeat only when enabled.\r\n                if let Ok(Some(st)) = omp_child.try_wait() {\r\n                    exit_reason = format!(\"omp child exited ({st})\");\r\n                    break 'main;\r\n                }\r\n                if let Ok(Some(st)) = ready_child.try_wait() {\r\n                    exit_reason = format!(\"`spt ready` listener exited ({st})\");\r\n                    break 'main;\r\n                }\r\n                if heartbeat_s != 0 {\r\n                    oplog!(\"heartbeat: idle, up {}m, {turns} turn(s) served\", started.elapsed().as_secs() / 60);\r\n                }\r\n            }\r\n            Err(RecvTimeoutError::Disconnected) => {\r\n                exit_reason = \"internal event channel closed\".into();\r\n                break 'main;\r\n            }\r\n        }\r\n    }\r\n\r\n    // ── Teardown (graceful path; the job object backstops every other path) ─────────────────────\r\n    oplog!(\"shutting down: {exit_reason}\");\r\n    let _ = ready_child.kill();\r\n    let _ = omp_child.kill();\r\n    // Soft-tear the perch so a dead endpoint never keeps advertising as listening (field incident\r\n    // symptom 3). Best-effort: auth via the bind token, else the session id.\r\n    let mut end_args = vec![\"api\", \"--adapter\", ADAPTER, \"session-end\", &id];\r\n    match token.as_deref() {\r\n        Some(t) => {\r\n            end_args.push(\"--token\");\r\n            end_args.push(t);\r\n        }\r\n        None => {\r\n            end_args.push(\"--session-id\");\r\n            end_args.push(&sid);\r\n        }\r\n    }\r\n    match spt_call(&spt, &end_args, None) {\r\n        Some(out) => oplog!(\"perch soft-teardown: {}\", first_line(&out)),\r\n        None => oplog!(\"perch soft-teardown: spt could not be spawned\"),\r\n    }\r\n    ExitCode::FAILURE // reaching here means a child died — loud, so the broker records a failure\r\n}\r\n\r\n/// The unified event stream (HIGH-4/MED-2): omp frames, listener deliveries, and child EOFs all\r\n/// arrive on one channel, so every wait is a bounded `recv_timeout`.\r\nenum Event {\r\n    OmpLine(String),\r\n    OmpEof,\r\n    ReadyLine(String),\r\n    ReadyEof,\r\n}\r\n\r\n/// Read `stream` line-by-line into the event channel from a dedicated thread; emit `eof` when the\r\n/// stream closes (child exit). Generic over the two children.\r\nfn spawn_line_reader<R: std::io::Read + Send + 'static>(\r\n    stream: Option<R>,\r\n    tx: Sender<Event>,\r\n    wrap: fn(String) -> Event,\r\n    eof: Event,\r\n) {\r\n    let Some(stream) = stream else { return };\r\n    std::thread::spawn(move || {\r\n        let reader = BufReader::new(stream);\r\n        for line in reader.lines() {\r\n            match line {\r\n                Ok(l) => {\r\n                    if tx.send(wrap(l)).is_err() {\r\n                        return; // main loop gone\r\n                    }\r\n                }\r\n                Err(_) => break,\r\n            }\r\n        }\r\n        let _ = tx.send(eof);\r\n    });\r\n}\r\n\r\n/// Await an omp frame matching `pick` within `deadline`. Non-matching omp frames are skipped;\r\n/// listener lines arriving early are DROPPED (nothing should be inbound before the perch exists).\r\nfn await_frame<T>(\r\n    rx: &Receiver<Event>,\r\n    deadline: Duration,\r\n    pick: impl Fn(&Value) -> Option<T>,\r\n) -> Result<T, String> {\r\n    let until = Instant::now() + deadline;\r\n    loop {\r\n        let left = until.saturating_duration_since(Instant::now());\r\n        if left.is_zero() {\r\n            return Err(\"deadline\".into());\r\n        }\r\n        match rx.recv_timeout(left) {\r\n            Ok(Event::OmpLine(line)) => {\r\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\r\n                    if let Some(t) = pick(&v) {\r\n                        return Ok(t);\r\n                    }\r\n                }\r\n            }\r\n            Ok(Event::OmpEof) => return Err(\"omp exited\".into()),\r\n            Ok(_) => {} // listener events before the relay exists — ignore\r\n            Err(RecvTimeoutError::Timeout) => return Err(\"deadline\".into()),\r\n            Err(RecvTimeoutError::Disconnected) => return Err(\"event channel closed\".into()),\r\n        }\r\n    }\r\n}\r\n\r\n/// Write one host→omp frame (newline-delimited, flushed).\r\nfn send_frame(stdin: &mut ChildStdin, frame: &Value) -> bool {\r\n    let mut line = frame.to_string();\r\n    line.push('\\n');\r\n    stdin.write_all(line.as_bytes()).is_ok() && stdin.flush().is_ok()\r\n}\r\n\r\n/// How a driven turn ended (HIGH-4). `OmpGone` tells the main loop the resident child is dead —\r\n/// the endpoint cannot serve again and must tear down; the other failures leave it serving.\r\nenum TurnFail {\r\n    /// The per-turn deadline passed with no `agent_end`; the late completion is marked stale.\r\n    Timeout(u64),\r\n    /// omp's stdout closed mid-turn (the child died).\r\n    OmpGone,\r\n    /// The prompt could not be written / the channel broke.\r\n    Io(String),\r\n}\r\n\r\nimpl TurnFail {\r\n    fn cause(&self) -> String {\r\n        match self {\r\n            TurnFail::Timeout(s) => format!(\"no answer within {s}s\"),\r\n            TurnFail::OmpGone => \"the omp process died mid-turn\".into(),\r\n            TurnFail::Io(e) => e.clone(),\r\n        }\r\n    }\r\n}\r\n\r\n/// Drive one turn: send the prompt, consume events until `agent_end`, within `deadline` (HIGH-4).\r\n/// Listener deliveries that arrive MID-TURN are not lost or reordered — they are pushed onto\r\n/// `pending`, which the main loop drains after this turn completes. On deadline the turn is\r\n/// abandoned (`stale_turns` marks the late agent_end for discard on arrival).\r\nfn drive_turn(\r\n    omp_stdin: &mut ChildStdin,\r\n    rx: &Receiver<Event>,\r\n    deadline: Duration,\r\n    prompt: &str,\r\n    pending: &mut std::collections::VecDeque<String>,\r\n    stale_turns: &mut u32,\r\n) -> Result<String, TurnFail> {\r\n    if !send_frame(omp_stdin, &json!({\"type\": \"prompt\", \"message\": prompt})) {\r\n        return Err(TurnFail::Io(\"could not write the prompt frame to omp\".into()));\r\n    }\r\n    let until = Instant::now() + deadline;\r\n    loop {\r\n        let left = until.saturating_duration_since(Instant::now());\r\n        if left.is_zero() {\r\n            *stale_turns += 1;\r\n            return Err(TurnFail::Timeout(deadline.as_secs()));\r\n        }\r\n        match rx.recv_timeout(left) {\r\n            Ok(Event::OmpLine(line)) => {\r\n                if let Ok(v) = serde_json::from_str::<Value>(line.trim()) {\r\n                    if v[\"type\"].as_str() == Some(\"agent_end\") {\r\n                        return Ok(extract_reply(&v));\r\n                    }\r\n                }\r\n                // prompt ack / agent_start / message_update — the turn stream; not the end.\r\n            }\r\n            Ok(Event::OmpEof) => return Err(TurnFail::OmpGone),\r\n            Ok(Event::ReadyLine(line)) => pending.push_back(line), // deliver after this turn\r\n            Ok(Event::ReadyEof) => {\r\n                // The delivery pipe died mid-turn; finish the turn (the answer may still be\r\n                // routable) — the main loop notices the EOF on its own next receive... except we\r\n                // just consumed it. Re-queue the fact as a poisoned pending marker instead.\r\n                pending.push_back(String::new()); // sentinel: empty line parses to no events\r\n                // The listener child's exit is re-detected by the heartbeat try_wait.\r\n            }\r\n            Err(RecvTimeoutError::Timeout) => {\r\n                *stale_turns += 1;\r\n                return Err(TurnFail::Timeout(deadline.as_secs()));\r\n            }\r\n            Err(RecvTimeoutError::Disconnected) => {\r\n                return Err(TurnFail::Io(\"event channel closed\".into()))\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Pull the capability token from `spt api bind` stdout. The line is `BOUND:<id> token=<hex>`; the\r\n/// token authenticates later `state` calls (`--token`). `None` if absent (e.g. a token-less revive).\r\nfn parse_bound_token(bind_out: &str) -> Option<String> {\r\n    for line in bind_out.lines() {\r\n        if let Some(i) = line.find(\"token=\") {\r\n            let tok = line[i + \"token=\".len()..]\r\n                .split_whitespace()\r\n                .next()\r\n                .unwrap_or(\"\");\r\n            if !tok.is_empty() {\r\n                return Some(tok.to_string());\r\n            }\r\n        }\r\n    }\r\n    None\r\n}\r\n\r\n/// First non-empty line of a multi-line string (for terse diagnostics).\r\nfn first_line(s: &str) -> &str {\r\n    s.lines().find(|l| !l.trim().is_empty()).unwrap_or(\"\")\r\n}\r\n\r\n/// Pull the assistant's answer out of an `agent_end` frame: the LAST `role:\"assistant\"` message,\r\n/// concatenating its `content[]` entries of `type:\"text\"`. Empty string if none.\r\nfn extract_reply(agent_end: &Value) -> String {\r\n    let msgs = match agent_end[\"messages\"].as_array() {\r\n        Some(m) => m,\r\n        None => return String::new(),\r\n    };\r\n    let assistant = msgs\r\n        .iter()\r\n        .rev()\r\n        .find(|m| m[\"role\"].as_str() == Some(\"assistant\"));\r\n    let assistant = match assistant {\r\n        Some(a) => a,\r\n        None => return String::new(),\r\n    };\r\n    let mut out = String::new();\r\n    if let Some(parts) = assistant[\"content\"].as_array() {\r\n        for p in parts {\r\n            if p[\"type\"].as_str() == Some(\"text\") {\r\n                if let Some(t) = p[\"text\"].as_str() {\r\n                    out.push_str(t);\r\n                }\r\n            }\r\n        }\r\n    }\r\n    out\r\n}\r\n\r\n/// Parse `spt ready`/`spt api poll` stdout into `(from, body)` messages. The wire grammar\r\n/// (spt-proto::event, per hook.rs and SPT_HARNESS_ADAPTER_TIPS.md §\"Message delivery is stdout\r\n/// framing\") is a stream of `<EVENT type=\"…\" from=\"<sender>\">body</EVENT>` envelopes. Only\r\n/// `type=\"msg\"` envelopes are returned — `notify`/`alarm`/`echo_commune`/`init_signoff` are dropped\r\n/// (they must NOT be forwarded to omp as a prompt). `from` is `None` when the envelope carries no\r\n/// sender attribute. Malformed/truncated tails are skipped (best-effort drain).\r\nfn parse_events(raw: &str) -> Vec<(Option<String>, String)> {\r\n    let mut out = Vec::new();\r\n    let mut rest = raw;\r\n    while let Some(start) = rest.find(\"<EVENT\") {\r\n        let after = &rest[start..];\r\n        let gt = match after.find('>') {\r\n            Some(g) => g,\r\n            None => break, // opening tag never closes — truncated tail\r\n        };\r\n        let open_tag = &after[..gt]; // \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\"\"\r\n        let etype = attr_value(open_tag, \"type\");\r\n        let from = attr_value(open_tag, \"from\");\r\n        let body_start = gt + 1;\r\n        let close = match after[body_start..].find(\"</EVENT>\") {\r\n            Some(c) => c,\r\n            None => break, // body never closes — truncated tail\r\n        };\r\n        if etype.as_deref() == Some(\"msg\") {\r\n            let body = after[body_start..body_start + close].to_string();\r\n            out.push((from, body));\r\n        }\r\n        let consumed = start + body_start + close + \"</EVENT>\".len();\r\n        rest = &rest[consumed..];\r\n    }\r\n    out\r\n}","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,167,168,169,170,171,172,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,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,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,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tools\\claude-spt\\src\\bridge.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1017,"totalBytes":29933,"outputLines":623,"outputBytes":29933,"shownRange":{"start":1,"end":623},"nextOffset":624}}},"isError":false,"timestamp":1784087175855}}
{"type":"message","id":"a3d4da74","parentId":"a7bb7414","timestamp":"2026-07-15T03:46:15.860Z","message":{"role":"toolResult","toolCallId":"call_i8pUlPiq9GiBOib3EL7lcaIA|fc_0ca4af36969c80fa016a570287c0588198892b2d3143988f57","toolName":"read","content":[{"type":"text","text":"[[requirements]]\n…\ntitle = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when spawning subagents or running long tasks, use run_in_background: true so you stay reachable for incoming messages'), appended AFTER any skill injection + drained messages (a notice never displaces a delivery); (2) PreToolUse — when the tool about to run is a sub…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure additionalContext string on hooks already int-covered (REQ-DIST-PRETOOL-POLL); live verify = operator observes agents backgrounding tasks.\n\n[[requirements]]\nid = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\ntitle = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped) plus a spool-recovery pointer (owlery/<id>/spool.db, messages table), riding the existing capped emits on both drain paths. Never field-observed (seed #9's head-truncations were core-side idle-inject pacing with COMPLETE spool rows — structurally unable to prod…\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-covered drain hooks (REQ-DIST-PRETOOL-POLL).\n\n[[requirements]]\nid = \"REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE\"\ntitle = \"Every PreToolUse emission rides the {\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"additionalContext\\\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2026-07-07, doyle node-wide RCA; three doyle->perri messages eaten including the outage evidence itself; binary exonerated by direct-drive repro). The cap decision is made on the WRAPPED byte size (JSON escaping can double newline-heavy drains); an over-cap drain …\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.4 urgent fix, doyle RCA screenshot). doc: docs/KNOWN-HAZARDS.md §2.8. impl: tools/claude-spt/src/hook.rs — wrap_pretool_context + emit_capped_pretool wired in handle_pre_tool_use. unit: pretool_envelope_shape_and_escaping (round-trip through serde) + pretool_overflow_spills_raw_and_wraps_the_pointer (wrapped-size cap decision) + pre_tool_use_busy_before_drain (envelope on the drain path) + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not (envelope on the nudge path). int DEFERRED: live mid-turn receive on-node post-apply (send while target busy, assert <sptc_messages> reaches the agent's turn — tonight's exact outage scenario).\n\n[[requirements]]\nid = \"REQ-SKILL-LIVE-SPT-HOSTED-BRANCH\"\ntitle = \"The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in an spt-hosted session an in-session `spt api listen` CANNOT re-arm the perch — the bash.exe child breaks by-pid host_binaries resolution (ADAPTER_UNRESOLVED) and the Monitor-child pid breaks seed lineage (NO_SEED); inbound is ALREADY broker-delivered to the se…\nrequired_stages = [\"doc\"]  # MINTED + ACTIVATED 2026-07-07 (adapter-debts work order; ADAPTER-DEBTS or the session commune). doc: adapter/strings/skills/live.md (the spt-hosted branch in Bringup + the two failure branches) + adapter/strings/skills/ready.md (the matching note). No impl/unit: these are agent-facing prose strings the hook only file-backs + resolves (same class as REQ-DIST-SESSIONSTART-BRIEF's string bodies); the branch is a documentation contract, not new binary logic. Source: flynn's Monitor-reachability diagnosis (spt-hosted sessions are broker-delivered; an in-session Monitor listener structurally cannot re-arm them).\n\n[[requirements]]\nid = \"REQ-SUPERVISOR-RELAY-RESPAWN\"\ntitle = \"DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-respawn / death-surfacing belt on top). SCOPE FENCE: this REQ is design/doc ONLY for now — do NOT build the watchdog or a respawn loop adapter-side until W4's activation ruling lands and assigns the boundary (core-vs-adapter). Captured now so the debt is tracked, …\nrequired_stages = []  # MINTED INACTIVE 2026-07-07 (adapter-debts work order). Design/doc candidate only — stays required_stages = [] (activation-model: never pre-fail a requirement not yet in build) until doyle's W4 supervisor-watchdog activation ruling assigns the core-vs-adapter boundary. Then activate with the real stages (doc the belt design → impl the relay-respawn/death-surfacing → unit → int the dead-relay recovery). Source: live-perch-monitor-reachability memory (a dead Monitor + trusting busy-path hook-injection = silent idle unreachability) + flynn's diagnosis. Do NOT implement the watchdog adapter-side pre-ruling — W4 is core's.\n\n[[requirements]]\nid = \"REQ-HAZARD-RESUME-REBIND-WEDGE\"\ntitle = \"CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ONLY clear/compact to the proofed `boundary` rotation; every other source (incl. resume) with an endpoint id present falls to a plain `bind`. That bind path (a) presented NO ownership proof and (b) used the exit-ignored `spt` seam (silent). But the perch is ALREA…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator /resume hunch; RESUME-REBIND-WEDGE, v0.15.7). impl: tools/claude-spt/src/hook.rs — handle_session_start bind branch (prior-sid proof from sid_state_rel state, prior!=sid guard, spt_strict + loud bind-fail log replacing the exit-ignored env.spt). unit: hook.rs — resume_rebind_presents_prior_proof (source=resume + state=s-old + sid=s-new → bind ... --set-session-id s-new --session-id s-old) + fresh_bind_has_no_proof_and_is_strict (source=startup, empty state → proofless bind line unchanged, no log) + resume_rebind_refusal_is_loud (with_strict_err → bind({src}) re-pin FAILED surfaced) + resume_rebind_same_sid_omits_proof (prior==sid → no --session-id). int DEFERRED: live in-TUI /resum…\n\n[[requirements]]\nid = \"REQ-INJECT-VERIFY-ECHO\"\ntitle = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapter DATA (a manifest [env] value), NOT a new spt-core requirement — the broker reads the injected capability at spawn and arms echo-verify for the session. FLOOR-SAFE (perri call, doyle-accepted 2026-07-07): the flag is inert on cores below the W5-A seam (an un…\nrequired_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC session spawned on a counter-49 core with the updated adapter has SPT_INJECT_VERIFY_ECHO=1 in its env AND the broker arms echo-verify (the full CC-sess→env-set→broker-arms E2E — the broker-arms half is unverifiable below counter-49 and out of the public surface, so …\n\n[[requirements]]\nid = \"REQ-HAZARD-STUCK-ACTIVE-NO-IDLE\"\ntitle = \"A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkpoint write — NEVER on a `startup` bringup and with NO handler for CC's idle-at-prompt Notification. So an endpoint launched then left (or a session whose first submission is a client-side `/login`/slash command that fires UserPromptSubmit→state busy but produ…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator /diagnose of lia; v0.15.9). impl: tools/claude-spt/src/hook.rs — the bind-branch `state idle` at bringup + handle_notification (idle_prompt self-heal, $SPT_ENDPOINT_ID gate, sid-required) + the Notification dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json Notification→dispatch.sh with the idle_prompt matcher. unit: hook.rs startup_bind_marks_idle_for_immediate_receivability + notification_idle_prompt_marks_idle_when_spt_hosted + notification_noop_when_not_spt_hosted + notification_without_sid_is_a_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (Notification added to the lockstep list) + tests/hooks-dispatch.sh (Notification routes to dispatch.sh + keeps…\n\n[[requirements]]\nid = \"REQ-HAZARD-STOPFAILURE-STUCK-BUSY\"\ntitle = \"An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO-IDLE, reached via API error instead of a never-completed first turn). FIX: wire `StopFailure` → `handle_stop_failure` → `state idle` (self_id-resolved, sid-scoped — mirrors the `Stop` idle mark); the error_type is logged (rare event; permanent observability + …\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1 fix 1; receive-heal). impl: tools/claude-spt/src/hook.rs handle_stop_failure + the StopFailure dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json StopFailure→dispatch.sh (matcherless — all error types). unit: hook.rs stop_failure_marks_idle_when_perched_else_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (StopFailure in the lockstep list) + tests/hooks-dispatch.sh (StopFailure wired to dispatch.sh). int DEFERRED to the live-verify window: drive a real rate-limit/server_error turn-end on a disposable spt-hosted endpoint and assert the perch marks idle + a spooled `spt send` drains (heavy — requires forcing an API error in a broker PTY, sid…\n\n[[requirements]]\nid = \"REQ-HAZARD-INTERRUPT-STUCK-BUSY\"\ntitle = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is_interrupt == true` → `state idle` (self_id-resolved, sid-scoped). A NON-interrupt tool failure (a command exited nonzero) is NOT a turn-end — the agent keeps working — so it must NOT mark idle (that would drain the spool mid-turn); absent/false `is_interrupt` is…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/session/<id>.sid carrier when the payload omits session_id — empty --session-id is core-AUTH-refused) + PostToolUseFailure dispatch arm + CC_HOOK_EVENTS; plugin/sptc/hooks/hooks.json PostToolUseFailure→dispatch.sh; AND tools/claude-spt/src/interrupt_watch.rs (fix 3, …\n\n[[requirements]]\nid = \"REQ-OMP-NATIVE-TUI\"\ntitle = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"[[requirements]]\n…\ntitle = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when spawning subagents or running long tasks, use run_in_background: true so you stay reachable for incoming messages'), appended AFTER any skill injection + drained messages (a notice never displaces a delivery); (2) PreToolUse — when the tool about to run is a sub…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure additionalContext string on hooks already int-covered (REQ-DIST-PRETOOL-POLL); live verify = operator observes agents backgrounding tasks.\n\n[[requirements]]\nid = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\ntitle = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped) plus a spool-recovery pointer (owlery/<id>/spool.db, messages table), riding the existing capped emits on both drain paths. Never field-observed (seed #9's head-truncations were core-side idle-inject pacing with COMPLETE spool rows — structurally unable to prod…\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-covered drain hooks (REQ-DIST-PRETOOL-POLL).\n\n[[requirements]]\nid = \"REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE\"\ntitle = \"Every PreToolUse emission rides the {\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"additionalContext\\\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2026-07-07, doyle node-wide RCA; three doyle->perri messages eaten including the outage evidence itself; binary exonerated by direct-drive repro). The cap decision is made on the WRAPPED byte size (JSON escaping can double newline-heavy drains); an over-cap drain …\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.4 urgent fix, doyle RCA screenshot). doc: docs/KNOWN-HAZARDS.md §2.8. impl: tools/claude-spt/src/hook.rs — wrap_pretool_context + emit_capped_pretool wired in handle_pre_tool_use. unit: pretool_envelope_shape_and_escaping (round-trip through serde) + pretool_overflow_spills_raw_and_wraps_the_pointer (wrapped-size cap decision) + pre_tool_use_busy_before_drain (envelope on the drain path) + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not (envelope on the nudge path). int DEFERRED: live mid-turn receive on-node post-apply (send while target busy, assert <sptc_messages> reaches the agent's turn — tonight's exact outage scenario).\n\n[[requirements]]\nid = \"REQ-SKILL-LIVE-SPT-HOSTED-BRANCH\"\ntitle = \"The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in an spt-hosted session an in-session `spt api listen` CANNOT re-arm the perch — the bash.exe child breaks by-pid host_binaries resolution (ADAPTER_UNRESOLVED) and the Monitor-child pid breaks seed lineage (NO_SEED); inbound is ALREADY broker-delivered to the se…\nrequired_stages = [\"doc\"]  # MINTED + ACTIVATED 2026-07-07 (adapter-debts work order; ADAPTER-DEBTS or the session commune). doc: adapter/strings/skills/live.md (the spt-hosted branch in Bringup + the two failure branches) + adapter/strings/skills/ready.md (the matching note). No impl/unit: these are agent-facing prose strings the hook only file-backs + resolves (same class as REQ-DIST-SESSIONSTART-BRIEF's string bodies); the branch is a documentation contract, not new binary logic. Source: flynn's Monitor-reachability diagnosis (spt-hosted sessions are broker-delivered; an in-session Monitor listener structurally cannot re-arm them).\n\n[[requirements]]\nid = \"REQ-SUPERVISOR-RELAY-RESPAWN\"\ntitle = \"DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-respawn / death-surfacing belt on top). SCOPE FENCE: this REQ is design/doc ONLY for now — do NOT build the watchdog or a respawn loop adapter-side until W4's activation ruling lands and assigns the boundary (core-vs-adapter). Captured now so the debt is tracked, …\nrequired_stages = []  # MINTED INACTIVE 2026-07-07 (adapter-debts work order). Design/doc candidate only — stays required_stages = [] (activation-model: never pre-fail a requirement not yet in build) until doyle's W4 supervisor-watchdog activation ruling assigns the core-vs-adapter boundary. Then activate with the real stages (doc the belt design → impl the relay-respawn/death-surfacing → unit → int the dead-relay recovery). Source: live-perch-monitor-reachability memory (a dead Monitor + trusting busy-path hook-injection = silent idle unreachability) + flynn's diagnosis. Do NOT implement the watchdog adapter-side pre-ruling — W4 is core's.\n\n[[requirements]]\nid = \"REQ-HAZARD-RESUME-REBIND-WEDGE\"\ntitle = \"CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ONLY clear/compact to the proofed `boundary` rotation; every other source (incl. resume) with an endpoint id present falls to a plain `bind`. That bind path (a) presented NO ownership proof and (b) used the exit-ignored `spt` seam (silent). But the perch is ALREA…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator /resume hunch; RESUME-REBIND-WEDGE, v0.15.7). impl: tools/claude-spt/src/hook.rs — handle_session_start bind branch (prior-sid proof from sid_state_rel state, prior!=sid guard, spt_strict + loud bind-fail log replacing the exit-ignored env.spt). unit: hook.rs — resume_rebind_presents_prior_proof (source=resume + state=s-old + sid=s-new → bind ... --set-session-id s-new --session-id s-old) + fresh_bind_has_no_proof_and_is_strict (source=startup, empty state → proofless bind line unchanged, no log) + resume_rebind_refusal_is_loud (with_strict_err → bind({src}) re-pin FAILED surfaced) + resume_rebind_same_sid_omits_proof (prior==sid → no --session-id). int DEFERRED: live in-TUI /resum…\n\n[[requirements]]\nid = \"REQ-INJECT-VERIFY-ECHO\"\ntitle = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapter DATA (a manifest [env] value), NOT a new spt-core requirement — the broker reads the injected capability at spawn and arms echo-verify for the session. FLOOR-SAFE (perri call, doyle-accepted 2026-07-07): the flag is inert on cores below the W5-A seam (an un…\nrequired_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC session spawned on a counter-49 core with the updated adapter has SPT_INJECT_VERIFY_ECHO=1 in its env AND the broker arms echo-verify (the full CC-sess→env-set→broker-arms E2E — the broker-arms half is unverifiable below counter-49 and out of the public surface, so …\n\n[[requirements]]\nid = \"REQ-HAZARD-STUCK-ACTIVE-NO-IDLE\"\ntitle = \"A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkpoint write — NEVER on a `startup` bringup and with NO handler for CC's idle-at-prompt Notification. So an endpoint launched then left (or a session whose first submission is a client-side `/login`/slash command that fires UserPromptSubmit→state busy but produ…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator /diagnose of lia; v0.15.9). impl: tools/claude-spt/src/hook.rs — the bind-branch `state idle` at bringup + handle_notification (idle_prompt self-heal, $SPT_ENDPOINT_ID gate, sid-required) + the Notification dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json Notification→dispatch.sh with the idle_prompt matcher. unit: hook.rs startup_bind_marks_idle_for_immediate_receivability + notification_idle_prompt_marks_idle_when_spt_hosted + notification_noop_when_not_spt_hosted + notification_without_sid_is_a_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (Notification added to the lockstep list) + tests/hooks-dispatch.sh (Notification routes to dispatch.sh + keeps…\n\n[[requirements]]\nid = \"REQ-HAZARD-STOPFAILURE-STUCK-BUSY\"\ntitle = \"An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO-IDLE, reached via API error instead of a never-completed first turn). FIX: wire `StopFailure` → `handle_stop_failure` → `state idle` (self_id-resolved, sid-scoped — mirrors the `Stop` idle mark); the error_type is logged (rare event; permanent observability + …\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1 fix 1; receive-heal). impl: tools/claude-spt/src/hook.rs handle_stop_failure + the StopFailure dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json StopFailure→dispatch.sh (matcherless — all error types). unit: hook.rs stop_failure_marks_idle_when_perched_else_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (StopFailure in the lockstep list) + tests/hooks-dispatch.sh (StopFailure wired to dispatch.sh). int DEFERRED to the live-verify window: drive a real rate-limit/server_error turn-end on a disposable spt-hosted endpoint and assert the perch marks idle + a spooled `spt send` drains (heavy — requires forcing an API error in a broker PTY, sid…\n\n[[requirements]]\nid = \"REQ-HAZARD-INTERRUPT-STUCK-BUSY\"\ntitle = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is_interrupt == true` → `state idle` (self_id-resolved, sid-scoped). A NON-interrupt tool failure (a command exited nonzero) is NOT a turn-end — the agent keeps working — so it must NOT mark idle (that would drain the spool mid-turn); absent/false `is_interrupt` is…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/session/<id>.sid carrier when the payload omits session_id — empty --session-id is core-AUTH-refused) + PostToolUseFailure dispatch arm + CC_HOOK_EVENTS; plugin/sptc/hooks/hooks.json PostToolUseFailure→dispatch.sh; AND tools/claude-spt/src/interrupt_watch.rs (fix 3, …\n\n[[requirements]]\nid = \"REQ-OMP-NATIVE-TUI\"\ntitle = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n","startLine":427,"lineNumbers":[427,null,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\traceable-reqs.toml"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784087175860}}
{"type":"message","id":"5a335144","parentId":"a3d4da74","timestamp":"2026-07-15T03:46:15.862Z","message":{"role":"toolResult","toolCallId":"call_wKjJYQNCNj1t9jIpzpCLo5AX|fc_0ca4af36969c80fa016a570287c04481988ec7ad4a0519f265","toolName":"read","content":[{"type":"text","text":"# traceable-reqs manifest for spt-claude-code — SEED.\n#\n# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n#\n# SEED YOUR REQs FROM SCOPE.md \"Parity-trim\" — the DROP / TRANSFORM / KEEP / ADD buckets are\n# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each\n# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).\n#\n# ACQUIRE the CLI (it is a released binary, not a crate dependency):\n#   gh release download --repo BigscreenVR/traceable-reqs \\\n#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x\n# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against\n# the installed CLI before relying on it: `traceable-reqs check`.\n#\n# THE FOUR-STAGE CONTRACT: every requirement is traced from\n#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)\n# Tag evidence in the SAME commit, on/above the real evidence:\n#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->\n#\n# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`\n# stays green pre-code. A milestone ACTIVATES its requirements by setting their real\n# `required_stages` when work on them begins. Deferred items stay [] until promoted.\n# Never declare the full end-state stage list up front — activate incrementally as evidence\n# lands (add \"int\" at the final integrating wave).\n\n[scan]\n# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —\n# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`\n# the moment those dirs are created (skeleton milestone, body #2) — until they exist a\n# nonexistent root is a hard manifest_error, so only list what is on disk now.\nroots = [\"plugin\", \"ci\", \"ci/acceptance\", \"ci/publish\", \"ci/docs\", \"ci/manifest\", \"ci/digest\", \"ci/idle-translate\", \"adapter\", \"tools\", \"tests\", \"docs\", \"docs-site\", \"CONTEXT.md\", \"SCOPE.md\", \"AGENTS.md\", \"HANDOFF.md\", \"README.md\"]\n\n[policy]\n# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node\n# reqs add \"int\". Until a req is activated, its per-req `required_stages = []` keeps it out\n# of the coverage gate.\nrequired_stages = []\n\n# ───────────────────────────── Requirements ─────────────────────────────────\n# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n#\n# [[requirements]]\n# id = \"REQ-EXAMPLE-1\"\n# title = \"One-line statement of the requirement\"\n# required_stages = []  # activate (e.g. [\"doc\",\"impl\",\"unit\"]) when the milestone starts\n#\n# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,\n# each requiring at least \"unit\" (add \"int\" where cross-process/cross-node):\n#\n# [[requirements]]\n# id = \"REQ-HAZARD-EXAMPLE\"\n# title = \"The invariant, stated so a test can prove it\"\n# required_stages = []\n\n# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not\n# the whole registry up front). All inactive (required_stages = []) so `check` stays\n# green until body #2 lands evidence and flips each to [\"doc\"] / [\"doc\",\"impl\"] etc.\n# Seeded from SCOPE.md \"Parity-trim\" (Distribution + ADD buckets).\n\n[[requirements]]\nid = \"REQ-DIST-PLUGIN-SKELETON\"\ntitle = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n\n[[requirements]]\nid = \"REQ-DIST-BOOTSTRAP-INSTALL\"\ntitle = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n\n[[requirements]]\nid = \"REQ-DIST-HOOKS-API\"\ntitle = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n\n[[requirements]]\nid = \"REQ-DIST-HOOK-BINARY\"\ntitle = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event>) + adapter/claude-spt.toml [strings].hook_cmd. unit: hook.rs #[cfg(test)] (json field parse, render_frames <EVENT>, unescape &amp;-last, skill_key, register_verb, cap/spill, checkpoint detect/wake, brief assemble + {id} subst + is_subagent + peer-gate + json_es…\n\n[[requirements]]\nid = \"REQ-DIST-MANIFEST-SCHEMA\"\ntitle = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source=\"{home}/.claude/projects\".\n\n[[requirements]]\nid = \"REQ-DIST-SHORTCUT-BASENAME\"\ntitle = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker \"lands in a later spt-core wave\" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establi…\n\n[[requirements]]\nid = \"REQ-CC-LAUNCHER-BIND\"\ntitle = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\nrequired_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoin…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-EXTRACTOR\"\ntitle = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-FETCHER\"\ntitle = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tree to the extractor in the DAEMON's context — the pre-0.19.0 inherited-env preference stays as the next precedence rung\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary running the fetcher-shape command (locate-in-tree, not --sample pre-read) — GREEN gates the v0.10.0 ship. doc deferred to the docs-site harness-contract slice (mirrors REQ-DIST-DIGEST-EXTRACTOR's precedent; the seam is documented in the manifest [digest]/[env] comme…\n\n[[requirements]]\nid = \"REQ-DIST-WHOAMI-JSON\"\ntitle = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\nrequired_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below our 0.19.0 floor everywhere, no version gate needed. int deferred: the SessionStart banner assertion rides the next hooks-int touch (poll-int.sh exercises self_id incidentally via the binary confirm-match).\n\n[[requirements]]\nid = \"REQ-SKILL-LIVE\"\ntitle = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemo…\n\n[[requirements]]\nid = \"REQ-UPS-INJECTION\"\ntitle = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n\n[[requirements]]\nid = \"REQ-DIST-SESSIONSTART-BRIEF\"\ntitle = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add \"int\" when the script lands + the published claude-spt adapter carries [string…\n\n# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.\n\n[[requirements]]\nid = \"REQ-CI-GATES\"\ntitle = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-MANUAL\"\ntitle = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-TRIGGER\"\ntitle = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-BUS\"\ntitle = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-OWL-DISCOVERY\"\ntitle = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-ACCEPTANCE\"\ntitle = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n\n[[requirements]]\nid = \"REQ-HAZARD-PERCH-COLLISION\"\ntitle = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\nrequired_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n\n[[requirements]]\nid = \"REQ-HAZARD-MSYS-PATHCONV\"\ntitle = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\nrequired_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\ntitle = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\nrequired_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\ntitle = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_error/residency machinery — a silent internal loop is invisible to it (field: ~3 boots/sec for 30min, ordinal 5358, F-h). Threshold coupling (doyle-ruled): core's C3(b) backstop trips at >=10 ledger boundaries/60s; for the pure instant-death class this guard fires FI…\nrequired_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger-rate territory BY DESIGN (the daemon's psyche_host_error surface consumes each death). Stages stay live on the SUCCESSOR invariant. doc: docs/KNOWN-HAZARDS.md §2.6 (resolution note kept for history). unit: tools/claude-spt/src/psyche.rs plan_is_stateless_one_tur…\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\ntitle = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamination guard pair; spt-core runtime honors role env_remove (public contract: manifest.schema.json SessionRole.env_remove).\"\nrequired_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child env) deferred to the next live-verify window.\n\n[[requirements]]\nid = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\ntitle = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — core writes the mind non-empty) ADOPTS the core-minted sid (`claude -p --session-id {session_id}`, prompt = mind + blank line + event); an EMPTY mind file (a ContinueExisting turn — core writes it truly empty, no sentinel) RESUMES (`claude -p --resume {session_id}…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_flag_is_an_error, unknown_arg_is_rejected, nonempty_context_is_a_fresh_turn_empty_is_continue, first_turn_adopts_the_minted_sid, continue_turn_resumes_by_sid, first_prompt_is_mind_blankline_event, continue_prompt_is_event_only, blank_continue_event_plans_the_ack…\n\n[[requirements]]\nid = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\ntitle = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to get the mind, then its fresh-vs-continue discrimination + prompt composition are byte-for-byte the pre-W4 logic on the file CONTENT. BINDING INVARIANT (doyle-pinned, core-side unit-asserted): a ContinueExisting turn writes the file TRULY EMPTY (0-byte or whites…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-empty→fresh, whitespace-only→continue), an_unreadable_mind_file_is_an_error_not_a_panic (missing file → Err → keep-custody fault). Manifest shape asserted by tests/manifest-shortcut.sh (psyche_resume command carries --psyche-context-file {psyche_context_file}; key…\n\n[[requirements]]\nid = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\ntitle = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\nrequired_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance roots ~/.ccs/instances/<x>/plugins, suspected miss) — the binary-side degrade above neutralizes the brick regardless, so that reconcile is a UX cleanup, not a blocker.\n\n# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n\n[[requirements]]\nid = \"REQ-DOCS-SITE\"\ntitle = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n\n[[requirements]]\nid = \"REQ-DOCS-DRIFT\"\ntitle = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n\n[[requirements]]\nid = \"REQ-PARITY-AUDIT\"\ntitle = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\nrequired_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n\n[[requirements]]\nid = \"REQ-SKILL-SUBNET\"\ntitle = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\nrequired_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve end-to-end through the live registry — same model as the commune/force-stop skills, which carry no per-skill impl). (Skill points at `spt how-to subnet` + `spt subnet --help` as the canonical self-drive anchors — the how-to topic now ships.)\n\n[[requirements]]\n…\n\n\n[Showing lines 1-223 of 481. Use :224 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"# traceable-reqs manifest for spt-claude-code — SEED.\n#\n# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n#\n# SEED YOUR REQs FROM SCOPE.md \"Parity-trim\" — the DROP / TRANSFORM / KEEP / ADD buckets are\n# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each\n# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).\n#\n# ACQUIRE the CLI (it is a released binary, not a crate dependency):\n#   gh release download --repo BigscreenVR/traceable-reqs \\\n#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x\n# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against\n# the installed CLI before relying on it: `traceable-reqs check`.\n#\n# THE FOUR-STAGE CONTRACT: every requirement is traced from\n#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)\n# Tag evidence in the SAME commit, on/above the real evidence:\n#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->\n#\n# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`\n# stays green pre-code. A milestone ACTIVATES its requirements by setting their real\n# `required_stages` when work on them begins. Deferred items stay [] until promoted.\n# Never declare the full end-state stage list up front — activate incrementally as evidence\n# lands (add \"int\" at the final integrating wave).\n\n[scan]\n# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —\n# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`\n# the moment those dirs are created (skeleton milestone, body #2) — until they exist a\n# nonexistent root is a hard manifest_error, so only list what is on disk now.\nroots = [\"plugin\", \"ci\", \"ci/acceptance\", \"ci/publish\", \"ci/docs\", \"ci/manifest\", \"ci/digest\", \"ci/idle-translate\", \"adapter\", \"tools\", \"tests\", \"docs\", \"docs-site\", \"CONTEXT.md\", \"SCOPE.md\", \"AGENTS.md\", \"HANDOFF.md\", \"README.md\"]\n\n[policy]\n# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node\n# reqs add \"int\". Until a req is activated, its per-req `required_stages = []` keeps it out\n# of the coverage gate.\nrequired_stages = []\n\n# ───────────────────────────── Requirements ─────────────────────────────────\n# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n#\n# [[requirements]]\n# id = \"REQ-EXAMPLE-1\"\n# title = \"One-line statement of the requirement\"\n# required_stages = []  # activate (e.g. [\"doc\",\"impl\",\"unit\"]) when the milestone starts\n#\n# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,\n# each requiring at least \"unit\" (add \"int\" where cross-process/cross-node):\n#\n# [[requirements]]\n# id = \"REQ-HAZARD-EXAMPLE\"\n# title = \"The invariant, stated so a test can prove it\"\n# required_stages = []\n\n# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not\n# the whole registry up front). All inactive (required_stages = []) so `check` stays\n# green until body #2 lands evidence and flips each to [\"doc\"] / [\"doc\",\"impl\"] etc.\n# Seeded from SCOPE.md \"Parity-trim\" (Distribution + ADD buckets).\n\n[[requirements]]\nid = \"REQ-DIST-PLUGIN-SKELETON\"\ntitle = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n\n[[requirements]]\nid = \"REQ-DIST-BOOTSTRAP-INSTALL\"\ntitle = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n\n[[requirements]]\nid = \"REQ-DIST-HOOKS-API\"\ntitle = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n\n[[requirements]]\nid = \"REQ-DIST-HOOK-BINARY\"\ntitle = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event>) + adapter/claude-spt.toml [strings].hook_cmd. unit: hook.rs #[cfg(test)] (json field parse, render_frames <EVENT>, unescape &amp;-last, skill_key, register_verb, cap/spill, checkpoint detect/wake, brief assemble + {id} subst + is_subagent + peer-gate + json_es…\n\n[[requirements]]\nid = \"REQ-DIST-MANIFEST-SCHEMA\"\ntitle = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source=\"{home}/.claude/projects\".\n\n[[requirements]]\nid = \"REQ-DIST-SHORTCUT-BASENAME\"\ntitle = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker \"lands in a later spt-core wave\" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establi…\n\n[[requirements]]\nid = \"REQ-CC-LAUNCHER-BIND\"\ntitle = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\nrequired_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoin…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-EXTRACTOR\"\ntitle = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-FETCHER\"\ntitle = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tree to the extractor in the DAEMON's context — the pre-0.19.0 inherited-env preference stays as the next precedence rung\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary running the fetcher-shape command (locate-in-tree, not --sample pre-read) — GREEN gates the v0.10.0 ship. doc deferred to the docs-site harness-contract slice (mirrors REQ-DIST-DIGEST-EXTRACTOR's precedent; the seam is documented in the manifest [digest]/[env] comme…\n\n[[requirements]]\nid = \"REQ-DIST-WHOAMI-JSON\"\ntitle = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\nrequired_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below our 0.19.0 floor everywhere, no version gate needed. int deferred: the SessionStart banner assertion rides the next hooks-int touch (poll-int.sh exercises self_id incidentally via the binary confirm-match).\n\n[[requirements]]\nid = \"REQ-SKILL-LIVE\"\ntitle = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemo…\n\n[[requirements]]\nid = \"REQ-UPS-INJECTION\"\ntitle = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n\n[[requirements]]\nid = \"REQ-DIST-SESSIONSTART-BRIEF\"\ntitle = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add \"int\" when the script lands + the published claude-spt adapter carries [string…\n\n# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.\n\n[[requirements]]\nid = \"REQ-CI-GATES\"\ntitle = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-MANUAL\"\ntitle = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-TRIGGER\"\ntitle = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-BUS\"\ntitle = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-OWL-DISCOVERY\"\ntitle = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-ACCEPTANCE\"\ntitle = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n\n[[requirements]]\nid = \"REQ-HAZARD-PERCH-COLLISION\"\ntitle = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\nrequired_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n\n[[requirements]]\nid = \"REQ-HAZARD-MSYS-PATHCONV\"\ntitle = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\nrequired_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\ntitle = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\nrequired_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\ntitle = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_error/residency machinery — a silent internal loop is invisible to it (field: ~3 boots/sec for 30min, ordinal 5358, F-h). Threshold coupling (doyle-ruled): core's C3(b) backstop trips at >=10 ledger boundaries/60s; for the pure instant-death class this guard fires FI…\nrequired_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger-rate territory BY DESIGN (the daemon's psyche_host_error surface consumes each death). Stages stay live on the SUCCESSOR invariant. doc: docs/KNOWN-HAZARDS.md §2.6 (resolution note kept for history). unit: tools/claude-spt/src/psyche.rs plan_is_stateless_one_tur…\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\ntitle = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamination guard pair; spt-core runtime honors role env_remove (public contract: manifest.schema.json SessionRole.env_remove).\"\nrequired_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child env) deferred to the next live-verify window.\n\n[[requirements]]\nid = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\ntitle = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — core writes the mind non-empty) ADOPTS the core-minted sid (`claude -p --session-id {session_id}`, prompt = mind + blank line + event); an EMPTY mind file (a ContinueExisting turn — core writes it truly empty, no sentinel) RESUMES (`claude -p --resume {session_id}…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_flag_is_an_error, unknown_arg_is_rejected, nonempty_context_is_a_fresh_turn_empty_is_continue, first_turn_adopts_the_minted_sid, continue_turn_resumes_by_sid, first_prompt_is_mind_blankline_event, continue_prompt_is_event_only, blank_continue_event_plans_the_ack…\n\n[[requirements]]\nid = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\ntitle = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to get the mind, then its fresh-vs-continue discrimination + prompt composition are byte-for-byte the pre-W4 logic on the file CONTENT. BINDING INVARIANT (doyle-pinned, core-side unit-asserted): a ContinueExisting turn writes the file TRULY EMPTY (0-byte or whites…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-empty→fresh, whitespace-only→continue), an_unreadable_mind_file_is_an_error_not_a_panic (missing file → Err → keep-custody fault). Manifest shape asserted by tests/manifest-shortcut.sh (psyche_resume command carries --psyche-context-file {psyche_context_file}; key…\n\n[[requirements]]\nid = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\ntitle = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\nrequired_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance roots ~/.ccs/instances/<x>/plugins, suspected miss) — the binary-side degrade above neutralizes the brick regardless, so that reconcile is a UX cleanup, not a blocker.\n\n# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n\n[[requirements]]\nid = \"REQ-DOCS-SITE\"\ntitle = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n\n[[requirements]]\nid = \"REQ-DOCS-DRIFT\"\ntitle = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n\n[[requirements]]\nid = \"REQ-PARITY-AUDIT\"\ntitle = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\nrequired_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n\n[[requirements]]\nid = \"REQ-SKILL-SUBNET\"\ntitle = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\nrequired_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve end-to-end through the live registry — same model as the commune/force-stop skills, which carry no per-skill impl). (Skill points at `spt how-to subnet` + `spt subnet --help` as the canonical self-drive anchors — the how-to topic now ships.)\n\n[[requirements]]","truncated":true,"truncatedBy":"lines","totalLines":481,"totalBytes":35170,"outputLines":223,"outputBytes":35170,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# traceable-reqs manifest for spt-claude-code — SEED.\n#\n# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS\n# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).\n#\n# SEED YOUR REQs FROM SCOPE.md \"Parity-trim\" — the DROP / TRANSFORM / KEEP / ADD buckets are\n# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each\n# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).\n#\n# ACQUIRE the CLI (it is a released binary, not a crate dependency):\n#   gh release download --repo BigscreenVR/traceable-reqs \\\n#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x\n# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against\n# the installed CLI before relying on it: `traceable-reqs check`.\n#\n# THE FOUR-STAGE CONTRACT: every requirement is traced from\n#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)\n# Tag evidence in the SAME commit, on/above the real evidence:\n#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->\n#\n# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`\n# stays green pre-code. A milestone ACTIVATES its requirements by setting their real\n# `required_stages` when work on them begins. Deferred items stay [] until promoted.\n# Never declare the full end-state stage list up front — activate incrementally as evidence\n# lands (add \"int\" at the final integrating wave).\n\n[scan]\n# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —\n# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`\n# the moment those dirs are created (skeleton milestone, body #2) — until they exist a\n# nonexistent root is a hard manifest_error, so only list what is on disk now.\nroots = [\"plugin\", \"ci\", \"ci/acceptance\", \"ci/publish\", \"ci/docs\", \"ci/manifest\", \"ci/digest\", \"ci/idle-translate\", \"adapter\", \"tools\", \"tests\", \"docs\", \"docs-site\", \"CONTEXT.md\", \"SCOPE.md\", \"AGENTS.md\", \"HANDOFF.md\", \"README.md\"]\n\n[policy]\n# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node\n# reqs add \"int\". Until a req is activated, its per-req `required_stages = []` keeps it out\n# of the coverage gate.\nrequired_stages = []\n\n# ───────────────────────────── Requirements ─────────────────────────────────\n# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n#\n# [[requirements]]\n# id = \"REQ-EXAMPLE-1\"\n# title = \"One-line statement of the requirement\"\n# required_stages = []  # activate (e.g. [\"doc\",\"impl\",\"unit\"]) when the milestone starts\n#\n# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,\n# each requiring at least \"unit\" (add \"int\" where cross-process/cross-node):\n#\n# [[requirements]]\n# id = \"REQ-HAZARD-EXAMPLE\"\n# title = \"The invariant, stated so a test can prove it\"\n# required_stages = []\n\n# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not\n# the whole registry up front). All inactive (required_stages = []) so `check` stays\n# green until body #2 lands evidence and flips each to [\"doc\"] / [\"doc\",\"impl\"] etc.\n# Seeded from SCOPE.md \"Parity-trim\" (Distribution + ADD buckets).\n\n[[requirements]]\nid = \"REQ-DIST-PLUGIN-SKELETON\"\ntitle = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n\n[[requirements]]\nid = \"REQ-DIST-BOOTSTRAP-INSTALL\"\ntitle = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n\n[[requirements]]\nid = \"REQ-DIST-HOOKS-API\"\ntitle = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n\n[[requirements]]\nid = \"REQ-DIST-HOOK-BINARY\"\ntitle = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event>) + adapter/claude-spt.toml [strings].hook_cmd. unit: hook.rs #[cfg(test)] (json field parse, render_frames <EVENT>, unescape &amp;-last, skill_key, register_verb, cap/spill, checkpoint detect/wake, brief assemble + {id} subst + is_subagent + peer-gate + json_es…\n\n[[requirements]]\nid = \"REQ-DIST-MANIFEST-SCHEMA\"\ntitle = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source=\"{home}/.claude/projects\".\n\n[[requirements]]\nid = \"REQ-DIST-SHORTCUT-BASENAME\"\ntitle = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker \"lands in a later spt-core wave\" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establi…\n\n[[requirements]]\nid = \"REQ-CC-LAUNCHER-BIND\"\ntitle = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\nrequired_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoin…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-EXTRACTOR\"\ntitle = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left…\n\n[[requirements]]\nid = \"REQ-DIST-DIGEST-FETCHER\"\ntitle = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tree to the extractor in the DAEMON's context — the pre-0.19.0 inherited-env preference stays as the next precedence rung\"\nrequired_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary running the fetcher-shape command (locate-in-tree, not --sample pre-read) — GREEN gates the v0.10.0 ship. doc deferred to the docs-site harness-contract slice (mirrors REQ-DIST-DIGEST-EXTRACTOR's precedent; the seam is documented in the manifest [digest]/[env] comme…\n\n[[requirements]]\nid = \"REQ-DIST-WHOAMI-JSON\"\ntitle = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\nrequired_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below our 0.19.0 floor everywhere, no version gate needed. int deferred: the SessionStart banner assertion rides the next hooks-int touch (poll-int.sh exercises self_id incidentally via the binary confirm-match).\n\n[[requirements]]\nid = \"REQ-SKILL-LIVE\"\ntitle = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemo…\n\n[[requirements]]\nid = \"REQ-UPS-INJECTION\"\ntitle = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\nrequired_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n\n[[requirements]]\nid = \"REQ-DIST-SESSIONSTART-BRIEF\"\ntitle = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add \"int\" when the script lands + the published claude-spt adapter carries [string…\n\n# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.\n\n[[requirements]]\nid = \"REQ-CI-GATES\"\ntitle = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-MANUAL\"\ntitle = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-TRIGGER\"\ntitle = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-BUS\"\ntitle = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\nrequired_stages = [\"doc\", \"impl\"]\n\n[[requirements]]\nid = \"REQ-CI-OWL-DISCOVERY\"\ntitle = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]\n\n[[requirements]]\nid = \"REQ-CI-ACCEPTANCE\"\ntitle = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\nrequired_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n\n[[requirements]]\nid = \"REQ-HAZARD-PERCH-COLLISION\"\ntitle = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\nrequired_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n\n[[requirements]]\nid = \"REQ-HAZARD-MSYS-PATHCONV\"\ntitle = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\nrequired_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\ntitle = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\nrequired_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\ntitle = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_error/residency machinery — a silent internal loop is invisible to it (field: ~3 boots/sec for 30min, ordinal 5358, F-h). Threshold coupling (doyle-ruled): core's C3(b) backstop trips at >=10 ledger boundaries/60s; for the pure instant-death class this guard fires FI…\nrequired_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger-rate territory BY DESIGN (the daemon's psyche_host_error surface consumes each death). Stages stay live on the SUCCESSOR invariant. doc: docs/KNOWN-HAZARDS.md §2.6 (resolution note kept for history). unit: tools/claude-spt/src/psyche.rs plan_is_stateless_one_tur…\n\n[[requirements]]\nid = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\ntitle = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamination guard pair; spt-core runtime honors role env_remove (public contract: manifest.schema.json SessionRole.env_remove).\"\nrequired_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child env) deferred to the next live-verify window.\n\n[[requirements]]\nid = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\ntitle = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — core writes the mind non-empty) ADOPTS the core-minted sid (`claude -p --session-id {session_id}`, prompt = mind + blank line + event); an EMPTY mind file (a ContinueExisting turn — core writes it truly empty, no sentinel) RESUMES (`claude -p --resume {session_id}…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_flag_is_an_error, unknown_arg_is_rejected, nonempty_context_is_a_fresh_turn_empty_is_continue, first_turn_adopts_the_minted_sid, continue_turn_resumes_by_sid, first_prompt_is_mind_blankline_event, continue_prompt_is_event_only, blank_continue_event_plans_the_ack…\n\n[[requirements]]\nid = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\ntitle = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to get the mind, then its fresh-vs-continue discrimination + prompt composition are byte-for-byte the pre-W4 logic on the file CONTENT. BINDING INVARIANT (doyle-pinned, core-side unit-asserted): a ContinueExisting turn writes the file TRULY EMPTY (0-byte or whites…\nrequired_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-empty→fresh, whitespace-only→continue), an_unreadable_mind_file_is_an_error_not_a_panic (missing file → Err → keep-custody fault). Manifest shape asserted by tests/manifest-shortcut.sh (psyche_resume command carries --psyche-context-file {psyche_context_file}; key…\n\n[[requirements]]\nid = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\ntitle = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\nrequired_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance roots ~/.ccs/instances/<x>/plugins, suspected miss) — the binary-side degrade above neutralizes the brick regardless, so that reconcile is a UX cleanup, not a blocker.\n\n# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n\n[[requirements]]\nid = \"REQ-DOCS-SITE\"\ntitle = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\nrequired_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n\n[[requirements]]\nid = \"REQ-DOCS-DRIFT\"\ntitle = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\nrequired_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n\n[[requirements]]\nid = \"REQ-PARITY-AUDIT\"\ntitle = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\nrequired_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n\n[[requirements]]\nid = \"REQ-SKILL-SUBNET\"\ntitle = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\nrequired_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve end-to-end through the live registry — same model as the commune/force-stop skills, which carry no per-skill impl). (Skill points at `spt how-to subnet` + `spt subnet --help` as the canonical self-drive anchors — the how-to topic now ships.)\n\n[[requirements]]\n…\n","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,167,168,169,170,171,172,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,null,237]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\traceable-reqs.toml"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":481,"totalBytes":35170,"outputLines":223,"outputBytes":35170,"shownRange":{"start":1,"end":223},"nextOffset":224},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784087175862}}
{"type":"message","id":"1bda1ada","parentId":"5a335144","timestamp":"2026-07-15T03:46:19.761Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning comprehensive requirement ID grep**"},{"type":"toolCall","id":"call_kaXyQjIPxXORewCMKDq8ey25|fc_0ca4af36969c80fa016a57028b0adc8198abd1ed3240dcc41b","name":"grep","arguments":{"i":"Listing requirement activation","pattern":"^id = |^required_stages = ","path":"traceable-reqs.toml","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":36194,"output":94,"cacheRead":107008,"cacheWrite":0,"totalTokens":143296,"reasoningTokens":33,"cost":{"input":0.18097000000000002,"output":0.00282,"cacheRead":0.053503999999999996,"cacheWrite":0,"total":0.237294}},"stopReason":"toolUse","timestamp":1784087175901,"responseId":"resp_0ca4af36969c80fa016a5702880ac88198813a64b2e2a13da4","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a570289ee90819890e8c91a597c7fd7","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKLHJHFooslz4f8c0kWPBUWpQnMVxGei5UOtGibtb_BSHnFbPzxYmOK1bbyy9bamN6Los2sbltXo-wci1E28D8rrW76i2mPiLP2JAatT6vu7LuXziS5fbPORddgjDWyMrHuBEC1b8TF-Hsy3xgju7RCZGjiL5MTyVo2Wivquxw-kawR_6ESu7HAshWGV0Z91FYydd-xGb9iQQOfFvl7Avk1m11J2y0w7Xx8GFj2JpQfyVy6VPukniuFMklkpnOpq6T7c0bCoBJq0Pt6AJCrrEkiUwQmEzbJiCkAqcEbCm3qB8EP8wbHsbbiS2iK6DJT8BRHxqqiEw4ngvkY-aBcOJUd3m9HmTMgKhT_iZ5jS0Wuna7f7XVxsRPA_YgG3uyXi_J3QAXx_NWwzLYemGzfIiAyuBtS6Y3C6FCuIGePs_EB10orIPZuCPJdb0V1JyDZTVkcKooVdRV4bTsz6JbzaJJGB6qUNp8SbAT09Nl5otyJwELzxaBQCwG3q6_Vdn9wCbm0qicBszZ7ud5vWvFVj5-oPj6d32MWlH2G9yJ8J1gf1V8R03IOi0Fg42PE8uJ_aFKl9ExHdtiUmsde8Z43QS-7HgZ0Hc1p8dXB9mIpiRoRQPSTx2Zs61dfsHsxGhcrDBegpxd2pkjR-4-V9zJg34BBfAxnMXBK67NrK2qG6lBkvopGaaKooJcvO28m8I5wm_M42DfnH1B0DPNwB59nv6JkRDZIdD3wCilmJQ29HxCPWf4BI4P5-ciFoeY4AGgb3sjbuyfM269uU-5Et-5ZWRNS9rQnOWG53AtkCSfLL80yUJPqkykcqVF2JcJ1YyDv3mdmASMKHhKAmA_VKJY9Ee3XtjDApMDg-5bNo_I_XGKU6KHHi2BY0-X45E52YK3kyLOG37A2nW8AZq2d9U4Do37HpyMNgD4bWtmdI9N2IbWk6gOSt4Mp_FJL_Rj4yLfB0A_lXRof86UIfB_NWLwAjBQGQUavKaOwWd9dJrHyMAffe8TvPFHUle6r_HhQtzkEE-eg1Vs1yVvlaPEhXvN4MiIfYxMgXfK0LY-AQzoeCnc5PS2ciJjnx4efyHDJRGRca0g5amHfF8qYRsb1kd-usmhS1Q==","summary":[{"type":"summary_text","text":"**Planning comprehensive requirement ID grep**"}]},{"id":"fc_0ca4af36969c80fa016a57028b0adc8198abd1ed3240dcc41b","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing requirement activation\",\"pattern\":\"^id = |^required_stages = \",\"path\":\"traceable-reqs.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_kaXyQjIPxXORewCMKDq8ey25","name":"grep"}]},"duration":3851.34440000006,"ttft":2041.5879000000423,"contextSnapshot":{"promptTokens":143202,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kaXyQjIPxXORewCMKDq8ey25|fc_0ca4af36969c80fa016a57028b0adc8198abd1ed3240dcc41b","toolName":"grep","startedAt":"2026-07-15T03:46:19.761Z","args":{"path":"traceable-reqs.toml"},"intent":"Listing requirement activation"},"id":"de68ad0d","parentId":"1bda1ada","timestamp":"2026-07-15T03:46:19.761Z"}
{"type":"message","id":"b3a3e850","parentId":"de68ad0d","timestamp":"2026-07-15T03:46:19.768Z","message":{"role":"toolResult","toolCallId":"call_kaXyQjIPxXORewCMKDq8ey25|fc_0ca4af36969c80fa016a57028b0adc8198abd1ed3240dcc41b","toolName":"grep","content":[{"type":"text","text":" 37|# of the coverage gate.\n*38|required_stages = []\n 39|\n 40|# ───────────────────────────── Requirements ─────────────────────────────────\n 41|# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n...\n 61|[[requirements]]\n*62|id = \"REQ-DIST-PLUGIN-SKELETON\"\n 63|title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n*64|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n 65|\n 66|[[requirements]]\n*67|id = \"REQ-DIST-BOOTSTRAP-INSTALL\"\n 68|title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n*69|required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n 70|\n 71|[[requirements]]\n*72|id = \"REQ-DIST-HOOKS-API\"\n 73|title = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\n*74|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n 75|\n 76|[[requirements]]\n*77|id = \"REQ-DIST-HOOK-BINARY\"\n 78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n*82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n 83|title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n*84|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n 85|\n 86|[[requirements]]\n*87|id = \"REQ-DIST-SHORTCUT-BASENAME\"\n 88|title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n*89|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n 90|\n 91|[[requirements]]\n*92|id = \"REQ-CC-LAUNCHER-BIND\"\n 93|title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n*94|required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n 95|\n 96|[[requirements]]\n*97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n 98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n*102|id = \"REQ-DIST-DIGEST-FETCHER\"\n 103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n*107|id = \"REQ-DIST-WHOAMI-JSON\"\n 108|title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109|required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110|\n 111|[[requirements]]\n*112|id = \"REQ-SKILL-LIVE\"\n 113|title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115|\n 116|[[requirements]]\n*117|id = \"REQ-UPS-INJECTION\"\n 118|title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120|\n 121|[[requirements]]\n*122|id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n 123|title = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\n*124|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int ...\n 125|\n 126|# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n 127|# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n...\n 130|[[requirements]]\n*131|id = \"REQ-CI-GATES\"\n 132|title = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\n*133|required_stages = [\"doc\", \"impl\", \"unit\"]\n 134|\n 135|[[requirements]]\n*136|id = \"REQ-CI-MANUAL\"\n 137|title = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\n*138|required_stages = [\"doc\", \"impl\"]\n 139|\n 140|[[requirements]]\n*141|id = \"REQ-CI-TRIGGER\"\n 142|title = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\n*143|required_stages = [\"doc\", \"impl\"]\n 144|\n 145|[[requirements]]\n*146|id = \"REQ-CI-BUS\"\n 147|title = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\n*148|required_stages = [\"doc\", \"impl\"]\n 149|\n 150|[[requirements]]\n*151|id = \"REQ-CI-OWL-DISCOVERY\"\n 152|title = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\n*153|required_stages = [\"doc\", \"impl\", \"unit\"]\n 154|\n 155|[[requirements]]\n*156|id = \"REQ-CI-ACCEPTANCE\"\n 157|title = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\n*158|required_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n 159|\n 160|[[requirements]]\n*161|id = \"REQ-HAZARD-PERCH-COLLISION\"\n 162|title = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\n*163|required_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n 164|\n 165|[[requirements]]\n*166|id = \"REQ-HAZARD-MSYS-PATHCONV\"\n 167|title = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\n*168|required_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n 169|\n 170|[[requirements]]\n*171|id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n 172|title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173|required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174|\n 175|[[requirements]]\n*176|id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n 177|title = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_erro...\n*178|required_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger...\n 179|\n 180|[[requirements]]\n*181|id = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\n 182|title = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamin...\n*183|required_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child en...\n 184|\n 185|[[requirements]]\n*186|id = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\n 187|title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189|\n 190|[[requirements]]\n*191|id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n 192|title = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to ...\n*193|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-e...\n 194|\n 195|[[requirements]]\n*196|id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n 197|title = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\n*198|required_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance ro...\n 199|\n 200|# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n 201|# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n...\n 203|[[requirements]]\n*204|id = \"REQ-DOCS-SITE\"\n 205|title = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\n*206|required_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n 207|\n 208|[[requirements]]\n*209|id = \"REQ-DOCS-DRIFT\"\n 210|title = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\n*211|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n 212|\n 213|[[requirements]]\n*214|id = \"REQ-PARITY-AUDIT\"\n 215|title = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\n*216|required_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n 217|\n 218|[[requirements]]\n*219|id = \"REQ-SKILL-SUBNET\"\n 220|title = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\n*221|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve ...\n 222|\n 223|[[requirements]]\n*224|id = \"REQ-CCS-PROFILES\"\n 225|title = \"The adapter ships a ccs profile template (claude-spt:ccs) — LOCKED-ADD overlay that retargets the spawn command template through `ccs` (a drop-in for the `claude` binary) and honors ccs's relocated CLAUDE_CONFIG_DIR transcript root in the digest extractor; templates only (user supplies their own ccs config/keys)\"\n 226|# Scoped to the default-profile shape the operator actually runs: `ccs` (bare), the account set via\n 227|# `ccs auth default <name>`, NOT glm/kimi (those were SCOPE *examples*; this profile IS the example +\n...\n 235|# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).\n*236|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n 237|\n 238|[[requirements]]\n*239|id = \"REQ-SKILL-VERSION\"\n 240|title = \"/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)\"\n*241|required_stages = []\n 242|\n 243|# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted\n 244|# 2026-06-15, seeded from SCOPE.md §\"/spt:setup\" (LOCKED v1 = {1..7}) + finding F-005 (the\n...\n 250|[[requirements]]\n*251|id = \"REQ-SETUP-SUBNET\"\n 252|title = \"/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.\"\n*253|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by...\n 254|\n 255|[[requirements]]\n*256|id = \"REQ-SETUP-CCS\"\n 257|title = \"/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).\"\n*258|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME d...\n 259|\n 260|[[requirements]]\n*261|id = \"REQ-SETUP-ACTIVATE\"\n 262|title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n*263|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264|\n 265|[[requirements]]\n*266|id = \"REQ-DIST-ADAPTER-RELEASE\"\n 267|title = \"The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)\"\n*268|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml name...\n 269|\n 270|[[requirements]]\n*271|id = \"REQ-DIST-ADAPTER-PEROS\"\n 272|title = \"The adapter ships ONE host-agnostic MULTI-PLATFORM `adapter.spt` (ADR-0024 W1, spt-core >= 0.13.2): a single fat archive bundles every recognized target-triple's tool binaries under a `<triple>/` dir beside the SHARED manifest.toml + strings/ at the archive root; install classifies the triple dirs and flattens THIS node's triple into the install dir, so a bare-name command token resolves (REQ-INSTALL-11). `spt adapter add --release` (default asset adapter.spt) + the `[update] gh_release` avenue ...\n*273|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs/RELEASE-RUNBOOK.md fat-archive packaging section (win native + linux cross-build via cargo-zigbuild — proven 2026-06-16, real ELF x86-64; bare `cargo build --target` fails `cc not found`, zig supplies the linker). impl: ci/publish/package-adapter.sh stages the shared root (manifest.toml + strings/) + each <triple>/ (win .exe, linux ELF) into ONE adapter.spt, refuses a < 0.13.2 floor, and guards the unrecognized-top-level-dir footgun. unit: tests/ad...\n 274|\n 275|# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from\n 276|# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/\n...\n 284|[[requirements]]\n*285|id = \"REQ-DIST-SESSION-RESUME\"\n 286|title = \"The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\\\"session_id\\\",\\\"id\\\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC chan...\n*287|required_stages = [\"unit\"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIE...\n 288|\n 289|[[requirements]]\n*290|id = \"REQ-DIST-IDLE-TRANSLATE\"\n 291|title = \"The adapter ships the [message-idle-translation-binary] (command=\\\"{adapter_dir}/claude-spt translate\\\", spt-core v0.16.0 seam — path deprecated; D3 fold into the consolidated binary) — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {comm...\n*292|required_stages = [\"impl\", \"unit\", \"int\"]  # D3 FOLD 2026-06-26: now the `translate` subcommand of tools/claude-spt (src/translate.rs) — seam is [message-idle-translation-binary].command=\"{adapter_dir}/claude-spt translate\" (spt-core v0.16.0; path deprecated); built+tested by ci/digest/build.sh (ci/idle-translate/build.sh is a shim); packed as the ONE tool binary; translate-proof-int --dir points at tools/claude-spt. ORIGINAL (pre-fold): impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — eve...\n 293|\n 294|# ─────────────── Checkpoint-commune wave (planned 2026-06-24, CHECKPOINT-COMMUNE-PLAN.md) ───────────────\n 295|# All inactive (required_stages = []) — activate-don't-pre-fail. Activate per-task as the build starts.\n...\n 297|[[requirements]]\n*298|id = \"REQ-DIST-IDLE-MULTILINE\"\n 299|title = \"The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed).\"\n*300|required_stages = [\"impl\", \"unit\", \"int\"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \\n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\\n/body/\\n/closing-tag, embedding two deliberate \\n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the ...\n 301|\n 302|[[requirements]]\n*303|id = \"REQ-DIST-CHECKPOINT-COMMUNE\"\n 304|title = \"Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which ARMS the wake and emits CLEAR-ONLY; the wake is fired separa...\n*305|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # Feature 2 / task T2 — ACTIVATED 2026-06-24. WIRE-SENTINEL is now spt-core v0.15.0 `spt send --json-payload '{\"checkpoint\":\"v1\",\"wake\":…}'` (doyle, grounded in the gated W4 code) — a structured json ATTR on the <EVENT> envelope, NOT the old string-prefix sniff (collision-proof: the marker lives inside the opaque attr, can't forge from/type). doc: full-fat /sptc:commune SKILL.md (--checkpoint: single trigger=default wake, paired triggers bracket a c...\n 306|\n 307|[[requirements]]\n*308|id = \"REQ-TAG-PEER-MESSAGING\"\n 309|title = \"An agent messages peers WITHOUT the Bash tool by embedding `@<t1,t2,t3 body @>` in its turn output: `@<` opens, the comma-separated target list runs to the first whitespace (no internal spaces), the body runs to the first `@>` (non-greedy, no escaping v1); a bare `@@id` is deliberately inert (prose discussing the syntax must not false-fire). A PreToolUse (mid-turn) + Stop (backstop) scan pulls the endpoint's own finalized digest output newer than a seq cursor (`endpoint digest <self> --after <cu...\n*310|required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: src/tag_scan.rs (pure grammar parse_tag_sections + seq-gated plan_dispatch) + hook.rs scan_and_dispatch wired into handle_pre_tool_use (mid-turn) and handle_stop (backstop), send_landed classifier, digest_cursor_rel cursor. unit: tag_scan.rs grammar + plan_dispatch tests + hook.rs scan_and_dispatch wiring tests (multi-target dispatch, unreachable-surfaced, self-drop, cursor advance, no-op-when-current). int DEFE...\n 311|\n 312|[[requirements]]\n*313|id = \"REQ-COMMUNE-OUTPUT-SHORTCUT\"\n 314|title = \"An agent writes its commune WITHOUT the Write tool by starting an output with `>>commune<<` (after leading whitespace); the rest of that output is the commune body, written to `.claude/<id>-commune.md` under the session cwd — the same file the /sptc:commune Write path targets, so the daemon's watch ingests+deletes it identically. The existing `!!checkpoint!!` in-body marker is reused for the checkpoint escalation (no new keyword): a commune body carrying it fires the same idle + checkpoint sel...\n*315|required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: tag_scan::commune_body detector + plan_dispatch commune routing (suppresses tag scan) + hook.rs scan_and_dispatch commune leg (write_commune_file to cwd .claude/<id>-commune.md via write_spill; has_checkpoint→checkpoint_payload/checkpoint_wake reuse). unit: tag_scan.rs commune_body tests (start-only, leading-ws, checkpoint-preserved) + plan_dispatch commune-suppresses-tags test + hook.rs scan_commune_shortcut_...\n 316|\n 317|[[requirements]]\n*318|id = \"REQ-COMMUNE-CONTEXT-TIER-SLICING\"\n 319|title = \"The two adapter-authored commune prompts must teach the two-slice context envelope so spt-core routes context per-agent-per-project instead of dumping everything in the live tier. spt-core ingests a commune (and the echo-commune summary) into durable tiers — `<live-context>` (agents/<id>/live-context.md, cross-project identity) and `<project-context>` (projects/<proj>/<id>/project-context.md, this-project detail), the same tiers `psyche-download` re-emits on resume — and routes by these tags...\n*320|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (doyle two-slice finding; folded into v0.17.3). impl: tools/claude-spt/src/echo_commune.rs compose_prompt two-slice directive + adapter/strings/briefs/live-ops.md commune brief two-slice guidance (both authoring paths). unit: echo_commune.rs prompt_teaches_context_tier_slicing (asserts both `<project-context>`/`<live-context>` slices + the default-tier rule in the summarizer directive). Prompt-only; the ingest routing itself is spt-core'...\n 321|\n 322|[[requirements]]\n*323|id = \"REQ-HAZARD-SEND-STATUS-STDERR\"\n 324|title = \"The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\\\"\\\"` for EVERY peer send and `send_landed(\\\"\\\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wro...\n*325|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator RCA directive; v0.17.3). impl: tools/claude-spt/src/hook.rs HookEnv::spt_send (SysEnv captures both streams, merged; trailing newlines stripped) + scan_and_dispatch peer-send switched from spt to spt_send. unit: hook.rs scan_classifies_send_by_stderr_token_not_empty_stdout (Recorder with_send_responder models empty-stdout + stderr-token; asserts the confirm-back body reports `delivered → lia`, never NO PERCH — FAILS on the ...\n 326|\n 327|[[requirements]]\n*328|id = \"REQ-DIST-RESUME-CONTEXT\"\n 329|title = \"claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path.\"\n*330|required_stages = [\"impl\", \"unit\", \"int\"]  # Parity wiring / task T3 — impl+unit ACTIVATED 2026-06-24 (coded against FROZEN contract B); int ACTIVATED 2026-06-24 when spt-core v0.15.0 (counter 34) PUBLISHED + verified on-node (doyle gate ruling: the CORE-CONTRACT half is deterministic CI, the SessionStart INJECTION E2E is logged dogfood). impl: session-start.sh bind|boundary brief branch -> sptc_psyche_download (mirrors `api poll` id-scoped auth: --session-id, no --project — project resolves from the...\n 331|\n 332|[[requirements]]\n*333|id = \"REQ-DIST-PRETOOL-POLL\"\n 334|title = \"claude-spt wires a PreToolUse hook firing `api poll` so a live agent receives messages MID-TURN (legacy-parity reachability; today claude-spt drains only on UserPromptSubmit = between turns). F-021.\"\n*335|required_stages = [\"impl\", \"unit\"]  # Reachability-parity item (F-021) — ACTIVATED 2026-06-25. Two coupled halves: (a) honest TURN-STATE lifecycle — UserPromptSubmit marks `api state busy` at turn-start, Stop marks `api state idle` at turn-end (spt-core never infers activity from quiescence — harness-contract/integration-checklist), so inbound DEFERS during a busy turn instead of idle-PTY-injecting; (b) the NEW PreToolUse hook drains those deferred messages MID-turn (`api poll {session_id} --includ...\n 336|\n 337|[[requirements]]\n*338|id = \"REQ-DIST-WORKER-PERCH-REACH\"\n 339|title = \"Subagent worker perches are runtime-REACHABLE: a `spt send` to a nested worker perch (created by subagent-start.sh -> api worker-start; hostable_types includes Worker) actually delivers. Validates existing wiring; F-022.\"\n*340|required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.\n 341|\n 342|# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────\n 343|# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent\n...\n 347|[[requirements]]\n*348|id = \"REQ-DIST-UPDATE-MESSAGE\"\n 349|title = \"The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1.\"\n*350|required_stages = [\"impl\", \"unit\"]  # U1 (the [update].message field) ACTIVATED 2026-06-25; D2 ([update.post] composite) PROMOTED to +impl 2026-06-26 (spt-core v0.16.0). impl: tools/claude-spt/src/post_update.rs — the [update.post] stdin/stdout contract (update_applied_in parses the additive JSON context; arbiter_line emits the `!!update-message!!` sentinel on a successful reconcile so the static [update].message fires; diagnostics→stderr, stdout=arbiter; fail-isolated; three modes incl --dry-run). T...\n 351|\n 352|[[requirements]]\n*353|id = \"REQ-DIST-BINARY-CONSOLIDATE\"\n 354|title = \"claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2.\"\n*355|required_stages = [\"impl\", \"unit\", \"int\"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consoli...\n 356|\n 357|[[requirements]]\n*358|id = \"REQ-DIST-NAME-UNIFY\"\n 359|title = \"GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/claude-spt reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3.\"\n*360|required_stages = [\"doc\", \"impl\", \"unit\"]  # U3 — ACTIVATED 2026-06-25. GitHub repo RENAMED SaberMage/spt-claude-code -> SaberMage/claude-spt (`gh repo rename`, ADMIN, local remote updated). Owner-qualified ref-flip done across the OPERATIVE surface (manifest [update].repo, README install chains + agent prompt, ci/publish/{package-adapter,release-acquire-int}.sh, adapter/strings/skills/setup.md + plugin setup SKILL.md, docs/RELEASE-RUNBOOK.md, docs-site/book.toml). Historical records intentionally LEFT...\n 361|\n 362|[[requirements]]\n*363|id = \"REQ-DIST-SKELETON-THIN\"\n 364|title = \"commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4.\"\n*365|required_stages = [\"doc\", \"impl\", \"unit\"]  # U4 — ACTIVATED 2026-06-25. Design Q RESOLVED: /sptc:live upgrades the session IN-PLACE (bare `spt api listen`), no Claude Code restart -> SessionStart does NOT re-fire; so the go-live guidance rides the /sptc:live UPS body, the perched brief only fires on bind/boundary. Both carry commune/signoff now. doc: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs ([doc->REQ-DIST-SKELETON-THIN]) + new adapter/strings/briefs/live-ops.md (commune incl --...\n 366|\n 367|[[requirements]]\n*368|id = \"REQ-DIST-INSTALL-UX\"\n 369|title = \"README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5.\"\n*370|required_stages = [\"doc\"]  # U5 — ACTIVATED 2026-06-25. doc-class (install instructions, like the REQ-SETUP-* skills — no adapter-side code of its own): README.md (added to [scan] roots) carries the per-OS install chains (bash / PowerShell / cmd: install spt-core via the published spt-releases script, then `spt adapter add --release SaberMage/claude-spt`) + the copy-paste agent-prompt skin + the symmetric `spt adapter update claude-spt` lever. doc tag rides README.md. llms.txt sync is unaffected (it ...\n 371|\n 372|[[requirements]]\n*373|id = \"REQ-DIST-CCS-PLUGIN-FOLLOWUP\"\n 374|title = \"When `ccs` is installed alongside `claude`, the post-update plugin reconcile ALSO runs `ccs plugin update sptc@cplugs` (best-effort): ccs-launched sessions read their own per-account plugin tree (the CLAUDE_CONFIG_DIR relocation, ~/.ccs/instances/<account>/.claude), which the claude-side reconcile never touches — without the follow-up a ccs session keeps running the STALE plugin after every `spt adapter update`. Not fired when ccs is absent or when ccs IS the primary CLI (already reconciled). ...\n*375|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-01 (v0.10.3, operator ask mid-cut). impl: tools/claude-spt/src/post_update.rs — ccs_followup_needed (pure gate: has_ccs && primary != ccs) + ccs_followup_cmd (plugin update sptc@cplugs) + the best-effort spawn block in run() (after the primary reconcile, before the arbiter print; dry-run prints `would run: ccs plugin update sptc@cplugs`); spawns via launch.rs resolve_program_from (the npm ccs.cmd shim is unspawnable from a bare name on Wi...\n 376|\n 377|[[requirements]]\n*378|id = \"REQ-DIST-RC-STARTUP\"\n 379|title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n*380|required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381|\n 382|[[requirements]]\n*383|id = \"REQ-DIST-BOUNDARY-ROTATE\"\n 384|title = \"The /clear|compact SessionStart boundary rotation actually rotates the perch: the hook resolves the endpoint id via $SPT_ENDPOINT_ID FIRST (whoami-by-NEW-sid is a catch-22 — the new sid is unregistered until this very call succeeds, so it resolves self:null and the old code silently skipped rotation), persists the CURRENT session id to an adapter-owned state file ({adapter_dir}/state/session/<endpoint_id>.sid) at EVERY SessionStart, and presents the PRIOR session's sid from that file as the bo...\n*385|required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-02 (F-024 correction arc; operator clean-room repro: fresh session + /clear wedges with zero hook errors). impl: tools/claude-spt/src/hook.rs — sid_state_rel (pure path builder, id sanitized), the boundary branch ($SPT_ENDPOINT_ID-first id, prior-sid proof read, spt_strict call), the every-SessionStart sid persistence after registration, HookEnv read_adapter_state/write_adapter_state/spt_strict seams (SysEnv resolves against curren...\n 386|\n 387|[[requirements]]\n*388|id = \"REQ-HAZARD-STALE-PLUGIN-RESOLUTION\"\n 389|title = \"The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispat...\n*390|required_stages = [\"doc\"]  # MINTED + doc-ACTIVATED 2026-07-02 (doyle ask off the F-024 arc: hazard writeup). doc: docs/KNOWN-HAZARDS.md §3.2. Halves already shipped ride their own REQs (reconcile = REQ-DIST-CCS-PLUGIN-FOLLOWUP + the v0.10.2 post-update work; degrade = REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP, stages [] until its guard slice). The NEW halves — stale-copy purge/flag + per-session plugin-version surfacing — are unbuilt guard-slice work: add impl/unit here when that slice starts (activate,...\n 391|\n 392|[[requirements]]\n*393|id = \"REQ-HAZARD-SESSION-PIN-WEDGE\"\n 394|title = \"A missed/failed boundary rotation must NEVER be silent: the rotation verb demands the DEPARTED session's proof, and a silently-skipped or silently-refused rotation pins the perch to a dead session id — after which EVERY id-scoped api call (poll/state/boundary) from the live session is AUTH_REFUSED (stderr-only) with NO self-heal: the endpoint looks healthy (brief renders, session runs) while cross-node AND local delivery is permanently dead (the F-024 ENLYZEAM wedge, root-caused 2026-07-02). T...\n*395|required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-02 (doyle KH cross-ref, F024D-BOUNDARY-RULING). doc: docs/KNOWN-HAZARDS.md §2 entry (rotation-wedge invariant). impl: tools/claude-spt/src/hook.rs boundary branch — the loud-skip and loud-refusal env.log paths (no silent return). unit: boundary_empty_id_is_loud_never_silent + boundary_refusal_is_loud_with_reason (Recorder asserts the log lines exist and no boundary call / a failed call respectively). The wedge RECOVERY path (relau...\n 396|\n 397|[[requirements]]\n*398|id = \"REQ-HAZARD-CHECKPOINT-CLEAR-RACE\"\n\n[Showing lines 1-325 of 407 (49.9KB limit). Use :326 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\traceable-reqs.toml","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":153,"fileCount":1,"files":["traceable-reqs.toml"],"fileMatches":[{"path":"traceable-reqs.toml","count":153}],"truncated":true,"displayContent":"  37│# of the coverage gate.\n *38│required_stages = []\n  39│\n  40│# ───────────────────────────── Requirements ─────────────────────────────────\n  41│# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n    │...\n  61│[[requirements]]\n *62│id = \"REQ-DIST-PLUGIN-SKELETON\"\n  63│title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n *64│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n  65│\n  66│[[requirements]]\n *67│id = \"REQ-DIST-BOOTSTRAP-INSTALL\"\n  68│title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n *69│required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n  70│\n  71│[[requirements]]\n *72│id = \"REQ-DIST-HOOKS-API\"\n  73│title = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\n *74│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n  75│\n  76│[[requirements]]\n *77│id = \"REQ-DIST-HOOK-BINARY\"\n  78│title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n *79│required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n  80│\n  81│[[requirements]]\n *82│id = \"REQ-DIST-MANIFEST-SCHEMA\"\n  83│title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n *84│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n  85│\n  86│[[requirements]]\n *87│id = \"REQ-DIST-SHORTCUT-BASENAME\"\n  88│title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n *89│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n  90│\n  91│[[requirements]]\n *92│id = \"REQ-CC-LAUNCHER-BIND\"\n  93│title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n *94│required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n  95│\n  96│[[requirements]]\n *97│id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n  98│title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n *99│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100│\n 101│[[requirements]]\n*102│id = \"REQ-DIST-DIGEST-FETCHER\"\n 103│title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105│\n 106│[[requirements]]\n*107│id = \"REQ-DIST-WHOAMI-JSON\"\n 108│title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109│required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110│\n 111│[[requirements]]\n*112│id = \"REQ-SKILL-LIVE\"\n 113│title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115│\n 116│[[requirements]]\n*117│id = \"REQ-UPS-INJECTION\"\n 118│title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120│\n 121│[[requirements]]\n*122│id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n 123│title = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\n*124│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int ...\n 125│\n 126│# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n 127│# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n    │...\n 130│[[requirements]]\n*131│id = \"REQ-CI-GATES\"\n 132│title = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\n*133│required_stages = [\"doc\", \"impl\", \"unit\"]\n 134│\n 135│[[requirements]]\n*136│id = \"REQ-CI-MANUAL\"\n 137│title = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\n*138│required_stages = [\"doc\", \"impl\"]\n 139│\n 140│[[requirements]]\n*141│id = \"REQ-CI-TRIGGER\"\n 142│title = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\n*143│required_stages = [\"doc\", \"impl\"]\n 144│\n 145│[[requirements]]\n*146│id = \"REQ-CI-BUS\"\n 147│title = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\n*148│required_stages = [\"doc\", \"impl\"]\n 149│\n 150│[[requirements]]\n*151│id = \"REQ-CI-OWL-DISCOVERY\"\n 152│title = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\n*153│required_stages = [\"doc\", \"impl\", \"unit\"]\n 154│\n 155│[[requirements]]\n*156│id = \"REQ-CI-ACCEPTANCE\"\n 157│title = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\n*158│required_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n 159│\n 160│[[requirements]]\n*161│id = \"REQ-HAZARD-PERCH-COLLISION\"\n 162│title = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\n*163│required_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n 164│\n 165│[[requirements]]\n*166│id = \"REQ-HAZARD-MSYS-PATHCONV\"\n 167│title = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\n*168│required_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n 169│\n 170│[[requirements]]\n*171│id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n 172│title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173│required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174│\n 175│[[requirements]]\n*176│id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n 177│title = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_erro...\n*178│required_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger...\n 179│\n 180│[[requirements]]\n*181│id = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\n 182│title = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamin...\n*183│required_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child en...\n 184│\n 185│[[requirements]]\n*186│id = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\n 187│title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189│\n 190│[[requirements]]\n*191│id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n 192│title = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to ...\n*193│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-e...\n 194│\n 195│[[requirements]]\n*196│id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n 197│title = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\n*198│required_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance ro...\n 199│\n 200│# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n 201│# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n    │...\n 203│[[requirements]]\n*204│id = \"REQ-DOCS-SITE\"\n 205│title = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\n*206│required_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n 207│\n 208│[[requirements]]\n*209│id = \"REQ-DOCS-DRIFT\"\n 210│title = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\n*211│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n 212│\n 213│[[requirements]]\n*214│id = \"REQ-PARITY-AUDIT\"\n 215│title = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\n*216│required_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n 217│\n 218│[[requirements]]\n*219│id = \"REQ-SKILL-SUBNET\"\n 220│title = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\n*221│required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve ...\n 222│\n 223│[[requirements]]\n*224│id = \"REQ-CCS-PROFILES\"\n 225│title = \"The adapter ships a ccs profile template (claude-spt:ccs) — LOCKED-ADD overlay that retargets the spawn command template through `ccs` (a drop-in for the `claude` binary) and honors ccs's relocated CLAUDE_CONFIG_DIR transcript root in the digest extractor; templates only (user supplies their own ccs config/keys)\"\n 226│# Scoped to the default-profile shape the operator actually runs: `ccs` (bare), the account set via\n 227│# `ccs auth default <name>`, NOT glm/kimi (those were SCOPE *examples*; this profile IS the example +\n    │...\n 235│# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).\n*236│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n 237│\n 238│[[requirements]]\n*239│id = \"REQ-SKILL-VERSION\"\n 240│title = \"/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)\"\n*241│required_stages = []\n 242│\n 243│# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted\n 244│# 2026-06-15, seeded from SCOPE.md §\"/spt:setup\" (LOCKED v1 = {1..7}) + finding F-005 (the\n    │...\n 250│[[requirements]]\n*251│id = \"REQ-SETUP-SUBNET\"\n 252│title = \"/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.\"\n*253│required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by...\n 254│\n 255│[[requirements]]\n*256│id = \"REQ-SETUP-CCS\"\n 257│title = \"/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).\"\n*258│required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME d...\n 259│\n 260│[[requirements]]\n*261│id = \"REQ-SETUP-ACTIVATE\"\n 262│title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n*263│required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264│\n 265│[[requirements]]\n*266│id = \"REQ-DIST-ADAPTER-RELEASE\"\n 267│title = \"The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)\"\n*268│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml name...\n 269│\n 270│[[requirements]]\n*271│id = \"REQ-DIST-ADAPTER-PEROS\"\n 272│title = \"The adapter ships ONE host-agnostic MULTI-PLATFORM `adapter.spt` (ADR-0024 W1, spt-core >= 0.13.2): a single fat archive bundles every recognized target-triple's tool binaries under a `<triple>/` dir beside the SHARED manifest.toml + strings/ at the archive root; install classifies the triple dirs and flattens THIS node's triple into the install dir, so a bare-name command token resolves (REQ-INSTALL-11). `spt adapter add --release` (default asset adapter.spt) + the `[update] gh_release` avenue ...\n*273│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs/RELEASE-RUNBOOK.md fat-archive packaging section (win native + linux cross-build via cargo-zigbuild — proven 2026-06-16, real ELF x86-64; bare `cargo build --target` fails `cc not found`, zig supplies the linker). impl: ci/publish/package-adapter.sh stages the shared root (manifest.toml + strings/) + each <triple>/ (win .exe, linux ELF) into ONE adapter.spt, refuses a < 0.13.2 floor, and guards the unrecognized-top-level-dir footgun. unit: tests/ad...\n 274│\n 275│# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from\n 276│# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/\n    │...\n 284│[[requirements]]\n*285│id = \"REQ-DIST-SESSION-RESUME\"\n 286│title = \"The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\\\"session_id\\\",\\\"id\\\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC chan...\n*287│required_stages = [\"unit\"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIE...\n 288│\n 289│[[requirements]]\n*290│id = \"REQ-DIST-IDLE-TRANSLATE\"\n 291│title = \"The adapter ships the [message-idle-translation-binary] (command=\\\"{adapter_dir}/claude-spt translate\\\", spt-core v0.16.0 seam — path deprecated; D3 fold into the consolidated binary) — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {comm...\n*292│required_stages = [\"impl\", \"unit\", \"int\"]  # D3 FOLD 2026-06-26: now the `translate` subcommand of tools/claude-spt (src/translate.rs) — seam is [message-idle-translation-binary].command=\"{adapter_dir}/claude-spt translate\" (spt-core v0.16.0; path deprecated); built+tested by ci/digest/build.sh (ci/idle-translate/build.sh is a shim); packed as the ONE tool binary; translate-proof-int --dir points at tools/claude-spt. ORIGINAL (pre-fold): impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — eve...\n 293│\n 294│# ─────────────── Checkpoint-commune wave (planned 2026-06-24, CHECKPOINT-COMMUNE-PLAN.md) ───────────────\n 295│# All inactive (required_stages = []) — activate-don't-pre-fail. Activate per-task as the build starts.\n    │...\n 297│[[requirements]]\n*298│id = \"REQ-DIST-IDLE-MULTILINE\"\n 299│title = \"The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed).\"\n*300│required_stages = [\"impl\", \"unit\", \"int\"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \\n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\\n/body/\\n/closing-tag, embedding two deliberate \\n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the ...\n 301│\n 302│[[requirements]]\n*303│id = \"REQ-DIST-CHECKPOINT-COMMUNE\"\n 304│title = \"Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which ARMS the wake and emits CLEAR-ONLY; the wake is fired separa...\n*305│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # Feature 2 / task T2 — ACTIVATED 2026-06-24. WIRE-SENTINEL is now spt-core v0.15.0 `spt send --json-payload '{\"checkpoint\":\"v1\",\"wake\":…}'` (doyle, grounded in the gated W4 code) — a structured json ATTR on the <EVENT> envelope, NOT the old string-prefix sniff (collision-proof: the marker lives inside the opaque attr, can't forge from/type). doc: full-fat /sptc:commune SKILL.md (--checkpoint: single trigger=default wake, paired triggers bracket a c...\n 306│\n 307│[[requirements]]\n*308│id = \"REQ-TAG-PEER-MESSAGING\"\n 309│title = \"An agent messages peers WITHOUT the Bash tool by embedding `@<t1,t2,t3 body @>` in its turn output: `@<` opens, the comma-separated target list runs to the first whitespace (no internal spaces), the body runs to the first `@>` (non-greedy, no escaping v1); a bare `@@id` is deliberately inert (prose discussing the syntax must not false-fire). A PreToolUse (mid-turn) + Stop (backstop) scan pulls the endpoint's own finalized digest output newer than a seq cursor (`endpoint digest <self> --after <cu...\n*310│required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: src/tag_scan.rs (pure grammar parse_tag_sections + seq-gated plan_dispatch) + hook.rs scan_and_dispatch wired into handle_pre_tool_use (mid-turn) and handle_stop (backstop), send_landed classifier, digest_cursor_rel cursor. unit: tag_scan.rs grammar + plan_dispatch tests + hook.rs scan_and_dispatch wiring tests (multi-target dispatch, unreachable-surfaced, self-drop, cursor advance, no-op-when-current). int DEFE...\n 311│\n 312│[[requirements]]\n*313│id = \"REQ-COMMUNE-OUTPUT-SHORTCUT\"\n 314│title = \"An agent writes its commune WITHOUT the Write tool by starting an output with `>>commune<<` (after leading whitespace); the rest of that output is the commune body, written to `.claude/<id>-commune.md` under the session cwd — the same file the /sptc:commune Write path targets, so the daemon's watch ingests+deletes it identically. The existing `!!checkpoint!!` in-body marker is reused for the checkpoint escalation (no new keyword): a commune body carrying it fires the same idle + checkpoint sel...\n*315│required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: tag_scan::commune_body detector + plan_dispatch commune routing (suppresses tag scan) + hook.rs scan_and_dispatch commune leg (write_commune_file to cwd .claude/<id>-commune.md via write_spill; has_checkpoint→checkpoint_payload/checkpoint_wake reuse). unit: tag_scan.rs commune_body tests (start-only, leading-ws, checkpoint-preserved) + plan_dispatch commune-suppresses-tags test + hook.rs scan_commune_shortcut_...\n 316│\n 317│[[requirements]]\n*318│id = \"REQ-COMMUNE-CONTEXT-TIER-SLICING\"\n 319│title = \"The two adapter-authored commune prompts must teach the two-slice context envelope so spt-core routes context per-agent-per-project instead of dumping everything in the live tier. spt-core ingests a commune (and the echo-commune summary) into durable tiers — `<live-context>` (agents/<id>/live-context.md, cross-project identity) and `<project-context>` (projects/<proj>/<id>/project-context.md, this-project detail), the same tiers `psyche-download` re-emits on resume — and routes by these tags...\n*320│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (doyle two-slice finding; folded into v0.17.3). impl: tools/claude-spt/src/echo_commune.rs compose_prompt two-slice directive + adapter/strings/briefs/live-ops.md commune brief two-slice guidance (both authoring paths). unit: echo_commune.rs prompt_teaches_context_tier_slicing (asserts both `<project-context>`/`<live-context>` slices + the default-tier rule in the summarizer directive). Prompt-only; the ingest routing itself is spt-core'...\n 321│\n 322│[[requirements]]\n*323│id = \"REQ-HAZARD-SEND-STATUS-STDERR\"\n 324│title = \"The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\\\"\\\"` for EVERY peer send and `send_landed(\\\"\\\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wro...\n*325│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator RCA directive; v0.17.3). impl: tools/claude-spt/src/hook.rs HookEnv::spt_send (SysEnv captures both streams, merged; trailing newlines stripped) + scan_and_dispatch peer-send switched from spt to spt_send. unit: hook.rs scan_classifies_send_by_stderr_token_not_empty_stdout (Recorder with_send_responder models empty-stdout + stderr-token; asserts the confirm-back body reports `delivered → lia`, never NO PERCH — FAILS on the ...\n 326│\n 327│[[requirements]]\n*328│id = \"REQ-DIST-RESUME-CONTEXT\"\n 329│title = \"claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path.\"\n*330│required_stages = [\"impl\", \"unit\", \"int\"]  # Parity wiring / task T3 — impl+unit ACTIVATED 2026-06-24 (coded against FROZEN contract B); int ACTIVATED 2026-06-24 when spt-core v0.15.0 (counter 34) PUBLISHED + verified on-node (doyle gate ruling: the CORE-CONTRACT half is deterministic CI, the SessionStart INJECTION E2E is logged dogfood). impl: session-start.sh bind|boundary brief branch -> sptc_psyche_download (mirrors `api poll` id-scoped auth: --session-id, no --project — project resolves from the...\n 331│\n 332│[[requirements]]\n*333│id = \"REQ-DIST-PRETOOL-POLL\"\n 334│title = \"claude-spt wires a PreToolUse hook firing `api poll` so a live agent receives messages MID-TURN (legacy-parity reachability; today claude-spt drains only on UserPromptSubmit = between turns). F-021.\"\n*335│required_stages = [\"impl\", \"unit\"]  # Reachability-parity item (F-021) — ACTIVATED 2026-06-25. Two coupled halves: (a) honest TURN-STATE lifecycle — UserPromptSubmit marks `api state busy` at turn-start, Stop marks `api state idle` at turn-end (spt-core never infers activity from quiescence — harness-contract/integration-checklist), so inbound DEFERS during a busy turn instead of idle-PTY-injecting; (b) the NEW PreToolUse hook drains those deferred messages MID-turn (`api poll {session_id} --includ...\n 336│\n 337│[[requirements]]\n*338│id = \"REQ-DIST-WORKER-PERCH-REACH\"\n 339│title = \"Subagent worker perches are runtime-REACHABLE: a `spt send` to a nested worker perch (created by subagent-start.sh -> api worker-start; hostable_types includes Worker) actually delivers. Validates existing wiring; F-022.\"\n*340│required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.\n 341│\n 342│# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────\n 343│# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent\n    │...\n 347│[[requirements]]\n*348│id = \"REQ-DIST-UPDATE-MESSAGE\"\n 349│title = \"The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1.\"\n*350│required_stages = [\"impl\", \"unit\"]  # U1 (the [update].message field) ACTIVATED 2026-06-25; D2 ([update.post] composite) PROMOTED to +impl 2026-06-26 (spt-core v0.16.0). impl: tools/claude-spt/src/post_update.rs — the [update.post] stdin/stdout contract (update_applied_in parses the additive JSON context; arbiter_line emits the `!!update-message!!` sentinel on a successful reconcile so the static [update].message fires; diagnostics→stderr, stdout=arbiter; fail-isolated; three modes incl --dry-run). T...\n 351│\n 352│[[requirements]]\n*353│id = \"REQ-DIST-BINARY-CONSOLIDATE\"\n 354│title = \"claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2.\"\n*355│required_stages = [\"impl\", \"unit\", \"int\"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consoli...\n 356│\n 357│[[requirements]]\n*358│id = \"REQ-DIST-NAME-UNIFY\"\n 359│title = \"GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/claude-spt reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3.\"\n*360│required_stages = [\"doc\", \"impl\", \"unit\"]  # U3 — ACTIVATED 2026-06-25. GitHub repo RENAMED SaberMage/spt-claude-code -> SaberMage/claude-spt (`gh repo rename`, ADMIN, local remote updated). Owner-qualified ref-flip done across the OPERATIVE surface (manifest [update].repo, README install chains + agent prompt, ci/publish/{package-adapter,release-acquire-int}.sh, adapter/strings/skills/setup.md + plugin setup SKILL.md, docs/RELEASE-RUNBOOK.md, docs-site/book.toml). Historical records intentionally LEFT...\n 361│\n 362│[[requirements]]\n*363│id = \"REQ-DIST-SKELETON-THIN\"\n 364│title = \"commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4.\"\n*365│required_stages = [\"doc\", \"impl\", \"unit\"]  # U4 — ACTIVATED 2026-06-25. Design Q RESOLVED: /sptc:live upgrades the session IN-PLACE (bare `spt api listen`), no Claude Code restart -> SessionStart does NOT re-fire; so the go-live guidance rides the /sptc:live UPS body, the perched brief only fires on bind/boundary. Both carry commune/signoff now. doc: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs ([doc->REQ-DIST-SKELETON-THIN]) + new adapter/strings/briefs/live-ops.md (commune incl --...\n 366│\n 367│[[requirements]]\n*368│id = \"REQ-DIST-INSTALL-UX\"\n 369│title = \"README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5.\"\n*370│required_stages = [\"doc\"]  # U5 — ACTIVATED 2026-06-25. doc-class (install instructions, like the REQ-SETUP-* skills — no adapter-side code of its own): README.md (added to [scan] roots) carries the per-OS install chains (bash / PowerShell / cmd: install spt-core via the published spt-releases script, then `spt adapter add --release SaberMage/claude-spt`) + the copy-paste agent-prompt skin + the symmetric `spt adapter update claude-spt` lever. doc tag rides README.md. llms.txt sync is unaffected (it ...\n 371│\n 372│[[requirements]]\n*373│id = \"REQ-DIST-CCS-PLUGIN-FOLLOWUP\"\n 374│title = \"When `ccs` is installed alongside `claude`, the post-update plugin reconcile ALSO runs `ccs plugin update sptc@cplugs` (best-effort): ccs-launched sessions read their own per-account plugin tree (the CLAUDE_CONFIG_DIR relocation, ~/.ccs/instances/<account>/.claude), which the claude-side reconcile never touches — without the follow-up a ccs session keeps running the STALE plugin after every `spt adapter update`. Not fired when ccs is absent or when ccs IS the primary CLI (already reconciled). ...\n*375│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-01 (v0.10.3, operator ask mid-cut). impl: tools/claude-spt/src/post_update.rs — ccs_followup_needed (pure gate: has_ccs && primary != ccs) + ccs_followup_cmd (plugin update sptc@cplugs) + the best-effort spawn block in run() (after the primary reconcile, before the arbiter print; dry-run prints `would run: ccs plugin update sptc@cplugs`); spawns via launch.rs resolve_program_from (the npm ccs.cmd shim is unspawnable from a bare name on Wi...\n 376│\n 377│[[requirements]]\n*378│id = \"REQ-DIST-RC-STARTUP\"\n 379│title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n*380│required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381│\n 382│[[requirements]]\n*383│id = \"REQ-DIST-BOUNDARY-ROTATE\"\n 384│title = \"The /clear|compact SessionStart boundary rotation actually rotates the perch: the hook resolves the endpoint id via $SPT_ENDPOINT_ID FIRST (whoami-by-NEW-sid is a catch-22 — the new sid is unregistered until this very call succeeds, so it resolves self:null and the old code silently skipped rotation), persists the CURRENT session id to an adapter-owned state file ({adapter_dir}/state/session/<endpoint_id>.sid) at EVERY SessionStart, and presents the PRIOR session's sid from that file as the bo...\n*385│required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-02 (F-024 correction arc; operator clean-room repro: fresh session + /clear wedges with zero hook errors). impl: tools/claude-spt/src/hook.rs — sid_state_rel (pure path builder, id sanitized), the boundary branch ($SPT_ENDPOINT_ID-first id, prior-sid proof read, spt_strict call), the every-SessionStart sid persistence after registration, HookEnv read_adapter_state/write_adapter_state/spt_strict seams (SysEnv resolves against curren...\n 386│\n 387│[[requirements]]\n*388│id = \"REQ-HAZARD-STALE-PLUGIN-RESOLUTION\"\n 389│title = \"The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispat...\n*390│required_stages = [\"doc\"]  # MINTED + doc-ACTIVATED 2026-07-02 (doyle ask off the F-024 arc: hazard writeup). doc: docs/KNOWN-HAZARDS.md §3.2. Halves already shipped ride their own REQs (reconcile = REQ-DIST-CCS-PLUGIN-FOLLOWUP + the v0.10.2 post-update work; degrade = REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP, stages [] until its guard slice). The NEW halves — stale-copy purge/flag + per-session plugin-version surfacing — are unbuilt guard-slice work: add impl/unit here when that slice starts (activate,...\n 391│\n 392│[[requirements]]\n*393│id = \"REQ-HAZARD-SESSION-PIN-WEDGE\"\n 394│title = \"A missed/failed boundary rotation must NEVER be silent: the rotation verb demands the DEPARTED session's proof, and a silently-skipped or silently-refused rotation pins the perch to a dead session id — after which EVERY id-scoped api call (poll/state/boundary) from the live session is AUTH_REFUSED (stderr-only) with NO self-heal: the endpoint looks healthy (brief renders, session runs) while cross-node AND local delivery is permanently dead (the F-024 ENLYZEAM wedge, root-caused 2026-07-02). T...\n*395│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-02 (doyle KH cross-ref, F024D-BOUNDARY-RULING). doc: docs/KNOWN-HAZARDS.md §2 entry (rotation-wedge invariant). impl: tools/claude-spt/src/hook.rs boundary branch — the loud-skip and loud-refusal env.log paths (no silent return). unit: boundary_empty_id_is_loud_never_silent + boundary_refusal_is_loud_with_reason (Recorder asserts the log lines exist and no boundary call / a failed call respectively). The wedge RECOVERY path (relau...\n 396│\n 397│[[requirements]]\n*398│id = \"REQ-HAZARD-CHECKPOINT-CLEAR-RACE\"\n 399│title = \"The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\\\"checkpoint\\\":\\\"v1\\\",…} envelope stashes the wa...\n*400│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-03 (checkpoint-clear-race fix, CHECKPOINT-CLEAR-RACE-PLAN.md). doc: docs/KNOWN-HAZARDS.md entry (the ordering invariant + the accepted orphan-wake residual). impl: tools/claude-spt/src/translate.rs — commands_for_clear (ARM emit) + commands_for_wake (FIRE emit) replace commands_for_checkpoint, is_checkpoint_fire parses {\\\"checkpoint_fire\\\":\\\"v1\\\"}, dispatch_event threads pending_wake (arm stores wake+emits clear-only; fire d...\n 401│\n 402│[[requirements]]\n*403│id = \"REQ-HAZARD-EMPTY-RESPONSE-COMMIT\"\n 404│title = \"EVERY event-typed delivery the translation binary answers must terminate with a {commit} — including deliveries it deliberately answers with no keystrokes (a checkpoint_fire with nothing armed; an event without an envelope). An event answered with ZERO records leaves the broker's inject sequence unterminated → the 5s INJECT_COMMIT_DEADLINE expires → TRANSLATION_FAULT terminates the binary (pinned 2026-07-04 via doyle's 3-discriminant collab, iso wtrace stderr: fault follows the DUPLICATE f...\n*405│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-04 (v0.13.1, the (B) zero-record pin); int ACTIVATED 2026-07-04 (core C-1 landed in spt-core 0.24.0). doc: docs/KNOWN-HAZARDS.md §2.7. impl: tools/claude-spt/src/translate.rs — dispatch_event's unarmed-fire branch + dispatch_line's event-without-envelope branch each answer vec![{commit:true}] instead of Vec::new(). unit: fire_with_no_armed_wake_answers_a_bare_commit + event_without_envelope_answers_a_bare_commit + non_event...\n 406│\n 407│[[requirements]]\n*408│id = \"REQ-DIST-BOUNDARY-RENAME\"\n 409│title = \"Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \\\"<id> @ <node> (<project>/)\\\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\\\"rename\\\":\\\"...\n*410│required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-05 (operator ask, grill-with-docs; v0.14.1). impl: tools/claude-spt/src/launch.rs — SPT_SESSION_NAME env on the spawned Command (both unix exec + windows spawn, same display_name computation as the -n argv element); tools/claude-spt/src/hook.rs — rename_payload helper + the clear-boundary rename self-send (before checkpoint_fire) + the RENAME_SKIP:no-name loud-skip path; tools/claude-spt/src/translate.rs — rename_directive (par...\n 411│\n 412│[[requirements]]\n*413│id = \"REQ-DIST-WORKER-LIFECYCLE\"\n 414│title = \"Subagent working perches follow the WORKER-TRUTH wave-1 contract (spt-core v0.27.0, doyle freeze 2026-07-06) and NEVER leak silently. SubagentStart (parent context) calls `api worker-start <parent> --session-id <parent sid> --agent-id <cc agent id> --agent-type <type>` — NO worker-id positional (core hard-mints `{parent}-w{N}`), NO token (operator ruling: sid-symmetric auth, registration stores the sid, stop accepts stored OR parent-current sid — /clear rotation covered from both ends). The ...\n*415│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-06 (WORKER-TRUTH wave-1 adapter migration, v0.15.0; supersedes the worker-* legs of REQ-DIST-HOOKS-API's original shape). impl: tools/claude-spt/src/hook.rs — handle_subagent_start (strict worker-start, minted-id stdout capture, loud-refusal/loud-empty, state persist via worker_state_rel) + handle_subagent_stop (state lookup, strict worker-stop, loud-refusal/loud-missing-mapping, state clear) + worker_state_rel (sanitized state path, sid_...\n 416│\n 417│[[requirements]]\n*418│id = \"REQ-HISTORY-FETCHER\"\n 419│title = \"The [history] transcript-access seam (the echo-commune's feeder — 'stays opaque + single-session and feeds the echo-commune verbatim', published contract) is declared strategy=\\\"fetcher\\\": `claude-spt history --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates the CC JSONL transcript with the SAME locate/precedence machinery as the [digest] fetcher (--config-dir read-var fill -> $CLAUDE_CONFIG_DIR env -> ~/.claude/projects; depth-1 then bounded-recursive slug sweep) and streams th...\n*420│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md — field-forced by flynn's psyche_host_error on core 0.28.0). impl: tools/claude-spt/src/history.rs (locate reuse from digest.rs, verbatim stream, literal-brace guard, loud-fail). unit: history.rs in-module tests (locate hit streams verbatim, missing transcript is loud exit-1, literal {CLAUDE_CONFIG_DIR} treated as absent, missing --session is loud). int DEFERRED: core-driven echo-commune round-trip on-nod...\n 421│\n 422│[[requirements]]\n*423│id = \"REQ-SESSION-ECHO-COMMUNE\"\n 424│title = \"The [session.echo_commune] bounded-summarizer role (published contract: 'when a session ends without a signoff, spt-core runs a bounded summarizer over the session's history so the context delta is captured anyway'; core 0.27/0.28 also spawns it from the commune-sync per-event turn — flynn 2026-07-07 field pin) is declared and shipped: `claude-spt echo-commune --id {id} --session-id {session_id}` accepts history on STDIN when fed (kept for the future published wiring — doyle field pin 2026-0...\n*425│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md); REVISED same day at v0.15.4 (doyle RCA: read_env rung + ccs sweep + graceful locate-miss). impl: tools/claude-spt/src/echo_commune.rs (stdin-or-locate input plan, read_env_config_dir/owlery_info_path/ccs_instance_roots/self_locate rungs, no_transcript_delta graceful miss, bounded tail, one claude -p turn) + tools/claude-spt/src/hook.rs SPT_ECHO_COMMUNE bail + adapter/claude-spt.toml [session.echo_commune]...\n 426│\n 427│[[requirements]]\n*428│id = \"REQ-DIST-REACHABILITY-NOTICE\"\n 429│title = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when...\n*430│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure ...\n 431│\n 432│[[requirements]]\n*433│id = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\n 434│title = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped)...\n*435│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-cover...\n 436│\n 437│[[requirements]]\n*438│id = \"REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE\"\n 439│title = \"Every PreToolUse emission rides the {\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"additionalContext\\\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2...\n*440│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.4 urgent fix, doyle RCA screenshot). doc: docs/KNOWN-HAZARDS.md §2.8. impl: tools/claude-spt/src/hook.rs — wrap_pretool_context + emit_capped_pretool wired in handle_pre_tool_use. unit: pretool_envelope_shape_and_escaping (round-trip through serde) + pretool_overflow_spills_raw_and_wraps_the_pointer (wrapped-size cap decision) + pre_tool_use_busy_before_drain (envelope on the drain path) + pre_tool_use_agent_spawn_gets_t...\n 441│\n 442│[[requirements]]\n*443│id = \"REQ-SKILL-LIVE-SPT-HOSTED-BRANCH\"\n 444│title = \"The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in...\n*445│required_stages = [\"doc\"]  # MINTED + ACTIVATED 2026-07-07 (adapter-debts work order; ADAPTER-DEBTS or the session commune). doc: adapter/strings/skills/live.md (the spt-hosted branch in Bringup + the two failure branches) + adapter/strings/skills/ready.md (the matching note). No impl/unit: these are agent-facing prose strings the hook only file-backs + resolves (same class as REQ-DIST-SESSIONSTART-BRIEF's string bodies); the branch is a documentation contract, not new binary logic. Source: flynn's Monit...\n 446│\n 447│[[requirements]]\n*448│id = \"REQ-SUPERVISOR-RELAY-RESPAWN\"\n 449│title = \"DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-res...\n*450│required_stages = []  # MINTED INACTIVE 2026-07-07 (adapter-debts work order). Design/doc candidate only — stays required_stages = [] (activation-model: never pre-fail a requirement not yet in build) until doyle's W4 supervisor-watchdog activation ruling assigns the core-vs-adapter boundary. Then activate with the real stages (doc the belt design → impl the relay-respawn/death-surfacing → unit → int the dead-relay recovery). Source: live-perch-monitor-reachability memory (a dead Monitor + trustin...\n 451│\n 452│[[requirements]]\n*453│id = \"REQ-HAZARD-RESUME-REBIND-WEDGE\"\n 454│title = \"CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ON...\n*455│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator /resume hunch; RESUME-REBIND-WEDGE, v0.15.7). impl: tools/claude-spt/src/hook.rs — handle_session_start bind branch (prior-sid proof from sid_state_rel state, prior!=sid guard, spt_strict + loud bind-fail log replacing the exit-ignored env.spt). unit: hook.rs — resume_rebind_presents_prior_proof (source=resume + state=s-old + sid=s-new → bind ... --set-session-id s-new --session-id s-old) + fresh_bind_has_no_proof_and_is_...\n 456│\n 457│[[requirements]]\n*458│id = \"REQ-INJECT-VERIFY-ECHO\"\n 459│title = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapte...\n*460│required_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC ses...\n 461│\n 462│[[requirements]]\n*463│id = \"REQ-HAZARD-STUCK-ACTIVE-NO-IDLE\"\n 464│title = \"A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkp...\n*465│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator /diagnose of lia; v0.15.9). impl: tools/claude-spt/src/hook.rs — the bind-branch `state idle` at bringup + handle_notification (idle_prompt self-heal, $SPT_ENDPOINT_ID gate, sid-required) + the Notification dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json Notification→dispatch.sh with the idle_prompt matcher. unit: hook.rs startup_bind_marks_idle_for_immediate_receivability + notification_idle_prompt_marks_...\n 466│\n 467│[[requirements]]\n*468│id = \"REQ-HAZARD-STOPFAILURE-STUCK-BUSY\"\n 469│title = \"An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO...\n*470│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1 fix 1; receive-heal). impl: tools/claude-spt/src/hook.rs handle_stop_failure + the StopFailure dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json StopFailure→dispatch.sh (matcherless — all error types). unit: hook.rs stop_failure_marks_idle_when_perched_else_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (StopFailure in the lockstep list) + tests/hooks-dispatch.sh (StopFailure wir...\n 471│\n 472│[[requirements]]\n*473│id = \"REQ-HAZARD-INTERRUPT-STUCK-BUSY\"\n 474│title = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is...\n*475│required_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/se...\n 476│\n 477│[[requirements]]\n*478│id = \"REQ-OMP-NATIVE-TUI\"\n 479│title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.","truncation":{"content":" 37|# of the coverage gate.\n*38|required_stages = []\n 39|\n 40|# ───────────────────────────── Requirements ─────────────────────────────────\n 41|# Add requirements as you scope milestones, seeded from SCOPE.md \"Parity-trim\". Shape:\n...\n 61|[[requirements]]\n*62|id = \"REQ-DIST-PLUGIN-SKELETON\"\n 63|title = \"Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic\"\n*64|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).\n 65|\n 66|[[requirements]]\n*67|id = \"REQ-DIST-BOOTSTRAP-INSTALL\"\n 68|title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n*69|required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n 70|\n 71|[[requirements]]\n*72|id = \"REQ-DIST-HOOKS-API\"\n 73|title = \"hooks.json delegates to `spt api` rather than carrying adapter logic\"\n*74|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).\n 75|\n 76|[[requirements]]\n*77|id = \"REQ-DIST-HOOK-BINARY\"\n 78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n*82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n 83|title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n*84|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n 85|\n 86|[[requirements]]\n*87|id = \"REQ-DIST-SHORTCUT-BASENAME\"\n 88|title = \"The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \\\"cc\\\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)\"\n*89|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (r...\n 90|\n 91|[[requirements]]\n*92|id = \"REQ-CC-LAUNCHER-BIND\"\n 93|title = \"spt-hosted bringup self-binds E2E: `spt endpoint run claude-spt <id>` spawns the [session.self] CC session which — via [env.SPT_ENDPOINT_ID]={id} (spt-core fills it, v0.11.0+ REQ-HAZARD-ENV-SUBST/F-013) + the SessionStart `bind` branch — registers a BOUND perch on disk reachable by `spt send` (live PTY inject, SENT). The M12 cc-launcher reachability, the gap that hid the wall-b zero-perch.\"\n*94|required_stages = [\"int\"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp...\n 95|\n 96|[[requirements]]\n*97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n 98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n*102|id = \"REQ-DIST-DIGEST-FETCHER\"\n 103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n*107|id = \"REQ-DIST-WHOAMI-JSON\"\n 108|title = \"The hook binary resolves its own perch id ONLY from `spt whoami --json` (.self.id; self:null ⇒ no-perch; any parse failure ⇒ empty) — NEVER from a line of the human view, whose first line is a `SUBNET <name>` roster header on any subnet-member node (the doyle bug 2026-07-01: SessionStart crowned an agent \\\"SUBNET SPT_DEV\\\" and the brief's don't-run-whoami instruction made the wrong identity self-reinforce through an orchestration round)\"\n*109|required_stages = [\"impl\", \"unit\"]  # impl: tools/claude-spt/src/hook.rs self_id + parse_whoami_self (pure over the output string). unit: whoami_human_roster_is_never_an_id (the load-bearing regression — grouped-roster input resolves EMPTY, perched human `SELF:` line too) + whoami_json_self_id_contract (id/null/garbage/missing-key). Live validation on this node (2-subnet repro per doyle's check): disposable perch → real hook binary → banner id = perch id. `--json` is a v0.16.0 global flag — below...\n 110|\n 111|[[requirements]]\n*112|id = \"REQ-SKILL-LIVE\"\n 113|title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115|\n 116|[[requirements]]\n*117|id = \"REQ-UPS-INJECTION\"\n 118|title = \"UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands\"\n*119|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.\n 120|\n 121|[[requirements]]\n*122|id = \"REQ-DIST-SESSIONSTART-BRIEF\"\n 123|title = \"SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped\"\n*124|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int ...\n 125|\n 126|# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +\n 127|# spt reporting bus. Seeded from SCOPE.md \"CI model\" + docs/CI.md. Real-claude acceptance is a\n...\n 130|[[requirements]]\n*131|id = \"REQ-CI-GATES\"\n 132|title = \"Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command\"\n*133|required_stages = [\"doc\", \"impl\", \"unit\"]\n 134|\n 135|[[requirements]]\n*136|id = \"REQ-CI-MANUAL\"\n 137|title = \"A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host\"\n*138|required_stages = [\"doc\", \"impl\"]\n 139|\n 140|[[requirements]]\n*141|id = \"REQ-CI-TRIGGER\"\n 142|title = \"A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling\"\n*143|required_stages = [\"doc\", \"impl\"]\n 144|\n 145|[[requirements]]\n*146|id = \"REQ-CI-BUS\"\n 147|title = \"CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)\"\n*148|required_stages = [\"doc\", \"impl\"]\n 149|\n 150|[[requirements]]\n*151|id = \"REQ-CI-OWL-DISCOVERY\"\n 152|title = \"The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path\"\n*153|required_stages = [\"doc\", \"impl\", \"unit\"]\n 154|\n 155|[[requirements]]\n*156|id = \"REQ-CI-ACCEPTANCE\"\n 157|title = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\n*158|required_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n 159|\n 160|[[requirements]]\n*161|id = \"REQ-HAZARD-PERCH-COLLISION\"\n 162|title = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\n*163|required_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n 164|\n 165|[[requirements]]\n*166|id = \"REQ-HAZARD-MSYS-PATHCONV\"\n 167|title = \"Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)\"\n*168|required_stages = [\"unit\"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).\n 169|\n 170|[[requirements]]\n*171|id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n 172|title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173|required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174|\n 175|[[requirements]]\n*176|id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n 177|title = \"The psyche host's resident loop is PACED and TERMINAL on persistent instant cycles: a healthy iteration blocks (perch poll or a real claude turn); when iterations complete sub-2s repeatedly (claude dying at spawn — the F-h untrusted-cwd trust prompt — or a thrashing poll), the host backs off exponentially (500ms doubling, 5s cap) and after 8 consecutive instant cycles exits LOUD + NONZERO (PSYCHE_HOST_GIVE_UP stderr, exit 3) so the wrapper DEATH becomes visible to spt-core's psyche_host_erro...\n*178|required_stages = [\"doc\", \"unit\"]  # RESOLVED-BY-DESIGN (v0.14.0 psyche-ephemeral redesign, F-030; PSYCHE-EPHEMERAL-PLAN.md): the resident loop this hazard guards — and its v0.13.4 per-pulse `spt ready --once` poll-fail leg — are DELETED. The shim is run-to-completion (one claude spawn per invocation, then exit), so the instant-cycle class + the multi-subnet READY_FAIL poll-refusal churn cannot occur adapter-side: there is no loop and no poll. Crashloop = repeated ephemeral deaths = core C3(b) ledger...\n 179|\n 180|[[requirements]]\n*181|id = \"REQ-HAZARD-PSYCHE-IDENTITY-ENV\"\n 182|title = \"Both psyche roles ([session.psyche_init] gate + [session.psyche_resume] spawn) declare env_remove = [\\\"OWL_SESSION_ID\\\", \\\"SPT_AGENT_ID\\\"] — a spawned Psyche must NEVER inherit its parent session's identity env, or its harness hooks resolve \\\"self\\\" to the PARENT (or a foreign top-level perch) and stamp/rebind THAT perch (field-observed F-028 C2: f015b-probe-psyche rebound hall-a's info.json.session_id via authenticate()'s dead-owner re-pin). The adapter's half of the f028 cross-perch-contamin...\n*183|required_stages = [\"doc\", \"unit\"]  # Declaration-only manifest seam (scanner does not read .toml — same convention as REQ-DIST-SESSION-RESUME): doc: docs/KNOWN-HAZARDS.md §2.5. unit: tests/manifest-shortcut.sh env-scrub block (env_remove present in BOTH [session.psyche_init] and [session.psyche_resume], both identity vars — v0.14.0 ephemeral: the scrub rides both roles now). Source: doyle's F-028 C2 adapter change brief 2026-07-03. int (a live psyche spawn asserting the vars absent from the child en...\n 184|\n 185|[[requirements]]\n*186|id = \"REQ-PSYCHE-EPHEMERAL-SHIM\"\n 187|title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189|\n 190|[[requirements]]\n*191|id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n 192|title = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to ...\n*193|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-e...\n 194|\n 195|[[requirements]]\n*196|id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n 197|title = \"The [strings].hook_cmd SHAPE and the plugin dispatch.sh shim move in LOCKSTEP (or the dispatch contract is pinned): a shape change in one layer with the other stale must DEGRADE, never brick — the 2026-07-01 mid-session skew (old 0.1.8 dispatch execing `\\\"$bin\\\" <event>` × new bare-path hook_cmd) ran `claude-spt PreToolUse` → unknown-subcommand → nonzero exit on EVERY hook event → CC blocked all tools AND looped the Stop hook; zero self-repair until the operator ran /reload-plugins\"\n*198|required_stages = [\"doc\", \"unit\"]  # ACTIVATED (guard slice): unit — dispatch.sh handles BOTH hook_cmd shapes (bare + legacy ` hook`-suffixed) in BOTH directions (tests/hooks-dispatch.sh) AND claude-spt DEGRADES (exit 0, pass-through + stderr note) when a stale dispatch execs `claude-spt <CCEvent>` without the `hook` token, so a stale dispatch never bricks CC (main.rs / hook.rs unit tests). Still OPEN for a later int wave: post-update reconcile refreshes the LIVE plugin cache dir (incl. ccs instance ro...\n 199|\n 200|# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs\n 201|# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.\n...\n 203|[[requirements]]\n*204|id = \"REQ-DOCS-SITE\"\n 205|title = \"docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)\"\n*206|required_stages = [\"doc\", \"impl\"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.\n 207|\n 208|[[requirements]]\n*209|id = \"REQ-DOCS-DRIFT\"\n 210|title = \"Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build\"\n*211|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.\n 212|\n 213|[[requirements]]\n*214|id = \"REQ-PARITY-AUDIT\"\n 215|title = \"The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on\"\n*216|required_stages = [\"doc\"]  # doc: docs/PARITY.md (the parity matrix + per-row evidence/finding). Pure reconciliation record (no code of its own); the per-surface impl/unit evidence lives on the surface's own REQ (REQ-SKILL-SUBNET, REQ-CCS-PROFILES, REQ-SKILL-LIVE, etc.).\n 217|\n 218|[[requirements]]\n*219|id = \"REQ-SKILL-SUBNET\"\n 220|title = \"The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)\"\n*221|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/subnet.md (one /sptc:subnet skill covering status/create/show-code/join — mirrors the single `spt subnet` verb-family CLI). int: ci/manifest/registration-int.sh file-backed-resolve assertion (skills.subnet -> body) on live spt — the wiring proof. No separate `impl`/`unit` stage: a file-backed skill has no unit-testable code of its own (the manifest [strings.skills].subnet pointer + plugin stub are config, and the int proves they resolve ...\n 222|\n 223|[[requirements]]\n*224|id = \"REQ-CCS-PROFILES\"\n 225|title = \"The adapter ships a ccs profile template (claude-spt:ccs) — LOCKED-ADD overlay that retargets the spawn command template through `ccs` (a drop-in for the `claude` binary) and honors ccs's relocated CLAUDE_CONFIG_DIR transcript root in the digest extractor; templates only (user supplies their own ccs config/keys)\"\n 226|# Scoped to the default-profile shape the operator actually runs: `ccs` (bare), the account set via\n 227|# `ccs auth default <name>`, NOT glm/kimi (those were SCOPE *examples*; this profile IS the example +\n...\n 235|# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).\n*236|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]\n 237|\n 238|[[requirements]]\n*239|id = \"REQ-SKILL-VERSION\"\n 240|title = \"/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)\"\n*241|required_stages = []\n 242|\n 243|# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted\n 244|# 2026-06-15, seeded from SCOPE.md §\"/spt:setup\" (LOCKED v1 = {1..7}) + finding F-005 (the\n...\n 250|[[requirements]]\n*251|id = \"REQ-SETUP-SUBNET\"\n 252|title = \"/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.\"\n*253|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by...\n 254|\n 255|[[requirements]]\n*256|id = \"REQ-SETUP-CCS\"\n 257|title = \"/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).\"\n*258|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME d...\n 259|\n 260|[[requirements]]\n*261|id = \"REQ-SETUP-ACTIVATE\"\n 262|title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n*263|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264|\n 265|[[requirements]]\n*266|id = \"REQ-DIST-ADAPTER-RELEASE\"\n 267|title = \"The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)\"\n*268|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml name...\n 269|\n 270|[[requirements]]\n*271|id = \"REQ-DIST-ADAPTER-PEROS\"\n 272|title = \"The adapter ships ONE host-agnostic MULTI-PLATFORM `adapter.spt` (ADR-0024 W1, spt-core >= 0.13.2): a single fat archive bundles every recognized target-triple's tool binaries under a `<triple>/` dir beside the SHARED manifest.toml + strings/ at the archive root; install classifies the triple dirs and flattens THIS node's triple into the install dir, so a bare-name command token resolves (REQ-INSTALL-11). `spt adapter add --release` (default asset adapter.spt) + the `[update] gh_release` avenue ...\n*273|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: docs/RELEASE-RUNBOOK.md fat-archive packaging section (win native + linux cross-build via cargo-zigbuild — proven 2026-06-16, real ELF x86-64; bare `cargo build --target` fails `cc not found`, zig supplies the linker). impl: ci/publish/package-adapter.sh stages the shared root (manifest.toml + strings/) + each <triple>/ (win .exe, linux ELF) into ONE adapter.spt, refuses a < 0.13.2 floor, and guards the unrecognized-top-level-dir footgun. unit: tests/ad...\n 274|\n 275|# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from\n 276|# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/\n...\n 284|[[requirements]]\n*285|id = \"REQ-DIST-SESSION-RESUME\"\n 286|title = \"The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\\\"session_id\\\",\\\"id\\\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC chan...\n*287|required_stages = [\"unit\"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIE...\n 288|\n 289|[[requirements]]\n*290|id = \"REQ-DIST-IDLE-TRANSLATE\"\n 291|title = \"The adapter ships the [message-idle-translation-binary] (command=\\\"{adapter_dir}/claude-spt translate\\\", spt-core v0.16.0 seam — path deprecated; D3 fold into the consolidated binary) — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope> (type the text, no trailing CR) · 50ms · {key:enter} (submit the PTY line) · {comm...\n*292|required_stages = [\"impl\", \"unit\", \"int\"]  # D3 FOLD 2026-06-26: now the `translate` subcommand of tools/claude-spt (src/translate.rs) — seam is [message-idle-translation-binary].command=\"{adapter_dir}/claude-spt translate\" (spt-core v0.16.0; path deprecated); built+tested by ci/digest/build.sh (ci/idle-translate/build.sh is a shim); packed as the ONE tool binary; translate-proof-int --dir points at tools/claude-spt. ORIGINAL (pre-fold): impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — eve...\n 293|\n 294|# ─────────────── Checkpoint-commune wave (planned 2026-06-24, CHECKPOINT-COMMUNE-PLAN.md) ───────────────\n 295|# All inactive (required_stages = []) — activate-don't-pre-fail. Activate per-task as the build starts.\n...\n 297|[[requirements]]\n*298|id = \"REQ-DIST-IDLE-MULTILINE\"\n 299|title = \"The idle-translation binary renders each inbound <EVENT> envelope across MULTIPLE LINES in CC's input box for visual distinction — an embedded raw newline byte in the {text} payload after the opening <EVENT ...> tag and before the closing </EVENT> (spt-core writes {text} byte-verbatim including newlines, doyle-confirmed; CC soft-newlines on a bare newline, empirically gated). Cyan color is impossible (SGR bytes eaten by CC input handling; user-turns theme-fixed).\"\n*300|required_stages = [\"impl\", \"unit\", \"int\"]  # Feature 1 / task T1 — ACTIVATED 2026-06-24 after the empirical soft-newline gate PASSED (probe binary on a live spt-hosted endpoint: a two-line {text} landed as ONE user turn with the \\n preserved, no early submit). impl: cc-spt-idle-translate frame_envelope splits the envelope opening-tag/\\n/body/\\n/closing-tag, embedding two deliberate \\n's AFTER the stray-CR/LF-neutralize (degenerate fallback for non-<EVENT>…</EVENT> shapes incl. EVENT-PART). unit: the ...\n 301|\n 302|[[requirements]]\n*303|id = \"REQ-DIST-CHECKPOINT-COMMUNE\"\n 304|title = \"Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which ARMS the wake and emits CLEAR-ONLY; the wake is fired separa...\n*305|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # Feature 2 / task T2 — ACTIVATED 2026-06-24. WIRE-SENTINEL is now spt-core v0.15.0 `spt send --json-payload '{\"checkpoint\":\"v1\",\"wake\":…}'` (doyle, grounded in the gated W4 code) — a structured json ATTR on the <EVENT> envelope, NOT the old string-prefix sniff (collision-proof: the marker lives inside the opaque attr, can't forge from/type). doc: full-fat /sptc:commune SKILL.md (--checkpoint: single trigger=default wake, paired triggers bracket a c...\n 306|\n 307|[[requirements]]\n*308|id = \"REQ-TAG-PEER-MESSAGING\"\n 309|title = \"An agent messages peers WITHOUT the Bash tool by embedding `@<t1,t2,t3 body @>` in its turn output: `@<` opens, the comma-separated target list runs to the first whitespace (no internal spaces), the body runs to the first `@>` (non-greedy, no escaping v1); a bare `@@id` is deliberately inert (prose discussing the syntax must not false-fire). A PreToolUse (mid-turn) + Stop (backstop) scan pulls the endpoint's own finalized digest output newer than a seq cursor (`endpoint digest <self> --after <cu...\n*310|required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: src/tag_scan.rs (pure grammar parse_tag_sections + seq-gated plan_dispatch) + hook.rs scan_and_dispatch wired into handle_pre_tool_use (mid-turn) and handle_stop (backstop), send_landed classifier, digest_cursor_rel cursor. unit: tag_scan.rs grammar + plan_dispatch tests + hook.rs scan_and_dispatch wiring tests (multi-target dispatch, unreachable-surfaced, self-drop, cursor advance, no-op-when-current). int DEFE...\n 311|\n 312|[[requirements]]\n*313|id = \"REQ-COMMUNE-OUTPUT-SHORTCUT\"\n 314|title = \"An agent writes its commune WITHOUT the Write tool by starting an output with `>>commune<<` (after leading whitespace); the rest of that output is the commune body, written to `.claude/<id>-commune.md` under the session cwd — the same file the /sptc:commune Write path targets, so the daemon's watch ingests+deletes it identically. The existing `!!checkpoint!!` in-body marker is reused for the checkpoint escalation (no new keyword): a commune body carrying it fires the same idle + checkpoint sel...\n*315|required_stages = [\"impl\", \"unit\"]  # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: tag_scan::commune_body detector + plan_dispatch commune routing (suppresses tag scan) + hook.rs scan_and_dispatch commune leg (write_commune_file to cwd .claude/<id>-commune.md via write_spill; has_checkpoint→checkpoint_payload/checkpoint_wake reuse). unit: tag_scan.rs commune_body tests (start-only, leading-ws, checkpoint-preserved) + plan_dispatch commune-suppresses-tags test + hook.rs scan_commune_shortcut_...\n 316|\n 317|[[requirements]]\n*318|id = \"REQ-COMMUNE-CONTEXT-TIER-SLICING\"\n 319|title = \"The two adapter-authored commune prompts must teach the two-slice context envelope so spt-core routes context per-agent-per-project instead of dumping everything in the live tier. spt-core ingests a commune (and the echo-commune summary) into durable tiers — `<live-context>` (agents/<id>/live-context.md, cross-project identity) and `<project-context>` (projects/<proj>/<id>/project-context.md, this-project detail), the same tiers `psyche-download` re-emits on resume — and routes by these tags...\n*320|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (doyle two-slice finding; folded into v0.17.3). impl: tools/claude-spt/src/echo_commune.rs compose_prompt two-slice directive + adapter/strings/briefs/live-ops.md commune brief two-slice guidance (both authoring paths). unit: echo_commune.rs prompt_teaches_context_tier_slicing (asserts both `<project-context>`/`<live-context>` slices + the default-tier rule in the summarizer directive). Prompt-only; the ingest routing itself is spt-core'...\n 321|\n 322|[[requirements]]\n*323|id = \"REQ-HAZARD-SEND-STATUS-STDERR\"\n 324|title = \"The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\\\"\\\"` for EVERY peer send and `send_landed(\\\"\\\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wro...\n*325|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator RCA directive; v0.17.3). impl: tools/claude-spt/src/hook.rs HookEnv::spt_send (SysEnv captures both streams, merged; trailing newlines stripped) + scan_and_dispatch peer-send switched from spt to spt_send. unit: hook.rs scan_classifies_send_by_stderr_token_not_empty_stdout (Recorder with_send_responder models empty-stdout + stderr-token; asserts the confirm-back body reports `delivered → lia`, never NO PERCH — FAILS on the ...\n 326|\n 327|[[requirements]]\n*328|id = \"REQ-DIST-RESUME-CONTEXT\"\n 329|title = \"claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path.\"\n*330|required_stages = [\"impl\", \"unit\", \"int\"]  # Parity wiring / task T3 — impl+unit ACTIVATED 2026-06-24 (coded against FROZEN contract B); int ACTIVATED 2026-06-24 when spt-core v0.15.0 (counter 34) PUBLISHED + verified on-node (doyle gate ruling: the CORE-CONTRACT half is deterministic CI, the SessionStart INJECTION E2E is logged dogfood). impl: session-start.sh bind|boundary brief branch -> sptc_psyche_download (mirrors `api poll` id-scoped auth: --session-id, no --project — project resolves from the...\n 331|\n 332|[[requirements]]\n*333|id = \"REQ-DIST-PRETOOL-POLL\"\n 334|title = \"claude-spt wires a PreToolUse hook firing `api poll` so a live agent receives messages MID-TURN (legacy-parity reachability; today claude-spt drains only on UserPromptSubmit = between turns). F-021.\"\n*335|required_stages = [\"impl\", \"unit\"]  # Reachability-parity item (F-021) — ACTIVATED 2026-06-25. Two coupled halves: (a) honest TURN-STATE lifecycle — UserPromptSubmit marks `api state busy` at turn-start, Stop marks `api state idle` at turn-end (spt-core never infers activity from quiescence — harness-contract/integration-checklist), so inbound DEFERS during a busy turn instead of idle-PTY-injecting; (b) the NEW PreToolUse hook drains those deferred messages MID-turn (`api poll {session_id} --includ...\n 336|\n 337|[[requirements]]\n*338|id = \"REQ-DIST-WORKER-PERCH-REACH\"\n 339|title = \"Subagent worker perches are runtime-REACHABLE: a `spt send` to a nested worker perch (created by subagent-start.sh -> api worker-start; hostable_types includes Worker) actually delivers. Validates existing wiring; F-022.\"\n*340|required_stages = []  # Reachability-parity validation (F-022). ACTIVATE at the reachability-parity milestone. int: bring up a CC session, spawn a subagent, confirm the worker perch appears in the roster + `spt send <worker-id>` delivers (worker-poll drains). Wiring is present (subagent-start.sh + hostable Worker); runtime reachability is the only open question.\n 341|\n 342|# ─────────────── UNIFY-CONSOLIDATE wave (planned 2026-06-24, UNIFY-CONSOLIDATE-PLAN.md) ───────────────\n 343|# ADR-0005 (name unification) + ADR-0006 (one-command update + consolidated binary), doyle-independent\n...\n 347|[[requirements]]\n*348|id = \"REQ-DIST-UPDATE-MESSAGE\"\n 349|title = \"The adapter manifest [update] table carries a `message` markdown field, printed by spt-core ONLY on a real version apply: it tells the user to run /reload-plugins (the unavoidable TUI step) and points at the more-powerful `spt` CLI endpoint route (`spt endpoint run`) alongside /sptc:live. U1.\"\n*350|required_stages = [\"impl\", \"unit\"]  # U1 (the [update].message field) ACTIVATED 2026-06-25; D2 ([update.post] composite) PROMOTED to +impl 2026-06-26 (spt-core v0.16.0). impl: tools/claude-spt/src/post_update.rs — the [update.post] stdin/stdout contract (update_applied_in parses the additive JSON context; arbiter_line emits the `!!update-message!!` sentinel on a successful reconcile so the static [update].message fires; diagnostics→stderr, stdout=arbiter; fail-isolated; three modes incl --dry-run). T...\n 351|\n 352|[[requirements]]\n*353|id = \"REQ-DIST-BINARY-CONSOLIDATE\"\n 354|title = \"claude-spt-digest + claude-spt-psyche merge into ONE `claude-spt` crate with clap subcommands (digest / psyche / post-update); post-update = the plugin-sync logic (detect claude|ccs CLI -> ensure cplugs marketplace -> claude plugin add|update -> print notice; does NOT run /reload-plugins). cc-spt-idle-translate stays separate (folds at D3). U2.\"\n*355|required_stages = [\"impl\", \"unit\", \"int\"]  # U2 (the big refactor; preps post-update for D2) — ACTIVATED 2026-06-25, int promoted same day (proofs ran GREEN on the consolidated binary). impl: tools/claude-spt crate (main.rs subcommand dispatch [classify] + post_update.rs CLI-detect/marketplace/plugin reconciliation; digest+psyche folded in as subcommands) + manifest [digest].extractor `claude-spt digest …` + [session.psyche_init].command `claude-spt psyche …` + ci/digest/build.sh (canonical consoli...\n 356|\n 357|[[requirements]]\n*358|id = \"REQ-DIST-NAME-UNIFY\"\n 359|title = \"GitHub repo renamed spt-claude-code -> claude-spt; [update].repo, README, CI, package scripts, every SaberMage/claude-spt reference + install-dir test assumptions (_github/SaberMage-claude-spt) updated. Adapter name claude-spt UNCHANGED; plugin sptc UNCHANGED this milestone (succession = D4, owl-gated). U3.\"\n*360|required_stages = [\"doc\", \"impl\", \"unit\"]  # U3 — ACTIVATED 2026-06-25. GitHub repo RENAMED SaberMage/spt-claude-code -> SaberMage/claude-spt (`gh repo rename`, ADMIN, local remote updated). Owner-qualified ref-flip done across the OPERATIVE surface (manifest [update].repo, README install chains + agent prompt, ci/publish/{package-adapter,release-acquire-int}.sh, adapter/strings/skills/setup.md + plugin setup SKILL.md, docs/RELEASE-RUNBOOK.md, docs-site/book.toml). Historical records intentionally LEFT...\n 361|\n 362|[[requirements]]\n*363|id = \"REQ-DIST-SKELETON-THIN\"\n 364|title = \"commune/send/signoff skill bodies move OUT of the plugin SKILL.md (stubs: frontmatter + 'live agents only — /sptc:live first') and INTO adapter strings: a new perched SessionStart brief string (commune incl. --checkpoint, signoff) + the existing /sptc:live UPS body for the go-live moment, so reactive-skill prose rides `spt adapter update`. U4.\"\n*365|required_stages = [\"doc\", \"impl\", \"unit\"]  # U4 — ACTIVATED 2026-06-25. Design Q RESOLVED: /sptc:live upgrades the session IN-PLACE (bare `spt api listen`), no Claude Code restart -> SessionStart does NOT re-fire; so the go-live guidance rides the /sptc:live UPS body, the perched brief only fires on bind/boundary. Both carry commune/signoff now. doc: thinned plugin/sptc/skills/{commune,send,signoff}/SKILL.md stubs ([doc->REQ-DIST-SKELETON-THIN]) + new adapter/strings/briefs/live-ops.md (commune incl --...\n 366|\n 367|[[requirements]]\n*368|id = \"REQ-DIST-INSTALL-UX\"\n 369|title = \"README ships platform-specific install chains (cmd / PowerShell / bash) — check-for/install spt-core (call the spt-releases per-platform install script; claude-spt may be a user's first spt-core exposure) then `spt adapter add --release SaberMage/claude-spt` — plus a copy-paste agent prompt (the casual-user skin) running the chain in one sequenced Bash call, symmetric with the update lever. U5.\"\n*370|required_stages = [\"doc\"]  # U5 — ACTIVATED 2026-06-25. doc-class (install instructions, like the REQ-SETUP-* skills — no adapter-side code of its own): README.md (added to [scan] roots) carries the per-OS install chains (bash / PowerShell / cmd: install spt-core via the published spt-releases script, then `spt adapter add --release SaberMage/claude-spt`) + the copy-paste agent-prompt skin + the symmetric `spt adapter update claude-spt` lever. doc tag rides README.md. llms.txt sync is unaffected (it ...\n 371|\n 372|[[requirements]]\n*373|id = \"REQ-DIST-CCS-PLUGIN-FOLLOWUP\"\n 374|title = \"When `ccs` is installed alongside `claude`, the post-update plugin reconcile ALSO runs `ccs plugin update sptc@cplugs` (best-effort): ccs-launched sessions read their own per-account plugin tree (the CLAUDE_CONFIG_DIR relocation, ~/.ccs/instances/<account>/.claude), which the claude-side reconcile never touches — without the follow-up a ccs session keeps running the STALE plugin after every `spt adapter update`. Not fired when ccs is absent or when ccs IS the primary CLI (already reconciled). ...\n*375|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-01 (v0.10.3, operator ask mid-cut). impl: tools/claude-spt/src/post_update.rs — ccs_followup_needed (pure gate: has_ccs && primary != ccs) + ccs_followup_cmd (plugin update sptc@cplugs) + the best-effort spawn block in run() (after the primary reconcile, before the arbiter print; dry-run prints `would run: ccs plugin update sptc@cplugs`); spawns via launch.rs resolve_program_from (the npm ccs.cmd shim is unspawnable from a bare name on Wi...\n 376|\n 377|[[requirements]]\n*378|id = \"REQ-DIST-RC-STARTUP\"\n 379|title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n*380|required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381|\n 382|[[requirements]]\n*383|id = \"REQ-DIST-BOUNDARY-ROTATE\"\n 384|title = \"The /clear|compact SessionStart boundary rotation actually rotates the perch: the hook resolves the endpoint id via $SPT_ENDPOINT_ID FIRST (whoami-by-NEW-sid is a catch-22 — the new sid is unregistered until this very call succeeds, so it resolves self:null and the old code silently skipped rotation), persists the CURRENT session id to an adapter-owned state file ({adapter_dir}/state/session/<endpoint_id>.sid) at EVERY SessionStart, and presents the PRIOR session's sid from that file as the bo...\n*385|required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-02 (F-024 correction arc; operator clean-room repro: fresh session + /clear wedges with zero hook errors). impl: tools/claude-spt/src/hook.rs — sid_state_rel (pure path builder, id sanitized), the boundary branch ($SPT_ENDPOINT_ID-first id, prior-sid proof read, spt_strict call), the every-SessionStart sid persistence after registration, HookEnv read_adapter_state/write_adapter_state/spt_strict seams (SysEnv resolves against curren...\n 386|\n 387|[[requirements]]\n*388|id = \"REQ-HAZARD-STALE-PLUGIN-RESOLUTION\"\n 389|title = \"The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispat...\n*390|required_stages = [\"doc\"]  # MINTED + doc-ACTIVATED 2026-07-02 (doyle ask off the F-024 arc: hazard writeup). doc: docs/KNOWN-HAZARDS.md §3.2. Halves already shipped ride their own REQs (reconcile = REQ-DIST-CCS-PLUGIN-FOLLOWUP + the v0.10.2 post-update work; degrade = REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP, stages [] until its guard slice). The NEW halves — stale-copy purge/flag + per-session plugin-version surfacing — are unbuilt guard-slice work: add impl/unit here when that slice starts (activate,...\n 391|\n 392|[[requirements]]\n*393|id = \"REQ-HAZARD-SESSION-PIN-WEDGE\"\n 394|title = \"A missed/failed boundary rotation must NEVER be silent: the rotation verb demands the DEPARTED session's proof, and a silently-skipped or silently-refused rotation pins the perch to a dead session id — after which EVERY id-scoped api call (poll/state/boundary) from the live session is AUTH_REFUSED (stderr-only) with NO self-heal: the endpoint looks healthy (brief renders, session runs) while cross-node AND local delivery is permanently dead (the F-024 ENLYZEAM wedge, root-caused 2026-07-02). T...\n*395|required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-02 (doyle KH cross-ref, F024D-BOUNDARY-RULING). doc: docs/KNOWN-HAZARDS.md §2 entry (rotation-wedge invariant). impl: tools/claude-spt/src/hook.rs boundary branch — the loud-skip and loud-refusal env.log paths (no silent return). unit: boundary_empty_id_is_loud_never_silent + boundary_refusal_is_loud_with_reason (Recorder asserts the log lines exist and no boundary call / a failed call respectively). The wedge RECOVERY path (relau...\n 396|\n 397|[[requirements]]\n*398|id = \"REQ-HAZARD-CHECKPOINT-CLEAR-RACE\"","truncated":true,"truncatedBy":"bytes","totalLines":407,"totalBytes":69295,"outputLines":325,"outputBytes":51065,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":407,"totalBytes":69295,"outputLines":325,"outputBytes":51065,"shownRange":{"start":1,"end":325},"nextOffset":326}}},"isError":false,"timestamp":1784087179768}}
{"type":"message","id":"b9007bf1","parentId":"b3a3e850","timestamp":"2026-07-15T03:46:24.164Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Requesting rest requirements from 398 onward**"},{"type":"toolCall","id":"call_1GQTqKeyPYFV8Khu2TKCypGz|fc_0ca4af36969c80fa016a57028f52c481989178e0b0bc62072f","name":"grep","arguments":{"i":"Listing later requirements","pattern":"^id = |^required_stages = ","path":"traceable-reqs.toml","selector":"398-500","case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14761,"output":75,"cacheRead":142848,"cacheWrite":0,"totalTokens":157684,"reasoningTokens":12,"cost":{"input":0.07380500000000001,"output":0.0022500000000000003,"cacheRead":0.071424,"cacheWrite":0,"total":0.14747900000000003}},"stopReason":"toolUse","timestamp":1784087179781,"responseId":"resp_0ca4af36969c80fa016a57028be19881988febb7e0673faa0a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a57028eb354819888798ec4146de8e9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKPwCo7mM1YvLpGKrbKnB7eL4uYk-Jjhz-m2R7lNE95NKlaoAo9BohF8Gkf4mKCSMvRrSJrUJK0BMSAjRwPE_9VhtXZ3VQxf8dXrZHaDwiLCJI2cDgolcs6BStbCeIwXLn0-d57Rmn9MQTTAYdODVbL-ivUs_b3P3_8R9bGQ6Py49N2UM2nKNZSaYKBS9fmuHKiomjg5MGLZwwqFJR2_nLmePlfNoSCodvh8PN2vs-h6riDG5KxoXqESHMQLmaYygc3d_30UoGbNwl55YUfqKdy08F4cDhw4-xfsyrjPxJPW-DBGivfZgl3p7xMT-4HID8lc8MiBiN8M8DxYEUZvE8m9A7FueXLWtsjE36Tsa9lc0ilTAlNBZJwiBlpqjPRezor79dJeN3gkpZZ8d2pUDE8d-hw_7GpTPKVeSDkip2OOnSgEOVqOkfHQlY7wBvy7QwpWfxg3XrPkPGITm8D2HuQLZwEwpbv9JFauQ0E8UvBw978GIcROFMEzPqJIElN4vn_wBrdEVMzImlMDhIryaoAzdbFe262sAW0DYm0cfsoAQceDOk5xmSE46qmLLe7tl0RtYemCdjkQ-MKfgLLWTaQkVYNxXM7fkZ7DK7ZfCRSFwDU4h00pQ9inlMlVZw6d-QhOAGbXp8bSRj98jr2AM4dQMI55kAtRfwGlFi-0xCKl_TSLFxdRii9mHhbW13venzbISKZ03GyyXKMnacNFr3zLSQMlMUUm9f2Pk24PAd6dmZJng0QXH_fkxu5LcnMcVzBibf9_TG6iC5bspfcmYLwQzK6Ry5FtySYLY2UGCc_8LDdR6vswyzUsj3gtC1JFWDwwrko811Km-3DqqgLfxtn5Z5z1Zz1qwlyzS070PR6mMoA1G30-CIADlePX5NDxVgKnRP6huJVG01G1eajT98TkTONkifpSYiKucBVl-brO9DIBh_FazIJgLt23JNY-wMKWuD2AGQJ-YDhvfhET7Juzg==","summary":[{"type":"summary_text","text":"**Requesting rest requirements from 398 onward**"}]},{"id":"fc_0ca4af36969c80fa016a57028f52c481989178e0b0bc62072f","type":"function_call","status":"completed","arguments":"{\"i\":\"Listing later requirements\",\"pattern\":\"^id = |^required_stages = \",\"path\":\"traceable-reqs.toml\",\"selector\":\"398-500\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_1GQTqKeyPYFV8Khu2TKCypGz","name":"grep"}]},"duration":4382.574900000007,"ttft":2930.1071999999695,"contextSnapshot":{"promptTokens":157609,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_1GQTqKeyPYFV8Khu2TKCypGz|fc_0ca4af36969c80fa016a57028f52c481989178e0b0bc62072f","toolName":"grep","startedAt":"2026-07-15T03:46:24.164Z","args":{"path":"traceable-reqs.toml"},"intent":"Listing later requirements"},"id":"312d00fe","parentId":"b9007bf1","timestamp":"2026-07-15T03:46:24.164Z"}
{"type":"message","id":"1aabee26","parentId":"312d00fe","timestamp":"2026-07-15T03:46:24.168Z","message":{"role":"toolResult","toolCallId":"call_1GQTqKeyPYFV8Khu2TKCypGz|fc_0ca4af36969c80fa016a57028f52c481989178e0b0bc62072f","toolName":"grep","content":[{"type":"text","text":"*398|id = \"REQ-HAZARD-CHECKPOINT-CLEAR-RACE\"\n 399|title = \"The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\\\"checkpoint\\\":\\\"v1\\\",…} envelope stashes the wa...\n*400|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-03 (checkpoint-clear-race fix, CHECKPOINT-CLEAR-RACE-PLAN.md). doc: docs/KNOWN-HAZARDS.md entry (the ordering invariant + the accepted orphan-wake residual). impl: tools/claude-spt/src/translate.rs — commands_for_clear (ARM emit) + commands_for_wake (FIRE emit) replace commands_for_checkpoint, is_checkpoint_fire parses {\\\"checkpoint_fire\\\":\\\"v1\\\"}, dispatch_event threads pending_wake (arm stores wake+emits clear-only; fire d...\n 401|\n 402|[[requirements]]\n*403|id = \"REQ-HAZARD-EMPTY-RESPONSE-COMMIT\"\n 404|title = \"EVERY event-typed delivery the translation binary answers must terminate with a {commit} — including deliveries it deliberately answers with no keystrokes (a checkpoint_fire with nothing armed; an event without an envelope). An event answered with ZERO records leaves the broker's inject sequence unterminated → the 5s INJECT_COMMIT_DEADLINE expires → TRANSLATION_FAULT terminates the binary (pinned 2026-07-04 via doyle's 3-discriminant collab, iso wtrace stderr: fault follows the DUPLICATE f...\n*405|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-04 (v0.13.1, the (B) zero-record pin); int ACTIVATED 2026-07-04 (core C-1 landed in spt-core 0.24.0). doc: docs/KNOWN-HAZARDS.md §2.7. impl: tools/claude-spt/src/translate.rs — dispatch_event's unarmed-fire branch + dispatch_line's event-without-envelope branch each answer vec![{commit:true}] instead of Vec::new(). unit: fire_with_no_armed_wake_answers_a_bare_commit + event_without_envelope_answers_a_bare_commit + non_event...\n 406|\n 407|[[requirements]]\n*408|id = \"REQ-DIST-BOUNDARY-RENAME\"\n 409|title = \"Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \\\"<id> @ <node> (<project>/)\\\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\\\"rename\\\":\\\"...\n*410|required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-05 (operator ask, grill-with-docs; v0.14.1). impl: tools/claude-spt/src/launch.rs — SPT_SESSION_NAME env on the spawned Command (both unix exec + windows spawn, same display_name computation as the -n argv element); tools/claude-spt/src/hook.rs — rename_payload helper + the clear-boundary rename self-send (before checkpoint_fire) + the RENAME_SKIP:no-name loud-skip path; tools/claude-spt/src/translate.rs — rename_directive (par...\n 411|\n 412|[[requirements]]\n*413|id = \"REQ-DIST-WORKER-LIFECYCLE\"\n 414|title = \"Subagent working perches follow the WORKER-TRUTH wave-1 contract (spt-core v0.27.0, doyle freeze 2026-07-06) and NEVER leak silently. SubagentStart (parent context) calls `api worker-start <parent> --session-id <parent sid> --agent-id <cc agent id> --agent-type <type>` — NO worker-id positional (core hard-mints `{parent}-w{N}`), NO token (operator ruling: sid-symmetric auth, registration stores the sid, stop accepts stored OR parent-current sid — /clear rotation covered from both ends). The ...\n*415|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-06 (WORKER-TRUTH wave-1 adapter migration, v0.15.0; supersedes the worker-* legs of REQ-DIST-HOOKS-API's original shape). impl: tools/claude-spt/src/hook.rs — handle_subagent_start (strict worker-start, minted-id stdout capture, loud-refusal/loud-empty, state persist via worker_state_rel) + handle_subagent_stop (state lookup, strict worker-stop, loud-refusal/loud-missing-mapping, state clear) + worker_state_rel (sanitized state path, sid_...\n 416|\n 417|[[requirements]]\n*418|id = \"REQ-HISTORY-FETCHER\"\n 419|title = \"The [history] transcript-access seam (the echo-commune's feeder — 'stays opaque + single-session and feeds the echo-commune verbatim', published contract) is declared strategy=\\\"fetcher\\\": `claude-spt history --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates the CC JSONL transcript with the SAME locate/precedence machinery as the [digest] fetcher (--config-dir read-var fill -> $CLAUDE_CONFIG_DIR env -> ~/.claude/projects; depth-1 then bounded-recursive slug sweep) and streams th...\n*420|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md — field-forced by flynn's psyche_host_error on core 0.28.0). impl: tools/claude-spt/src/history.rs (locate reuse from digest.rs, verbatim stream, literal-brace guard, loud-fail). unit: history.rs in-module tests (locate hit streams verbatim, missing transcript is loud exit-1, literal {CLAUDE_CONFIG_DIR} treated as absent, missing --session is loud). int DEFERRED: core-driven echo-commune round-trip on-nod...\n 421|\n 422|[[requirements]]\n*423|id = \"REQ-SESSION-ECHO-COMMUNE\"\n 424|title = \"The [session.echo_commune] bounded-summarizer role (published contract: 'when a session ends without a signoff, spt-core runs a bounded summarizer over the session's history so the context delta is captured anyway'; core 0.27/0.28 also spawns it from the commune-sync per-event turn — flynn 2026-07-07 field pin) is declared and shipped: `claude-spt echo-commune --id {id} --session-id {session_id}` accepts history on STDIN when fed (kept for the future published wiring — doyle field pin 2026-0...\n*425|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md); REVISED same day at v0.15.4 (doyle RCA: read_env rung + ccs sweep + graceful locate-miss). impl: tools/claude-spt/src/echo_commune.rs (stdin-or-locate input plan, read_env_config_dir/owlery_info_path/ccs_instance_roots/self_locate rungs, no_transcript_delta graceful miss, bounded tail, one claude -p turn) + tools/claude-spt/src/hook.rs SPT_ECHO_COMMUNE bail + adapter/claude-spt.toml [session.echo_commune]...\n 426|\n 427|[[requirements]]\n*428|id = \"REQ-DIST-REACHABILITY-NOTICE\"\n 429|title = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when...\n*430|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure ...\n 431|\n 432|[[requirements]]\n*433|id = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\n 434|title = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped)...\n*435|required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-cover...\n 436|\n 437|[[requirements]]\n*438|id = \"REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE\"\n 439|title = \"Every PreToolUse emission rides the {\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"additionalContext\\\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2...\n*440|required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.4 urgent fix, doyle RCA screenshot). doc: docs/KNOWN-HAZARDS.md §2.8. impl: tools/claude-spt/src/hook.rs — wrap_pretool_context + emit_capped_pretool wired in handle_pre_tool_use. unit: pretool_envelope_shape_and_escaping (round-trip through serde) + pretool_overflow_spills_raw_and_wraps_the_pointer (wrapped-size cap decision) + pre_tool_use_busy_before_drain (envelope on the drain path) + pre_tool_use_agent_spawn_gets_t...\n 441|\n 442|[[requirements]]\n*443|id = \"REQ-SKILL-LIVE-SPT-HOSTED-BRANCH\"\n 444|title = \"The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in...\n*445|required_stages = [\"doc\"]  # MINTED + ACTIVATED 2026-07-07 (adapter-debts work order; ADAPTER-DEBTS or the session commune). doc: adapter/strings/skills/live.md (the spt-hosted branch in Bringup + the two failure branches) + adapter/strings/skills/ready.md (the matching note). No impl/unit: these are agent-facing prose strings the hook only file-backs + resolves (same class as REQ-DIST-SESSIONSTART-BRIEF's string bodies); the branch is a documentation contract, not new binary logic. Source: flynn's Monit...\n 446|\n 447|[[requirements]]\n*448|id = \"REQ-SUPERVISOR-RELAY-RESPAWN\"\n 449|title = \"DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-res...\n*450|required_stages = []  # MINTED INACTIVE 2026-07-07 (adapter-debts work order). Design/doc candidate only — stays required_stages = [] (activation-model: never pre-fail a requirement not yet in build) until doyle's W4 supervisor-watchdog activation ruling assigns the core-vs-adapter boundary. Then activate with the real stages (doc the belt design → impl the relay-respawn/death-surfacing → unit → int the dead-relay recovery). Source: live-perch-monitor-reachability memory (a dead Monitor + trustin...\n 451|\n 452|[[requirements]]\n*453|id = \"REQ-HAZARD-RESUME-REBIND-WEDGE\"\n 454|title = \"CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ON...\n*455|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator /resume hunch; RESUME-REBIND-WEDGE, v0.15.7). impl: tools/claude-spt/src/hook.rs — handle_session_start bind branch (prior-sid proof from sid_state_rel state, prior!=sid guard, spt_strict + loud bind-fail log replacing the exit-ignored env.spt). unit: hook.rs — resume_rebind_presents_prior_proof (source=resume + state=s-old + sid=s-new → bind ... --set-session-id s-new --session-id s-old) + fresh_bind_has_no_proof_and_is_...\n 456|\n 457|[[requirements]]\n*458|id = \"REQ-INJECT-VERIFY-ECHO\"\n 459|title = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapte...\n*460|required_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC ses...\n 461|\n 462|[[requirements]]\n*463|id = \"REQ-HAZARD-STUCK-ACTIVE-NO-IDLE\"\n 464|title = \"A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkp...\n*465|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator /diagnose of lia; v0.15.9). impl: tools/claude-spt/src/hook.rs — the bind-branch `state idle` at bringup + handle_notification (idle_prompt self-heal, $SPT_ENDPOINT_ID gate, sid-required) + the Notification dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json Notification→dispatch.sh with the idle_prompt matcher. unit: hook.rs startup_bind_marks_idle_for_immediate_receivability + notification_idle_prompt_marks_...\n 466|\n 467|[[requirements]]\n*468|id = \"REQ-HAZARD-STOPFAILURE-STUCK-BUSY\"\n 469|title = \"An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO...\n*470|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1 fix 1; receive-heal). impl: tools/claude-spt/src/hook.rs handle_stop_failure + the StopFailure dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json StopFailure→dispatch.sh (matcherless — all error types). unit: hook.rs stop_failure_marks_idle_when_perched_else_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (StopFailure in the lockstep list) + tests/hooks-dispatch.sh (StopFailure wir...\n 471|\n 472|[[requirements]]\n*473|id = \"REQ-HAZARD-INTERRUPT-STUCK-BUSY\"\n 474|title = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is...\n*475|required_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/se...\n 476|\n 477|[[requirements]]\n*478|id = \"REQ-OMP-NATIVE-TUI\"\n 479|title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"traceable-reqs.toml","searchPath":"C:/Users/decid/Documents/projects/omp-spt/traceable-reqs.toml","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":34,"fileCount":1,"files":["traceable-reqs.toml"],"fileMatches":[{"path":"traceable-reqs.toml","count":34}],"truncated":true,"displayContent":"*398│id = \"REQ-HAZARD-CHECKPOINT-CLEAR-RACE\"\n 399│title = \"The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\\\"checkpoint\\\":\\\"v1\\\",…} envelope stashes the wa...\n*400│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-03 (checkpoint-clear-race fix, CHECKPOINT-CLEAR-RACE-PLAN.md). doc: docs/KNOWN-HAZARDS.md entry (the ordering invariant + the accepted orphan-wake residual). impl: tools/claude-spt/src/translate.rs — commands_for_clear (ARM emit) + commands_for_wake (FIRE emit) replace commands_for_checkpoint, is_checkpoint_fire parses {\\\"checkpoint_fire\\\":\\\"v1\\\"}, dispatch_event threads pending_wake (arm stores wake+emits clear-only; fire d...\n 401│\n 402│[[requirements]]\n*403│id = \"REQ-HAZARD-EMPTY-RESPONSE-COMMIT\"\n 404│title = \"EVERY event-typed delivery the translation binary answers must terminate with a {commit} — including deliveries it deliberately answers with no keystrokes (a checkpoint_fire with nothing armed; an event without an envelope). An event answered with ZERO records leaves the broker's inject sequence unterminated → the 5s INJECT_COMMIT_DEADLINE expires → TRANSLATION_FAULT terminates the binary (pinned 2026-07-04 via doyle's 3-discriminant collab, iso wtrace stderr: fault follows the DUPLICATE f...\n*405│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-04 (v0.13.1, the (B) zero-record pin); int ACTIVATED 2026-07-04 (core C-1 landed in spt-core 0.24.0). doc: docs/KNOWN-HAZARDS.md §2.7. impl: tools/claude-spt/src/translate.rs — dispatch_event's unarmed-fire branch + dispatch_line's event-without-envelope branch each answer vec![{commit:true}] instead of Vec::new(). unit: fire_with_no_armed_wake_answers_a_bare_commit + event_without_envelope_answers_a_bare_commit + non_event...\n 406│\n 407│[[requirements]]\n*408│id = \"REQ-DIST-BOUNDARY-RENAME\"\n 409│title = \"Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \\\"<id> @ <node> (<project>/)\\\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\\\"rename\\\":\\\"...\n*410│required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-05 (operator ask, grill-with-docs; v0.14.1). impl: tools/claude-spt/src/launch.rs — SPT_SESSION_NAME env on the spawned Command (both unix exec + windows spawn, same display_name computation as the -n argv element); tools/claude-spt/src/hook.rs — rename_payload helper + the clear-boundary rename self-send (before checkpoint_fire) + the RENAME_SKIP:no-name loud-skip path; tools/claude-spt/src/translate.rs — rename_directive (par...\n 411│\n 412│[[requirements]]\n*413│id = \"REQ-DIST-WORKER-LIFECYCLE\"\n 414│title = \"Subagent working perches follow the WORKER-TRUTH wave-1 contract (spt-core v0.27.0, doyle freeze 2026-07-06) and NEVER leak silently. SubagentStart (parent context) calls `api worker-start <parent> --session-id <parent sid> --agent-id <cc agent id> --agent-type <type>` — NO worker-id positional (core hard-mints `{parent}-w{N}`), NO token (operator ruling: sid-symmetric auth, registration stores the sid, stop accepts stored OR parent-current sid — /clear rotation covered from both ends). The ...\n*415│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-06 (WORKER-TRUTH wave-1 adapter migration, v0.15.0; supersedes the worker-* legs of REQ-DIST-HOOKS-API's original shape). impl: tools/claude-spt/src/hook.rs — handle_subagent_start (strict worker-start, minted-id stdout capture, loud-refusal/loud-empty, state persist via worker_state_rel) + handle_subagent_stop (state lookup, strict worker-stop, loud-refusal/loud-missing-mapping, state clear) + worker_state_rel (sanitized state path, sid_...\n 416│\n 417│[[requirements]]\n*418│id = \"REQ-HISTORY-FETCHER\"\n 419│title = \"The [history] transcript-access seam (the echo-commune's feeder — 'stays opaque + single-session and feeds the echo-commune verbatim', published contract) is declared strategy=\\\"fetcher\\\": `claude-spt history --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates the CC JSONL transcript with the SAME locate/precedence machinery as the [digest] fetcher (--config-dir read-var fill -> $CLAUDE_CONFIG_DIR env -> ~/.claude/projects; depth-1 then bounded-recursive slug sweep) and streams th...\n*420│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md — field-forced by flynn's psyche_host_error on core 0.28.0). impl: tools/claude-spt/src/history.rs (locate reuse from digest.rs, verbatim stream, literal-brace guard, loud-fail). unit: history.rs in-module tests (locate hit streams verbatim, missing transcript is loud exit-1, literal {CLAUDE_CONFIG_DIR} treated as absent, missing --session is loud). int DEFERRED: core-driven echo-commune round-trip on-nod...\n 421│\n 422│[[requirements]]\n*423│id = \"REQ-SESSION-ECHO-COMMUNE\"\n 424│title = \"The [session.echo_commune] bounded-summarizer role (published contract: 'when a session ends without a signoff, spt-core runs a bounded summarizer over the session's history so the context delta is captured anyway'; core 0.27/0.28 also spawns it from the commune-sync per-event turn — flynn 2026-07-07 field pin) is declared and shipped: `claude-spt echo-commune --id {id} --session-id {session_id}` accepts history on STDIN when fed (kept for the future published wiring — doyle field pin 2026-0...\n*425│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.2, ECHO-COMMUNE-PLAN.md); REVISED same day at v0.15.4 (doyle RCA: read_env rung + ccs sweep + graceful locate-miss). impl: tools/claude-spt/src/echo_commune.rs (stdin-or-locate input plan, read_env_config_dir/owlery_info_path/ccs_instance_roots/self_locate rungs, no_transcript_delta graceful miss, bounded tail, one claude -p turn) + tools/claude-spt/src/hook.rs SPT_ECHO_COMMUNE bail + adapter/claude-spt.toml [session.echo_commune]...\n 426│\n 427│[[requirements]]\n*428│id = \"REQ-DIST-REACHABILITY-NOTICE\"\n 429│title = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when...\n*430│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure ...\n 431│\n 432│[[requirements]]\n*433│id = \"REQ-HAZARD-DANGLING-FRAME-LOUD\"\n 434│title = \"A poll frame truncated mid-frame is NEVER dropped silently by render_frames. The original loop break'd on a dangling frame (opening <EVENT with no '>', or a body with no closing </EVENT>) — the message was taken from the spool (delivered=1) but nothing surfaced to the agent: the §2.8 silent-loss family, one seam upstream (framing instead of stdout channel). Hardening invariant: the dangling-frame arm emits a loud <sptc_frame_integrity> marker carrying the surviving partial content (unescaped)...\n*435│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (perri code-walk during seed-#9 seam exoneration; doyle ship-it ACK; TRUNCATION-PROBE-PLAN.md). doc: docs/KNOWN-HAZARDS.md §2.9. impl: tools/claude-spt/src/hook.rs — dangling-frame arm in render_frames. unit: render_frames dangling-frame cases (no-close body, mangled opening, partial-content survival, complete-frames-then-dangling mix, no marker on clean input). int: not planned — pure string transform under already-int-cover...\n 436│\n 437│[[requirements]]\n*438│id = \"REQ-HAZARD-PRETOOL-CONTEXT-ENVELOPE\"\n 439│title = \"Every PreToolUse emission rides the {\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PreToolUse\\\",\\\"additionalContext\\\":...}} JSON envelope — NEVER raw stdout. CC discards plain PreToolUse stdout (unlike UserPromptSubmit, whose raw stdout IS the context channel), so the F-021 mid-turn delivery leg's raw emission made every message drained during a busy turn a SILENT BLACK HOLE: taken from the spool (delivered=1, core honest) then thrown away by CC — invisible to agent, sender, and spool (field 2...\n*440│required_stages = [\"doc\", \"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (v0.15.4 urgent fix, doyle RCA screenshot). doc: docs/KNOWN-HAZARDS.md §2.8. impl: tools/claude-spt/src/hook.rs — wrap_pretool_context + emit_capped_pretool wired in handle_pre_tool_use. unit: pretool_envelope_shape_and_escaping (round-trip through serde) + pretool_overflow_spills_raw_and_wraps_the_pointer (wrapped-size cap decision) + pre_tool_use_busy_before_drain (envelope on the drain path) + pre_tool_use_agent_spawn_gets_t...\n 441│\n 442│[[requirements]]\n*443│id = \"REQ-SKILL-LIVE-SPT-HOSTED-BRANCH\"\n 444│title = \"The /sptc:live bringup instructions BRANCH on the session's delivery substrate: a NORMAL (operator-launched) CC session arms the step-2 Monitor `spt api listen <id>` resident relay as its single delivery pipe; an SPT-HOSTED session (daemon-launched via `spt endpoint run`, broker-delivered — recognized by the SessionStart identity brief already present + the perch bound before bringup) must NOT arm that Monitor listener. Field ground truth (flynn 2026-07-07, live-perch-monitor-reachability): in...\n*445│required_stages = [\"doc\"]  # MINTED + ACTIVATED 2026-07-07 (adapter-debts work order; ADAPTER-DEBTS or the session commune). doc: adapter/strings/skills/live.md (the spt-hosted branch in Bringup + the two failure branches) + adapter/strings/skills/ready.md (the matching note). No impl/unit: these are agent-facing prose strings the hook only file-backs + resolves (same class as REQ-DIST-SESSIONSTART-BRIEF's string bodies); the branch is a documentation contract, not new binary logic. Source: flynn's Monit...\n 446│\n 447│[[requirements]]\n*448│id = \"REQ-SUPERVISOR-RELAY-RESPAWN\"\n 449│title = \"DESIGN CANDIDATE (not yet implemented): the adapter's belt-and-suspenders answer to a dead in-session relay — when a live agent's resident Monitor relay dies mid-session (the delivery pipe silently stops), SOMETHING must notice and either respawn it or surface the death, so the agent does not go silently unreachable while appearing online. This is the ADAPTER half that folds into doyle's W4 supervisor-watchdog activation ruling (the core owns the watchdog itself; the adapter owns the relay-res...\n*450│required_stages = []  # MINTED INACTIVE 2026-07-07 (adapter-debts work order). Design/doc candidate only — stays required_stages = [] (activation-model: never pre-fail a requirement not yet in build) until doyle's W4 supervisor-watchdog activation ruling assigns the core-vs-adapter boundary. Then activate with the real stages (doc the belt design → impl the relay-respawn/death-surfacing → unit → int the dead-relay recovery). Source: live-perch-monitor-reachability memory (a dead Monitor + trustin...\n 451│\n 452│[[requirements]]\n*453│id = \"REQ-HAZARD-RESUME-REBIND-WEDGE\"\n 454│title = \"CC's in-TUI /resume must NEVER silently strand an spt-hosted endpoint's perch. Mechanism (operator hunch, perri-confirmed 2026-07-07 across our hook.rs + the CC SessionStart hook contract + the live `spt api boundary/bind` CLI contract): the /resume slash command ends the current session (SessionEnd reason=resume) and re-fires SessionStart(source=resume) in the SAME broker child process, so $SPT_ENDPOINT_ID is STILL set — the hook takes the spt-hosted registration path. register_verb routes ON...\n*455│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator /resume hunch; RESUME-REBIND-WEDGE, v0.15.7). impl: tools/claude-spt/src/hook.rs — handle_session_start bind branch (prior-sid proof from sid_state_rel state, prior!=sid guard, spt_strict + loud bind-fail log replacing the exit-ignored env.spt). unit: hook.rs — resume_rebind_presents_prior_proof (source=resume + state=s-old + sid=s-new → bind ... --set-session-id s-new --session-id s-old) + fresh_bind_has_no_proof_and_is_...\n 456│\n 457│[[requirements]]\n*458│id = \"REQ-INJECT-VERIFY-ECHO\"\n 459│title = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapte...\n*460│required_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC ses...\n 461│\n 462│[[requirements]]\n*463│id = \"REQ-HAZARD-STUCK-ACTIVE-NO-IDLE\"\n 464│title = \"A freshly-launched spt-hosted claude-spt endpoint that has never completed a turn must NOT silently black-hole every inbound message. The daemon's idle-gated delivery pushes to the translation binary only when the endpoint is IDLE; while ACTIVE it defers each message to the spool ('ENDPOINT_INJECT:<id>: endpoint ACTIVE -> spool, not injected') and drains only on the NEXT idle transition (IDLE_PARKED_DRAIN). The adapter marked `state idle` ONLY on Stop (turn end), a `/clear` boundary, or a checkp...\n*465│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator /diagnose of lia; v0.15.9). impl: tools/claude-spt/src/hook.rs — the bind-branch `state idle` at bringup + handle_notification (idle_prompt self-heal, $SPT_ENDPOINT_ID gate, sid-required) + the Notification dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json Notification→dispatch.sh with the idle_prompt matcher. unit: hook.rs startup_bind_marks_idle_for_immediate_receivability + notification_idle_prompt_marks_...\n 466│\n 467│[[requirements]]\n*468│id = \"REQ-HAZARD-STOPFAILURE-STUCK-BUSY\"\n 469│title = \"An API-error turn-end must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. CC fires NO `Stop` when a turn ends with an API error (matcher error types: rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown) — it fires `StopFailure` instead. The adapter marked idle ONLY on `Stop`, so an errored turn-end never got its idle mark and the daemon spooled every inbound without draining (same black-hole class as REQ-HAZARD-STUCK-ACTIVE-NO...\n*470│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1 fix 1; receive-heal). impl: tools/claude-spt/src/hook.rs handle_stop_failure + the StopFailure dispatch arm + CC_HOOK_EVENTS entry; plugin/sptc/hooks/hooks.json StopFailure→dispatch.sh (matcherless — all error types). unit: hook.rs stop_failure_marks_idle_when_perched_else_noop + cc_hook_events_recognizer_matches_the_dispatch_arms (StopFailure in the lockstep list) + tests/hooks-dispatch.sh (StopFailure wir...\n 471│\n 472│[[requirements]]\n*473│id = \"REQ-HAZARD-INTERRUPT-STUCK-BUSY\"\n 474│title = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is...\n*475│required_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/se...\n 476│\n 477│[[requirements]]\n*478│id = \"REQ-OMP-NATIVE-TUI\"\n 479│title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087184167}}
{"type":"message","id":"355d6e64","parentId":"1aabee26","timestamp":"2026-07-15T03:46:37.841Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing requirement and trace inconsistencies**\n\n**Planning OMP-specific test and docs review**"},{"type":"toolCall","id":"call_HLKQh59YXHhgV186PHHZwTw5|fc_0ca4af36969c80fa016a57029db9ac81989c2088c6242fe954","name":"read","arguments":{"i":"Reading bridge unit coverage","path":"tools/claude-spt/src/bridge.rs","selector":"620-1017"}},{"type":"toolCall","id":"call_FTtv6BgI4ZnQYBh800p7BXUb|fc_0ca4af36969c80fa016a57029db9c08198aedbc7962064de09","name":"grep","arguments":{"i":"Tracing field issue closure","pattern":"HIGH-[1-4]|MEDIUM-[1-4]|MED-[1-4]|LOW-[1-2]|orphan|JobObject|kill_on_close|timeout|heartbeat|steer|follow_up|session-end|unbind|switch","path":"tools/claude-spt/src/bridge.rs;tests/bridge-acceptance.sh;adapter/omp-spt.toml;traceable-reqs.toml","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_laCTURXa8Kif1TllFqXEzrMq|fc_0ca4af36969c80fa016a57029db9cc81989359b5b3f7a5ff52","name":"grep","arguments":{"i":"Finding OMP test intent","pattern":"omp|OMP|bridge|native TUI|extension|digest-omp|psyche-omp|history-omp|echo-commune-omp","path":"tests;ci","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_xG4wC1g9XW6Vm6R6JfcvUbVW|fc_0ca4af36969c80fa016a57029db9d48198b1856d015dcdd780","name":"grep","arguments":{"i":"Finding obsolete path claims","pattern":"adapter/claude-spt\\.toml|plugin/sptc|claude-spt-psyche|claude-spt-digest|tools/claude-spt-psyche|tools/claude-spt-digest|cc-spt-idle-translate|SaberMage/claude-spt|sptc-ci","path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_f7xWRVmO7jvjeuM3B3Gz4PRF|fc_0ca4af36969c80fa016a57029db9e08198a4d524ff289d24d9","name":"glob","arguments":{"i":"Mapping OMP plugin","path":"plugin/omps/**/*;adapter/strings/**/*","hidden":true,"gitignore":true,"limit":300}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5490,"output":668,"cacheRead":157184,"cacheWrite":0,"totalTokens":163342,"reasoningTokens":207,"cost":{"input":0.027450000000000002,"output":0.02004,"cacheRead":0.078592,"cacheWrite":0,"total":0.126082}},"stopReason":"toolUse","timestamp":1784087184171,"responseId":"resp_0ca4af36969c80fa016a57029041cc81988cd3210d88b29086","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a570291a8fc8198a4807e7b2e8dea3d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKdjcvg2G5C_3S3feEc0VqCAflHHhcIfBMzgGcqKsmXvq5ZRJzf_ugxwm2XIKC2OSFB1sl4Dha7EhgSfVHkXK9qltfoC1swItTinNWwNAjpyjWhgvm4NsM4t6vESiwdQV9TIv1b6TKfBLuYozIuYTDUofiUTKxQ7ZSqUEMPEZFAWMfFkdRb4qo8PNR8FTOZx8XTU0C6cPGL3Spyeib6JldBW6X2g9_sjkYnrk7h0U7bx-oRQOtFntGz2kUNdoKm8Nt2iAcZ1DxoNTSKXj8XMgqIbezDWduJarpm_9IpmqopSYyldACd7iGLW8bL4fYIh3XWCYvduwYlpqtorG-UGY0phXrKEPaV1r2B3OxXamybpPgSXAIQ4yREQpAuEICgeImbG_e7BftaHHd6AsvElZBx2H0AgSxNsIViXpzNs3EMVw3DuNMRCPT8Diw9Ao-vT-76ecVuGXDgltk6VebwhveFKfQ74-1If1BJUbJa4vl7db-bfQnruRPPQZcjyHerTbpCUjCzr6tyvw30GF6IgDUyl4FiHFrBgnPlOqDjHHNadvzFH63cV-o6tEy_PjxizuVrk8UgXU-bmpUWa1_ZsqgDPsSrvxFO89NgiAOy28mqbufSFoWxGH5SN3QYbXaYKjd6Z1_yGWgMHz2YyNFt9axD3asGD5T12j8cdQXY1ODXPnVmWHWCmL24QGGBVJqEQWx8hIIw88oJou--boT3DZeGSvNsw5T_HdKq5hRLKsSZ9EidVSF4HJwm5V7be7qyZ5G7rlXlj-eGNGIJe_xp7S7SC9V2g3DQ6-pCZXQx1Q_1t7SEAd8T3LKtLnbZ5BM2bIiRbSAeAMBz8HmdNtrqqA5tOS2glYYVrMV3qlV9A7cgc7SPYl2g3FywWrasvBNQWJglD2zG0GuBQdRrWArTBzLNm1cEXYSxZDURtkMg9e0oA8-MCPyx8b1EeGjYElV1K_e8EQlXHL-Ti3bC9OtdH_uEOOUwMSGluHXWsvnvbczsgUcCvAz9kJ4OWmz-uULbd0A0LLsOLz2c_ribXmUeNSUi3l3K0oZ4N2jRcL8yyL7ohciZXLo96XDHnYGnjiEbSpdiqAuNKTdfmXzmhIhChtqkkVqUsi4uHK8rpadT02k0rxk3Icoy8CCI5xABRmUydhGKAo8yJKGe7AQU6T2LXqS5y2QGLDO53Z0Ev5C31b6sKgH_Rq9ZUlEZMXsuFuAHdE5qGp3DlLJS-j83W0ajkfW9FTwIYBr8UNI_JFo5YdkyLYdj0Kkm2I7FiHppHl3s-u-NcdLPRnMBINTjurQYo6gC-FaTntByIrvLM63r4_wzvA6Mg4vuKtr8cqjPpKzZhYxryuUUXYCw1ENLw5T5j-pjBIwWfBiC2TwPxq3ZyGu1SHp4PAgrXfCAKymxIjE1iLJJtjHxZnyQkvm8o9gIzKljZSLUJxAidgQQCvcxYMe0ZTYXOrcdaeuGUjlAml76BsT-sRE2fRwcJuFzuGI_NjIFi1y1BnEREHhI1JwiAOKNhgaAcLM-oZ4RMbMp2ITWuk7Rlw2gUo45_wTK3Xj8DBB3ksQNiJmZ1sb8BcxfQME5TO31AuuSAP2c7feqaot7MPc5VNpS2B5KTRPdvWJCDOLwYv-VDX8ffhfpO11RhzD74LO7X0Ri0otbYjaEOwujgWVKOZaP2gY-_S9E7c6-CS60tfGGN6kf_2SL_N1S3pDjHZesJICkDsOUTTQCrCcyPV5IozJnt14HHmQZsWPlnUNQsDzMp4n6B7rhVDYPs7P0wHeUi815_VIcd3-zdg1YLJMvX3kyQ-sjNlbunm0c5vV1tXZiXekE3TXhcx8mog_ogqvngSdVrKRf5quTN8g6TRcaxYiaogef7sY4EP1qae7BCDC1vpkPg4ZN9ufI7R4h1XtxLMfPdXv7KKyY4rhnki6jmDA9beO51sh3Sq1dZhXrqWSaguGGvCGnDqcB4arnOK6QzSy6MraVhtqZURpPFvFL5zNhpaFpZxT-9clEx0-YQ1AccIlt5-8kX51IbaUCxeyvGBQxsQpL_fd2HsWqqGDY6hnQTaH785VkfAK1A97Q69ytzCyFbXA50qryxXVXoOqZac15YtNBke9J84ekhV6xoNjLsPhrDlcoWSWUmRiXLOI7M0aL6NJahGvh1AxRe8HsX_hKzjdK19A0QsfHwqhbyqk3km8LVBrCk74V_CKfbV6dBFDw4zj0_IbH0Ft8nPA=","summary":[{"type":"summary_text","text":"**Assessing requirement and trace inconsistencies**"},{"type":"summary_text","text":"**Planning OMP-specific test and docs review**"}]},{"id":"fc_0ca4af36969c80fa016a57029db9ac81989c2088c6242fe954","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading bridge unit coverage\",\"path\":\"tools/claude-spt/src/bridge.rs\",\"selector\":\"620-1017\"}","call_id":"call_HLKQh59YXHhgV186PHHZwTw5","name":"read"},{"id":"fc_0ca4af36969c80fa016a57029db9c08198aedbc7962064de09","type":"function_call","status":"completed","arguments":"{\"i\":\"Tracing field issue closure\",\"pattern\":\"HIGH-[1-4]|MEDIUM-[1-4]|MED-[1-4]|LOW-[1-2]|orphan|JobObject|kill_on_close|timeout|heartbeat|steer|follow_up|session-end|unbind|switch\",\"path\":\"tools/claude-spt/src/bridge.rs;tests/bridge-acceptance.sh;adapter/omp-spt.toml;traceable-reqs.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_FTtv6BgI4ZnQYBh800p7BXUb","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57029db9cc81989359b5b3f7a5ff52","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding OMP test intent\",\"pattern\":\"omp|OMP|bridge|native TUI|extension|digest-omp|psyche-omp|history-omp|echo-commune-omp\",\"path\":\"tests;ci\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_laCTURXa8Kif1TllFqXEzrMq","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57029db9d48198b1856d015dcdd780","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding obsolete path claims\",\"pattern\":\"adapter/claude-spt\\\\.toml|plugin/sptc|claude-spt-psyche|claude-spt-digest|tools/claude-spt-psyche|tools/claude-spt-digest|cc-spt-idle-translate|SaberMage/claude-spt|sptc-ci\",\"path\":\"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_xG4wC1g9XW6Vm6R6JfcvUbVW","name":"grep"},{"id":"fc_0ca4af36969c80fa016a57029db9e08198a4d524ff289d24d9","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping OMP plugin\",\"path\":\"plugin/omps/**/*;adapter/strings/**/*\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_f7xWRVmO7jvjeuM3B3Gz4PRF","name":"glob"}]},"duration":13652.446999999927,"ttft":1498.9498999998905,"contextSnapshot":{"promptTokens":162674,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HLKQh59YXHhgV186PHHZwTw5|fc_0ca4af36969c80fa016a57029db9ac81989c2088c6242fe954","toolName":"read","startedAt":"2026-07-15T03:46:37.841Z","args":{"path":"tools/claude-spt/src/bridge.rs"},"intent":"Reading bridge unit coverage"},"id":"c0dd6de9","parentId":"355d6e64","timestamp":"2026-07-15T03:46:37.841Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FTtv6BgI4ZnQYBh800p7BXUb|fc_0ca4af36969c80fa016a57029db9c08198aedbc7962064de09","toolName":"grep","startedAt":"2026-07-15T03:46:37.841Z","args":{"path":"tools/claude-spt/src/bridge.rs;tests/bridge-acceptance.sh;adapter/omp-spt.toml;traceable-reqs.toml"},"intent":"Tracing field issue closure"},"id":"7eafd492","parentId":"c0dd6de9","timestamp":"2026-07-15T03:46:37.841Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_laCTURXa8Kif1TllFqXEzrMq|fc_0ca4af36969c80fa016a57029db9cc81989359b5b3f7a5ff52","toolName":"grep","startedAt":"2026-07-15T03:46:37.841Z","args":{"path":"tests;ci"},"intent":"Finding OMP test intent"},"id":"f7242e93","parentId":"7eafd492","timestamp":"2026-07-15T03:46:37.841Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xG4wC1g9XW6Vm6R6JfcvUbVW|fc_0ca4af36969c80fa016a57029db9d48198b1856d015dcdd780","toolName":"grep","startedAt":"2026-07-15T03:46:37.841Z","args":{"path":"CONTEXT.md;SCOPE.md;OMP-ADAPTER-PLAN.md;docs/adr;docs/KNOWN-HAZARDS.md;docs/OMP-BRIDGE-FIELD-ISSUES.md;traceable-reqs.toml;tests"},"intent":"Finding obsolete path claims"},"id":"31382170","parentId":"f7242e93","timestamp":"2026-07-15T03:46:37.841Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_f7xWRVmO7jvjeuM3B3Gz4PRF|fc_0ca4af36969c80fa016a57029db9e08198a4d524ff289d24d9","toolName":"glob","startedAt":"2026-07-15T03:46:37.841Z","args":{"path":"plugin/omps/**/*;adapter/strings/**/*"},"intent":"Mapping OMP plugin"},"id":"23c6de64","parentId":"31382170","timestamp":"2026-07-15T03:46:37.841Z"}
{"type":"message","id":"1d8aa9e2","parentId":"23c6de64","timestamp":"2026-07-15T03:46:37.851Z","message":{"role":"toolResult","toolCallId":"call_f7xWRVmO7jvjeuM3B3Gz4PRF|fc_0ca4af36969c80fa016a57029db9e08198a4d524ff289d24d9","toolName":"glob","content":[{"type":"text","text":"# adapter/strings/\nomp-spt.mjs\n## skills/\nsetup.md\nsubnet.md\nversion.md\nforce-stop.md\nlist-agents.md\nlive.md\nready.md\n## briefs/\nmessaging-perch.md\nmessaging-no-perch.md\nidentity.md\nlive-ops.md\n# plugin/omps/\nbootstrap.ps1\nbootstrap.sh\n## skills/\n### version/\nSKILL.md\n### subnet/\nSKILL.md\n### signoff/\nSKILL.md\n### setup/\nSKILL.md\n### ready/\nSKILL.md\n### send/\nSKILL.md\n### live/\nSKILL.md\n### list-agents/\nSKILL.md\n### commune/\nSKILL.md\n### force-stop/\nSKILL.md\n## hooks/\ndispatch.sh\nhooks.json\n## .claude-plugin/\nplugin.json"}],"details":{"scopePath":"plugin/omps/**/*, adapter/strings/**/*","fileCount":42,"files":["adapter/strings/omp-spt.mjs","plugin/omps/skills/","plugin/omps/skills/version/","plugin/omps/skills/version/SKILL.md","plugin/omps/skills/subnet/","plugin/omps/skills/subnet/SKILL.md","plugin/omps/skills/signoff/","plugin/omps/skills/signoff/SKILL.md","plugin/omps/skills/setup/","plugin/omps/skills/setup/SKILL.md","plugin/omps/skills/ready/SKILL.md","plugin/omps/skills/send/","plugin/omps/skills/send/SKILL.md","plugin/omps/skills/live/SKILL.md","plugin/omps/skills/ready/","plugin/omps/skills/list-agents/SKILL.md","plugin/omps/skills/live/","plugin/omps/skills/commune/","plugin/omps/skills/commune/SKILL.md","plugin/omps/skills/force-stop/","plugin/omps/skills/force-stop/SKILL.md","plugin/omps/skills/list-agents/","plugin/omps/hooks/","plugin/omps/hooks/dispatch.sh","plugin/omps/hooks/hooks.json","plugin/omps/.claude-plugin/","plugin/omps/.claude-plugin/plugin.json","plugin/omps/bootstrap.ps1","plugin/omps/bootstrap.sh","adapter/strings/skills/","adapter/strings/skills/setup.md","adapter/strings/skills/subnet.md","adapter/strings/skills/version.md","adapter/strings/skills/force-stop.md","adapter/strings/skills/list-agents.md","adapter/strings/skills/live.md","adapter/strings/skills/ready.md","adapter/strings/briefs/messaging-perch.md","adapter/strings/briefs/","adapter/strings/briefs/messaging-no-perch.md","adapter/strings/briefs/identity.md","adapter/strings/briefs/live-ops.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt"},"isError":false,"timestamp":1784087197851}}
{"type":"message","id":"542009ea","parentId":"1d8aa9e2","timestamp":"2026-07-15T03:46:37.866Z","message":{"role":"toolResult","toolCallId":"call_laCTURXa8Kif1TllFqXEzrMq|fc_0ca4af36969c80fa016a57029db9cc81989359b5b3f7a5ff52","toolName":"grep","content":[{"type":"text","text":"# tests/\n## acceptance-harness.sh\n 28|[ -f \"$proj/.claude/hooks/ups-digest.sh\" ] && echo \"ok   scaffold writes UPS hook\" || { echo \"FAIL no UPS hook\"; fail=1; }\n*29|grep -q 'UserPromptSubmit' \"$proj/.claude/settings.json\" && echo \"ok   settings wires UserPromptSubmit\" || { echo \"FAIL UPS not wired\"; fail=1; }\n 30|# Fixture hook reads stdin, not a /-leading argv (KH 1.1 immunity).\n 31|grep -q 'input=$(cat)' \"$proj/.claude/hooks/ups-digest.sh\" && echo \"ok   fixture reads stdin (MSYS-safe)\" || { echo \"FAIL fixture not stdin-driven\"; fail=1; }\n 32|\n 33|# Fixture actually produces the digest marker when fed a real CC-shaped payload on stdin.\n*34|printf '%s' '{\"session_id\":\"x\",\"prompt\":\"hello-probe\"}' | sh \"$proj/.claude/hooks/ups-digest.sh\"\n 35|sptc_ci_assert \"fixture emits digest marker\" \"UPS_FIRED:hello-probe\" \"$digest\" >/dev/null \\\n 36|  && echo \"ok   fixture emits digest marker\" || { echo \"FAIL fixture no marker\"; fail=1; }\n 37|\n## adapter-archive.sh\n 13|LINUX_TRIPLE=x86_64-unknown-linux-gnu\n*14|BINS=\"omp-spt\"  # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)\n 15|rc=0\n 16|fail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n 17|\n## bridge-acceptance.sh\n 1|#!/bin/sh\n*2|# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n*3|# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n 4|#\n*5|# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n*6|# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n*7|# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n*8|#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n*9|#   2. the bridge sends a reply back to the sender (the bridge trace).\n 10|#\n 11|# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n*12|# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n*13|# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\n 14|set -u\n 15|\n*16|if [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n*17|  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n 18|  exit 0\n 19|fi\n 20|\n...\n 25|# ── Resolve binaries ─────────────────────────────────────────────────────────\n*26|# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n*27|( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n 28|  || { echo \"FAIL cargo build\"; exit 1; }\n 29|exe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\n*30|BIN=$(exe \"$CRATE/target/release/omp-spt\")\n*31|FAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n*32|# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\n 33|if command -v spt >/dev/null 2>&1; then SPT=spt\n 34|elif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\n 35|else echo \"FAIL spt binary not found\"; exit 1; fi\n...\n 38|# Never the operator's perch (name-keyed, last-establish-wins). A fixed uniquifier keeps replays clean.\n*39|ID=\"omp-spt-acc-1\"\n*40|TX=\"omp-spt-acc-tx\"\n 41|work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n*42|LOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\n 43|SUBNET_ARG=\"\"\n*44|[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n 45|\n 46|cleanup() {\n 47|  [ -n \"${BPID:-}\" ] && kill \"$BPID\" 2>/dev/null\n## hooks-dispatch.sh\n 14|ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n*15|HK=\"$ROOT/plugin/omps/hooks\"\n*16|MAN=\"$ROOT/adapter/omp-spt.toml\"\n 17|BR=\"$ROOT/adapter/strings/briefs\"\n*18|SK=\"$ROOT/plugin/omps/skills\"\n 19|LV=\"$ROOT/adapter/strings/skills/live.md\"\n 20|\n 21|# --- hooks.json: every CC event routes to dispatch.sh with its own event token (static-forever) ---\n*22|for ev in SessionStart UserPromptSubmit PreToolUse Stop SessionEnd SubagentStart SubagentStop PostToolUse PostToolUseFailure Notification StopFailure; do\n 23|  grep -q \"\\\"$ev\\\"\" \"$HK/hooks.json\" && r=yes || r=no\n 24|  check \"hooks.json declares $ev\" \"yes\" \"$r\"\n 25|  grep -q \"dispatch.sh\\\\\\\\\\\" $ev\" \"$HK/hooks.json\" && r=yes || r=no\n...\n 28|# No per-event logic .sh remain (the eight wrappers + _common.sh were folded into the binary).\n*29|n=$(ls \"$HK\"/*.sh 2>/dev/null | grep -Ec 'session-start|user-prompt-submit|pre-tool-use|stop|session-end|subagent-start|subagent-stop|post-tool-use|_common')\n 30|check \"old per-hook wrappers removed\" \"0\" \"$n\"\n 31|# PostToolUse keeps its Write matcher (the checkpoint detector scope).\n 32|grep -q '\"matcher\": \"Write\"' \"$HK/hooks.json\" && r=yes || r=no\n...\n 35|# binary, which is TYPE-AWARE — it LOGS every type (Step-0 instrumentation) but marks idle ONLY on\n*36|# idle_prompt (REQ-HAZARD-STUCK-ACTIVE-NO-IDLE). A permission_prompt must NOT drain the spool mid-turn.\n 37|grep -q '\"matcher\": \"\\*\"' \"$HK/hooks.json\" && r=yes || r=no\n 38|check \"Notification uses the * matcher (type-aware in-binary)\" \"yes\" \"$r\"\n 39|# StopFailure + PostToolUseFailure are matcherless (all error types / all tools route through — the\n...\n 88|check \"dispatch passes the seed pid (--host-pid)\" \"yes\" \"$r\"\n*89|# No-op (exit 0) when the binary is unresolvable (adapter not registered yet / pre-/omps:setup).\n 90|grep -q '\\[ -z \"\\$bin\" \\] && exit 0' \"$HK/dispatch.sh\" && r=yes || r=no\n 91|check \"dispatch no-ops when the adapter is not yet registered\" \"yes\" \"$r\"\n 92|\n...\n 94|# v0.9.1: hook_cmd is the bare binary PATH (no trailing ` hook`); dispatch appends the subcommand.\n*95|grep -q 'hook_cmd = \"{adapter_dir}/omp-spt\"' \"$MAN\" && r=yes || r=no\n*96|check \"manifest [strings].hook_cmd = {adapter_dir}/omp-spt (bare path)\" \"yes\" \"$r\"\n 97|# And the ASSIGNMENT line (anchored at col 0, not the explanatory comment) must not carry the old\n 98|# space-before-hook value (the env-file regression).\n 99|grep -qE '^hook_cmd = \"\\{adapter_dir\\}/claude-spt hook\"' \"$MAN\" && r=present || r=absent\n## manifest-schema.sh\n 7|SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n*8|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 9|VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 10|\n 11|# Same interpreter probe as the gate wrapper — needs (tomllib|tomli)+jsonschema.\n## manifest-shortcut.sh\n 1|#!/bin/sh\n*2|# Brand-value + manifest-contract regression guard for omp-spt (rewritten from the claude-spt\n*3|# version at the S4 cut — the CC launch-shim/ccs assertions it carried now guard the native OMP\n 4|# hosting seam; see ADR-0007).\n 5|# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n 6|# [unit->REQ-DIST-SHORTCUT-BASENAME]\n 7|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n*8|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n*9|EXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\n 10|fail=0\n 11|\n*12|# ── shortcut brand: `spt endpoint run` picker shortcut is omp-<id> ────────────────────────────────\n 13|line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\n 14|if [ -z \"$line\" ]; then echo \"FAIL no shortcut_basename assignment in manifest\"; exit 1; fi\n 15|case \"$line\" in\n*16|  *'\"omp\"'*) echo \"ok   shortcut_basename = \\\"omp\\\" (omp-<id> brand intact)\" ;;\n*17|  *) echo \"FAIL shortcut_basename is not \\\"omp\\\": $line\"; fail=1 ;;\n 18|esac\n 19|n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\n 20|if [ \"$n\" -eq 1 ]; then echo \"ok   single shortcut_basename assignment\"; else echo \"FAIL $n shortcut_basename assignments (want 1)\"; fail=1; fi\n...\n 22|# ── adapter identity ──────────────────────────────────────────────────────────────────────────────\n*23|grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*\"omp-spt\"' \"$MANIFEST\" \\\n*24|  && echo 'ok   [adapter].name = \"omp-spt\"' || { echo \"FAIL [adapter].name != omp-spt\"; fail=1; }\n*25|grep -Eq '^[[:space:]]*host_binaries[[:space:]]*=.*\"omp\"' \"$MANIFEST\" \\\n*26|  && echo 'ok   host_binaries includes \"omp\"' || { echo \"FAIL host_binaries misses omp\"; fail=1; }\n 27|\n 28|field_of() { awk -v h=\"[$1]\" -v f=\"$2\" '$0 == h {s=1;next} /^\\[/{s=0} s && $0 ~ f' \"$MANIFEST\"; }\n 29|\n*30|# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n*31|# [unit->REQ-OMP-NATIVE-TUI]\n*32|spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\n 33|nspawn=$(printf '%s' \"$spawn\" | grep -c .)\n*34|if [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\n 35|if [ \"$(printf '%s\\n' \"$spawn\" | grep -c -- '--mode rpc')\" -eq 0 ]; then echo \"ok   hosted sessions never use headless RPC mode\"; else echo \"FAIL a hosted session still uses --mode rpc: $spawn\"; fail=1; fi\n 36|resume=$(printf '%s\\n' \"$spawn\" | grep -- '-r {session_id}')\n 37|nresume=$(printf '%s' \"$resume\" | grep -c .)\n 38|if [ \"$nresume\" -eq 1 ]; then echo \"ok   exactly one native resume command carries -r {session_id}\"; else echo \"FAIL expected 1 -r {session_id} command, found $nresume\"; fail=1; fi\n*39|if [ -f \"$EXTENSION\" ]; then echo \"ok   packaged omp-spt extension exists\"; else echo \"FAIL packaged omp-spt extension missing\"; fail=1; fi\n 40|if command -v node >/dev/null 2>&1; then NODE=node\n 41|elif command -v node.exe >/dev/null 2>&1; then NODE=node.exe\n*42|else echo \"FAIL node is required for the OMP extension unit test\"; fail=1; NODE=:\n 43|fi\n*44|NODE_TEST=\"$ROOT/tests/omp-extension.mjs\"\n 45|case \"$NODE\" in *.exe) NODE_TEST=$(wslpath -w \"$NODE_TEST\") ;; esac\n 46|\"$NODE\" \"$NODE_TEST\" || fail=1\n 47|\n*48|# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\n*49|grep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n## msys-hazard.sh\n 2|# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain\n*3|# /-leading content (e.g. a prompt \"/omps:send doyle\") — must reach the handler via STDIN (immune to\n 4|# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see\n 5|# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,\n*6|# …) + the numeric --host-pid — never message/prompt content, and never /-leading.\n 7|# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]\n 8|ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n*9|HK=\"$ROOT/plugin/omps/hooks\"\n 10|HOOKRS=\"$ROOT/tools/claude-spt/src/hook.rs\"\n 11|fail=0\n 12|yn() { [ \"$1\" -eq 0 ] && echo yes || echo no; }\n...\n 29|fi\n*30|if grep -Eq '(prompt|msg|body|message|content|file_path)=(\"?\\$[1-9])' \"$HK/dispatch.sh\"; then\n 31|  printf 'FAIL dispatch.sh: parses payload content from a positional argv (MSYS-unsafe)\\n'; fail=1\n 32|fi\n 33|\n## omp-extension.mjs\n 1|import assert from \"node:assert/strict\";\n*2|import { decodeBody, drainEvents, extractReply } from \"../adapter/strings/omp-spt.mjs\";\n 3|\n*4|// [unit->REQ-OMP-NATIVE-TUI]\n 5|assert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\n 6|\n 7|const partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\n...\n 10|\n*11|const complete = drainEvents(`${partial.rest}rld</EVENT><EVENT type=\"notify\">skip</EVENT>`);\n*12|assert.deepEqual(complete.events, [{ from: \"doyle\", body: \"hello\\nworld\" }]);\n*13|assert.equal(complete.rest, \"\");\n 14|\n 15|assert.equal(\n 16|\textractReply([\n...\n 24|\n*25|console.log(\"OMP-EXTENSION OK\");\n## skeleton-validate.sh\n 10|# 1. Real skeleton is installable.\n*11|if sh \"$VALIDATE\" \"$ROOT/plugin/omps\" >/dev/null 2>&1; then echo \"ok   real skeleton validates\"; else echo \"FAIL real skeleton rejected\"; sh \"$VALIDATE\" \"$ROOT/plugin/omps\"; fail=1; fi\n 12|\n 13|# Build a clean fixture copy we can tamper.\n 14|work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n 15|trap 'rm -rf \"$work\"' EXIT INT TERM\n*16|sk=\"$work/omps\"\n 17|mkdir -p \"$sk\"\n*18|cp -r \"$ROOT/plugin/omps/.claude-plugin\" \"$sk/\"\n*19|cp -r \"$ROOT/plugin/omps/hooks\" \"$sk/\"\n*20|cp -r \"$ROOT/plugin/omps/skills\" \"$sk/\"\n*21|cp \"$ROOT/plugin/omps/bootstrap.sh\" \"$sk/\" 2>/dev/null || true\n 22|\n 23|# Sanity: the clean copy validates (else later negatives are meaningless).\n 24|if sh \"$VALIDATE\" \"$sk\" >/dev/null 2>&1; then echo \"ok   clean fixture validates\"; else echo \"FAIL clean fixture rejected\"; fail=1; fi\n...\n 44|# 5. Wrong plugin name must FAIL.\n*45|sed 's/\"name\": \"omps\"/\"name\": \"wrong\"/' \"$sk/.claude-plugin/plugin.json\" > \"$sk/.claude-plugin/plugin.json.tmp\" && mv \"$sk/.claude-plugin/plugin.json.tmp\" \"$sk/.claude-plugin/plugin.json\"\n 46|if sh \"$VALIDATE\" \"$sk\" >/dev/null 2>&1; then echo \"FAIL wrong plugin name not caught\"; fail=1; else echo \"ok   catches wrong plugin name\"; fi\n 47|\n 48|[ \"$fail\" -eq 0 ] && { echo \"SKELETON-VALIDATE OK\"; exit 0; } || { echo \"SKELETON-VALIDATE FAIL\"; exit 1; }\n\n# ci/\n\n## acceptance/\n### lib.sh\n 32|# ── Isolated project scaffold ────────────────────────────────────────────────\n*33|# Build a throwaway CC project that wires a UserPromptSubmit hook writing a digest marker to\n 34|# $2 (the digest file). Echoes the project dir. The fixture hook is deliberately self-contained\n 35|# (no spt bus, no auth beyond claude's own) so slice-1 acceptance asserts purely that a real\n 36|# harness FIRES the hook entry point. $1 = project dir (created), $2 = digest file path.\n...\n 42|#!/bin/sh\n*43|# Acceptance fixture: prove a real claude fires UserPromptSubmit. Reads stdin payload, never argv.\n 44|input=\\$(cat)\n*45|prompt=\\$(printf '%s' \"\\$input\" | sed -n 's/.*\"prompt\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\n*46|printf 'UPS_FIRED:%s\\n' \"\\$prompt\" >> \"$_digest\"\n 47|exit 0\n 48|HOOK\n 49|  chmod +x \"$_proj/.claude/hooks/ups-digest.sh\" 2>/dev/null || true\n...\n 52|  \"hooks\": {\n*53|    \"UserPromptSubmit\": [\n 54|      { \"hooks\": [ { \"type\": \"command\", \"command\": \"sh \\\"$_proj/.claude/hooks/ups-digest.sh\\\"\" } ] }\n 55|    ]\n 56|  }\n### run-acceptance.sh\n 2|# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired\n*3|# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this\n 4|# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]\n 5|#\n 6|# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so\n\n## digest/\n### digest-proof-int.sh\n 27|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*28|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 29|SAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\n 30|RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n 31|\n### sample.jsonl\n 6|\n*7|{\"type\":\"last-prompt\",\"sessionId\":\"S\",\"leafUuid\":\"u\",\"lastPrompt\":\"x\"}\n 8|this line is not valid json and must be skipped, never emitted\n 9|{\"type\":\"assistant\",\"timestamp\":\"2026-06-15T10:00:05.000Z\",\"sessionId\":\"S\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Done — built clean.\"}]}}\n\n## hooks/\n### poll-int.sh\n 1|#!/bin/sh\n*2|# Integration proof for the UserPromptSubmit message-drain path against a REAL spt-core (>= v0.7.1):\n 3|# the published `api poll` surface emits the canonical self-delimiting <EVENT> envelope,\n 4|# and our hook parser (render_frames) decodes it correctly. This is the confirm-match that closes\n 5|# REQ-MSG-ENVELOPE end-to-end: design -> impl -> ship -> real-surface-verify. F-002 (no inter-frame\n...\n 51|\n*52|# 2. Hook BINARY confirm-match (D1): the real `claude-spt hook UserPromptSubmit` drains the live perch\n 53|#    and renders the canonical <EVENT> drain to our <sptc_messages> additionalContext shape — the same\n 54|#    parser, now in the binary (was render_frames in _common.sh). Send a fresh message with body\n 55|#    specials, run the hook (it resolves the perch via whoami off OWL_SESSION_ID, marks busy, polls\n...\n 57|#    [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]\n*58|HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*59|[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 60|if [ -x \"$HOOKBIN\" ]; then\n 61|  printf 'hello from probe<NL>second <line> & \"stuff\"' | sed 's/<NL>/\\\n 62|/' | spt send \"$BID\" --from probe-int >/dev/null 2>&1\n 63|  # The binary resolves the perch via `spt whoami` with OWL_SESSION_ID set from the stdin session_id.\n*64|  rendered=$(printf '%s' \"{\\\"session_id\\\":\\\"$BSID\\\",\\\"prompt\\\":\\\"\\\"}\" | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 65|  want=$(printf '<sptc_messages from=\"probe-int\">\\nhello from probe\\nsecond <line> & \"stuff\"\\n</sptc_messages>')\n 66|  case \"$rendered\" in *\"$want\"*) ok \"binary hook confirm-match: live drain -> <sptc_messages>\" ;; *) bad \"binary render mismatch; got=[$rendered]\" ;; esac\n 67|  # Re-idle the perch (the hook marked it busy) and drain any residue so step 3 is unaffected.\n\n## idle-translate/\n### translate-proof-int.sh\n 22|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*23|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n*24|RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; manifest command = {adapter_dir}/omp-spt translate (D3)\n 25|EVENT='<EVENT type=\"msg\" from=\"ci\">translate-proof int probe</EVENT>'\n 26|\n 27|command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n...\n 73|# and emits CLEAR-ONLY — the wake is WITHHELD until a later `{\"checkpoint_fire\":\"v1\"}` signal (self-sent\n*74|# by the SessionStart hook AFTER /clear completes). translate-proof feeds a single event to a FRESH\n 75|# process, so it proves the ARM emit (clear-only, no wake, no long settle); the stateful arm→fire→wake\n 76|# ordering is proven by the crate unit tests (arm_then_fire_orders_clear_before_wake) + the live E2E.\n 77|# The same block is the int evidence for the checkpoint-commune feature (envelope → detect → arm/clear)\n### wake-survival-int.sh\n 122|\n*123|# Settle the session (drives CC past its first prompt) so the boundary machinery stamps ready.\n 124|{ sleep 2; printf 'standby'; sleep 1; printf '\\r'; sleep 3; } | timeout 9 spt rc \"$PROBE\" >/dev/null 2>&1\n 125|RDY=no\n 126|for i in $(seq 1 30); do [ -f \"$OWLERY/$PROBE/ready\" ] && { RDY=yes; break; }; sleep 2; done\n...\n 158|else\n*159|  bad \"wake NEVER executed in 180s (no proof file) — the checkpoint macro did not complete the boundary\"\n 160|fi\n 161|\n 162|# B6 survival: the translate binary must still be alive after the clear boundary (the every-clear\n\n## manifest/\n### check-manifest.sh\n 9|SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n*10|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 11|VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 12|\n 13|for f in \"$SCHEMA\" \"$MANIFEST\" \"$VALIDATOR\"; do\n### registration-int.sh\n 12|ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*13|MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 14|\n 15|if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 16|command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n...\n 31|\n*32|# 2. Listed, with the shipped profile resolved as a composite option.\n 33|list=$(spt adapter list 2>&1)\n 34|case \"$list\" in *claude-spt*) ok \"listed: claude-spt active\" ;; *) bad \"claude-spt not listed\" ;; esac\n 35|# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable\n 36|# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at\n*37|# step 4d below, not a composite resolve.\n 38|# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —\n 39|# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).\n 40|case \"$list\" in *claude-spt:ccs*) ok \"shipped profile resolves: claude-spt:ccs (ccs overlay)\" ;; *) bad \"ccs profile not resolved\" ;; esac\n...\n 67|\n*68|# 4c. UPS skill-injection end-to-end via the BINARY (D1): `claude-spt hook UserPromptSubmit` resolves a\n*69|#     /sptc:<skill> prompt to the wrapped operative body via get-string on the registered adapter. No\n 70|#     perch (whoami empty) → only skill-injection emits, no drain. (REQ-UPS-INJECTION impl now in the\n 71|#     binary.) Prefer the release build; the dev `adapter add` above registered the manifest, so\n 72|#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\n*73|HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*74|[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 75|if [ -x \"$HOOKBIN\" ]; then\n*76|  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 77|  case \"$inj\" in\n 78|    '<sptc_skill name=\"ready\">'*'# /sptc:ready'*'</sptc_skill>'*) ok \"UPS skill-injection (binary): /sptc:ready -> wrapped body\" ;;\n 79|    *) bad \"skill-injection did not emit wrapped body: $(printf %.60s \"$inj\")\" ;;\n\n## psyche/\n### live-relay-int.sh\n 15|# the RESIDENT runner process + nested `{id}-psyche` perch dir (FINALIZED on v0.8.1 + the adapter\n*16|# greedy-prompt fix, 2026-06-16; see the >=0.8.0 leg below). (2) RELAY — the resident listen pipe delivers the probe (BOUND/READY/<EVENT> off the child,\n 17|# unchanged across versions). The per-pulse runner command construction is additionally covered by\n 18|# claude-spt-psyche unit tests (ci/psyche/build.sh).\n 19|#\n...\n 31|A=claude-spt\n*32|MAN=\"$ROOT/adapter/omp-spt.toml\"\n 33|# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity\n 34|# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):\n 35|# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a\n...\n 37|# Option A (PREP-4): [session.psyche_init] is in the BASE manifest — NO `:live` profile. The adapter\n*38|# is BASE claude-spt; the live `api listen` COMMAND (not a composite) is what stamps state=live_agent\n 39|# and actualizes the Psyche. We still pass an explicit `--adapter claude-spt --manifest <man>` here:\n 40|# CI has no real `claude` parent process, so spt-core's bare-by-pid host_binaries resolution cannot\n 41|# fire (the anchor is `sh`/`timeout`, not `claude`). The explicit override remains valid on 0.9.0 and\n...\n 117|    # status (the `{id}-psyche` perch comes online; `LIVEHOST_PSYCHE:{id}` on the daemon's stderr).\n*118|    # RESOLVED (v0.8.1 + adapter greedy-prompt fix, 2026-06-16): hosting succeeds iff the daemon\n 119|    # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core\n 120|    # <0.8.1 livehost did not reconcile (no spawn at all); v0.8.1 fixed it. (2) spt-core substitutes\n*121|    # `{psyche_prompt}` into the psyche_init command STRING then whitespace-SPLITS, so the multi-word\n*122|    # prompt arrived as stray argv tokens — the runner's non-greedy --prompt rejected the 2nd word\n 123|    # (\"unknown arg\") and exited 2 instantly → the daemon recorded a phantom hosted perch (nested\n 124|    # info.json status=online, real-looking pid) with NO live process and NO psyche_host_error. The\n*125|    # runner now parses --prompt greedily (slurps trailing tokens). DETECTION: the nested {id}-psyche\n 126|    # perch does NOT surface in `endpoint list` (it lives under the parent in the owlery), so assert on\n 127|    # the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11\n 128|    # install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.\n...\n 137|    if resident && [ -f \"$OWL\" ]; then\n*138|      ok \"Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)\"\n 139|    else\n*140|      bad \"psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs\"\n 141|    fi\n 142|    ;;\n 143|esac\n\n## publish/\n### package-adapter.sh\n 2|# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by\n*3|# `spt adapter add --release BigscreenVR/omp-spt` and carried by the `[update] gh_release`\n 4|# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2).\n*5|# The release/install/update repo is `BigscreenVR/omp-spt` (renamed from spt-claude-code, U3/ADR-0005).\n 6|# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS] [impl->REQ-DIST-NAME-UNIFY]\n 7|#\n 8|#   adapter.spt (tar.gz)\n*9|#   ├── manifest.toml                 ← SHARED, at archive root (renamed from omp-spt.toml)\n 10|#   ├── strings/                      ← SHARED, at archive root\n 11|#   ├── x86_64-pc-windows-msvc/       ← this triple's binary, mirroring the flat-root tree\n 12|#   │   ├── claude-spt.exe\n...\n 33|ADAPTER=\"$ROOT/adapter\"\n*34|MANIFEST=\"$ADAPTER/omp-spt.toml\"   # renamed to manifest.toml INSIDE the archive (root-only rule)\n 35|STRINGS=\"$ADAPTER/strings\"\n 36|TOOLS=\"$ROOT/tools\"\n 37|OUT=\"${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}\"   # overridable so the unit test writes to a tmp file\n...\n 41|# The recognized triples and where each platform's release binaries live. A native Windows build lands\n*42|# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into\n 43|# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a\n 44|# non-default layout. There is now ONE tool binary: the consolidated claude-spt crate carries all four\n 45|# subcommands — digest / psyche / post-update / translate (the last folded in at the v0.8.0 cut once\n...\n 50|LINUX_RELSUB=\"${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}\"\n*51|BINS=\"omp-spt\"\n 52|\n 53|# Validate the manifest first — refuse to ship an invalid adapter.\n 54|echo \"== validate manifest ==\"\n...\n 94|  echo \"DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'\"\n*95|  echo \"to a GitHub release on the monorepo. End users: 'spt adapter add --release BigscreenVR/omp-spt'\"\n 96|  echo \"(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.\"\n 97|  exit 0\n 98|fi\n...\n 120|echo \"$listing\" | grep -qx \"manifest.toml\" || { echo \"FATAL: manifest.toml not at archive root\" >&2; fatal=1; }\n*121|echo \"$listing\" | grep -q \"^$WIN_TRIPLE/omp-spt.exe$\" || { echo \"FATAL: missing $WIN_TRIPLE/ binaries\" >&2; fatal=1; }\n*122|echo \"$listing\" | grep -q \"^$LINUX_TRIPLE/omp-spt$\"   || { echo \"FATAL: missing $LINUX_TRIPLE/ binaries\" >&2; fatal=1; }\n 123|# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).\n 124|badtop=$(echo \"$listing\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\n 125|[ -n \"$badtop\" ] && { echo \"FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat\" >&2; fatal=1; }\n...\n 130|echo \"WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release\"\n*131|echo \"on BigscreenVR/omp-spt; end users 'spt adapter add --release BigscreenVR/omp-spt' (spt\"\n 132|echo \"v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.\"\n 133|exit 0\n\nShowing files 1-20 of 24+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"tests, ci","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":116,"fileCount":20,"files":["tests/acceptance-harness.sh","tests/adapter-archive.sh","tests/bridge-acceptance.sh","tests/hooks-dispatch.sh","tests/manifest-schema.sh","tests/manifest-shortcut.sh","tests/msys-hazard.sh","tests/omp-extension.mjs","tests/skeleton-validate.sh","ci/acceptance/lib.sh","ci/acceptance/run-acceptance.sh","ci/digest/digest-proof-int.sh","ci/digest/sample.jsonl","ci/hooks/poll-int.sh","ci/idle-translate/translate-proof-int.sh","ci/idle-translate/wake-survival-int.sh","ci/manifest/check-manifest.sh","ci/manifest/registration-int.sh","ci/psyche/live-relay-int.sh","ci/publish/package-adapter.sh"],"fileMatches":[{"path":"tests/acceptance-harness.sh","count":2},{"path":"tests/adapter-archive.sh","count":1},{"path":"tests/bridge-acceptance.sh","count":20},{"path":"tests/hooks-dispatch.sh","count":9},{"path":"tests/manifest-schema.sh","count":1},{"path":"tests/manifest-shortcut.sh","count":20},{"path":"tests/msys-hazard.sh","count":4},{"path":"tests/omp-extension.mjs","count":6},{"path":"tests/skeleton-validate.sh","count":7},{"path":"ci/acceptance/lib.sh","count":5},{"path":"ci/acceptance/run-acceptance.sh","count":1},{"path":"ci/digest/digest-proof-int.sh","count":1},{"path":"ci/digest/sample.jsonl","count":1},{"path":"ci/hooks/poll-int.sh","count":5},{"path":"ci/idle-translate/translate-proof-int.sh","count":3},{"path":"ci/idle-translate/wake-survival-int.sh","count":2},{"path":"ci/manifest/check-manifest.sh","count":1},{"path":"ci/manifest/registration-int.sh","count":8},{"path":"ci/psyche/live-relay-int.sh","count":9},{"path":"ci/publish/package-adapter.sh","count":10}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# tests/\n## acceptance-harness.sh\n 28│[ -f \"$proj/.claude/hooks/ups-digest.sh\" ] && echo \"ok   scaffold writes UPS hook\" || { echo \"FAIL no UPS hook\"; fail=1; }\n*29│grep -q 'UserPromptSubmit' \"$proj/.claude/settings.json\" && echo \"ok   settings wires UserPromptSubmit\" || { echo \"FAIL UPS not wired\"; fail=1; }\n 30│# Fixture hook reads stdin, not a /-leading argv (KH 1.1 immunity).\n 31│grep -q 'input=$(cat)' \"$proj/.claude/hooks/ups-digest.sh\" && echo \"ok   fixture reads stdin (MSYS-safe)\" || { echo \"FAIL fixture not stdin-driven\"; fail=1; }\n 32│\n 33│# Fixture actually produces the digest marker when fed a real CC-shaped payload on stdin.\n*34│printf '%s' '{\"session_id\":\"x\",\"prompt\":\"hello-probe\"}' | sh \"$proj/.claude/hooks/ups-digest.sh\"\n 35│sptc_ci_assert \"fixture emits digest marker\" \"UPS_FIRED:hello-probe\" \"$digest\" >/dev/null \\\n 36│  && echo \"ok   fixture emits digest marker\" || { echo \"FAIL fixture no marker\"; fail=1; }\n 37│\n## adapter-archive.sh\n 13│LINUX_TRIPLE=x86_64-unknown-linux-gnu\n*14│BINS=\"omp-spt\"  # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)\n 15│rc=0\n 16│fail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n 17│\n## bridge-acceptance.sh\n  1│#!/bin/sh\n *2│# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n *3│# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n  4│#\n *5│# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n *6│# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n *7│# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n *8│#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n *9│#   2. the bridge sends a reply back to the sender (the bridge trace).\n 10│#\n 11│# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n*12│# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n*13│# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\n 14│set -u\n 15│\n*16│if [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n*17│  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n 18│  exit 0\n 19│fi\n 20│\n   │...\n 25│# ── Resolve binaries ─────────────────────────────────────────────────────────\n*26│# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n*27│( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n 28│  || { echo \"FAIL cargo build\"; exit 1; }\n 29│exe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\n*30│BIN=$(exe \"$CRATE/target/release/omp-spt\")\n*31│FAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n*32│# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\n 33│if command -v spt >/dev/null 2>&1; then SPT=spt\n 34│elif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\n 35│else echo \"FAIL spt binary not found\"; exit 1; fi\n   │...\n 38│# Never the operator's perch (name-keyed, last-establish-wins). A fixed uniquifier keeps replays clean.\n*39│ID=\"omp-spt-acc-1\"\n*40│TX=\"omp-spt-acc-tx\"\n 41│work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n*42│LOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\n 43│SUBNET_ARG=\"\"\n*44│[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n 45│\n 46│cleanup() {\n 47│  [ -n \"${BPID:-}\" ] && kill \"$BPID\" 2>/dev/null\n## hooks-dispatch.sh\n 14│ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n*15│HK=\"$ROOT/plugin/omps/hooks\"\n*16│MAN=\"$ROOT/adapter/omp-spt.toml\"\n 17│BR=\"$ROOT/adapter/strings/briefs\"\n*18│SK=\"$ROOT/plugin/omps/skills\"\n 19│LV=\"$ROOT/adapter/strings/skills/live.md\"\n 20│\n 21│# --- hooks.json: every CC event routes to dispatch.sh with its own event token (static-forever) ---\n*22│for ev in SessionStart UserPromptSubmit PreToolUse Stop SessionEnd SubagentStart SubagentStop PostToolUse PostToolUseFailure Notification StopFailure; do\n 23│  grep -q \"\\\"$ev\\\"\" \"$HK/hooks.json\" && r=yes || r=no\n 24│  check \"hooks.json declares $ev\" \"yes\" \"$r\"\n 25│  grep -q \"dispatch.sh\\\\\\\\\\\" $ev\" \"$HK/hooks.json\" && r=yes || r=no\n   │...\n 28│# No per-event logic .sh remain (the eight wrappers + _common.sh were folded into the binary).\n*29│n=$(ls \"$HK\"/*.sh 2>/dev/null | grep -Ec 'session-start|user-prompt-submit|pre-tool-use|stop|session-end|subagent-start|subagent-stop|post-tool-use|_common')\n 30│check \"old per-hook wrappers removed\" \"0\" \"$n\"\n 31│# PostToolUse keeps its Write matcher (the checkpoint detector scope).\n 32│grep -q '\"matcher\": \"Write\"' \"$HK/hooks.json\" && r=yes || r=no\n   │...\n 35│# binary, which is TYPE-AWARE — it LOGS every type (Step-0 instrumentation) but marks idle ONLY on\n*36│# idle_prompt (REQ-HAZARD-STUCK-ACTIVE-NO-IDLE). A permission_prompt must NOT drain the spool mid-turn.\n 37│grep -q '\"matcher\": \"\\*\"' \"$HK/hooks.json\" && r=yes || r=no\n 38│check \"Notification uses the * matcher (type-aware in-binary)\" \"yes\" \"$r\"\n 39│# StopFailure + PostToolUseFailure are matcherless (all error types / all tools route through — the\n   │...\n 88│check \"dispatch passes the seed pid (--host-pid)\" \"yes\" \"$r\"\n*89│# No-op (exit 0) when the binary is unresolvable (adapter not registered yet / pre-/omps:setup).\n 90│grep -q '\\[ -z \"\\$bin\" \\] && exit 0' \"$HK/dispatch.sh\" && r=yes || r=no\n 91│check \"dispatch no-ops when the adapter is not yet registered\" \"yes\" \"$r\"\n 92│\n   │...\n 94│# v0.9.1: hook_cmd is the bare binary PATH (no trailing ` hook`); dispatch appends the subcommand.\n*95│grep -q 'hook_cmd = \"{adapter_dir}/omp-spt\"' \"$MAN\" && r=yes || r=no\n*96│check \"manifest [strings].hook_cmd = {adapter_dir}/omp-spt (bare path)\" \"yes\" \"$r\"\n 97│# And the ASSIGNMENT line (anchored at col 0, not the explanatory comment) must not carry the old\n 98│# space-before-hook value (the env-file regression).\n 99│grep -qE '^hook_cmd = \"\\{adapter_dir\\}/claude-spt hook\"' \"$MAN\" && r=present || r=absent\n## manifest-schema.sh\n  7│SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n *8│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n  9│VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 10│\n 11│# Same interpreter probe as the gate wrapper — needs (tomllib|tomli)+jsonschema.\n## manifest-shortcut.sh\n  1│#!/bin/sh\n *2│# Brand-value + manifest-contract regression guard for omp-spt (rewritten from the claude-spt\n *3│# version at the S4 cut — the CC launch-shim/ccs assertions it carried now guard the native OMP\n  4│# hosting seam; see ADR-0007).\n  5│# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).\n  6│# [unit->REQ-DIST-SHORTCUT-BASENAME]\n  7│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\n *8│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n *9│EXTENSION=\"$ROOT/adapter/strings/omp-spt.mjs\"\n 10│fail=0\n 11│\n*12│# ── shortcut brand: `spt endpoint run` picker shortcut is omp-<id> ────────────────────────────────\n 13│line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\n 14│if [ -z \"$line\" ]; then echo \"FAIL no shortcut_basename assignment in manifest\"; exit 1; fi\n 15│case \"$line\" in\n*16│  *'\"omp\"'*) echo \"ok   shortcut_basename = \\\"omp\\\" (omp-<id> brand intact)\" ;;\n*17│  *) echo \"FAIL shortcut_basename is not \\\"omp\\\": $line\"; fail=1 ;;\n 18│esac\n 19│n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' \"$MANIFEST\")\n 20│if [ \"$n\" -eq 1 ]; then echo \"ok   single shortcut_basename assignment\"; else echo \"FAIL $n shortcut_basename assignments (want 1)\"; fail=1; fi\n   │...\n 22│# ── adapter identity ──────────────────────────────────────────────────────────────────────────────\n*23│grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*\"omp-spt\"' \"$MANIFEST\" \\\n*24│  && echo 'ok   [adapter].name = \"omp-spt\"' || { echo \"FAIL [adapter].name != omp-spt\"; fail=1; }\n*25│grep -Eq '^[[:space:]]*host_binaries[[:space:]]*=.*\"omp\"' \"$MANIFEST\" \\\n*26│  && echo 'ok   host_binaries includes \"omp\"' || { echo \"FAIL host_binaries misses omp\"; fail=1; }\n 27│\n 28│field_of() { awk -v h=\"[$1]\" -v f=\"$2\" '$0 == h {s=1;next} /^\\[/{s=0} s && $0 ~ f' \"$MANIFEST\"; }\n 29│\n*30│# ── bringup: native OMP owns the broker PTY and loads the packaged SPT extension ─────────────────\n*31│# [unit->REQ-OMP-NATIVE-TUI]\n*32│spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp .*--extension \\{adapter_dir\\}/strings/omp-spt\\.mjs' \"$MANIFEST\")\n 33│nspawn=$(printf '%s' \"$spawn\" | grep -c .)\n*34│if [ \"$nspawn\" -eq 2 ]; then echo \"ok   2 native omp spawn commands (self + resume)\"; else echo \"FAIL expected 2 native omp spawn commands, found $nspawn\"; fail=1; fi\n 35│if [ \"$(printf '%s\\n' \"$spawn\" | grep -c -- '--mode rpc')\" -eq 0 ]; then echo \"ok   hosted sessions never use headless RPC mode\"; else echo \"FAIL a hosted session still uses --mode rpc: $spawn\"; fail=1; fi\n 36│resume=$(printf '%s\\n' \"$spawn\" | grep -- '-r {session_id}')\n 37│nresume=$(printf '%s' \"$resume\" | grep -c .)\n 38│if [ \"$nresume\" -eq 1 ]; then echo \"ok   exactly one native resume command carries -r {session_id}\"; else echo \"FAIL expected 1 -r {session_id} command, found $nresume\"; fail=1; fi\n*39│if [ -f \"$EXTENSION\" ]; then echo \"ok   packaged omp-spt extension exists\"; else echo \"FAIL packaged omp-spt extension missing\"; fail=1; fi\n 40│if command -v node >/dev/null 2>&1; then NODE=node\n 41│elif command -v node.exe >/dev/null 2>&1; then NODE=node.exe\n*42│else echo \"FAIL node is required for the OMP extension unit test\"; fail=1; NODE=:\n 43│fi\n*44│NODE_TEST=\"$ROOT/tests/omp-extension.mjs\"\n 45│case \"$NODE\" in *.exe) NODE_TEST=$(wslpath -w \"$NODE_TEST\") ;; esac\n 46│\"$NODE\" \"$NODE_TEST\" || fail=1\n 47│\n*48│# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\n*49│grep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n## msys-hazard.sh\n  2│# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain\n *3│# /-leading content (e.g. a prompt \"/omps:send doyle\") — must reach the handler via STDIN (immune to\n  4│# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see\n  5│# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,\n *6│# …) + the numeric --host-pid — never message/prompt content, and never /-leading.\n  7│# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]\n  8│ROOT=\"$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\"\n *9│HK=\"$ROOT/plugin/omps/hooks\"\n 10│HOOKRS=\"$ROOT/tools/claude-spt/src/hook.rs\"\n 11│fail=0\n 12│yn() { [ \"$1\" -eq 0 ] && echo yes || echo no; }\n   │...\n 29│fi\n*30│if grep -Eq '(prompt|msg|body|message|content|file_path)=(\"?\\$[1-9])' \"$HK/dispatch.sh\"; then\n 31│  printf 'FAIL dispatch.sh: parses payload content from a positional argv (MSYS-unsafe)\\n'; fail=1\n 32│fi\n 33│\n## omp-extension.mjs\n  1│import assert from \"node:assert/strict\";\n *2│import { decodeBody, drainEvents, extractReply } from \"../adapter/strings/omp-spt.mjs\";\n  3│\n *4│// [unit->REQ-OMP-NATIVE-TUI]\n  5│assert.equal(decodeBody('a&lt;b&gt;<br>&quot;c&quot; &amp; &amp;lt;'), 'a<b>\\n\"c\" & &lt;');\n  6│\n  7│const partial = drainEvents('noise<EVENT type=\"msg\" from=\"doyle\">hello<br>wo');\n   │...\n 10│\n*11│const complete = drainEvents(`${partial.rest}rld</EVENT><EVENT type=\"notify\">skip</EVENT>`);\n*12│assert.deepEqual(complete.events, [{ from: \"doyle\", body: \"hello\\nworld\" }]);\n*13│assert.equal(complete.rest, \"\");\n 14│\n 15│assert.equal(\n 16│\textractReply([\n   │...\n 24│\n*25│console.log(\"OMP-EXTENSION OK\");\n## skeleton-validate.sh\n 10│# 1. Real skeleton is installable.\n*11│if sh \"$VALIDATE\" \"$ROOT/plugin/omps\" >/dev/null 2>&1; then echo \"ok   real skeleton validates\"; else echo \"FAIL real skeleton rejected\"; sh \"$VALIDATE\" \"$ROOT/plugin/omps\"; fail=1; fi\n 12│\n 13│# Build a clean fixture copy we can tamper.\n 14│work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n 15│trap 'rm -rf \"$work\"' EXIT INT TERM\n*16│sk=\"$work/omps\"\n 17│mkdir -p \"$sk\"\n*18│cp -r \"$ROOT/plugin/omps/.claude-plugin\" \"$sk/\"\n*19│cp -r \"$ROOT/plugin/omps/hooks\" \"$sk/\"\n*20│cp -r \"$ROOT/plugin/omps/skills\" \"$sk/\"\n*21│cp \"$ROOT/plugin/omps/bootstrap.sh\" \"$sk/\" 2>/dev/null || true\n 22│\n 23│# Sanity: the clean copy validates (else later negatives are meaningless).\n 24│if sh \"$VALIDATE\" \"$sk\" >/dev/null 2>&1; then echo \"ok   clean fixture validates\"; else echo \"FAIL clean fixture rejected\"; fail=1; fi\n   │...\n 44│# 5. Wrong plugin name must FAIL.\n*45│sed 's/\"name\": \"omps\"/\"name\": \"wrong\"/' \"$sk/.claude-plugin/plugin.json\" > \"$sk/.claude-plugin/plugin.json.tmp\" && mv \"$sk/.claude-plugin/plugin.json.tmp\" \"$sk/.claude-plugin/plugin.json\"\n 46│if sh \"$VALIDATE\" \"$sk\" >/dev/null 2>&1; then echo \"FAIL wrong plugin name not caught\"; fail=1; else echo \"ok   catches wrong plugin name\"; fi\n 47│\n 48│[ \"$fail\" -eq 0 ] && { echo \"SKELETON-VALIDATE OK\"; exit 0; } || { echo \"SKELETON-VALIDATE FAIL\"; exit 1; }\n\n# ci/\n\n## acceptance/\n### lib.sh\n 32│# ── Isolated project scaffold ────────────────────────────────────────────────\n*33│# Build a throwaway CC project that wires a UserPromptSubmit hook writing a digest marker to\n 34│# $2 (the digest file). Echoes the project dir. The fixture hook is deliberately self-contained\n 35│# (no spt bus, no auth beyond claude's own) so slice-1 acceptance asserts purely that a real\n 36│# harness FIRES the hook entry point. $1 = project dir (created), $2 = digest file path.\n   │...\n 42│#!/bin/sh\n*43│# Acceptance fixture: prove a real claude fires UserPromptSubmit. Reads stdin payload, never argv.\n 44│input=\\$(cat)\n*45│prompt=\\$(printf '%s' \"\\$input\" | sed -n 's/.*\"prompt\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)\n*46│printf 'UPS_FIRED:%s\\n' \"\\$prompt\" >> \"$_digest\"\n 47│exit 0\n 48│HOOK\n 49│  chmod +x \"$_proj/.claude/hooks/ups-digest.sh\" 2>/dev/null || true\n   │...\n 52│  \"hooks\": {\n*53│    \"UserPromptSubmit\": [\n 54│      { \"hooks\": [ { \"type\": \"command\", \"command\": \"sh \\\"$_proj/.claude/hooks/ups-digest.sh\\\"\" } ] }\n 55│    ]\n 56│  }\n### run-acceptance.sh\n 2│# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired\n*3│# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this\n 4│# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]\n 5│#\n 6│# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so\n\n## digest/\n### digest-proof-int.sh\n 27│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*28│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 29│SAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\n 30│RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n 31│\n### sample.jsonl\n 6│\n*7│{\"type\":\"last-prompt\",\"sessionId\":\"S\",\"leafUuid\":\"u\",\"lastPrompt\":\"x\"}\n 8│this line is not valid json and must be skipped, never emitted\n 9│{\"type\":\"assistant\",\"timestamp\":\"2026-06-15T10:00:05.000Z\",\"sessionId\":\"S\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Done — built clean.\"}]}}\n\n## hooks/\n### poll-int.sh\n  1│#!/bin/sh\n *2│# Integration proof for the UserPromptSubmit message-drain path against a REAL spt-core (>= v0.7.1):\n  3│# the published `api poll` surface emits the canonical self-delimiting <EVENT> envelope,\n  4│# and our hook parser (render_frames) decodes it correctly. This is the confirm-match that closes\n  5│# REQ-MSG-ENVELOPE end-to-end: design -> impl -> ship -> real-surface-verify. F-002 (no inter-frame\n   │...\n 51│\n*52│# 2. Hook BINARY confirm-match (D1): the real `claude-spt hook UserPromptSubmit` drains the live perch\n 53│#    and renders the canonical <EVENT> drain to our <sptc_messages> additionalContext shape — the same\n 54│#    parser, now in the binary (was render_frames in _common.sh). Send a fresh message with body\n 55│#    specials, run the hook (it resolves the perch via whoami off OWL_SESSION_ID, marks busy, polls\n   │...\n 57│#    [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]\n*58│HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*59│[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 60│if [ -x \"$HOOKBIN\" ]; then\n 61│  printf 'hello from probe<NL>second <line> & \"stuff\"' | sed 's/<NL>/\\\n 62│/' | spt send \"$BID\" --from probe-int >/dev/null 2>&1\n 63│  # The binary resolves the perch via `spt whoami` with OWL_SESSION_ID set from the stdin session_id.\n*64│  rendered=$(printf '%s' \"{\\\"session_id\\\":\\\"$BSID\\\",\\\"prompt\\\":\\\"\\\"}\" | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 65│  want=$(printf '<sptc_messages from=\"probe-int\">\\nhello from probe\\nsecond <line> & \"stuff\"\\n</sptc_messages>')\n 66│  case \"$rendered\" in *\"$want\"*) ok \"binary hook confirm-match: live drain -> <sptc_messages>\" ;; *) bad \"binary render mismatch; got=[$rendered]\" ;; esac\n 67│  # Re-idle the perch (the hook marked it busy) and drain any residue so step 3 is unaffected.\n\n## idle-translate/\n### translate-proof-int.sh\n 22│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*23│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n*24│RELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; manifest command = {adapter_dir}/omp-spt translate (D3)\n 25│EVENT='<EVENT type=\"msg\" from=\"ci\">translate-proof int probe</EVENT>'\n 26│\n 27│command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n   │...\n 73│# and emits CLEAR-ONLY — the wake is WITHHELD until a later `{\"checkpoint_fire\":\"v1\"}` signal (self-sent\n*74│# by the SessionStart hook AFTER /clear completes). translate-proof feeds a single event to a FRESH\n 75│# process, so it proves the ARM emit (clear-only, no wake, no long settle); the stateful arm→fire→wake\n 76│# ordering is proven by the crate unit tests (arm_then_fire_orders_clear_before_wake) + the live E2E.\n 77│# The same block is the int evidence for the checkpoint-commune feature (envelope → detect → arm/clear)\n### wake-survival-int.sh\n 122│\n*123│# Settle the session (drives CC past its first prompt) so the boundary machinery stamps ready.\n 124│{ sleep 2; printf 'standby'; sleep 1; printf '\\r'; sleep 3; } | timeout 9 spt rc \"$PROBE\" >/dev/null 2>&1\n 125│RDY=no\n 126│for i in $(seq 1 30); do [ -f \"$OWLERY/$PROBE/ready\" ] && { RDY=yes; break; }; sleep 2; done\n    │...\n 158│else\n*159│  bad \"wake NEVER executed in 180s (no proof file) — the checkpoint macro did not complete the boundary\"\n 160│fi\n 161│\n 162│# B6 survival: the translate binary must still be alive after the clear boundary (the every-clear\n\n## manifest/\n### check-manifest.sh\n  9│SCHEMA=\"$ROOT/adapter/manifest.schema.json\"\n*10│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 11│VALIDATOR=\"$ROOT/ci/manifest/validate_manifest.py\"\n 12│\n 13│for f in \"$SCHEMA\" \"$MANIFEST\" \"$VALIDATOR\"; do\n### registration-int.sh\n 12│ROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n*13│MANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n 14│\n 15│if [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\n 16│command -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n   │...\n 31│\n*32│# 2. Listed, with the shipped profile resolved as a composite option.\n 33│list=$(spt adapter list 2>&1)\n 34│case \"$list\" in *claude-spt*) ok \"listed: claude-spt active\" ;; *) bad \"claude-spt not listed\" ;; esac\n 35│# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable\n 36│# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at\n*37│# step 4d below, not a composite resolve.\n 38│# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —\n 39│# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).\n 40│case \"$list\" in *claude-spt:ccs*) ok \"shipped profile resolves: claude-spt:ccs (ccs overlay)\" ;; *) bad \"ccs profile not resolved\" ;; esac\n   │...\n 67│\n*68│# 4c. UPS skill-injection end-to-end via the BINARY (D1): `claude-spt hook UserPromptSubmit` resolves a\n*69│#     /sptc:<skill> prompt to the wrapped operative body via get-string on the registered adapter. No\n 70│#     perch (whoami empty) → only skill-injection emits, no drain. (REQ-UPS-INJECTION impl now in the\n 71│#     binary.) Prefer the release build; the dev `adapter add` above registered the manifest, so\n 72│#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\n*73│HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n*74│[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\n 75│if [ -x \"$HOOKBIN\" ]; then\n*76│  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n 77│  case \"$inj\" in\n 78│    '<sptc_skill name=\"ready\">'*'# /sptc:ready'*'</sptc_skill>'*) ok \"UPS skill-injection (binary): /sptc:ready -> wrapped body\" ;;\n 79│    *) bad \"skill-injection did not emit wrapped body: $(printf %.60s \"$inj\")\" ;;\n\n## psyche/\n### live-relay-int.sh\n  15│# the RESIDENT runner process + nested `{id}-psyche` perch dir (FINALIZED on v0.8.1 + the adapter\n *16│# greedy-prompt fix, 2026-06-16; see the >=0.8.0 leg below). (2) RELAY — the resident listen pipe delivers the probe (BOUND/READY/<EVENT> off the child,\n  17│# unchanged across versions). The per-pulse runner command construction is additionally covered by\n  18│# claude-spt-psyche unit tests (ci/psyche/build.sh).\n  19│#\n    │...\n  31│A=claude-spt\n *32│MAN=\"$ROOT/adapter/omp-spt.toml\"\n  33│# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity\n  34│# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):\n  35│# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a\n    │...\n  37│# Option A (PREP-4): [session.psyche_init] is in the BASE manifest — NO `:live` profile. The adapter\n *38│# is BASE claude-spt; the live `api listen` COMMAND (not a composite) is what stamps state=live_agent\n  39│# and actualizes the Psyche. We still pass an explicit `--adapter claude-spt --manifest <man>` here:\n  40│# CI has no real `claude` parent process, so spt-core's bare-by-pid host_binaries resolution cannot\n  41│# fire (the anchor is `sh`/`timeout`, not `claude`). The explicit override remains valid on 0.9.0 and\n    │...\n 117│    # status (the `{id}-psyche` perch comes online; `LIVEHOST_PSYCHE:{id}` on the daemon's stderr).\n*118│    # RESOLVED (v0.8.1 + adapter greedy-prompt fix, 2026-06-16): hosting succeeds iff the daemon\n 119│    # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core\n 120│    # <0.8.1 livehost did not reconcile (no spawn at all); v0.8.1 fixed it. (2) spt-core substitutes\n*121│    # `{psyche_prompt}` into the psyche_init command STRING then whitespace-SPLITS, so the multi-word\n*122│    # prompt arrived as stray argv tokens — the runner's non-greedy --prompt rejected the 2nd word\n 123│    # (\"unknown arg\") and exited 2 instantly → the daemon recorded a phantom hosted perch (nested\n 124│    # info.json status=online, real-looking pid) with NO live process and NO psyche_host_error. The\n*125│    # runner now parses --prompt greedily (slurps trailing tokens). DETECTION: the nested {id}-psyche\n 126│    # perch does NOT surface in `endpoint list` (it lives under the parent in the owlery), so assert on\n 127│    # the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11\n 128│    # install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.\n    │...\n 137│    if resident && [ -f \"$OWL\" ]; then\n*138│      ok \"Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)\"\n 139│    else\n*140│      bad \"psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs\"\n 141│    fi\n 142│    ;;\n 143│esac\n\n## publish/\n### package-adapter.sh\n   2│# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by\n  *3│# `spt adapter add --release BigscreenVR/omp-spt` and carried by the `[update] gh_release`\n   4│# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2).\n  *5│# The release/install/update repo is `BigscreenVR/omp-spt` (renamed from spt-claude-code, U3/ADR-0005).\n   6│# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS] [impl->REQ-DIST-NAME-UNIFY]\n   7│#\n   8│#   adapter.spt (tar.gz)\n  *9│#   ├── manifest.toml                 ← SHARED, at archive root (renamed from omp-spt.toml)\n  10│#   ├── strings/                      ← SHARED, at archive root\n  11│#   ├── x86_64-pc-windows-msvc/       ← this triple's binary, mirroring the flat-root tree\n  12│#   │   ├── claude-spt.exe\n    │...\n  33│ADAPTER=\"$ROOT/adapter\"\n *34│MANIFEST=\"$ADAPTER/omp-spt.toml\"   # renamed to manifest.toml INSIDE the archive (root-only rule)\n  35│STRINGS=\"$ADAPTER/strings\"\n  36│TOOLS=\"$ROOT/tools\"\n  37│OUT=\"${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}\"   # overridable so the unit test writes to a tmp file\n    │...\n  41│# The recognized triples and where each platform's release binaries live. A native Windows build lands\n *42│# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into\n  43│# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a\n  44│# non-default layout. There is now ONE tool binary: the consolidated claude-spt crate carries all four\n  45│# subcommands — digest / psyche / post-update / translate (the last folded in at the v0.8.0 cut once\n    │...\n  50│LINUX_RELSUB=\"${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}\"\n *51│BINS=\"omp-spt\"\n  52│\n  53│# Validate the manifest first — refuse to ship an invalid adapter.\n  54│echo \"== validate manifest ==\"\n    │...\n  94│  echo \"DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'\"\n *95│  echo \"to a GitHub release on the monorepo. End users: 'spt adapter add --release BigscreenVR/omp-spt'\"\n  96│  echo \"(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.\"\n  97│  exit 0\n  98│fi\n    │...\n 120│echo \"$listing\" | grep -qx \"manifest.toml\" || { echo \"FATAL: manifest.toml not at archive root\" >&2; fatal=1; }\n*121│echo \"$listing\" | grep -q \"^$WIN_TRIPLE/omp-spt.exe$\" || { echo \"FATAL: missing $WIN_TRIPLE/ binaries\" >&2; fatal=1; }\n*122│echo \"$listing\" | grep -q \"^$LINUX_TRIPLE/omp-spt$\"   || { echo \"FATAL: missing $LINUX_TRIPLE/ binaries\" >&2; fatal=1; }\n 123│# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).\n 124│badtop=$(echo \"$listing\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\n 125│[ -n \"$badtop\" ] && { echo \"FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat\" >&2; fatal=1; }\n    │...\n 130│echo \"WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release\"\n*131│echo \"on BigscreenVR/omp-spt; end users 'spt adapter add --release BigscreenVR/omp-spt' (spt\"\n 132│echo \"v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.\"\n 133│exit 0"},"isError":false,"timestamp":1784087197866}}
{"type":"message","id":"a1e126c6","parentId":"542009ea","timestamp":"2026-07-15T03:46:37.876Z","message":{"role":"toolResult","toolCallId":"call_FTtv6BgI4ZnQYBh800p7BXUb|fc_0ca4af36969c80fa016a57029db9c08198aedbc7962064de09","toolName":"grep","content":[{"type":"text","text":"# traceable-reqs.toml\n 324|title = \"The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\\\"\\\"` for EVERY peer send and `send_landed(\\\"\\\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wro...\n*325|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator RCA directive; v0.17.3). impl: tools/claude-spt/src/hook.rs HookEnv::spt_send (SysEnv captures both streams, merged; trailing newlines stripped) + scan_and_dispatch peer-send switched from spt to spt_send. unit: hook.rs scan_classifies_send_by_stderr_token_not_empty_stdout (Recorder with_send_responder models empty-stdout + stderr-token; asserts the confirm-back body reports `delivered → lia`, never NO PERCH — FAILS on the ...\n 326|\n 327|[[requirements]]\n 328|id = \"REQ-DIST-RESUME-CONTEXT\"\n...\n 388|id = \"REQ-HAZARD-STALE-PLUGIN-RESOLUTION\"\n*389|title = \"The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispat...\n 390|required_stages = [\"doc\"]  # MINTED + doc-ACTIVATED 2026-07-02 (doyle ask off the F-024 arc: hazard writeup). doc: docs/KNOWN-HAZARDS.md §3.2. Halves already shipped ride their own REQs (reconcile = REQ-DIST-CCS-PLUGIN-FOLLOWUP + the v0.10.2 post-update work; degrade = REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP, stages [] until its guard slice). The NEW halves — stale-copy purge/flag + per-session plugin-version surfacing — are unbuilt guard-slice work: add impl/unit here when that slice starts (activate,...\n 391|\n 392|[[requirements]]\n...\n 399|title = \"The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\\\"checkpoint\\\":\\\"v1\\\",…} envelope stashes the wa...\n*400|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-03 (checkpoint-clear-race fix, CHECKPOINT-CLEAR-RACE-PLAN.md). doc: docs/KNOWN-HAZARDS.md entry (the ordering invariant + the accepted orphan-wake residual). impl: tools/claude-spt/src/translate.rs — commands_for_clear (ARM emit) + commands_for_wake (FIRE emit) replace commands_for_checkpoint, is_checkpoint_fire parses {\\\"checkpoint_fire\\\":\\\"v1\\\"}, dispatch_event threads pending_wake (arm stores wake+emits clear-only; fire d...\n 401|\n 402|[[requirements]]\n 403|id = \"REQ-HAZARD-EMPTY-RESPONSE-COMMIT\"\n...\n 428|id = \"REQ-DIST-REACHABILITY-NOTICE\"\n*429|title = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when...\n 430|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure ...\n 431|\n 432|[[requirements]]\n...\n 458|id = \"REQ-INJECT-VERIFY-ECHO\"\n*459|title = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapte...\n 460|required_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC ses...\n 461|\n 462|[[requirements]]\n...\n 474|title = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is...\n*475|required_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/se...\n 476|\n 477|[[requirements]]\n 478|id = \"REQ-OMP-NATIVE-TUI\"\n\n# tools/claude-spt/src/\n## bridge.rs\n 8|//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\n*9|//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\n 10|//!                          └  prompt / get_state                                    (to omp)\n 11|//! ```\n 12|//!\n*13|//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\n 14|//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\n 15|//! compat alias for the same entry.\n 16|//!\n...\n 28|//!\n*29|//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\n 30|//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\n 31|//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\n 32|//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\n*33|//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\n*34|//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\n 35|//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\n*36|//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\n 37|//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\n*38|//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\n 39|//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\n*40|//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\n 41|//!   before-handshake); a turn breach recovers the perch to idle and tells the sender.\n*42|//! * **MED-2**: the event loop notices a dead omp/listener child immediately (reader-thread EOF),\n 43|//!   even while idle — not on the next message.\n*44|//! * **LOW-1/2**: spt bind/state/send failures land on the operational log, and a failed turn sends\n 45|//!   the sender an explicit failure notice, never a silent empty reply.\n 46|\n 47|use serde_json::{json, Value};\n...\n 58|/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\n*59|/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\n 60|const DEFAULT_READY_TIMEOUT_S: u64 = 120;\n 61|/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\n 62|const DEFAULT_STATE_TIMEOUT_S: u64 = 30;\n...\n 65|const DEFAULT_TURN_TIMEOUT_S: u64 = 600;\n*66|/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\n 67|const DEFAULT_HEARTBEAT_S: u64 = 300;\n 68|\n*69|/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\n*70|/// message/reply/error/heartbeat lines without any env flag.\n 71|macro_rules! oplog {\n 72|    ($($a:tt)*) => {\n 73|        eprintln!(\"[omp-spt] {}\", format!($($a)*))\n...\n 135|\n*136|/// Entry for the `omp-spt` binary (HIGH-1 — argv starts right after the binary name).\n 137|pub fn run_argv<I: Iterator<Item = String>>(argv: I) -> ExitCode {\n 138|    let args = parse_args(argv);\n 139|\n...\n 154|\n*155|    // HIGH-2: put OURSELVES in a kill-on-close job object BEFORE spawning any child, so both\n 156|    // children inherit membership and the OS reaps the whole tree when the bridge dies — however it\n 157|    // dies. Best-effort: a failure is logged, never fatal (explicit kills still cover the graceful\n 158|    // path). The returned handle is deliberately held (leaked) for the process lifetime; the OS\n 159|    // closes it at process exit, which is exactly the kill trigger.\n*160|    match teardown::arm_kill_on_close() {\n 161|        Ok(()) => trace!(\"job-object armed (children die with the bridge)\"),\n*162|        Err(e) => oplog!(\"WARN: could not arm child teardown ({e}) — orphan risk on hard kill\"),\n 163|    }\n 164|\n 165|    let spt = spt_bin();\n...\n 198|\n*199|    // HIGH-4/MED-2: one event channel, fed by dedicated reader threads — every wait below is a\n 200|    // bounded recv, and a child dying is an Event even while the bridge is idle.\n 201|    let (tx, rx) = std::sync::mpsc::channel::<Event>();\n 202|    spawn_line_reader(omp_child.stdout.take(), tx.clone(), Event::OmpLine, Event::OmpEof);\n...\n 204|    // Startup gate: omp's `ready` frame within the deadline. Breaching exits loud BEFORE any bind,\n*205|    // so a failed bringup never poisons the endpoint's session ledger (MED-4 discipline).\n 206|    let ready_deadline = Duration::from_secs(env_secs(\"OMP_SPT_READY_TIMEOUT_S\", DEFAULT_READY_TIMEOUT_S));\n 207|    match await_frame(&rx, ready_deadline, |f| (f[\"type\"].as_str() == Some(\"ready\")).then_some(())) {\n 208|        Ok(()) => oplog!(\"omp ready\"),\n\n# tests/\n## bridge-acceptance.sh\n 73|\n*74|# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\n 75|if grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n 76|  echo \"ok   outbound: bridge replied to sender ($TX)\"\n 77|else\n...\n 87|\n*88|# ── HIGH-4: a hung turn breaches the deadline, sender gets a failure notice ──────\n 89|kill \"$BPID\" 2>/dev/null; sleep 1\n 90|\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n 91|ERR2=\"$work/bridge-mute.err\"\n...\n 106|if grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n*107|  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\n 108|else\n 109|  echo \"FAIL no failure notice was sent\"; fail=1\n 110|fi\n 111|\n*112|# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\n 113|if command -v powershell >/dev/null 2>&1; then\n 114|  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n 115|  if [ -n \"$WPID\" ]; then\n...\n 119|    if [ \"${SURV:-1}\" -eq 0 ]; then\n*120|      echo \"ok   hard kill reaped the child tree (no fake-omp orphan)\"\n 121|    else\n*122|      echo \"FAIL $SURV fake-omp orphan(s) survived a hard kill\"; fail=1\n 123|      powershell -NoProfile -Command \"Stop-Process -Name fake-omp -Force -ErrorAction SilentlyContinue\"\n 124|    fi\n 125|  else\n*126|    echo \"SKIP orphan check (bridge already gone)\"\n 127|  fi\n 128|else\n*129|  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\n 130|fi\n 131|\n 132|[ \"$fail\" -eq 0 ] && { echo \"BRIDGE-ACCEPTANCE OK\"; exit 0; } || { echo \"BRIDGE-ACCEPTANCE FAIL\"; exit 1; }\n\n# adapter/\n## omp-spt.toml\n 265|# and the endpoint sat deaf (probes QUEUED, 50s+). NEW tools/claude-spt/src/interrupt_watch.rs: a 15s\n*266|# heartbeat thread hosted by the persistent translation binary pulls `spt endpoint digest <id> --last 1\n 267|# --json`, detects the `[Request interrupted by user` marker (both variants — plain + \"for tool use\") on\n 268|# the latest turn, and marks idle via the bind-time carrier sid (heal-once, re-arm when the agent moves\n 269|# on; state idle is idempotent). Chain proven MANUALLY against a stuck lia first (state idle + carrier\n...\n 379|# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n*380|# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n 381|# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n*382|# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n 383|# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n*384|# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n 385|# breach exits loud BEFORE bind, so a failed bringup cannot poison the session ledger), and a\n*386|# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n 387|# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n 388|# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n 389|# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n...\n 556|[hooks.SessionEnd]\n*557|fires = \"api session-end {session_id}\"\n 558|reads = [\"session_id\"]\n 559|can_inject = false\n 560|\n...\n 806|# echoes back is a VALID verification belt — a CC-hosted session opts IN by declaring the flag =1.\n*807|# This is adapter DATA (the ON switch), NOT a spt-core requirement (per the adapter-glue boundary):\n 808|# the broker reads the injected capability at spawn and arms echo-verify for the session. Inert +\n 809|# forward-safe on cores below the W5-A seam (an unknown env var the session ignores; the broker's\n 810|# default stays OFF), so the floor is NOT bumped here — the counter-49 floor bump is doyle's\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"tools/claude-spt/src/bridge.rs, tests/bridge-acceptance.sh, adapter/omp-spt.toml, traceable-reqs.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":41,"fileCount":4,"files":["tools/claude-spt/src/bridge.rs","tests/bridge-acceptance.sh","adapter/omp-spt.toml","traceable-reqs.toml"],"fileMatches":[{"path":"tools/claude-spt/src/bridge.rs","count":20},{"path":"tests/bridge-acceptance.sh","count":8},{"path":"adapter/omp-spt.toml","count":7},{"path":"traceable-reqs.toml","count":6}],"truncated":true,"perFileLimitReached":20,"displayContent":"# traceable-reqs.toml\n 324│title = \"The tag-send classifier must read `spt send`'s result token from STDERR, not stdout. `spt send` emits its `SENT`/`QUEUED`/`DEFERRED`/`NO_PERCH:` status on STDERR (stdout is empty) — the adapter's stdout-only `HookEnv::spt` helper (which nulls stderr) therefore saw `\\\"\\\"` for EVERY peer send and `send_landed(\\\"\\\")` classified it as unreachable, so the confirm-back reported `NO PERCH (not delivered)` for messages that HAD delivered (field 2026-07-08: every hook-driven `@<peer …@>` tag-send wro...\n*325│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-08 (operator RCA directive; v0.17.3). impl: tools/claude-spt/src/hook.rs HookEnv::spt_send (SysEnv captures both streams, merged; trailing newlines stripped) + scan_and_dispatch peer-send switched from spt to spt_send. unit: hook.rs scan_classifies_send_by_stderr_token_not_empty_stdout (Recorder with_send_responder models empty-stdout + stderr-token; asserts the confirm-back body reports `delivered → lia`, never NO PERCH — FAILS on the ...\n 326│\n 327│[[requirements]]\n 328│id = \"REQ-DIST-RESUME-CONTEXT\"\n    │...\n 388│id = \"REQ-HAZARD-STALE-PLUGIN-RESOLUTION\"\n*389│title = \"The adapter stays SAFE and DIAGNOSABLE when the harness resolves a STALE cached plugin version at session boot (observed live: ENLYZEAM booted the 0.1.8 dispatch 24 min after 0.1.9 was installed — F-024 domino #1; the buggy dispatch corrupted the per-session CLAUDE_ENV_FILE, which survives every resume, so ONE bad boot poisons the session lifetime and swallowed the /clear boundary). The harness picker is not adapter-controllable; the adapter therefore (a) degrades-never-bricks under any dispat...\n 390│required_stages = [\"doc\"]  # MINTED + doc-ACTIVATED 2026-07-02 (doyle ask off the F-024 arc: hazard writeup). doc: docs/KNOWN-HAZARDS.md §3.2. Halves already shipped ride their own REQs (reconcile = REQ-DIST-CCS-PLUGIN-FOLLOWUP + the v0.10.2 post-update work; degrade = REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP, stages [] until its guard slice). The NEW halves — stale-copy purge/flag + per-session plugin-version surfacing — are unbuilt guard-slice work: add impl/unit here when that slice starts (activate,...\n 391│\n 392│[[requirements]]\n    │...\n 399│title = \"The checkpoint clear+wake must NEVER submit the wake before /clear takes effect. A single inject sequence cannot straddle a /clear (the async clear re-runs SessionStart with network I/O, and every sequence must {commit} within the 5s INJECT_COMMIT_DEADLINE), so a fixed post-/clear delay races and the wake lands in the OLD session (field-observed: the followup hit first). The macro is SPLIT and synchronized on CC's own clear-done signal: (ARM) a {\\\"checkpoint\\\":\\\"v1\\\",…} envelope stashes the wa...\n*400│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-03 (checkpoint-clear-race fix, CHECKPOINT-CLEAR-RACE-PLAN.md). doc: docs/KNOWN-HAZARDS.md entry (the ordering invariant + the accepted orphan-wake residual). impl: tools/claude-spt/src/translate.rs — commands_for_clear (ARM emit) + commands_for_wake (FIRE emit) replace commands_for_checkpoint, is_checkpoint_fire parses {\\\"checkpoint_fire\\\":\\\"v1\\\"}, dispatch_event threads pending_wake (arm stores wake+emits clear-only; fire d...\n 401│\n 402│[[requirements]]\n 403│id = \"REQ-HAZARD-EMPTY-RESPONSE-COMMIT\"\n    │...\n 428│id = \"REQ-DIST-REACHABILITY-NOTICE\"\n*429│title = \"Perched sessions are steered to BACKGROUND long-running work so the perch stays reachable — the spt reachability notice, ported from legacy owl's <owl_reachability_notice> (sibling claude_skill_owl hook_prompt.rs + hook_check.rs) and renamed to spt semantics (<spt_reachability_notice>, 'active spt perch'). Two injection sites, both perch-gated (a plain CC session is never told it has a perch): (1) UserPromptSubmit — EVERY perched turn's additionalContext closes with the general notice ('when...\n 430│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-07 (operator port ask; v0.15.3). impl: tools/claude-spt/src/hook.rs — reachability_notice (two variants) + is_agent_spawn_tool + the UPS always-append + the PreToolUse spawn-nudge wiring. unit: ups_appends_reachability_notice_on_perched_turns_only + ups_notice_rides_after_drained_messages + pre_tool_use_agent_spawn_gets_the_nudge_other_tools_do_not + agent_spawn_tool_matcher_covers_both_cc_names. int: not planned — the notice is a pure ...\n 431│\n 432│[[requirements]]\n    │...\n 458│id = \"REQ-INJECT-VERIFY-ECHO\"\n*459│title = \"The claude-spt manifest opts CC-hosted sessions into spt-core's Layer-2 inject echo-verify by declaring [env.SPT_INJECT_VERIFY_ECHO] direction=\\\"inject\\\" value=\\\"1\\\" (doyle W6 LIFECYCLE-TRUTH final wave, W6-DISPATCH-RULING item 3). W5-A shipped the broker seam as a declared-capability defaulting OFF; because CC's TUI re-renders typed text, an inject that echoes back is a valid delivery-verification belt, so a CC session opts IN via this flag. Per the adapter-glue boundary the ON switch is adapte...\n 460│required_stages = [\"unit\"]  # MINTED + ACTIVATED 2026-07-07 (doyle W6 leg; v0.15.8). Declaration-only manifest seam (the .toml is scan-invisible — same convention as REQ-DIST-SESSION-RESUME / REQ-HAZARD-PSYCHE-IDENTITY-ENV, whose manifest evidence lives in the shell test). unit: tests/manifest-shortcut.sh — the [env.SPT_INJECT_VERIFY_ECHO] block asserts (section present + direction=\"inject\" + value=\"1\"). doc rides the manifest [env] comment. int DEFERRED to the counter-49 live-verify window: a CC ses...\n 461│\n 462│[[requirements]]\n    │...\n 474│title = \"An operator Esc-interrupt must not leave an spt-hosted endpoint stuck ACTIVE/undrainable. Field-proven (lia 2026-07-08): an Esc fires NO `Stop` and NO `idle_prompt`, so the endpoint stays ACTIVE and the daemon black-holes every inbound (20+ min silence until the next UserPromptSubmit happens to drain the spool). An Esc DURING a tool call fires `PostToolUseFailure` with `is_interrupt == true` — the zero-latency heal hook. FIX: wire `PostToolUseFailure` → `handle_post_tool_use_failure`; on `is...\n*475│required_stages = [\"impl\", \"unit\"]  # MINTED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 1). Covers fix 2 (hook fast-path) AND fix 3 (the hook-independent watcher — the real fix; Step-0 proved an Esc fires NO usable hook: Stop=clean-finish-only, StopFailure=API-error-only, PostToolUseFailure=tool-only-and-unreliable, pure-thinking Esc=nothing; field lia 2026-07-08). impl: tools/claude-spt/src/hook.rs handle_post_tool_use_failure + mark_idle_receive_heal (v0.16.1 hardening: id←$SPT_ENDPOINT_ID, sid←state/se...\n 476│\n 477│[[requirements]]\n 478│id = \"REQ-OMP-NATIVE-TUI\"\n\n# tools/claude-spt/src/\n## bridge.rs\n   8│//! spt daemon ──spawn──▶ omp-spt ──stdio NDJSON──▶ omp --mode rpc\n  *9│//!                          │  spt api bind/state/session-end, spt ready, spt send   (to spt-core)\n  10│//!                          └  prompt / get_state                                    (to omp)\n  11│//! ```\n  12│//!\n *13│//! Shipped as the `omp-spt` binary (HIGH-1: a distinct basename — the process is self-identifying\n  14│//! in a task list, never confusable with a genuine claude-spt). `claude-spt bridge` remains as a\n  15│//! compat alias for the same entry.\n  16│//!\n    │...\n  28│//!\n *29│//! * **HIGH-2 teardown**: at startup the bridge assigns ITSELF to a Windows Job Object with\n  30│//!   kill-on-close, so its children (omp + `spt ready`) inherit membership and die with the bridge\n  31│//!   no matter HOW the bridge dies (broker kill, crash, taskkill without /T). Explicit `kill()`s on\n  32│//!   the graceful path remain. A dying bridge also best-effort soft-tears the perch\n *33│//!   (`spt api session-end`) so a dead endpoint never keeps advertising as listening.\n *34│//! * **HIGH-3 visibility**: an ALWAYS-ON operational log rides stderr (the broker PTY): resolved\n  35│//!   binary paths, bind/listen breadcrumbs, one line per message in / reply out, every spt/omp\n *36│//!   failure, child exits, and a periodic heartbeat — a working, idle, and wedged bridge all look\n  37│//!   different now. `OMP_SPT_DEBUG` still gates the frame-level trace.\n *38│//! * **HIGH-4 deadlines**: omp stdout and the `spt ready` stream are read by dedicated threads into\n  39│//!   ONE event channel; every wait is bounded (`ready`/`get_state` at bringup, per-turn while\n *40│//!   driving). A bringup breach exits loud BEFORE bind (no ledger poisoning — MED-4's no-bind-\n  41│//!   before-handshake); a turn breach recovers the perch to idle and tells the sender.\n *42│//! * **MED-2**: the event loop notices a dead omp/listener child immediately (reader-thread EOF),\n  43│//!   even while idle — not on the next message.\n *44│//! * **LOW-1/2**: spt bind/state/send failures land on the operational log, and a failed turn sends\n  45│//!   the sender an explicit failure notice, never a silent empty reply.\n  46│\n  47│use serde_json::{json, Value};\n    │...\n  58│/// omp start loads providers/extensions; but bounded, so a wrong binary (the OneCLI gateway alias,\n *59│/// MED-3) or an auth prompt fails LOUD instead of wedging forever.\n  60│const DEFAULT_READY_TIMEOUT_S: u64 = 120;\n  61│/// `get_state` response deadline at bringup (same env-knob family: OMP_SPT_STATE_TIMEOUT_S).\n  62│const DEFAULT_STATE_TIMEOUT_S: u64 = 30;\n    │...\n  65│const DEFAULT_TURN_TIMEOUT_S: u64 = 600;\n *66│/// Idle heartbeat cadence on the operational log (override: OMP_SPT_HEARTBEAT_S; 0 disables).\n  67│const DEFAULT_HEARTBEAT_S: u64 = 300;\n  68│\n *69│/// ALWAYS-ON operational log (HIGH-3). Rides stderr = the broker PTY, so an attached operator sees\n *70│/// message/reply/error/heartbeat lines without any env flag.\n  71│macro_rules! oplog {\n  72│    ($($a:tt)*) => {\n  73│        eprintln!(\"[omp-spt] {}\", format!($($a)*))\n    │...\n 135│\n*136│/// Entry for the `omp-spt` binary (HIGH-1 — argv starts right after the binary name).\n 137│pub fn run_argv<I: Iterator<Item = String>>(argv: I) -> ExitCode {\n 138│    let args = parse_args(argv);\n 139│\n    │...\n 154│\n*155│    // HIGH-2: put OURSELVES in a kill-on-close job object BEFORE spawning any child, so both\n 156│    // children inherit membership and the OS reaps the whole tree when the bridge dies — however it\n 157│    // dies. Best-effort: a failure is logged, never fatal (explicit kills still cover the graceful\n 158│    // path). The returned handle is deliberately held (leaked) for the process lifetime; the OS\n 159│    // closes it at process exit, which is exactly the kill trigger.\n*160│    match teardown::arm_kill_on_close() {\n 161│        Ok(()) => trace!(\"job-object armed (children die with the bridge)\"),\n*162│        Err(e) => oplog!(\"WARN: could not arm child teardown ({e}) — orphan risk on hard kill\"),\n 163│    }\n 164│\n 165│    let spt = spt_bin();\n    │...\n 198│\n*199│    // HIGH-4/MED-2: one event channel, fed by dedicated reader threads — every wait below is a\n 200│    // bounded recv, and a child dying is an Event even while the bridge is idle.\n 201│    let (tx, rx) = std::sync::mpsc::channel::<Event>();\n 202│    spawn_line_reader(omp_child.stdout.take(), tx.clone(), Event::OmpLine, Event::OmpEof);\n    │...\n 204│    // Startup gate: omp's `ready` frame within the deadline. Breaching exits loud BEFORE any bind,\n*205│    // so a failed bringup never poisons the endpoint's session ledger (MED-4 discipline).\n 206│    let ready_deadline = Duration::from_secs(env_secs(\"OMP_SPT_READY_TIMEOUT_S\", DEFAULT_READY_TIMEOUT_S));\n 207│    match await_frame(&rx, ready_deadline, |f| (f[\"type\"].as_str() == Some(\"ready\")).then_some(())) {\n 208│        Ok(()) => oplog!(\"omp ready\"),\n\n# tests/\n## bridge-acceptance.sh\n  73│\n *74│# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\n  75│if grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n  76│  echo \"ok   outbound: bridge replied to sender ($TX)\"\n  77│else\n    │...\n  87│\n *88│# ── HIGH-4: a hung turn breaches the deadline, sender gets a failure notice ──────\n  89│kill \"$BPID\" 2>/dev/null; sleep 1\n  90│\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n  91│ERR2=\"$work/bridge-mute.err\"\n    │...\n 106│if grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n*107│  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\n 108│else\n 109│  echo \"FAIL no failure notice was sent\"; fail=1\n 110│fi\n 111│\n*112│# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\n 113│if command -v powershell >/dev/null 2>&1; then\n 114│  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n 115│  if [ -n \"$WPID\" ]; then\n    │...\n 119│    if [ \"${SURV:-1}\" -eq 0 ]; then\n*120│      echo \"ok   hard kill reaped the child tree (no fake-omp orphan)\"\n 121│    else\n*122│      echo \"FAIL $SURV fake-omp orphan(s) survived a hard kill\"; fail=1\n 123│      powershell -NoProfile -Command \"Stop-Process -Name fake-omp -Force -ErrorAction SilentlyContinue\"\n 124│    fi\n 125│  else\n*126│    echo \"SKIP orphan check (bridge already gone)\"\n 127│  fi\n 128│else\n*129│  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\n 130│fi\n 131│\n 132│[ \"$fail\" -eq 0 ] && { echo \"BRIDGE-ACCEPTANCE OK\"; exit 0; } || { echo \"BRIDGE-ACCEPTANCE FAIL\"; exit 1; }\n\n# adapter/\n## omp-spt.toml\n 265│# and the endpoint sat deaf (probes QUEUED, 50s+). NEW tools/claude-spt/src/interrupt_watch.rs: a 15s\n*266│# heartbeat thread hosted by the persistent translation binary pulls `spt endpoint digest <id> --last 1\n 267│# --json`, detects the `[Request interrupted by user` marker (both variants — plain + \"for tool use\") on\n 268│# the latest turn, and marks idle via the bind-time carrier sid (heal-once, re-arm when the agent moves\n 269│# on; state idle is idempotent). Chain proven MANUALLY against a stuck lia first (state idle + carrier\n    │...\n 379│# 0.1.1 (2026-07-09): bridge HARDENING per docs/OMP-BRIDGE-FIELD-ISSUES.md (the doyle/todlando field\n*380│# incident, PR #1): the shipped binary is renamed claude-spt -> omp-spt (HIGH-1, every manifest\n 381│# command follows); the bridge arms a kill-on-close Job Object so its omp + `spt ready` children die\n*382│# with it however it dies, and soft-tears the perch on the way out (HIGH-2); an always-on\n 383│# operational log rides the broker PTY — bringup breadcrumbs, one line per message/reply, every\n*384│# failure, an idle heartbeat (HIGH-3); every omp wait is bounded — ready/get_state at bringup (a\n 385│# breach exits loud BEFORE bind, so a failed bringup cannot poison the session ledger), and a\n*386│# per-turn deadline that recovers the perch and tells the sender (HIGH-4 + MED-2 + LOW-1/2).\n 387│# Env knobs: OMP_SPT_READY_TIMEOUT_S / OMP_SPT_STATE_TIMEOUT_S / OMP_SPT_TURN_TIMEOUT_S /\n 388│# OMP_SPT_HEARTBEAT_S. Floor unchanged (0.27.0).\n 389│# 0.1.2 (2026-07-10): liaison bugfix rollup (#2/#3). The update notice's fresh-endpoint command now\n    │...\n 556│[hooks.SessionEnd]\n*557│fires = \"api session-end {session_id}\"\n 558│reads = [\"session_id\"]\n 559│can_inject = false\n 560│\n    │...\n 806│# echoes back is a VALID verification belt — a CC-hosted session opts IN by declaring the flag =1.\n*807│# This is adapter DATA (the ON switch), NOT a spt-core requirement (per the adapter-glue boundary):\n 808│# the broker reads the injected capability at spawn and arms echo-verify for the session. Inert +\n 809│# forward-safe on cores below the W5-A seam (an unknown env var the session ignores; the broker's\n 810│# default stays OFF), so the floor is NOT bumped here — the counter-49 floor bump is doyle's","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087197876}}
{"type":"message","id":"51c99c0c","parentId":"a1e126c6","timestamp":"2026-07-15T03:46:37.879Z","message":{"role":"toolResult","toolCallId":"call_xG4wC1g9XW6Vm6R6JfcvUbVW|fc_0ca4af36969c80fa016a57029db9d48198b1856d015dcdd780","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n 64|exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65|profile. (Realized here by the `claude-spt-psyche` runner.)\n 66|\n 67|**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68|parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n\n# SCOPE.md\n 203|- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every\n*204|  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands\n 205|  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded\n 206|  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was\n 207|  previously bare `claude`).\n\n# OMP-ADAPTER-PLAN.md\n 75|\n*76|| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77||---|---|---|\n 78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n 79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n...\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n 83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n 84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n 86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n 90|### Session/transcript (for the `[digest]` extractor)\n\n# traceable-reqs.toml\n 68|title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n*69|required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n 70|\n 71|[[requirements]]\n 72|id = \"REQ-DIST-HOOKS-API\"\n...\n 78|title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n*79|required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n 80|\n 81|[[requirements]]\n 82|id = \"REQ-DIST-MANIFEST-SCHEMA\"\n 83|title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n*84|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n 85|\n 86|[[requirements]]\n 87|id = \"REQ-DIST-SHORTCUT-BASENAME\"\n...\n 97|id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n*98|title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n*99|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100|\n 101|[[requirements]]\n 102|id = \"REQ-DIST-DIGEST-FETCHER\"\n 103|title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104|required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105|\n 106|[[requirements]]\n 107|id = \"REQ-DIST-WHOAMI-JSON\"\n...\n 112|id = \"REQ-SKILL-LIVE\"\n*113|title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115|\n 116|[[requirements]]\n 117|id = \"REQ-UPS-INJECTION\"\n...\n 157|title = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\n*158|required_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n 159|\n 160|[[requirements]]\n 161|id = \"REQ-HAZARD-PERCH-COLLISION\"\n*162|title = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\n 163|required_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n 164|\n 165|[[requirements]]\n...\n 171|id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n*172|title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173|required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174|\n 175|[[requirements]]\n 176|id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n...\n 187|title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189|\n 190|[[requirements]]\n 191|id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n 192|title = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to ...\n*193|required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-e...\n 194|\n 195|[[requirements]]\n 196|id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n...\n 231|# owlery::claude_projects_root ($CLAUDE_CONFIG_DIR/projects || $HOME/.claude/projects).\n*232|# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml\n 233|# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).\n 234|# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves\n 235|# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).\n...\n 252|title = \"/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.\"\n*253|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by...\n 254|\n 255|[[requirements]]\n 256|id = \"REQ-SETUP-CCS\"\n 257|title = \"/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).\"\n*258|required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME d...\n 259|\n 260|[[requirements]]\n 261|id = \"REQ-SETUP-ACTIVATE\"\n 262|title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n*263|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264|\n 265|[[requirements]]\n 266|id = \"REQ-DIST-ADAPTER-RELEASE\"\n*267|title = \"The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)\"\n*268|required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml name...\n 269|\n 270|[[requirements]]\n 271|id = \"REQ-DIST-ADAPTER-PEROS\"\n\n# docs/\n## KNOWN-HAZARDS.md\n 49|  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n*50|- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n 51|  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n 52|  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n 53|  A test asserts the stdin path is honored (no argv `/`-token dependency).\n...\n 72|- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n*73|  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n 74|  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n 75|  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n*76|  overrides both to a `sptc-ci-` id and never emits a live id.\n 77|- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n 78|  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n 79|  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n...\n 105|  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n*106|  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n 107|  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n 108|  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n 109|  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n...\n 225|  side, but the adapter must not hand the child a foreign identity in the first place.\n*226|- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n 227|  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n 228|  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229|  child that runs harness hooks, it needs the same scrub.\n...\n 392|  shape can never change unilaterally.\n*393|- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n 394|  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n 395|  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n 396|  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n\n## adr/\n### 0001-distribution-splits-by-volatility.md\n 97|  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n*98|  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n 99|  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100|  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101|- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 27|\n*28|**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n 29|`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n 30|(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n 31|substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n...\n 75|\n*76|Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\n 77|the `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\n 78|unescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\n 79|gate, Psyche routing), never silently stripped. Covered by\n### 0004-checkpoint-via-self-send-loopback.md\n 69|- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n*70|  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n 71|  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n 72|- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n 73|  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n### 0005-name-unification.md\n 6|accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n*7|`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n 8|\n 9|## Context\n 10|\n...\n 25|\n*26|- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n 27|- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n 28|- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29|\n### 0006-one-command-update-and-consolidated-binary.md\n 36|- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37|- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38|\n 39|### Binary consolidation\n 40|\n...\n 42|\n*43|- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n 44|- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45|\n 46|The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n\n# tests/\n## acceptance-harness.sh\n 11|sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n*12|[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n*13|[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n*14|[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n 15|[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n 16|\n 17|# is_disposable: accepts a minted id, rejects any live name.\n*18|sptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\n 19|sptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\n 20|sptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n 21|\n## manifest-shortcut.sh\n 85|if grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"{adapter_dir}/omp-spt translate\"' \"$MANIFEST\"; then echo \"ok   [message-idle-translation-binary].command = translate\"; else echo \"FAIL idle-translate command missing/wrong\"; fail=1; fi\n*86|if grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*\"cc-spt-idle-translate\"' \"$MANIFEST\"; then echo \"FAIL deprecated idle-translate path still present\"; fail=1; else echo \"ok   no deprecated idle-translate path (command-only)\"; fi\n 87|\n 88|# ── no shipped profile overlays (ccs was CC-specific; omp profiles ride `-- --profile`) ───────────\n 89|if grep -Eq '^\\[profiles\\.' \"$MANIFEST\"; then echo \"FAIL a shipped [profiles.*] table lingers (ccs was removed for omp)\"; fail=1; else echo \"ok   no shipped profile overlays\"; fi\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md, SCOPE.md, OMP-ADAPTER-PLAN.md, docs/adr, docs/KNOWN-HAZARDS.md, docs/OMP-BRIDGE-FIELD-ISSUES.md, traceable-reqs.toml, tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":44,"fileCount":12,"files":["CONTEXT.md","SCOPE.md","OMP-ADAPTER-PLAN.md","docs/adr/0001-distribution-splits-by-volatility.md","docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","docs/adr/0004-checkpoint-via-self-send-loopback.md","docs/adr/0005-name-unification.md","docs/adr/0006-one-command-update-and-consolidated-binary.md","docs/KNOWN-HAZARDS.md","traceable-reqs.toml","tests/acceptance-harness.sh","tests/manifest-shortcut.sh"],"fileMatches":[{"path":"CONTEXT.md","count":1},{"path":"SCOPE.md","count":1},{"path":"OMP-ADAPTER-PLAN.md","count":3},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","count":1},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","count":2},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","count":1},{"path":"docs/adr/0005-name-unification.md","count":2},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","count":2},{"path":"docs/KNOWN-HAZARDS.md","count":6},{"path":"traceable-reqs.toml","count":20},{"path":"tests/acceptance-harness.sh","count":4},{"path":"tests/manifest-shortcut.sh","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# CONTEXT.md\n 64│exits at session end. A *ready* agent has no Psyche — live-vs-ready is the command, not a\n*65│profile. (Realized here by the `claude-spt-psyche` runner.)\n 66│\n 67│**commune** — the context-delta a Psyche writes on each pulse: a brief that lets the\n 68│parent agent resume coherently after a context wipe (`/clear` / compact). A file-drop the\n\n# SCOPE.md\n 203│- **Psyche runs sandboxed at legacy-owl parity — LOCKED (→ `docs/adr/0003-*`).** Every\n*204│  `claude-spt-psyche` turn (seed + each pulse): `--tools Read,Edit,Write --disable-slash-commands\n 205│  --dangerously-skip-permissions --model sonnet --fallback-model opus --effort medium`. Bounded\n 206│  blast radius + no detached-deadlock; mirrors `claude_skill_owl`. Closes a parity gap (psyche was\n 207│  previously bare `claude`).\n\n# OMP-ADAPTER-PLAN.md\n 75│\n*76│| Seam (manifest `adapter/claude-spt.toml` → `adapter/omp-spt.toml`) | claude-spt | omp-spt |\n 77│|---|---|---|\n 78│| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n 79│| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n   │...\n 81│| `[adapter].kind` | `harness` | `harness` (unchanged) |\n*82│| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n 83│| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n 84│| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85│| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n 86│| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87│| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n 88│| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89│\n 90│### Session/transcript (for the `[digest]` extractor)\n\n# traceable-reqs.toml\n  68│title = \"SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)\"\n *69│required_stages = [\"doc\", \"impl\"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.\n  70│\n  71│[[requirements]]\n  72│id = \"REQ-DIST-HOOKS-API\"\n    │...\n  78│title = \"Hook LOGIC lives in the consolidated `claude-spt hook <event>` binary (so it rides `spt adapter update`, not a cplugs republish); the plugin ships only a static-forever hooks.json + dispatch wrapper that resolves the binary via the lazily-substituted `[strings].hook_cmd = {adapter_dir}/claude-spt hook` (ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy [strings] subst). D1 / v0.9.0.\"\n *79│required_stages = [\"doc\", \"impl\", \"unit\"]  # D1 — ACTIVATED 2026-06-28. doc: ADR-0006 (binary consolidation + ask#1 resolution) + UPDATE-NAMING-DOYLE-ASKS Ask 1 + V09-PLAN. impl: tools/claude-spt/src/hook.rs (the `hook <event>` subcommand — faithful port of the 8 wrappers + _common.sh) + main.rs Sub::Hook dispatch + plugin/sptc/hooks/dispatch.sh (static-forever resolve+exec, bootstrap-on-session-start, $SPTC_HOOK_BIN cache, --host-pid) + plugin/sptc/hooks/hooks.json (every event -> dispatch.sh <event...\n  80│\n  81│[[requirements]]\n  82│id = \"REQ-DIST-MANIFEST-SCHEMA\"\n  83│title = \"CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)\"\n *84│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-...\n  85│\n  86│[[requirements]]\n  87│id = \"REQ-DIST-SHORTCUT-BASENAME\"\n    │...\n  97│id = \"REQ-DIST-DIGEST-EXTRACTOR\"\n *98│title = \"The claude-spt [digest] extractor (claude-spt-digest) maps Claude Code's JSONL transcript to the published digest-record NDJSON contract (role∈{input,agent,tool}; text/tool/ts), emitting RAW records for spt-core's renderer\"\n *99│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ...\n 100│\n 101│[[requirements]]\n 102│id = \"REQ-DIST-DIGEST-FETCHER\"\n 103│title = \"The [digest] seam runs under strategy=\\\"fetcher\\\" (spt-core v0.19.0, #17): the extractor IS the locator — `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}` locates <config-root>/projects/<cwd-slug>/<session_id>.jsonl itself (no spt-core pre-read, no `source`, no harness slug in the catalog); the NEW [env.CLAUDE_CONFIG_DIR] direction=\\\"read\\\" capture (bind-time env → info.json.read_env → fill → ~expand, value=\\\"~/.claude\\\" fallback) delivers the ccs-relocated tre...\n*104│required_stages = [\"impl\", \"unit\", \"int\"]  # impl: adapter/claude-spt.toml [digest] strategy=\"fetcher\" + [env.CLAUDE_CONFIG_DIR] (declaration; .toml unscanned so the scannable impl is the extractor's locator chain: tools/claude-spt/src/digest.rs resolve_projects_root + --config-dir parse). unit: digest.rs cargo tests (config-dir parse, precedence config-dir→env→--in→~/.claude default, empty-capture guard) via ci/digest/build.sh. int: `spt adapter digest-proof claude-spt` on the live 0.19.0 binary r...\n 105│\n 106│[[requirements]]\n 107│id = \"REQ-DIST-WHOAMI-JSON\"\n    │...\n 112│id = \"REQ-SKILL-LIVE\"\n*113│title = \"/sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero...\n*114│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command cons...\n 115│\n 116│[[requirements]]\n 117│id = \"REQ-UPS-INJECTION\"\n    │...\n 157│title = \"Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)\"\n*158│required_stages = [\"doc\", \"impl\", \"int\"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).\n 159│\n 160│[[requirements]]\n 161│id = \"REQ-HAZARD-PERCH-COLLISION\"\n*162│title = \"The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)\"\n 163│required_stages = [\"unit\"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under \"perri\" and displaced the live poll stream.\n 164│\n 165│[[requirements]]\n    │...\n 171│id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n*172│title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173│required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174│\n 175│[[requirements]]\n 176│id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n    │...\n 187│title = \"The psyche runner is a run-to-completion EPHEMERAL shim, never a resident driver (PSYCHE-EPHEMERAL-PLAN.md; adapter half of the joint F-030 wave, frozen against doyle's W1/W2 psyche-turn contract 944a74b0 + the evidence answers). Contract: one invocation = at most ONE headless claude turn, then exit. ONE spawned role ([session.psyche_resume]) with TWO key shapes, discriminated on the mind FILE's content emptiness (W4, {psyche_context_file}): a NON-EMPTY mind file (a FreshWithPreload turn — cor...\n*188│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (build phase of the gated plan; registered before satisfying, rule 3). impl: tools/claude-spt/src/psyche.rs — the ephemeral shim (Args parse, is_first_turn/compose_prompt/turn_cmd, plan planner, classify_failure exit-95 discriminator, run_claude capture + ack, run() wiring) + adapter/claude-spt.toml [session.psyche_resume] table (+ [session.psyche_init] gate-only). unit: all_three_keys_required_order_independent, missing_any_required_f...\n 189│\n 190│[[requirements]]\n 191│id = \"REQ-PSYCHE-CONTEXT-FILE-INDIRECTION\"\n 192│title = \"The composed mind reaches the ephemeral psyche shim as a FILE, not an argv value (W4 contract amendment, doyle 9e6863d0, 2026-07-04). {psyche_context} (an argv value) is REPLACED by {psyche_context_file} — a path, ALWAYS filled, to a core-written mind file in the psyche's nested perch dir. Rationale: a ~20KB composed mind on argv risks the win32 ~32k CreateProcess cmdline cap; a file-always path kills the cliff with ONE code path (no inline-small/file-large branch). The shim READS the file to ...\n*193│required_stages = [\"impl\", \"unit\"]  # MINTED + ACTIVATED 2026-07-04 (W4 amendment; adapter builds to the frozen shape ahead of todlando's core W4). impl: tools/claude-spt/src/psyche.rs — Args.psyche_context_file (required, always-filled) + read_context() + run()'s read-file prefix (read-fail → generic keep-custody exit before spawn) + adapter/claude-spt.toml both psyche roles' command/keys use {psyche_context_file}. unit: reads_the_mind_file_and_content_drives_the_discriminator (real temp file: non-e...\n 194│\n 195│[[requirements]]\n 196│id = \"REQ-HAZARD-HOOKCMD-DISPATCH-LOCKSTEP\"\n    │...\n 231│# owlery::claude_projects_root ($CLAUDE_CONFIG_DIR/projects || $HOME/.claude/projects).\n*232│# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml\n 233│# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).\n 234│# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves\n 235│# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).\n    │...\n 252│title = \"/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.\"\n*253│required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the subnet-onboarding step (detect via `spt subnet status`; branch create/join/show-code; delegate to /sptc:subnet for full verb guidance; note elevation: Windows UAC | Linux desktop pkexec/polkit | Linux TTY sudo | headless -> print the command for the user). Doc-class instruction prose, no adapter-side code (same model as REQ-SETUP-ACTIVATE/REQ-SETUP-CCS). The subnet verbs themselves are wrapped by...\n 254│\n 255│[[requirements]]\n 256│id = \"REQ-SETUP-CCS\"\n 257│title = \"/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).\"\n*258│required_stages = [\"doc\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME d...\n 259│\n 260│[[requirements]]\n 261│id = \"REQ-SETUP-ACTIVATE\"\n 262│title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n*263│required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264│\n 265│[[requirements]]\n 266│id = \"REQ-DIST-ADAPTER-RELEASE\"\n*267│title = \"The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/claude-spt` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)\"\n*268│required_stages = [\"doc\", \"impl\", \"unit\", \"int\"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml name...\n 269│\n 270│[[requirements]]\n 271│id = \"REQ-DIST-ADAPTER-PEROS\"\n\n# docs/\n## KNOWN-HAZARDS.md\n  49│  take such an argument uses a stdin/`--message-file` transport or `MSYS_NO_PATHCONV=1`.\n *50│- **Mapping / notes:** the hook wrappers (`plugin/sptc/hooks/*.sh`) are immune by construction —\n  51│  they parse the CC hook payload from stdin (`json_str`), not from argv. The invariant is the\n  52│  *commitment* to keep it that way (and to apply it to any future `/sptc:*` arg-taking surface).\n  53│  A test asserts the stdin path is honored (no argv `/`-token dependency).\n    │...\n  72│- **Invariant:** Every nested SUT the acceptance harness spawns MUST run under a **disposable\n *73│  identity** distinct from any live agent — `SPT_AGENT_ID=sptc-ci-<n>` (and the matching\n  74│  `OWL_SESSION_ID`), never a live agent name. The harness MUST set this for every spawn; it MUST\n  75│  NOT inherit the operator's `SPT_AGENT_ID`/`OWL_SESSION_ID`. A test asserts the harness always\n *76│  overrides both to a `sptc-ci-` id and never emits a live id.\n  77│- **Mapping / notes:** `ci/acceptance/lib.sh` `sptc_ci_identity` mints the disposable id and\n  78│  exports it into the SUT env; `ci/acceptance/run-acceptance.sh` spawns `claude -p` only through\n  79│  that env. The deterministic guard lives in `tests/acceptance-harness.sh` (no real `claude`\n    │...\n 105│  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a\n*106│  manifest property — `adapter/claude-spt.toml` `[session.self].command` and\n 107│  `[profiles.ccs.session.self].command` must both contain `--dangerously-skip-permissions`; a unit\n 108│  assertion over the manifest covers it. The legacy sibling `claude_skill_owl`\n 109│  (`src/live/wrapper/claude.rs`) applies the identical flag to init/resume/final, which is why this\n    │...\n 225│  side, but the adapter must not hand the child a foreign identity in the first place.\n*226│- **Mapping / notes:** `adapter/claude-spt.toml` `env_remove` on BOTH `[session.psyche_init]` and\n 227│  `[session.psyche_resume]` (the declarations); binding evidence = the env-scrub block in\n 228│  `tests/manifest-shortcut.sh` (loops over both roles). If a future role spawns any other detached\n 229│  child that runs harness hooks, it needs the same scrub.\n    │...\n 392│  shape can never change unilaterally.\n*393│- **Mapping / notes:** the two halves live in `plugin/sptc/hooks/dispatch.sh` (cplugs-shipped,\n 394│  slow channel) and `tools/claude-spt/src/{main,hook}.rs` (adapter-shipped, fast channel) — the\n 395│  asymmetric ship cadence IS the hazard. A `claude-spt post-update` reconcile of the LIVE plugin\n 396│  cache dir (including ccs instance roots `~/.ccs/instances/<x>/plugins/…`, suspected missed today —\n\n## adr/\n### 0001-distribution-splits-by-volatility.md\n  97│  `/spt:version`), **not** the marketplace `plugin.json` version. The CC adapter manifest\n *98│  (`adapter/claude-spt.toml`) is authored against spt-core's **published** `manifest.schema.json`\n  99│  (vendored at `adapter/manifest.schema.json`) and CI-gated against it (`ci/manifest/`). These are distinct numbers on\n 100│  independent schedules (see `docs/RELEASE-RUNBOOK.md`).\n 101│- New obligation: a dual update path kept in sync — file-pull (real) + a cautionary\n### 0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\n 27│\n*28│**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to\n 29│`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input\n 30│(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`\n 31│substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).\n   │...\n 75│\n*76│Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read\n 77│the `from` attr → `<sptc_messages from=\"<sender>\">`, decode the body (`<br>` → newline, then entity\n 78│unescape `&lt; &gt; &quot;` then `&amp;` **last**). **Sender preserved** (reply-correlation: access\n 79│gate, Psyche routing), never silently stripped. Covered by\n### 0004-checkpoint-via-self-send-loopback.md\n 69│- **New surfaces:** `[hooks.PostToolUse]` (manifest + hooks.json) + the detector hook; a checkpoint\n*70│  branch in `cc-spt-idle-translate` (wire-sentinel filter → macro + custom-wake parse); `/sptc:commune`\n 71│  becomes a full-fat skill doc (+ `--checkpoint` + hint) so live agents are natively aware.\n 72│- **New/extended requirements to mint:** `REQ-DIST-CHECKPOINT-MACRO`, `REQ-DIST-RESUME-CONTEXT`\n 73│  (SessionStart `psyche-download` wiring), extend `REQ-DIST-IDLE-TRANSLATE` (multi-line envelope).\n### 0005-name-unification.md\n  6│accepted (2026-06-24); repo rename executed 2026-06-25 (U3 — `SaberMage/spt-claude-code` →\n *7│`SaberMage/claude-spt` via `gh repo rename`; all owner-qualified refs flipped).\n  8│\n  9│## Context\n 10│\n   │...\n 25│\n*26│- **spt-core-facing identity = `claude-spt`** — the **repo** (rename `spt-claude-code` → `claude-spt`), the **adapter** (unchanged), and the **consolidated binary** (see ADR-0006). After the rename, install and update both read `claude-spt`: `spt adapter add --release SaberMage/claude-spt` ↔ `spt adapter update claude-spt`.\n 27│- **Claude-Code-facing identity = `spt`** — the **plugin** and its **skills** (`/spt:*`), reached via the already-planned `sptc`→`spt` succession (a single `s/sptc/spt/` substitution), gated on retiring legacy owl's cplugs `spt` plugin (two plugins cannot share the name). Until owl retires, the plugin stays `sptc` / `/sptc:*`.\n 28│- **launcher shortcut stays `cc`** (`cc-<id>`) — decoupled from the plugin name, unchanged (per ADR-0001).\n 29│\n### 0006-one-command-update-and-consolidated-binary.md\n 36│- **\"Install is the first update\"** is docs-confirmed, so `spt adapter add` conducts the same `[update]` flow — one acquisition bootstraps the adapter.\n*37│- **Symmetric levers:** install = `spt adapter add --release SaberMage/claude-spt`, update = `spt adapter update claude-spt` (the verbs mirror). A README **copy-paste agent prompt** is an optional skin over the install command (the casual-user onboarding), with **platform-specific chains** (cmd / PowerShell / bash) that check-for / install spt-core (claude-spt may be a user's first exposure to spt-core) then `adapter add`.\n 38│\n 39│### Binary consolidation\n 40│\n   │...\n 42│\n*43│- **Now** (no spt-core dependency — these seams take a command string): `claude-spt digest` (`[digest].extractor`), `claude-spt psyche` (`[session.psyche_init]`), `claude-spt post-update` (the delegated update step). Drops the fat `.spt` from 3 binaries/triple toward 1, dedupes deps, and retires the odd-one-out `cc-spt-idle-translate` name.\n 44│- **Later** (gated on doyle ask #3): fold in `claude-spt translate` once `[message-idle-translation-binary]` accepts a command/subcommand rather than a bare `path`; and `claude-spt hook <event>` if generic hook dispatch (ask #1) lands.\n 45│\n 46│The **post-update step is cross-platform** because it is a compiled subcommand of this binary, **not** a `.sh` (Windows has no bash; a stray script would also stress spt-core's archive handling). It runs `claude plugin add|update <plugin>` + prints the notice; it cannot run `/reload-plugins` (TUI).\n\n# tests/\n## acceptance-harness.sh\n 11│sptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n*12│[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n*13│[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n*14│[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n 15│[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n 16│\n 17│# is_disposable: accepts a minted id, rejects any live name.\n*18│sptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\n 19│sptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\n 20│sptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n 21│\n## manifest-shortcut.sh\n 85│if grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"{adapter_dir}/omp-spt translate\"' \"$MANIFEST\"; then echo \"ok   [message-idle-translation-binary].command = translate\"; else echo \"FAIL idle-translate command missing/wrong\"; fail=1; fi\n*86│if grep -Eq '^[[:space:]]*path[[:space:]]*=[[:space:]]*\"cc-spt-idle-translate\"' \"$MANIFEST\"; then echo \"FAIL deprecated idle-translate path still present\"; fail=1; else echo \"ok   no deprecated idle-translate path (command-only)\"; fi\n 87│\n 88│# ── no shipped profile overlays (ccs was CC-specific; omp profiles ride `-- --profile`) ───────────\n 89│if grep -Eq '^\\[profiles\\.' \"$MANIFEST\"; then echo \"FAIL a shipped [profiles.*] table lingers (ccs was removed for omp)\"; fail=1; else echo \"ok   no shipped profile overlays\"; fi","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087197879}}
{"type":"message","id":"8dd0c50a","parentId":"51c99c0c","timestamp":"2026-07-15T03:46:37.888Z","message":{"role":"toolResult","toolCallId":"call_HLKQh59YXHhgV186PHHZwTw5|fc_0ca4af36969c80fa016a57029db9ac81989c2088c6242fe954","toolName":"read","content":[{"type":"text","text":"fn parse_events(raw: &str) -> Vec<(Option<String>, String)> {\n…\n    while let Some(start) = rest.find(\"<EVENT\") {\n…\n        if etype.as_deref() == Some(\"msg\") {\n…\n        }\n        let consumed = start + body_start + close + \"</EVENT>\".len();\n        rest = &rest[consumed..];\n    }\n    out\n}\n\n/// Extract `name=\"value\"` from an opening-tag substring. Space-anchored so `from` never matches a\n/// substring of another attribute (mirrors hook.rs's EVENT attribute reader).\nfn attr_value(open_tag: &str, name: &str) -> Option<String> {\n    let needle = format!(\" {name}=\\\"\");\n    let i = open_tag.find(&needle)? + needle.len();\n    let end = open_tag[i..].find('\"')? + i;\n    Some(open_tag[i..end].to_string())\n}\n\n/// `spt api state <busy|idle> <id>` with auth. Prefers the bind `--token` (the reliable credential);\n/// falls back to `--session-id` when no token was minted. Failures LAND ON THE LOG (LOW-1) — state\n/// is advisory, but an auth refusal here is the first symptom of a wedged perch.\nfn set_state(spt: &str, id: &str, sid: &str, token: Option<&str>, state: &str) {\n    let mut a = vec![\"api\", \"--adapter\", ADAPTER, \"state\", state, id];\n    match token {\n        Some(t) => {\n            a.push(\"--token\");\n            a.push(t);\n        }\n        None => {\n            a.push(\"--session-id\");\n            a.push(sid);\n        }\n    }\n    match spt_call(spt, &a, None) {\n        Some(out) if out.contains(\"REFUSED\") || out.contains(\"FAIL\") => {\n            oplog!(\"state {state} REFUSED: {}\", first_line(&out));\n        }\n        Some(_) => {}\n        None => oplog!(\"state {state}: spt could not be spawned\"),\n    }\n}\n\n/// Invoke `spt` with args (optional stdin body), returning trimmed stdout+stderr, or `None` on spawn\n/// failure. Both streams are captured because `spt send`'s delivery token rides stderr (F-031).\nfn spt_call(spt: &str, args: &[&str], stdin: Option<&str>) -> Option<String> {\n    let mut cmd = Command::new(spt);\n    cmd.args(args);\n    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());\n    cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() });\n    let mut child = cmd.spawn().ok()?;\n    if let Some(s) = stdin {\n        if let Some(mut si) = child.stdin.take() {\n            let _ = si.write_all(s.as_bytes());\n        }\n    }\n    let out = child.wait_with_output().ok()?;\n    let mut s = String::from_utf8_lossy(&out.stdout).into_owned();\n    s.push_str(&String::from_utf8_lossy(&out.stderr));\n    while s.ends_with('\\n') || s.ends_with('\\r') {\n        s.pop();\n    }\n    Some(s)\n}\n\n/// Resolve the `spt` binary: PATH first (post-bootstrap), then known install locations. Mirrors\n/// hook.rs's `SysEnv::spt_bin` (kept independent to preserve this module's self-contained ethos).\nfn spt_bin() -> String {\n    if which(\"spt\") {\n        return \"spt\".to_string();\n    }\n    let home = std::env::var(\"HOME\").unwrap_or_default();\n    let local = std::env::var(\"LOCALAPPDATA\").unwrap_or_default();\n    for p in [\n        format!(\"{home}/.local/bin/spt\"),\n        format!(\"{local}/spt-core/bin/spt.exe\"),\n        format!(\"{home}/AppData/Local/spt-core/bin/spt.exe\"),\n    ] {\n        if std::path::Path::new(&p).is_file() {\n            return p;\n        }\n    }\n    \"spt\".to_string()\n}\n\n/// Resolve the oh-my-pi `omp` binary. IMPORTANT: PATH is checked LAST — on the Librarian host `omp`\n/// on PATH is the OneCLI *gateway* alias (a vaulted-API proxy), a name collision with the coding\n/// agent. So an explicit `OMP_SPT_OMP_BIN` override wins, then the known install path, and only then\n/// PATH. Shared with the psyche/echo-commune omp shims. The resolved path is always on the\n/// operational log, and the bringup ready-deadline turns a wrong-binary spawn into a loud failure\n/// instead of a wedge (MED-3 mitigation). [OMP-ADAPTER-PLAN.md host note]\npub(crate) fn omp_bin() -> String {\n    if let Ok(p) = std::env::var(\"OMP_SPT_OMP_BIN\") {\n        if !p.is_empty() {\n            return p;\n        }\n    }\n    let home = std::env::var(\"HOME\").unwrap_or_default();\n    let local = std::env::var(\"LOCALAPPDATA\").unwrap_or_default();\n    for p in [\n        format!(\"{local}/omp/omp.exe\"),\n        format!(\"{home}/AppData/Local/omp/omp.exe\"),\n        format!(\"{home}/.local/bin/omp\"),\n    ] {\n        if std::path::Path::new(&p).is_file() {\n            return p;\n        }\n    }\n    \"omp\".to_string() // last resort; may be the gateway alias — prefer OMP_SPT_OMP_BIN\n}\n\n/// Is `name` resolvable on PATH? (cross-platform `command -v`).\nfn which(name: &str) -> bool {\n    let path = std::env::var_os(\"PATH\").unwrap_or_default();\n    let exts: Vec<String> = if cfg!(windows) {\n        std::env::var(\"PATHEXT\")\n            .unwrap_or_else(|_| \".EXE;.CMD;.BAT;.COM\".into())\n            .split(';')\n            .map(|e| e.to_ascii_lowercase())\n            .collect()\n    } else {\n        vec![String::new()]\n    };\n    for dir in std::env::split_paths(&path) {\n        if dir.join(name).is_file() {\n            return true;\n        }\n        for ext in &exts {\n            if !ext.is_empty() && dir.join(format!(\"{name}{ext}\")).is_file() {\n                return true;\n            }\n        }\n    }\n    false\n}\n\n/// HIGH-2 — OS-level child-tree teardown.\n///\n/// Windows: assign the CURRENT process to a Job Object created with\n/// `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. Children spawned afterwards inherit job membership, so the\n/// OS terminates the whole tree when the job's last handle closes — which happens when this process\n/// exits, HOWEVER it exits (broker kill, crash, taskkill without /T). Hand-rolled minimal FFI: the\n/// crate stays dependency-light (serde_json only) by design.\n///\n/// Unix: no-op today — the graceful-path kills cover the common case, and the field incident\n/// platform is Windows. (A process-group + signal handler is the follow-on; tracked in\n/// docs/OMP-BRIDGE-FIELD-ISSUES.md HIGH-2.)\nmod teardown {\n    #[cfg(windows)]\n    pub fn arm_kill_on_close() -> Result<(), String> {\n        use std::ffi::c_void;\n        type Handle = *mut c_void;\n        type Bool = i32;\n        #[repr(C)]\n        #[derive(Default)]\n        struct JobObjectBasicLimitInformation {\n            per_process_user_time_limit: i64,\n            per_job_user_time_limit: i64,\n            limit_flags: u32,\n            minimum_working_set_size: usize,\n            maximum_working_set_size: usize,\n            active_process_limit: u32,\n            affinity: usize,\n            priority_class: u32,\n            scheduling_class: u32,\n        }\n        #[repr(C)]\n        #[derive(Default)]\n        struct IoCounters {\n            read_operation_count: u64,\n            write_operation_count: u64,\n            other_operation_count: u64,\n            read_transfer_count: u64,\n            write_transfer_count: u64,\n            other_transfer_count: u64,\n        }\n        #[repr(C)]\n        #[derive(Default)]\n        struct JobObjectExtendedLimitInformation {\n            basic_limit_information: JobObjectBasicLimitInformation,\n            io_info: IoCounters,\n            process_memory_limit: usize,\n            job_memory_limit: usize,\n            peak_process_memory_used: usize,\n            peak_job_memory_used: usize,\n        }\n        const JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS: i32 = 9;\n        const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: u32 = 0x2000;\n        #[link(name = \"kernel32\")]\n        extern \"system\" {\n            fn CreateJobObjectW(attrs: *mut c_void, name: *const u16) -> Handle;\n            fn SetInformationJobObject(\n                job: Handle,\n                class: i32,\n                info: *mut c_void,\n                len: u32,\n            ) -> Bool;\n            fn AssignProcessToJobObject(job: Handle, process: Handle) -> Bool;\n            fn GetCurrentProcess() -> Handle;\n            fn GetLastError() -> u32;\n        }\n        unsafe {\n            let job = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());\n            if job.is_null() {\n                return Err(format!(\"CreateJobObjectW failed (err {})\", GetLastError()));\n            }\n            let mut info = JobObjectExtendedLimitInformation::default();\n            info.basic_limit_information.limit_flags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;\n            let ok = SetInformationJobObject(\n                job,\n                JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS,\n                &mut info as *mut _ as *mut c_void,\n                std::mem::size_of::<JobObjectExtendedLimitInformation>() as u32,\n            );\n            if ok == 0 {\n                return Err(format!(\"SetInformationJobObject failed (err {})\", GetLastError()));\n            }\n            if AssignProcessToJobObject(job, GetCurrentProcess()) == 0 {\n                return Err(format!(\"AssignProcessToJobObject failed (err {})\", GetLastError()));\n            }\n            // The job handle is intentionally NOT closed: it lives for the process lifetime, and\n            // the OS closing it at process exit is exactly the kill-on-close trigger.\n            Ok(())\n        }\n    }\n\n    #[cfg(not(windows))]\n    pub fn arm_kill_on_close() -> Result<(), String> {\n        Err(\"no-op on this platform (unix process-group teardown is a tracked follow-on)\".into())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use serde_json::json;\n    use std::collections::VecDeque;\n    use std::sync::mpsc::channel;\n\n    // ── EVENT-envelope parsing (unchanged contract) ──────────────────────────\n\n    #[test]\n    fn parses_single_event() {\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">hello there</EVENT>\";\n        assert_eq!(parse_events(raw), vec![(Some(\"doyle\".into()), \"hello there\".into())]);\n    }\n\n    #[test]\n    fn parses_multiple_events_and_preserves_order() {\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"a\\\">one</EVENT><EVENT type=\\\"msg\\\" from=\\\"b\\\">two</EVENT>\";\n        assert_eq!(\n            parse_events(raw),\n            vec![(Some(\"a\".into()), \"one\".into()), (Some(\"b\".into()), \"two\".into())]\n        );\n    }\n\n    #[test]\n    fn event_without_from_yields_none_sender() {\n        let raw = \"<EVENT type=\\\"msg\\\">anon body</EVENT>\";\n        assert_eq!(parse_events(raw), vec![(None, \"anon body\".into())]);\n    }\n\n    #[test]\n    fn empty_and_non_event_drain_is_empty() {\n        assert!(parse_events(\"\").is_empty());\n        assert!(parse_events(\"no perch\\n\").is_empty());\n    }\n\n    #[test]\n    fn non_msg_event_types_are_filtered_out() {\n        let notify = \"<EVENT type=\\\"notify\\\" from=\\\"spt-update\\\" subnet=\\\"Phils-Den\\\">update available</EVENT>\";\n        assert!(parse_events(notify).is_empty());\n        let mixed = format!(\"{notify}<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">real work</EVENT>\");\n        assert_eq!(parse_events(&mixed), vec![(Some(\"doyle\".into()), \"real work\".into())]);\n    }\n\n    #[test]\n    fn truncated_tail_is_skipped_not_panicked() {\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"x\\\">unterminated body\";\n        assert!(parse_events(raw).is_empty());\n    }\n\n    #[test]\n    fn body_may_contain_angle_brackets() {\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"x\\\">a < b && c > d</EVENT>\";\n        assert_eq!(parse_events(raw), vec![(Some(\"x\".into()), \"a < b && c > d\".into())]);\n    }\n\n    #[test]\n    fn attr_value_is_space_anchored() {\n        let tag = \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\"\";\n        assert_eq!(attr_value(tag, \"from\"), Some(\"doyle\".into()));\n        assert_eq!(attr_value(tag, \"type\"), Some(\"msg\".into()));\n        assert_eq!(attr_value(tag, \"missing\"), None);\n    }\n\n    // ── reply extraction + bind token (unchanged contract) ──────────────────\n\n    #[test]\n    fn extract_reply_takes_last_assistant_text() {\n        let f = json!({\n            \"type\": \"agent_end\",\n            \"messages\": [\n                {\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"hi\"}]},\n                {\"role\": \"assistant\", \"content\": [\n                    {\"type\": \"text\", \"text\": \"ok\"},\n                    {\"type\": \"text\", \"text\": \" done\"}\n                ]}\n            ]\n        });\n        assert_eq!(extract_reply(&f), \"ok done\");\n    }\n\n    #[test]\n    fn extract_reply_empty_without_assistant() {\n        let f = json!({\"type\": \"agent_end\", \"messages\": [{\"role\": \"user\", \"content\": []}]});\n        assert_eq!(extract_reply(&f), \"\");\n        let f2 = json!({\"type\": \"agent_end\"});\n        assert_eq!(extract_reply(&f2), \"\");\n    }\n\n    #[test]\n    fn parses_bound_token() {\n        assert_eq!(\n            parse_bound_token(\"BOUND:omp-spt-seq token=9eb0a091e24a045b\").as_deref(),\n            Some(\"9eb0a091e24a045b\")\n        );\n        let multi = \"ADAPTER_SKIP:omp-spt — …\\nBOUND:x token=abc123\\nNOTIFS_RESURFACED:x:1\";\n        assert_eq!(parse_bound_token(multi).as_deref(), Some(\"abc123\"));\n        assert_eq!(parse_bound_token(\"BOUND:x\"), None);\n        assert_eq!(parse_bound_token(\"\"), None);\n    }\n\n    // ── HIGH-4: bounded waits over the event channel ─────────────────────────\n\n    #[test]\n    fn await_frame_picks_matching_frame_and_skips_noise() {\n        let (tx, rx) = channel();\n        tx.send(Event::OmpLine(\"not json\".into())).unwrap();\n        tx.send(Event::OmpLine(r#\"{\"type\":\"available_commands_update\"}\"#.into())).unwrap();\n        tx.send(Event::OmpLine(r#\"{\"type\":\"ready\"}\"#.into())).unwrap();\n        let got = await_frame(&rx, Duration::from_secs(2), |f| {\n            (f[\"type\"].as_str() == Some(\"ready\")).then_some(())\n        });\n        assert!(got.is_ok());\n    }\n\n    #[test]\n    fn await_frame_times_out_and_reports_omp_exit() {\n        let (tx, rx) = channel::<Event>();\n        let err = await_frame(&rx, Duration::from_millis(30), |_: &Value| Some(())).unwrap_err();\n        assert!(err.contains(\"deadline\"));\n        tx.send(Event::OmpEof).unwrap();\n        let err = await_frame(&rx, Duration::from_secs(1), |_: &Value| Option::<()>::None).unwrap_err();\n        assert!(err.contains(\"omp exited\"));\n    }\n\n    // ── drive_turn: buffering + deadline + failure classification ───────────\n    // (drive_turn needs a live ChildStdin to write the prompt; the pure receive-side behaviors are\n    // covered through await_frame above and the TurnFail causes below.)\n\n    #[test]\n    fn turn_fail_causes_are_operator_readable() {\n        assert!(TurnFail::Timeout(600).cause().contains(\"600s\"));\n        assert!(TurnFail::OmpGone.cause().contains(\"died\"));\n        assert_eq!(TurnFail::Io(\"x\".into()).cause(), \"x\");\n    }\n\n    #[test]\n    fn pending_deque_preserves_delivery_order() {\n        // The mid-turn buffering contract: lines pushed during a turn come back FIFO.\n        let mut pending: VecDeque<String> = VecDeque::new();\n        pending.push_back(\"<EVENT type=\\\"msg\\\" from=\\\"a\\\">first</EVENT>\".into());\n        pending.push_back(\"<EVENT type=\\\"msg\\\" from=\\\"b\\\">second</EVENT>\".into());\n        let first = pending.pop_front().unwrap();\n        assert_eq!(parse_events(&first)[0].0.as_deref(), Some(\"a\"));\n        let second = pending.pop_front().unwrap();\n        assert_eq!(parse_events(&second)[0].0.as_deref(), Some(\"b\"));\n    }\n\n    // ── env knobs ────────────────────────────────────────────────────────────\n\n    #[test]\n    fn env_secs_defaults_and_parses() {\n        assert_eq!(env_secs(\"OMP_SPT_TEST_UNSET_KNOB\", 42), 42);\n        std::env::set_var(\"OMP_SPT_TEST_SET_KNOB\", \"7\");\n        assert_eq!(env_secs(\"OMP_SPT_TEST_SET_KNOB\", 42), 7);\n        std::env::set_var(\"OMP_SPT_TEST_BAD_KNOB\", \"not-a-number\");\n        assert_eq!(env_secs(\"OMP_SPT_TEST_BAD_KNOB\", 42), 42);\n    }\n\n    // ── HIGH-2: the job object arms on Windows ───────────────────────────────\n\n    #[cfg(windows)]\n    #[test]\n    fn job_object_arms_on_windows() {\n        // Assigning the TEST process to a kill-on-close job is safe: the handle leaks for the\n        // process lifetime and the test process owns no children.\n        assert!(teardown::arm_kill_on_close().is_ok());\n    }\n}\n"}],"details":{"displayContent":{"text":"fn parse_events(raw: &str) -> Vec<(Option<String>, String)> {\n…\n    while let Some(start) = rest.find(\"<EVENT\") {\n…\n        if etype.as_deref() == Some(\"msg\") {\n…\n        }\r\n        let consumed = start + body_start + close + \"</EVENT>\".len();\r\n        rest = &rest[consumed..];\r\n    }\r\n    out\r\n}\r\n\r\n/// Extract `name=\"value\"` from an opening-tag substring. Space-anchored so `from` never matches a\r\n/// substring of another attribute (mirrors hook.rs's EVENT attribute reader).\r\nfn attr_value(open_tag: &str, name: &str) -> Option<String> {\r\n    let needle = format!(\" {name}=\\\"\");\r\n    let i = open_tag.find(&needle)? + needle.len();\r\n    let end = open_tag[i..].find('\"')? + i;\r\n    Some(open_tag[i..end].to_string())\r\n}\r\n\r\n/// `spt api state <busy|idle> <id>` with auth. Prefers the bind `--token` (the reliable credential);\r\n/// falls back to `--session-id` when no token was minted. Failures LAND ON THE LOG (LOW-1) — state\r\n/// is advisory, but an auth refusal here is the first symptom of a wedged perch.\r\nfn set_state(spt: &str, id: &str, sid: &str, token: Option<&str>, state: &str) {\r\n    let mut a = vec![\"api\", \"--adapter\", ADAPTER, \"state\", state, id];\r\n    match token {\r\n        Some(t) => {\r\n            a.push(\"--token\");\r\n            a.push(t);\r\n        }\r\n        None => {\r\n            a.push(\"--session-id\");\r\n            a.push(sid);\r\n        }\r\n    }\r\n    match spt_call(spt, &a, None) {\r\n        Some(out) if out.contains(\"REFUSED\") || out.contains(\"FAIL\") => {\r\n            oplog!(\"state {state} REFUSED: {}\", first_line(&out));\r\n        }\r\n        Some(_) => {}\r\n        None => oplog!(\"state {state}: spt could not be spawned\"),\r\n    }\r\n}\r\n\r\n/// Invoke `spt` with args (optional stdin body), returning trimmed stdout+stderr, or `None` on spawn\r\n/// failure. Both streams are captured because `spt send`'s delivery token rides stderr (F-031).\r\nfn spt_call(spt: &str, args: &[&str], stdin: Option<&str>) -> Option<String> {\r\n    let mut cmd = Command::new(spt);\r\n    cmd.args(args);\r\n    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());\r\n    cmd.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() });\r\n    let mut child = cmd.spawn().ok()?;\r\n    if let Some(s) = stdin {\r\n        if let Some(mut si) = child.stdin.take() {\r\n            let _ = si.write_all(s.as_bytes());\r\n        }\r\n    }\r\n    let out = child.wait_with_output().ok()?;\r\n    let mut s = String::from_utf8_lossy(&out.stdout).into_owned();\r\n    s.push_str(&String::from_utf8_lossy(&out.stderr));\r\n    while s.ends_with('\\n') || s.ends_with('\\r') {\r\n        s.pop();\r\n    }\r\n    Some(s)\r\n}\r\n\r\n/// Resolve the `spt` binary: PATH first (post-bootstrap), then known install locations. Mirrors\r\n/// hook.rs's `SysEnv::spt_bin` (kept independent to preserve this module's self-contained ethos).\r\nfn spt_bin() -> String {\r\n    if which(\"spt\") {\r\n        return \"spt\".to_string();\r\n    }\r\n    let home = std::env::var(\"HOME\").unwrap_or_default();\r\n    let local = std::env::var(\"LOCALAPPDATA\").unwrap_or_default();\r\n    for p in [\r\n        format!(\"{home}/.local/bin/spt\"),\r\n        format!(\"{local}/spt-core/bin/spt.exe\"),\r\n        format!(\"{home}/AppData/Local/spt-core/bin/spt.exe\"),\r\n    ] {\r\n        if std::path::Path::new(&p).is_file() {\r\n            return p;\r\n        }\r\n    }\r\n    \"spt\".to_string()\r\n}\r\n\r\n/// Resolve the oh-my-pi `omp` binary. IMPORTANT: PATH is checked LAST — on the Librarian host `omp`\r\n/// on PATH is the OneCLI *gateway* alias (a vaulted-API proxy), a name collision with the coding\r\n/// agent. So an explicit `OMP_SPT_OMP_BIN` override wins, then the known install path, and only then\r\n/// PATH. Shared with the psyche/echo-commune omp shims. The resolved path is always on the\r\n/// operational log, and the bringup ready-deadline turns a wrong-binary spawn into a loud failure\r\n/// instead of a wedge (MED-3 mitigation). [OMP-ADAPTER-PLAN.md host note]\r\npub(crate) fn omp_bin() -> String {\r\n    if let Ok(p) = std::env::var(\"OMP_SPT_OMP_BIN\") {\r\n        if !p.is_empty() {\r\n            return p;\r\n        }\r\n    }\r\n    let home = std::env::var(\"HOME\").unwrap_or_default();\r\n    let local = std::env::var(\"LOCALAPPDATA\").unwrap_or_default();\r\n    for p in [\r\n        format!(\"{local}/omp/omp.exe\"),\r\n        format!(\"{home}/AppData/Local/omp/omp.exe\"),\r\n        format!(\"{home}/.local/bin/omp\"),\r\n    ] {\r\n        if std::path::Path::new(&p).is_file() {\r\n            return p;\r\n        }\r\n    }\r\n    \"omp\".to_string() // last resort; may be the gateway alias — prefer OMP_SPT_OMP_BIN\r\n}\r\n\r\n/// Is `name` resolvable on PATH? (cross-platform `command -v`).\r\nfn which(name: &str) -> bool {\r\n    let path = std::env::var_os(\"PATH\").unwrap_or_default();\r\n    let exts: Vec<String> = if cfg!(windows) {\r\n        std::env::var(\"PATHEXT\")\r\n            .unwrap_or_else(|_| \".EXE;.CMD;.BAT;.COM\".into())\r\n            .split(';')\r\n            .map(|e| e.to_ascii_lowercase())\r\n            .collect()\r\n    } else {\r\n        vec![String::new()]\r\n    };\r\n    for dir in std::env::split_paths(&path) {\r\n        if dir.join(name).is_file() {\r\n            return true;\r\n        }\r\n        for ext in &exts {\r\n            if !ext.is_empty() && dir.join(format!(\"{name}{ext}\")).is_file() {\r\n                return true;\r\n            }\r\n        }\r\n    }\r\n    false\r\n}\r\n\r\n/// HIGH-2 — OS-level child-tree teardown.\r\n///\r\n/// Windows: assign the CURRENT process to a Job Object created with\r\n/// `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. Children spawned afterwards inherit job membership, so the\r\n/// OS terminates the whole tree when the job's last handle closes — which happens when this process\r\n/// exits, HOWEVER it exits (broker kill, crash, taskkill without /T). Hand-rolled minimal FFI: the\r\n/// crate stays dependency-light (serde_json only) by design.\r\n///\r\n/// Unix: no-op today — the graceful-path kills cover the common case, and the field incident\r\n/// platform is Windows. (A process-group + signal handler is the follow-on; tracked in\r\n/// docs/OMP-BRIDGE-FIELD-ISSUES.md HIGH-2.)\r\nmod teardown {\r\n    #[cfg(windows)]\r\n    pub fn arm_kill_on_close() -> Result<(), String> {\r\n        use std::ffi::c_void;\r\n        type Handle = *mut c_void;\r\n        type Bool = i32;\r\n        #[repr(C)]\r\n        #[derive(Default)]\r\n        struct JobObjectBasicLimitInformation {\r\n            per_process_user_time_limit: i64,\r\n            per_job_user_time_limit: i64,\r\n            limit_flags: u32,\r\n            minimum_working_set_size: usize,\r\n            maximum_working_set_size: usize,\r\n            active_process_limit: u32,\r\n            affinity: usize,\r\n            priority_class: u32,\r\n            scheduling_class: u32,\r\n        }\r\n        #[repr(C)]\r\n        #[derive(Default)]\r\n        struct IoCounters {\r\n            read_operation_count: u64,\r\n            write_operation_count: u64,\r\n            other_operation_count: u64,\r\n            read_transfer_count: u64,\r\n            write_transfer_count: u64,\r\n            other_transfer_count: u64,\r\n        }\r\n        #[repr(C)]\r\n        #[derive(Default)]\r\n        struct JobObjectExtendedLimitInformation {\r\n            basic_limit_information: JobObjectBasicLimitInformation,\r\n            io_info: IoCounters,\r\n            process_memory_limit: usize,\r\n            job_memory_limit: usize,\r\n            peak_process_memory_used: usize,\r\n            peak_job_memory_used: usize,\r\n        }\r\n        const JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS: i32 = 9;\r\n        const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: u32 = 0x2000;\r\n        #[link(name = \"kernel32\")]\r\n        extern \"system\" {\r\n            fn CreateJobObjectW(attrs: *mut c_void, name: *const u16) -> Handle;\r\n            fn SetInformationJobObject(\r\n                job: Handle,\r\n                class: i32,\r\n                info: *mut c_void,\r\n                len: u32,\r\n            ) -> Bool;\r\n            fn AssignProcessToJobObject(job: Handle, process: Handle) -> Bool;\r\n            fn GetCurrentProcess() -> Handle;\r\n            fn GetLastError() -> u32;\r\n        }\r\n        unsafe {\r\n            let job = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());\r\n            if job.is_null() {\r\n                return Err(format!(\"CreateJobObjectW failed (err {})\", GetLastError()));\r\n            }\r\n            let mut info = JobObjectExtendedLimitInformation::default();\r\n            info.basic_limit_information.limit_flags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;\r\n            let ok = SetInformationJobObject(\r\n                job,\r\n                JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS,\r\n                &mut info as *mut _ as *mut c_void,\r\n                std::mem::size_of::<JobObjectExtendedLimitInformation>() as u32,\r\n            );\r\n            if ok == 0 {\r\n                return Err(format!(\"SetInformationJobObject failed (err {})\", GetLastError()));\r\n            }\r\n            if AssignProcessToJobObject(job, GetCurrentProcess()) == 0 {\r\n                return Err(format!(\"AssignProcessToJobObject failed (err {})\", GetLastError()));\r\n            }\r\n            // The job handle is intentionally NOT closed: it lives for the process lifetime, and\r\n            // the OS closing it at process exit is exactly the kill-on-close trigger.\r\n            Ok(())\r\n        }\r\n    }\r\n\r\n    #[cfg(not(windows))]\r\n    pub fn arm_kill_on_close() -> Result<(), String> {\r\n        Err(\"no-op on this platform (unix process-group teardown is a tracked follow-on)\".into())\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use serde_json::json;\r\n    use std::collections::VecDeque;\r\n    use std::sync::mpsc::channel;\r\n\r\n    // ── EVENT-envelope parsing (unchanged contract) ──────────────────────────\r\n\r\n    #[test]\r\n    fn parses_single_event() {\r\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">hello there</EVENT>\";\r\n        assert_eq!(parse_events(raw), vec![(Some(\"doyle\".into()), \"hello there\".into())]);\r\n    }\r\n\r\n    #[test]\r\n    fn parses_multiple_events_and_preserves_order() {\r\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"a\\\">one</EVENT><EVENT type=\\\"msg\\\" from=\\\"b\\\">two</EVENT>\";\r\n        assert_eq!(\r\n            parse_events(raw),\r\n            vec![(Some(\"a\".into()), \"one\".into()), (Some(\"b\".into()), \"two\".into())]\r\n        );\r\n    }\r\n\r\n    #[test]\r\n    fn event_without_from_yields_none_sender() {\r\n        let raw = \"<EVENT type=\\\"msg\\\">anon body</EVENT>\";\r\n        assert_eq!(parse_events(raw), vec![(None, \"anon body\".into())]);\r\n    }\r\n\r\n    #[test]\r\n    fn empty_and_non_event_drain_is_empty() {\r\n        assert!(parse_events(\"\").is_empty());\r\n        assert!(parse_events(\"no perch\\n\").is_empty());\r\n    }\r\n\r\n    #[test]\r\n    fn non_msg_event_types_are_filtered_out() {\r\n        let notify = \"<EVENT type=\\\"notify\\\" from=\\\"spt-update\\\" subnet=\\\"Phils-Den\\\">update available</EVENT>\";\r\n        assert!(parse_events(notify).is_empty());\r\n        let mixed = format!(\"{notify}<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">real work</EVENT>\");\r\n        assert_eq!(parse_events(&mixed), vec![(Some(\"doyle\".into()), \"real work\".into())]);\r\n    }\r\n\r\n    #[test]\r\n    fn truncated_tail_is_skipped_not_panicked() {\r\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"x\\\">unterminated body\";\r\n        assert!(parse_events(raw).is_empty());\r\n    }\r\n\r\n    #[test]\r\n    fn body_may_contain_angle_brackets() {\r\n        let raw = \"<EVENT type=\\\"msg\\\" from=\\\"x\\\">a < b && c > d</EVENT>\";\r\n        assert_eq!(parse_events(raw), vec![(Some(\"x\".into()), \"a < b && c > d\".into())]);\r\n    }\r\n\r\n    #[test]\r\n    fn attr_value_is_space_anchored() {\r\n        let tag = \"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\"\";\r\n        assert_eq!(attr_value(tag, \"from\"), Some(\"doyle\".into()));\r\n        assert_eq!(attr_value(tag, \"type\"), Some(\"msg\".into()));\r\n        assert_eq!(attr_value(tag, \"missing\"), None);\r\n    }\r\n\r\n    // ── reply extraction + bind token (unchanged contract) ──────────────────\r\n\r\n    #[test]\r\n    fn extract_reply_takes_last_assistant_text() {\r\n        let f = json!({\r\n            \"type\": \"agent_end\",\r\n            \"messages\": [\r\n                {\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"hi\"}]},\r\n                {\"role\": \"assistant\", \"content\": [\r\n                    {\"type\": \"text\", \"text\": \"ok\"},\r\n                    {\"type\": \"text\", \"text\": \" done\"}\r\n                ]}\r\n            ]\r\n        });\r\n        assert_eq!(extract_reply(&f), \"ok done\");\r\n    }\r\n\r\n    #[test]\r\n    fn extract_reply_empty_without_assistant() {\r\n        let f = json!({\"type\": \"agent_end\", \"messages\": [{\"role\": \"user\", \"content\": []}]});\r\n        assert_eq!(extract_reply(&f), \"\");\r\n        let f2 = json!({\"type\": \"agent_end\"});\r\n        assert_eq!(extract_reply(&f2), \"\");\r\n    }\r\n\r\n    #[test]\r\n    fn parses_bound_token() {\r\n        assert_eq!(\r\n            parse_bound_token(\"BOUND:omp-spt-seq token=9eb0a091e24a045b\").as_deref(),\r\n            Some(\"9eb0a091e24a045b\")\r\n        );\r\n        let multi = \"ADAPTER_SKIP:omp-spt — …\\nBOUND:x token=abc123\\nNOTIFS_RESURFACED:x:1\";\r\n        assert_eq!(parse_bound_token(multi).as_deref(), Some(\"abc123\"));\r\n        assert_eq!(parse_bound_token(\"BOUND:x\"), None);\r\n        assert_eq!(parse_bound_token(\"\"), None);\r\n    }\r\n\r\n    // ── HIGH-4: bounded waits over the event channel ─────────────────────────\r\n\r\n    #[test]\r\n    fn await_frame_picks_matching_frame_and_skips_noise() {\r\n        let (tx, rx) = channel();\r\n        tx.send(Event::OmpLine(\"not json\".into())).unwrap();\r\n        tx.send(Event::OmpLine(r#\"{\"type\":\"available_commands_update\"}\"#.into())).unwrap();\r\n        tx.send(Event::OmpLine(r#\"{\"type\":\"ready\"}\"#.into())).unwrap();\r\n        let got = await_frame(&rx, Duration::from_secs(2), |f| {\r\n            (f[\"type\"].as_str() == Some(\"ready\")).then_some(())\r\n        });\r\n        assert!(got.is_ok());\r\n    }\r\n\r\n    #[test]\r\n    fn await_frame_times_out_and_reports_omp_exit() {\r\n        let (tx, rx) = channel::<Event>();\r\n        let err = await_frame(&rx, Duration::from_millis(30), |_: &Value| Some(())).unwrap_err();\r\n        assert!(err.contains(\"deadline\"));\r\n        tx.send(Event::OmpEof).unwrap();\r\n        let err = await_frame(&rx, Duration::from_secs(1), |_: &Value| Option::<()>::None).unwrap_err();\r\n        assert!(err.contains(\"omp exited\"));\r\n    }\r\n\r\n    // ── drive_turn: buffering + deadline + failure classification ───────────\r\n    // (drive_turn needs a live ChildStdin to write the prompt; the pure receive-side behaviors are\r\n    // covered through await_frame above and the TurnFail causes below.)\r\n\r\n    #[test]\r\n    fn turn_fail_causes_are_operator_readable() {\r\n        assert!(TurnFail::Timeout(600).cause().contains(\"600s\"));\r\n        assert!(TurnFail::OmpGone.cause().contains(\"died\"));\r\n        assert_eq!(TurnFail::Io(\"x\".into()).cause(), \"x\");\r\n    }\r\n\r\n    #[test]\r\n    fn pending_deque_preserves_delivery_order() {\r\n        // The mid-turn buffering contract: lines pushed during a turn come back FIFO.\r\n        let mut pending: VecDeque<String> = VecDeque::new();\r\n        pending.push_back(\"<EVENT type=\\\"msg\\\" from=\\\"a\\\">first</EVENT>\".into());\r\n        pending.push_back(\"<EVENT type=\\\"msg\\\" from=\\\"b\\\">second</EVENT>\".into());\r\n        let first = pending.pop_front().unwrap();\r\n        assert_eq!(parse_events(&first)[0].0.as_deref(), Some(\"a\"));\r\n        let second = pending.pop_front().unwrap();\r\n        assert_eq!(parse_events(&second)[0].0.as_deref(), Some(\"b\"));\r\n    }\r\n\r\n    // ── env knobs ────────────────────────────────────────────────────────────\r\n\r\n    #[test]\r\n    fn env_secs_defaults_and_parses() {\r\n        assert_eq!(env_secs(\"OMP_SPT_TEST_UNSET_KNOB\", 42), 42);\r\n        std::env::set_var(\"OMP_SPT_TEST_SET_KNOB\", \"7\");\r\n        assert_eq!(env_secs(\"OMP_SPT_TEST_SET_KNOB\", 42), 7);\r\n        std::env::set_var(\"OMP_SPT_TEST_BAD_KNOB\", \"not-a-number\");\r\n        assert_eq!(env_secs(\"OMP_SPT_TEST_BAD_KNOB\", 42), 42);\r\n    }\r\n\r\n    // ── HIGH-2: the job object arms on Windows ───────────────────────────────\r\n\r\n    #[cfg(windows)]\r\n    #[test]\r\n    fn job_object_arms_on_windows() {\r\n        // Assigning the TEST process to a kill-on-close job is safe: the handle leaks for the\r\n        // process lifetime and the test process owns no children.\r\n        assert!(teardown::arm_kill_on_close().is_ok());\r\n    }\r\n}\r\n","startLine":599,"lineNumbers":[599,null,602,null,616,null,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tools\\claude-spt\\src\\bridge.rs"}}},"isError":false,"timestamp":1784087197888}}
{"type":"message","id":"e3b5dee7","parentId":"8dd0c50a","timestamp":"2026-07-15T03:46:58.284Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying unresolved native extension issues**\n\n**Analyzing OMP plugin lifecycle contradictions**\n\n**Noting design decision needed for omps**"},{"type":"thinking","thinking":"**Examining manifest environment and hooks**"},{"type":"toolCall","id":"call_m1pGcC42vdc7SK7i4JuNe7jo|fc_0ca4af36969c80fa016a5702b226748198ad6f40de88e2bb82","name":"grep","arguments":{"i":"Mapping manifest tables","pattern":"^\\[[^]]+\\]","path":"adapter/omp-spt.toml","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_NZ3QBRTpU47uCssolQGmYa5D|fc_0ca4af36969c80fa016a5702b2269481988c3e31b1d33996ed","name":"grep","arguments":{"i":"Finding plugin namespace choices","pattern":"omps|sptc|/spt:|/omps:|name","path":"plugin/omps;adapter/strings;adapter/omp-spt.toml","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_a6BPA71pp58IQNQ67LCjQRdS|fc_0ca4af36969c80fa016a5702b226b48198860ceb0ac370680a","name":"grep","arguments":{"i":"Checking OMP plan slices","pattern":"digest-omp|psyche-omp|echo-commune-omp|history-omp|OpenRouter|models\\.yml|session\\.resume|native TUI|bridge","path":"traceable-reqs.toml;tests;ci;docs;OMP-ADAPTER-PLAN.md","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DWuuQLYGwEDsKerjxZpwflXg|fc_0ca4af36969c80fa016a5702b226bc81989d69b3ab16d85961","name":"read","arguments":{"i":"Reading plugin identity","path":"plugin/omps/.claude-plugin/plugin.json","selector":"1-120"}},{"type":"toolCall","id":"call_A2xW7ZMsOOWI25Gc5vhXMcHb|fc_0ca4af36969c80fa016a5702b226c48198809149e67a3b4929","name":"read","arguments":{"i":"Reading OMP live skill","path":"adapter/strings/skills/live.md","selector":"1-220"}},{"type":"toolCall","id":"call_FerlmZY0bMbefUKT7mM7K3RA|fc_0ca4af36969c80fa016a5702b226c8819881d04e38a7f65d69","name":"read","arguments":{"i":"Reading OMP setup skill","path":"adapter/strings/skills/setup.md","selector":"1-200"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":26142,"output":981,"cacheRead":162304,"cacheWrite":0,"totalTokens":189427,"reasoningTokens":632,"cost":{"input":0.13071000000000002,"output":0.02943,"cacheRead":0.081152,"cacheWrite":0,"total":0.24129200000000003}},"stopReason":"toolUse","timestamp":1784087197891,"responseId":"resp_0ca4af36969c80fa016a57029e0ea48198862bd325c6b56216","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a5702a06e608198882d08e0206f557a","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKq-NDFaB2FjEO0yhSR89RJkD44IfV1XzI2dmYMXvC5oDylvvtZpA034yl4cFb0P7fZJDwDxa86KM62O-H3Io9icrPQOEQV_dQsjRc10-YDtAPozjiCXtMpRlU2Rzbsd7YZZzrRjPUhh81t8aCP3uY_bmU0Ea9kLjJCKwXIN1A43aZnyU_LLnt6VgVCtLZjOujO3i-ZktNYGZPbmttrxChiHqB-X-vZ7vfp8Kr12Aj2Nt6x1Kj0hZhjzXS3pYR35-2i1GcijS8nd0MxAWxj6Ot4wsdrjxikH1Y0OyoaAig9neb7cJPSdvSHtGR97RCl2mMay5nA_g4CTRCLUT27CykoJ2GFfJY9MsvWNZmaMBSM4gqIuF8WTm415KE6jZ-5_3ey-jvWMTpRsr9Uq6emlaKO4gyn3tvBhZRGgzirdqk19-2ixPZEYi5hZYFksN2dz_0SdVFk_39ieikfOks62BVhbZDcRtyj-NJqtabFE6cM6r3xDOIiU1bP36aR6fA5CaJcqoRZVfGZb1irK8KVu3WlE8_h28e2Wac4-5rlXU2Rj7K2b2y7sEXWsos4SJC6wUbheSmFcFRP47ML93i6hIlDNkm4KwqEoDF_QbtLc5AB5qZsKGQnviXTY3G1dvVH7ElG2LXePFf9-wHCs9sRSuHOmki-249HBJgO0Hd2GAz0DIrZ53mVQdBXmFtkrEYeFZ94lplUUEhuMpLAPEKXgnObnlK2zF_qhDrPal2m8o8rgghItBESp8bFJdYcPJILq_muAxLRj-cIEx9KBODhD4cYWMnpXTt8XDQOWTwUy25GSsQSM0-3yIGEQozrpc89V0buMnLuyX2FzAmYJ-6bgndCOpCKjDJqT1RutEWQ5VJ-fCckQ73pSgdeKjYboHyOuJwfg4Ue9YfwlBLKm9Af_1rettnxaSzb-W2hh64236rJzj7E0K2HK_NjSzwy_Ssd9MmobB8zaQ4sk_fDv1gK8nfyT8IxJ7uu60BX07YgmOnc0ZpIePnMhJxtfMOrVnc47WzqMc05CVSZnWRNNLo7Ri7c5symnTYL9xoszA6m1dIOa-eLZrSj1GqcX-Vq9XCpmXlM6CA1c6AHsW68bwkvbSt4wECjBrWdKdL7xttX8I_-6QKTopxLUtkNHaRGCGEmS8leRrIuvvl54Yqd3-ZwzZj6dm0HoUby7EWe0Iqa8RH5jgBPIxepoZe5iVin8BP0den5OylHHtPkeSFCsjQATif2FlCP0DlVa7yRIrgifQRTrMqOkHPD1KoRkKJIJryhATclsFvgvsnqz6cNEM8_tpYPviRWmfwwNYtGQUeY9zu-Rp7JwHjXvxPQyLP3xvhoqAzZ1Q2pk52lHeWKAgQWUl0pRaSuAUwC5D8I-tNePfN27plV8Mf_1nMJfEERtiF9n56P5K9J3KqkaB6OFEZWgffUmJikkuhW3TPPCpXomTelKG8zfwyUcUQk6Z_SI-wcim-xiMNPWi_1AS5nmMTb8LKmJP0nhlE7jkYyoK6eB-eiqzj1How060Fkmw-RxlcsELCLP18Ql77zIiQoDKE7UWOERW2xDoIC9GU8ipmje5YlJtTPIORtHQhiWQSj9Ok7TmqLqA3mx6V2XcpYCJynI0hp5UPh7oiFCumyGg9BXv0uQR3s2SVJud3tjEhyDoHdUAwklZ8oJGon45a9b0-rwJnVQhc2zRC52xuQhfBAS49LRENizZL0fItO2NQr6y90I7ba1JcYpSDk6BoCR7Raku7Gbr19q5SmO_JRYIUoePiuen2039piZoOCT5mVJulqlbU0tc4NH-xfHLZJx1ghMXW4OqE3kkVI7Ay-8PR4IJPz3hgj4Qn9miyZoHfzjkM0oWRSURUtWjCtd9QhvVAXi_FaIZ6z9s2MO0osoEk41FHfLnNIBBzhML1DNd1um1YITk3gyd9rM-GSJC2Gay_s9EP28sM-p5cUwIgFXfi_wsT2OrQCR45EVTFVezj-brvtYHR7zL1erhM25tFLVB_7E4Agn3nxLQqyki8BOS5cJq7IllKcC12w6Mit2EZJXfRzMpdNdB5pQ0ueUtR9Fbz8CYsWD3i7Ie8MgGjn4D6hh_ERgTkj0jGUJKX1xnNgOKcglwdf1Wqr3OxEm5GwjotmptVAwjzIql6aLfdVPTWHA-OGkg6yhR-iG7XMCR7DcLckNurUWzVL2u8FGajRzzHFSUxJr0pDLbIY3noXLNndp3mpM2e2hPcSACVDDw-zFwmwnqD3tSTSaS7HVNaHjNktlJYlbzU6XM-70zqj368pUOVHmMcMjdewlNsnvi6R2wkWhttnw67un9riVsVBudD09NDDRvHQIT7K703sVAURH_wrRMc8QqBNx8JWDWKRSFs4_1ozoC9rLSPJVTv7HhtgYDZn5dkAGX6rRtWVIJmzTm76yo-JOXkScEcwoRvU34bKbCVb-VgasjsvxKBPdzyWGmgbEwdZKsfeDnjMxkRHgMKKG1i3PQqAW-e0gTPWmNyT_GEM6vApYA-p6ly-i9T0KzXN9sA0_vDJQkn0C6GLxOC8OeK-26XrGu_3bPQD63-gXl-d--Ksdx4k2PuxsnFO7IHWIJSprD76nNmIBtuVJ5PaGClbTiU_V4sGmu04UM1H0gbxsklv0dq43PiNc0A86TvlZx7l9gHmmwN27NqHSgiXl6-OqBFdoHCZiKLqinHWMByLtnBrKxmN9Fj4i6hnB8I4wWIXfeIqUS9JxiNuQXEKo5B7i5zkuz-nVGP73obOb4k_Xr5ipsMsnwjXjYJEUXAGcgf2PX6D54rwVKCdl12e4vRH4jXO4VUFi6wM4VdEyoPyPQCo_6k1VaSi30WVcpcLUygymikPheIl0fx96xwe80l9NfnhRKqf_taoGKxOugLWdqDy_BF8e70Cnv3f0SRpQqtjGzY9xEUoxfeaTmCe-r5IEn4x3gAXjxq6srbApYmNn03G91tT74BxilB2FPDaWqI586-ANSRls6au9eO9Dh_BZWuVOJj2FAypZIlo_XfXJeVBJtVpHz1-0rp3u4c96eIx9aXPMMrwvKuET7vkB4jo2eXW8IvWTB-uxL0bRtwWB76UvBHNSgrR2EOAZMnr8jcWUIdIWRG62DWXGHk04w9N4g0Qh5lw6KARr6IicGDMPL9JlFkXFBYtxL8_PcQQ9DAuSYT-PozAUzMECJsJVj9MU8QofVMessHm6NzHLn5oyAdAEGTjhL0NuHcm3F64sDt_MfS4Yv4NOPd3LAziSgjBzev8454Jzwi75VaKxJwYyPXPRuAri9BSDDKyFfNC-4qFLtKLDmS4Cb1blFJjoiSwa74qCAvhdQR25mGKIWY-KeNVqjGMxJJlOXR4hXqFpFl06fzqX3avUb5f3lCuThmOp57jGcmp0Rh5WRoDtZSSOHvQz7y4gCLkzNb9sjs0ZqzV7Fr0_Lh8LvugcZZoxui1HW_N4Bfg1UhY1A6UZGUuWPSS6TM2xlyagquvhG-dXnAYGxJEIDn9Q7xUOKaX4vxjcj3-PZe4zU6fsmdzVvXbnDzwGF1l3oDRmUFnXN5tx3MzPana-xZxjjJLzMYT-Y_efay_ncQtmdJ8jyCOje_hriAGNVPxmgajC8IpFieXJM9nKKeM6cGZcze9qtX_dtSMCnRDsd40C89slb07La34unbanHTjff367DxauJgAn7mSq42BUikTmrY0i9HdmgwJmeFjL-bovBMJfNFazeXt5bXJGlMPrrZnPH6ANBQLqB6Y40jhoISv8Dgjvi3DqvpvJHVJQ22rQducdZy0lAShPRwYXv4jZKwoSWWudvxxIlixJQHqesiDw05mxju7R02y_mBDatUj7EJ4BJdhPMbtdxL-cAZx6AEBSNywwGlFm8yvxGltzRQJNPk0BfMuLAUq0yEfBWmfIgDZ7JB4u0VZXj_0-UOZVmmfNQVWco5kTNLbkZkKLFzCzY8y9R3c2gifHPJrqNAdRJTNuuCfz4fPq1xhVxII2Cm-AmXryjKJ60WwI2F-JMjbKGCpHZVt78QUYPkm17qUKxVW47j7OFCoLyRYkME-K_kvwrvlpV5HsfAsuhP6aoc8E-kelpHjBv1_DHULMIQLc6UlEcFGF73BcHNfmGWMApRLouRQgJAj_vPDuawvQDnX2fUa393bEoA0g_cSkS878_3OI0jwd981lS_DiCHwU1UEJFaUZn_GLYfzG-XG3EvL4pNfzV16W-RSYN8iuU3MRwElw_UyCFnccnjHb39vmq7FgNaBDK0uvA==","summary":[{"type":"summary_text","text":"**Identifying unresolved native extension issues**"},{"type":"summary_text","text":"**Analyzing OMP plugin lifecycle contradictions**"},{"type":"summary_text","text":"**Noting design decision needed for omps**"}]},{"id":"rs_0ca4af36969c80fa016a5702aa101c8198abbf545e9beff3a3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwKytB0FARb4yVR_lkokCMigNdO4pkZaHUozkHNwEVqjnaA5_1NtiIWfHWnS3Q5seYgcdnLP6aycLcOm7nI7Aw8PHUYY24ab98rjjiXLtzfzoOgg-KDaHwuJKCgwrpj92P9uuAsdmYrOF_XvfjbiivezMyDEmh8FuJxrnsJSaoHq1TxdRom9imyZVwTv82e9SpzSijzAHADWoCmFz6BpwTfQY_ix8rLROdnReGieS_ORDzlqMs3X2pRJOa_MHORGTdFvaRH-WJIlS4PRuwU40RUGuLsey8CnTPbCOXHm6CKhR8N5wDh5zYoCQvMbpEjSS3Bt1I-x19oMXoQ5EZpE6a2icUMboHgMp1M6NwyMaaQW53gVEF5THOfdXEcVn2-zIuSOjoEKszJfITe6m9H567UbaWlywDVE0jwas03toNVaMxBShXMKuDemtg_kVMGMqb57erOl0Ecgh2OQDgIBzcqUgfTP4qap6_3D1WKw8Vla8n-7wqn098nH1bVyodHXuI7Mqe3-WZ4G6ni2IEihk3DBFnqdzA9dbJqiX-T7wGi4jfvuIlLbO8gqxwU_8PFDTyQUO8TTn6-hFaLNZH-WqGF5zBl7IVl-qsL7HnJ1FwdcMA4gv3rAWZRYF1h2deNJl9UGX4KFaAkAhxmhJ-PtXrZjDGCYQLYk7u0qfj9Rmff5POXJOEoqIcE0wqRBbYGH4I73r8ClfhQJs6JT3k22-7tiPljpHWGoJOIpbZ017q5oM-8M-Ch-W7bI-aBZ2HU9vFhztEHkELOR9QlYNqjeQjASGlgSvVjrEDLgMd1ldeCPPeRJhz2f_uJTJGF2WDDkgAuuesaD1BgfRGh-JY6tfV680U2O8NEinLImQiPArA_-Ud_2ROXu7z62PLU21cF7nsPtizsytaDLpQhBeERbenxCSYJeVA_fHiWGPIDbadAP8LbuZINzuQw5sVRB2iJAjyg2PTcda2cY76wX-bFmDX2BuAsynYFQg5-0akJIwZoaY5C65r_8efIFM0OfjDkCCMo3W0LrIgzVu0sdVKePs1xajT2RYLXiFJ6ff8kZFo7g5ZW2EbR4EV18bX_F0xSI1t8tM8_DS6XJVhYnbWQwzKoRSXWNIRkMjZW9Xtf9DoHx1U-mRQQ1wuBHYcrMKVvGU33rlKRt0DA7uKGuQv5lWyJ5ea2haTfZEpIcXgTM7QUQgfSQMCABHDRDe13OGov_5F68Vntklct4pFUJpNyqJ8mwc0w2ndPr_NcZPMhlRojHBB0DsqG9TEE0Hkn1_ziJzm88q3Rz_8fRyZkEKQGM2BsQvj8R-PaYSxJOhFnbeSThf_DnX3509cCnrku6dIUbmu2dtls4Nfo36FHQR_ZqdER8HPh_-YqQTAFB0aAbSNTAxnX27Dhi7yXKyJxlKdx2fh-eSkN9GtH5ZwVJypqlTIh0y2QES2bnrg3Bj5KJqH1JDgIqvWj9tSysRLd-gSNFMmi2WwunpKKRkgrbwkt1RA9RfLYgn4qcTtxturmSGV63R_hWp6ieHNGIKlmuMCA98m1YAMPWlgAYcdBTHKE07t8yOw==","summary":[{"type":"summary_text","text":"**Examining manifest environment and hooks**"}]},{"id":"fc_0ca4af36969c80fa016a5702b226748198ad6f40de88e2bb82","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping manifest tables\",\"pattern\":\"^\\\\[[^]]+\\\\]\",\"path\":\"adapter/omp-spt.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_m1pGcC42vdc7SK7i4JuNe7jo","name":"grep"},{"id":"fc_0ca4af36969c80fa016a5702b2269481988c3e31b1d33996ed","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding plugin namespace choices\",\"pattern\":\"omps|sptc|/spt:|/omps:|name\",\"path\":\"plugin/omps;adapter/strings;adapter/omp-spt.toml\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_NZ3QBRTpU47uCssolQGmYa5D","name":"grep"},{"id":"fc_0ca4af36969c80fa016a5702b226b48198860ceb0ac370680a","type":"function_call","status":"completed","arguments":"{\"i\":\"Checking OMP plan slices\",\"pattern\":\"digest-omp|psyche-omp|echo-commune-omp|history-omp|OpenRouter|models\\\\.yml|session\\\\.resume|native TUI|bridge\",\"path\":\"traceable-reqs.toml;tests;ci;docs;OMP-ADAPTER-PLAN.md\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_a6BPA71pp58IQNQ67LCjQRdS","name":"grep"},{"id":"fc_0ca4af36969c80fa016a5702b226bc81989d69b3ab16d85961","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading plugin identity\",\"path\":\"plugin/omps/.claude-plugin/plugin.json\",\"selector\":\"1-120\"}","call_id":"call_DWuuQLYGwEDsKerjxZpwflXg","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702b226c48198809149e67a3b4929","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading OMP live skill\",\"path\":\"adapter/strings/skills/live.md\",\"selector\":\"1-220\"}","call_id":"call_A2xW7ZMsOOWI25Gc5vhXMcHb","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702b226c8819881d04e38a7f65d69","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading OMP setup skill\",\"path\":\"adapter/strings/skills/setup.md\",\"selector\":\"1-200\"}","call_id":"call_FerlmZY0bMbefUKT7mM7K3RA","name":"read"}]},"duration":20383.943300000043,"ttft":2550.2826999999816,"contextSnapshot":{"promptTokens":188446,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_m1pGcC42vdc7SK7i4JuNe7jo|fc_0ca4af36969c80fa016a5702b226748198ad6f40de88e2bb82","toolName":"grep","startedAt":"2026-07-15T03:46:58.284Z","args":{"path":"adapter/omp-spt.toml"},"intent":"Mapping manifest tables"},"id":"86da68b9","parentId":"e3b5dee7","timestamp":"2026-07-15T03:46:58.284Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NZ3QBRTpU47uCssolQGmYa5D|fc_0ca4af36969c80fa016a5702b2269481988c3e31b1d33996ed","toolName":"grep","startedAt":"2026-07-15T03:46:58.284Z","args":{"path":"plugin/omps;adapter/strings;adapter/omp-spt.toml"},"intent":"Finding plugin namespace choices"},"id":"78159422","parentId":"86da68b9","timestamp":"2026-07-15T03:46:58.284Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_a6BPA71pp58IQNQ67LCjQRdS|fc_0ca4af36969c80fa016a5702b226b48198860ceb0ac370680a","toolName":"grep","startedAt":"2026-07-15T03:46:58.285Z","args":{"path":"traceable-reqs.toml;tests;ci;docs;OMP-ADAPTER-PLAN.md"},"intent":"Checking OMP plan slices"},"id":"d3c441be","parentId":"78159422","timestamp":"2026-07-15T03:46:58.285Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DWuuQLYGwEDsKerjxZpwflXg|fc_0ca4af36969c80fa016a5702b226bc81989d69b3ab16d85961","toolName":"read","startedAt":"2026-07-15T03:46:58.285Z","args":{"path":"plugin/omps/.claude-plugin/plugin.json"},"intent":"Reading plugin identity"},"id":"f0b26c76","parentId":"d3c441be","timestamp":"2026-07-15T03:46:58.285Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_A2xW7ZMsOOWI25Gc5vhXMcHb|fc_0ca4af36969c80fa016a5702b226c48198809149e67a3b4929","toolName":"read","startedAt":"2026-07-15T03:46:58.285Z","args":{"path":"adapter/strings/skills/live.md"},"intent":"Reading OMP live skill"},"id":"9e8819b6","parentId":"f0b26c76","timestamp":"2026-07-15T03:46:58.285Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_FerlmZY0bMbefUKT7mM7K3RA|fc_0ca4af36969c80fa016a5702b226c8819881d04e38a7f65d69","toolName":"read","startedAt":"2026-07-15T03:46:58.285Z","args":{"path":"adapter/strings/skills/setup.md"},"intent":"Reading OMP setup skill"},"id":"9f262c4f","parentId":"9e8819b6","timestamp":"2026-07-15T03:46:58.285Z"}
{"type":"message","id":"6969317c","parentId":"9f262c4f","timestamp":"2026-07-15T03:46:58.287Z","message":{"role":"toolResult","toolCallId":"call_m1pGcC42vdc7SK7i4JuNe7jo|fc_0ca4af36969c80fa016a5702b226748198ad6f40de88e2bb82","toolName":"grep","content":[{"type":"text","text":" 35|\n*36|[adapter]\n 37|name = \"omp-spt\"\n 38|kind = \"harness\"\n 39|# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n...\n 477|# [impl->REQ-DIST-ADAPTER-RELEASE]\n*478|[update]\n 479|avenue = \"gh_release\"\n 480|repo = \"BigscreenVR/omp-spt\"\n 481|transport = \"gh\"\n...\n 503|# [impl->REQ-DIST-UPDATE-MESSAGE]\n*504|[update.post]\n 505|command = \"{adapter_dir}/omp-spt post-update\"\n 506|self_verifies = false\n 507|\n...\n 516|\n*517|[hooks.SessionStart]\n 518|# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning\n 519|# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the\n 520|# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the\n...\n 526|\n*527|[hooks.UserPromptSubmit]\n 528|# TURN-START. Three jobs (plugin user-prompt-submit.sh): (1) mark the perch BUSY (`api state busy`)\n 529|# so inbound DEFERS for mid-turn PreToolUse delivery instead of idle-PTY-inject — honest activity,\n 530|# spt-core never infers it (integration-checklist); (2) DRAIN the inbox (`api poll … --include-deferred`\n...\n 543|# drain — together the legacy spt poll+inject-on-PreToolUse reachability. [impl->REQ-DIST-PRETOOL-POLL]\n*544|[hooks.PreToolUse]\n 545|fires = \"api poll {session_id} --include-deferred\"\n 546|reads = [\"session_id\", \"tool_name\"]\n 547|can_inject = true\n 548|\n*549|[hooks.Stop]\n 550|# TURN-END. Idle marker (`api state busy`'s counterpart) — also arms the echo-gate sentinel since Stop\n 551|# cannot inject (F-001 §Gap3). Pairs with the UserPromptSubmit busy mark for honest turn-state.\n 552|fires = \"api state idle\"\n...\n 555|\n*556|[hooks.SessionEnd]\n 557|fires = \"api session-end {session_id}\"\n 558|reads = [\"session_id\"]\n 559|can_inject = false\n 560|\n*561|[hooks.SubagentStart]\n 562|fires = \"api worker-start {session_id}\"\n 563|reads = [\"session_id\"]\n 564|can_inject = false\n 565|\n*566|[hooks.SubagentStop]\n 567|fires = \"api worker-stop {session_id}\"\n 568|reads = [\"session_id\"]\n 569|can_inject = false\n...\n 582|# `--json-payload` self-send is the DELIVERY-side trigger. [impl->REQ-DIST-CHECKPOINT-COMMUNE]\n*583|[hooks.PostToolUse]\n 584|fires = \"api state idle {session_id}\"\n 585|reads = [\"session_id\", \"tool_name\", \"tool_input\"]\n 586|can_inject = false\n...\n 588|# ── [identity] ─────────────────────────────────────────────────────────────────────────────\n*589|[identity]\n 590|# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).\n 591|session_id_source = \"post_spawn\"\n 592|# Process-tree anchor when session_id is absent: the omp / omp-spt bridge binary.\n...\n 603|# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]\n*604|[session]\n 605|commune_dir = \".claude\"\n 606|signoff_dir = \".claude\"\n 607|\n...\n 631|# the composed prompt rides an @file (omp -p ignores a stdin-only prompt, verified).\n*632|[session.psyche_init]\n 633|command = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n 634|# Identity-env scrub (F-028 C2): a spawned psyche must never inherit its parent's identity env —\n 635|# inherited OWL_SESSION_ID/SPT_AGENT_ID make the psyche's hooks resolve \"self\" to the PARENT and\n...\n 661|# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n*662|[session.psyche_resume]\n 663|command = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n 664|detach = false\n 665|# Identity-env scrub on BOTH psyche roles (F-028 C2). [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\n...\n 704|# reply integration through OMP's public extension API. See ADR-0007. [impl->REQ-OMP-NATIVE-TUI]\n*705|[session.self]\n 706|command = \"omp --extension {adapter_dir}/strings/omp-spt.mjs\"\n 707|keys = []\n 708|\n...\n 731|# transcript. The extension binds the resumed OMP session id to the current SPT endpoint.\n*732|[session.resume]\n 733|command = \"omp -r {session_id} --extension {adapter_dir}/strings/omp-spt.mjs\"\n 734|keys = [\"session_id\"]\n 735|\n...\n 758|# spt hook layer at all (--no-extensions; the RPC bridge owns lifecycle).\n*759|[session.echo_commune]\n 760|command = \"omp-spt echo-commune-omp --id {id} --session-id {session_id}\"\n 761|detach = false\n 762|recursion_guard_env = \"SPT_ECHO_COMMUNE\"\n...\n 778|# {CLAUDE_CONFIG_DIR} dependency. Missing transcript = HISTORY_FAIL: + exit 1, never silent-empty.\n*779|[history]\n 780|strategy = \"fetcher\"\n 781|fetcher = \"omp-spt history-omp --session {session_id}\"\n 782|\n...\n 798|# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.\n*799|[env.SPT_ENDPOINT_ID]\n 800|direction = \"inject\"\n 801|value = \"{id}\"\n 802|\n...\n 812|# REQ-INJECT-VERIFY-ECHO.\n*813|[env.SPT_INJECT_VERIFY_ECHO]\n 814|direction = \"inject\"\n 815|value = \"1\"\n 816|\n 817|# ── [inject] ───────────────────────────────────────────────────────────────────────────────\n*818|[inject]\n 819|# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).\n 820|# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.\n 821|activity = [\"hook\"]\n...\n 844|# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).\n*845|[message-idle-translation-binary]\n 846|command = \"{adapter_dir}/omp-spt translate\"\n 847|\n 848|# ── [digest] ───────────────────────────────────────────────────────────────────────────────\n...\n 874|# --sample <omp-session.jsonl>`. [impl->REQ-DIST-DIGEST-EXTRACTOR]\n*875|[digest]\n 876|strategy = \"fetcher\"\n 877|extractor = \"omp-spt digest-omp --session {session_id}\"\n 878|window_turns = 6\n...\n 890|# [impl->REQ-DIST-DIGEST-FETCHER]\n*891|[env.CLAUDE_CONFIG_DIR]\n 892|direction = \"read\"\n 893|value = \"~/.claude\"\n 894|\n...\n 911|# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.\n*912|[strings]\n 913|adapter_label = \"oh-my-pi (spt)\"\n 914|# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's\n 915|# hooks.json + dispatch.sh go static-forever. `{adapter_dir}` is lazily substituted at\n...\n 930|\n*931|[strings.notice]\n 932|not_installed = \"spt-core not found on PATH — run /omps:setup to install it.\"\n 933|\n*934|[strings.skills]\n 935|# Operative UPS-injection skill bodies. File-backed values (`{ file = \"skills/<x>.md\" }`) live in the\n 936|# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,\n 937|# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the\n...\n 976|# [impl->REQ-DIST-SESSIONSTART-BRIEF]\n*977|[strings.briefs]\n 978|identity = { file = \"briefs/identity.md\" }                      # {id}-templated: who + perch-live + don't-re-arm\n 979|messaging-perch = { file = \"briefs/messaging-perch.md\" }        # has-id reach: spt send + reply\n 980|messaging-no-perch = { file = \"briefs/messaging-no-perch.md\" }  # no-id reach: spt ring\n...\n 985|# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).\n*986|[[hints]]\n 987|keywords = [\"go live\", \"live agent\", \"start live\"]\n 988|text = \"Tip: /omps:live starts or resumes a live agent session.\"\n 989|\n*990|[[hints]]\n 991|keywords = [\"who am i\", \"my owl name\", \"my agent name\", \"my endpoint id\"]\n 992|text = \"Tip: run `spt whoami` to see this session's spt endpoint id.\"\n 993|\n*994|[[hints]]\n 995|keywords = [\"message another agent\", \"send to agent\", \"tell the other agent\"]\n 996|text = \"Tip: /omps:send messages another spt agent; /omps:ready first to receive replies.\"\n 997|\n*998|[[hints]]\n 999|keywords = [\"another machine\", \"pair machine\", \"across machines\", \"subnet\", \"join my network\"]\n 1000|text = \"Tip: /omps:subnet pairs machines into a private network so agents reach each other across nodes.\"\n 1001|\n*1002|[[hints]]\n 1003|keywords = [\"checkpoint\", \"compact my context\", \"context is getting long\", \"clear and continue\", \"self clear\"]\n 1004|text = \"Tip: /omps:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only).\""}],"details":{"scopePath":"adapter/omp-spt.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\omp-spt.toml","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":34,"fileCount":1,"files":["adapter/omp-spt.toml"],"fileMatches":[{"path":"adapter/omp-spt.toml","count":34}],"truncated":false,"displayContent":"   35│\n  *36│[adapter]\n   37│name = \"omp-spt\"\n   38│kind = \"harness\"\n   39│# 0.6.1 (2026-06-22): PACKAGING modernization — the release now ships a single MULTI-PLATFORM fat\n     │...\n  477│# [impl->REQ-DIST-ADAPTER-RELEASE]\n *478│[update]\n  479│avenue = \"gh_release\"\n  480│repo = \"BigscreenVR/omp-spt\"\n  481│transport = \"gh\"\n     │...\n  503│# [impl->REQ-DIST-UPDATE-MESSAGE]\n *504│[update.post]\n  505│command = \"{adapter_dir}/omp-spt post-update\"\n  506│self_verifies = false\n  507│\n     │...\n  516│\n *517│[hooks.SessionStart]\n  518│# Seed the endpoint — ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter. The owning\n  519│# adapter resolves at bind time from this seed's {parent_pid} via [adapter] host_binaries (the\n  520│# legacy-parity bare flow). {parent_pid} is a spt-filled key; session_id is a CC stdin field. NOT the\n     │...\n  526│\n *527│[hooks.UserPromptSubmit]\n  528│# TURN-START. Three jobs (plugin user-prompt-submit.sh): (1) mark the perch BUSY (`api state busy`)\n  529│# so inbound DEFERS for mid-turn PreToolUse delivery instead of idle-PTY-inject — honest activity,\n  530│# spt-core never infers it (integration-checklist); (2) DRAIN the inbox (`api poll … --include-deferred`\n     │...\n  543│# drain — together the legacy spt poll+inject-on-PreToolUse reachability. [impl->REQ-DIST-PRETOOL-POLL]\n *544│[hooks.PreToolUse]\n  545│fires = \"api poll {session_id} --include-deferred\"\n  546│reads = [\"session_id\", \"tool_name\"]\n  547│can_inject = true\n  548│\n *549│[hooks.Stop]\n  550│# TURN-END. Idle marker (`api state busy`'s counterpart) — also arms the echo-gate sentinel since Stop\n  551│# cannot inject (F-001 §Gap3). Pairs with the UserPromptSubmit busy mark for honest turn-state.\n  552│fires = \"api state idle\"\n     │...\n  555│\n *556│[hooks.SessionEnd]\n  557│fires = \"api session-end {session_id}\"\n  558│reads = [\"session_id\"]\n  559│can_inject = false\n  560│\n *561│[hooks.SubagentStart]\n  562│fires = \"api worker-start {session_id}\"\n  563│reads = [\"session_id\"]\n  564│can_inject = false\n  565│\n *566│[hooks.SubagentStop]\n  567│fires = \"api worker-stop {session_id}\"\n  568│reads = [\"session_id\"]\n  569│can_inject = false\n     │...\n  582│# `--json-payload` self-send is the DELIVERY-side trigger. [impl->REQ-DIST-CHECKPOINT-COMMUNE]\n *583│[hooks.PostToolUse]\n  584│fires = \"api state idle {session_id}\"\n  585│reads = [\"session_id\", \"tool_name\", \"tool_input\"]\n  586│can_inject = false\n     │...\n  588│# ── [identity] ─────────────────────────────────────────────────────────────────────────────\n *589│[identity]\n  590│# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).\n  591│session_id_source = \"post_spawn\"\n  592│# Process-tree anchor when session_id is absent: the omp / omp-spt bridge binary.\n     │...\n  603│# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]\n *604│[session]\n  605│commune_dir = \".claude\"\n  606│signoff_dir = \".claude\"\n  607│\n     │...\n  631│# the composed prompt rides an @file (omp -p ignores a stdin-only prompt, verified).\n *632│[session.psyche_init]\n  633│command = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n  634│# Identity-env scrub (F-028 C2): a spawned psyche must never inherit its parent's identity env —\n  635│# inherited OWL_SESSION_ID/SPT_AGENT_ID make the psyche's hooks resolve \"self\" to the PARENT and\n     │...\n  661│# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n *662│[session.psyche_resume]\n  663│command = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n  664│detach = false\n  665│# Identity-env scrub on BOTH psyche roles (F-028 C2). [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\n     │...\n  704│# reply integration through OMP's public extension API. See ADR-0007. [impl->REQ-OMP-NATIVE-TUI]\n *705│[session.self]\n  706│command = \"omp --extension {adapter_dir}/strings/omp-spt.mjs\"\n  707│keys = []\n  708│\n     │...\n  731│# transcript. The extension binds the resumed OMP session id to the current SPT endpoint.\n *732│[session.resume]\n  733│command = \"omp -r {session_id} --extension {adapter_dir}/strings/omp-spt.mjs\"\n  734│keys = [\"session_id\"]\n  735│\n     │...\n  758│# spt hook layer at all (--no-extensions; the RPC bridge owns lifecycle).\n *759│[session.echo_commune]\n  760│command = \"omp-spt echo-commune-omp --id {id} --session-id {session_id}\"\n  761│detach = false\n  762│recursion_guard_env = \"SPT_ECHO_COMMUNE\"\n     │...\n  778│# {CLAUDE_CONFIG_DIR} dependency. Missing transcript = HISTORY_FAIL: + exit 1, never silent-empty.\n *779│[history]\n  780│strategy = \"fetcher\"\n  781│fetcher = \"omp-spt history-omp --session {session_id}\"\n  782│\n     │...\n  798│# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.\n *799│[env.SPT_ENDPOINT_ID]\n  800│direction = \"inject\"\n  801│value = \"{id}\"\n  802│\n     │...\n  812│# REQ-INJECT-VERIFY-ECHO.\n *813│[env.SPT_INJECT_VERIFY_ECHO]\n  814│direction = \"inject\"\n  815│value = \"1\"\n  816│\n  817│# ── [inject] ───────────────────────────────────────────────────────────────────────────────\n *818│[inject]\n  819│# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).\n  820│# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.\n  821│activity = [\"hook\"]\n     │...\n  844│# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).\n *845│[message-idle-translation-binary]\n  846│command = \"{adapter_dir}/omp-spt translate\"\n  847│\n  848│# ── [digest] ───────────────────────────────────────────────────────────────────────────────\n     │...\n  874│# --sample <omp-session.jsonl>`. [impl->REQ-DIST-DIGEST-EXTRACTOR]\n *875│[digest]\n  876│strategy = \"fetcher\"\n  877│extractor = \"omp-spt digest-omp --session {session_id}\"\n  878│window_turns = 6\n     │...\n  890│# [impl->REQ-DIST-DIGEST-FETCHER]\n *891│[env.CLAUDE_CONFIG_DIR]\n  892│direction = \"read\"\n  893│value = \"~/.claude\"\n  894│\n     │...\n  911│# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.\n *912│[strings]\n  913│adapter_label = \"oh-my-pi (spt)\"\n  914│# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's\n  915│# hooks.json + dispatch.sh go static-forever. `{adapter_dir}` is lazily substituted at\n     │...\n  930│\n *931│[strings.notice]\n  932│not_installed = \"spt-core not found on PATH — run /omps:setup to install it.\"\n  933│\n *934│[strings.skills]\n  935│# Operative UPS-injection skill bodies. File-backed values (`{ file = \"skills/<x>.md\" }`) live in the\n  936│# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,\n  937│# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the\n     │...\n  976│# [impl->REQ-DIST-SESSIONSTART-BRIEF]\n *977│[strings.briefs]\n  978│identity = { file = \"briefs/identity.md\" }                      # {id}-templated: who + perch-live + don't-re-arm\n  979│messaging-perch = { file = \"briefs/messaging-perch.md\" }        # has-id reach: spt send + reply\n  980│messaging-no-perch = { file = \"briefs/messaging-no-perch.md\" }  # no-id reach: spt ring\n     │...\n  985│# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).\n *986│[[hints]]\n  987│keywords = [\"go live\", \"live agent\", \"start live\"]\n  988│text = \"Tip: /omps:live starts or resumes a live agent session.\"\n  989│\n *990│[[hints]]\n  991│keywords = [\"who am i\", \"my owl name\", \"my agent name\", \"my endpoint id\"]\n  992│text = \"Tip: run `spt whoami` to see this session's spt endpoint id.\"\n  993│\n *994│[[hints]]\n  995│keywords = [\"message another agent\", \"send to agent\", \"tell the other agent\"]\n  996│text = \"Tip: /omps:send messages another spt agent; /omps:ready first to receive replies.\"\n  997│\n *998│[[hints]]\n  999│keywords = [\"another machine\", \"pair machine\", \"across machines\", \"subnet\", \"join my network\"]\n 1000│text = \"Tip: /omps:subnet pairs machines into a private network so agents reach each other across nodes.\"\n 1001│\n*1002│[[hints]]\n 1003│keywords = [\"checkpoint\", \"compact my context\", \"context is getting long\", \"clear and continue\", \"self clear\"]\n 1004│text = \"Tip: /omps:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only).\""},"isError":false,"timestamp":1784087218287}}
{"type":"message","id":"6bf827b2","parentId":"6969317c","timestamp":"2026-07-15T03:46:58.293Z","message":{"role":"toolResult","toolCallId":"call_DWuuQLYGwEDsKerjxZpwflXg|fc_0ca4af36969c80fa016a5702b226bc81989d69b3ab16d85961","toolName":"read","content":[{"type":"text","text":"{\n  \"name\": \"omps\",\n  \"version\": \"0.1.11\",\n  \"description\": \"Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).\",\n  \"author\": {\n    \"name\": \"perri\"\n  },\n  \"keywords\": [\"spt\", \"spacetime\", \"messaging\", \"agents\", \"live\", \"claude-spt\"]\n}\n"}],"details":{"displayContent":{"text":"{\n  \"name\": \"omps\",\n  \"version\": \"0.1.11\",\n  \"description\": \"Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).\",\n  \"author\": {\n    \"name\": \"perri\"\n  },\n  \"keywords\": [\"spt\", \"spacetime\", \"messaging\", \"agents\", \"live\", \"claude-spt\"]\n}\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\plugin\\omps\\.claude-plugin\\plugin.json"}}},"isError":false,"timestamp":1784087218293}}
{"type":"message","id":"9651b243","parentId":"6bf827b2","timestamp":"2026-07-15T03:46:58.293Z","message":{"role":"toolResult","toolCallId":"call_FerlmZY0bMbefUKT7mM7K3RA|fc_0ca4af36969c80fa016a5702b226c8819881d04e38a7f65d69","toolName":"read","content":[{"type":"text","text":"# /omps:setup — operative instructions\n\n**Goal:** install or repair spt-core for this session **and activate the claude-spt adapter** — the\nmid-session installer covering the gap when no SessionStart bootstrap has fired. Installing the\nbinary is only half the job: a present binary with a **deregistered** adapter has no\nprofiles/strings/hints/`[digest]`, so the `/omps:*` surface is inert until activation.\n\n<!-- [doc->REQ-SETUP-ACTIVATE] -->\n\n**Do this:**\n\n1. **Binary.** If spt-core is absent (`command -v spt && spt --version` reports nothing), run the\n   published install-on-demand bootstrap (spt-releases `harness-contract/install-on-demand.md`):\n   - **POSIX:** `curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh`\n   - **Windows (PowerShell):** `irm https://sabermage.github.io/spt-releases/install.ps1 | iex`\n\n   `PATH` is not reloaded in this shell after a fresh install — verify with the absolute path:\n   `\"$HOME/.local/bin/spt\" --version`. After this, `spt update` handles signed self-updates.\n\n2. **Activate the adapter.** Run `spt adapter list` and find `claude-spt`:\n   - Listed and **not** `deregistered` → already active; report it and skip to step 3.\n   - Missing or `deregistered` → activate it:\n     - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` near cwd):\n       `spt adapter add ./adapter/claude-spt.toml`.\n     - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the\n       single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries\n       beside a shared manifest; install auto-resolves the host's), extracts to the durable home,\n       registers. No `--asset` / os-detection needed — the fat archive is host-agnostic (ADR-0024 W1).\n       `--tag <ver>` pins a version. Needs spt **v0.13.2+** (the version that reads a fat archive).\n       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->\n\n3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no\n   `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`\n   host (the legacy-parity bare flow — `/omps:live`/`/omps:ready` need no `--adapter`). The\n   ONE `claude-spt` tool binary (its `digest` / `psyche` / `post-update` / `translate` subcommands) is\n   resolved **from the adapter install dir** (the `from …` path in `spt adapter list`), where\n   activation extracted it — no PATH copy needed. If it fails to start, confirm it is present in that\n   dir (a packaging defect, not a PATH problem).\n\n4. **ccs wiring (optional).** Detect `~/.ccs`:\n   - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a\n     drop-in for `claude`), so live/ready agents can run on ccs backends via `--adapter\n     claude-spt:ccs`. Check `command -v ccs`; if `~/.ccs` exists but `ccs` isn't on PATH, point the\n     user at their ccs bin dir.\n   - Absent → ccs is an optional CLI router for alternate model backends in place of `claude`. To\n     enable: install ccs, then re-run `/omps:setup`. Skip if unwanted.\n   <!-- [doc->REQ-SETUP-CCS] -->\n\n5. **Subnet onboarding (optional).** A subnet is the private group of paired machines that makes\n   `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use needs none). Check\n   `spt subnet status`:\n   - In a subnet → invite a machine with `spt subnet show-code`; on the joiner, `spt subnet join`.\n   - Not in one → offer **create** (`spt subnet create` — this node becomes seed-holder) or **join**\n     (`spt subnet join`). Full verb guidance → **/omps:subnet**.\n   - **Elevation:** create/join/show-code are OS-elevation-gated — Windows: elevated (UAC) shell;\n     Linux desktop: pkexec/polkit or sudo terminal; Linux TTY: inline sudo; headless: print the exact\n     command for the user to run elevated.\n   <!-- [doc->REQ-SETUP-SUBNET] -->\n\nIdempotent and safe to re-run — the same bootstrap + activation the SessionStart hook performs.\n"}],"details":{"displayContent":{"text":"# /omps:setup — operative instructions\n\n**Goal:** install or repair spt-core for this session **and activate the claude-spt adapter** — the\nmid-session installer covering the gap when no SessionStart bootstrap has fired. Installing the\nbinary is only half the job: a present binary with a **deregistered** adapter has no\nprofiles/strings/hints/`[digest]`, so the `/omps:*` surface is inert until activation.\n\n<!-- [doc->REQ-SETUP-ACTIVATE] -->\n\n**Do this:**\n\n1. **Binary.** If spt-core is absent (`command -v spt && spt --version` reports nothing), run the\n   published install-on-demand bootstrap (spt-releases `harness-contract/install-on-demand.md`):\n   - **POSIX:** `curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh`\n   - **Windows (PowerShell):** `irm https://sabermage.github.io/spt-releases/install.ps1 | iex`\n\n   `PATH` is not reloaded in this shell after a fresh install — verify with the absolute path:\n   `\"$HOME/.local/bin/spt\" --version`. After this, `spt update` handles signed self-updates.\n\n2. **Activate the adapter.** Run `spt adapter list` and find `claude-spt`:\n   - Listed and **not** `deregistered` → already active; report it and skip to step 3.\n   - Missing or `deregistered` → activate it:\n     - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` near cwd):\n       `spt adapter add ./adapter/claude-spt.toml`.\n     - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the\n       single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries\n       beside a shared manifest; install auto-resolves the host's), extracts to the durable home,\n       registers. No `--asset` / os-detection needed — the fat archive is host-agnostic (ADR-0024 W1).\n       `--tag <ver>` pins a version. Needs spt **v0.13.2+** (the version that reads a fat archive).\n       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->\n\n3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no\n   `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`\n   host (the legacy-parity bare flow — `/omps:live`/`/omps:ready` need no `--adapter`). The\n   ONE `claude-spt` tool binary (its `digest` / `psyche` / `post-update` / `translate` subcommands) is\n   resolved **from the adapter install dir** (the `from …` path in `spt adapter list`), where\n   activation extracted it — no PATH copy needed. If it fails to start, confirm it is present in that\n   dir (a packaging defect, not a PATH problem).\n\n4. **ccs wiring (optional).** Detect `~/.ccs`:\n   - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a\n     drop-in for `claude`), so live/ready agents can run on ccs backends via `--adapter\n     claude-spt:ccs`. Check `command -v ccs`; if `~/.ccs` exists but `ccs` isn't on PATH, point the\n     user at their ccs bin dir.\n   - Absent → ccs is an optional CLI router for alternate model backends in place of `claude`. To\n     enable: install ccs, then re-run `/omps:setup`. Skip if unwanted.\n   <!-- [doc->REQ-SETUP-CCS] -->\n\n5. **Subnet onboarding (optional).** A subnet is the private group of paired machines that makes\n   `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use needs none). Check\n   `spt subnet status`:\n   - In a subnet → invite a machine with `spt subnet show-code`; on the joiner, `spt subnet join`.\n   - Not in one → offer **create** (`spt subnet create` — this node becomes seed-holder) or **join**\n     (`spt subnet join`). Full verb guidance → **/omps:subnet**.\n   - **Elevation:** create/join/show-code are OS-elevation-gated — Windows: elevated (UAC) shell;\n     Linux desktop: pkexec/polkit or sudo terminal; Linux TTY: inline sudo; headless: print the exact\n     command for the user to run elevated.\n   <!-- [doc->REQ-SETUP-SUBNET] -->\n\nIdempotent and safe to re-run — the same bootstrap + activation the SessionStart hook performs.\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\strings\\skills\\setup.md"}}},"isError":false,"timestamp":1784087218293}}
{"type":"message","id":"cf91f8c3","parentId":"9651b243","timestamp":"2026-07-15T03:46:58.294Z","message":{"role":"toolResult","toolCallId":"call_A2xW7ZMsOOWI25Gc5vhXMcHb|fc_0ca4af36969c80fa016a5702b226c48198809149e67a3b4929","toolName":"read","content":[{"type":"text","text":"# /omps:live — operative instructions\n\n**Goal:** upgrade this Claude Code session to a **LiveAgent** — a reachable agent whose context is\nbacked by a **Psyche** (a daemon-managed companion that briefs your resume across `/clear`/compact\nvia commune deltas). This makes the session you are in right now live.\n\n## Bringup — do this\n\n**First, recognize your delivery substrate — it decides whether step 2 applies.**\n<!-- [doc->REQ-SKILL-LIVE-SPT-HOSTED-BRANCH] -->\n\n- **spt-hosted session** (this session was launched by the spt daemon — `spt endpoint run` — and is\n  **broker-delivered**). Tell-tale: your SessionStart identity brief is **already present** and your\n  perch is **already bound** before you run anything. In this case **inbound messages already reach you\n  automatically** — on your existing turn as `<omps_messages>` the hook injects, exactly like your\n  SessionStart brief said. **Do NOT arm the step-2 Monitor listener.** An in-session `spt api listen`\n  **cannot** re-arm an spt-hosted perch and will only fault: the Monitor's `bash.exe` child breaks\n  by-pid `host_binaries` resolution (**ADAPTER_UNRESOLVED**), and the Monitor-child pid breaks seed\n  lineage (**NO_SEED**). You are already live and reachable — **skip to step 4** (reply-and-continue);\n  to reach someone, `spt send <target>` and continue, and any reply surfaces on your turn on its own.\n- **Normal session** (you launched this CC yourself, no pre-bound perch). Do the full bringup below,\n  **including step 2** — the resident Monitor relay IS your one delivery pipe.\n\n1. **Pick the perch id.** Use a short lowercase id from the user, or the id you already know for this\n   session (from your SessionStart identity brief, if present). Call it `<id>`. Only fall back to\n   `spt whoami` if you genuinely have no id yet. SessionStart already seeded this session's perch — do\n   not re-seed. Once you go live as `<id>`, that IS your id — do not look it up again later.\n2. **(Normal sessions only — an spt-hosted session skips this; see the substrate branch above.)**\n   **Run the relay as a single PERSISTENT background task** via the **Monitor** tool. One resident\n   process IS the live delivery pipe. The body is one bare command:\n\n   ### Invocation (Monitor)\n\n   Invoke `spt api listen <id>` via the Monitor tool with:\n   - `command: \"spt api listen <id>\"`\n   - `persistent: true`\n   - `description: \"« spt event »\"`\n\n   - `spt api listen` enters the poll loop inline; the stream stays alive across messages and emits\n   one `<EVENT type=\"msg|alarm|echo_commune|init_signoff\" ...>body</EVENT>` line per delivery.\n   - It **blocks for the session's life**: backlog drains first, then each delivery streams to stdout.\n3. **Readiness.** The relay output will show `BOUND:<id>`, then `READY:<id>`. Check it only if\n   connectivity complications are encountered.\n4. **Reply.** Inbound messages — including replies to messages YOU send — arrive as\n   `<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>` (body HTML-escaped, newlines `<br>`) on THIS ONE\n   relay you started in step 2. To reply, pipe the message body as stdin to `spt send <sender-id>`,\n   then continue. Do NOT arm a SECOND Monitor/poll to wait for a reply — your relay already delivers it.\n5. **Across boundaries — commune + signoff (you are live now, so these apply to you).** Going live in\n   this session does NOT re-fire your SessionStart brief, so here are the mechanics:\n   - **Commune** after a significant body of work, before a `/clear`/`/compact`: write\n     `.claude/<id>-commune.md` in ONE atomic write — a concise delta (task + status, decisions,\n     immediate next steps), NOT a transcript. The daemon ingests + deletes it; the file disappearing is\n     success. It is what rebuilds you after a reset.\n   - **Checkpoint** = a commune that ALSO wipes + rebuilds your context from it (agent-driven `/clear`):\n     embed `!!checkpoint!!` in the body — one marker ⇒ default wake; a PAIR ⇒ the text between them is\n     your custom wake directive. Author it inline this turn; the clear+wake fire automatically.\n   - **Sign off** when done: `spt endpoint shutdown` — stops the listener, saves final context, takes\n     your Psyche down. `/omps:ready`/`/omps:live` brings you back.\n\n## Output — what the user sees\n\nThe relay emits machine markers (`BOUND:<id>`, `READY:<id>`, raw tokens). **Read them to drive\nbringup; never echo them to the user.** The only user-facing surface is the LIVE block below — emit\nit verbatim (substituting `<id>`), and nothing else from bringup:\n\n```\n**LIVE.** Now running as `<id>`.\n- Reachable — other agents reach me with `/omps:send <id>`.\n- Inbound — messages arrive via the Monitor.\n- Across resets — `/omps:commune` before a `/clear`/`/compact`; `/omps:signoff` to go offline.\n```\n\nIf bringup fails, report a short plain-language failure and the likely cause (run `/omps:setup`),\nstill without dumping the raw markers.\n<!-- [doc->REQ-SKILL-LIVE] -->\n"}],"details":{"displayContent":{"text":"# /omps:live — operative instructions\n\n**Goal:** upgrade this Claude Code session to a **LiveAgent** — a reachable agent whose context is\nbacked by a **Psyche** (a daemon-managed companion that briefs your resume across `/clear`/compact\nvia commune deltas). This makes the session you are in right now live.\n\n## Bringup — do this\n\n**First, recognize your delivery substrate — it decides whether step 2 applies.**\n<!-- [doc->REQ-SKILL-LIVE-SPT-HOSTED-BRANCH] -->\n\n- **spt-hosted session** (this session was launched by the spt daemon — `spt endpoint run` — and is\n  **broker-delivered**). Tell-tale: your SessionStart identity brief is **already present** and your\n  perch is **already bound** before you run anything. In this case **inbound messages already reach you\n  automatically** — on your existing turn as `<omps_messages>` the hook injects, exactly like your\n  SessionStart brief said. **Do NOT arm the step-2 Monitor listener.** An in-session `spt api listen`\n  **cannot** re-arm an spt-hosted perch and will only fault: the Monitor's `bash.exe` child breaks\n  by-pid `host_binaries` resolution (**ADAPTER_UNRESOLVED**), and the Monitor-child pid breaks seed\n  lineage (**NO_SEED**). You are already live and reachable — **skip to step 4** (reply-and-continue);\n  to reach someone, `spt send <target>` and continue, and any reply surfaces on your turn on its own.\n- **Normal session** (you launched this CC yourself, no pre-bound perch). Do the full bringup below,\n  **including step 2** — the resident Monitor relay IS your one delivery pipe.\n\n1. **Pick the perch id.** Use a short lowercase id from the user, or the id you already know for this\n   session (from your SessionStart identity brief, if present). Call it `<id>`. Only fall back to\n   `spt whoami` if you genuinely have no id yet. SessionStart already seeded this session's perch — do\n   not re-seed. Once you go live as `<id>`, that IS your id — do not look it up again later.\n2. **(Normal sessions only — an spt-hosted session skips this; see the substrate branch above.)**\n   **Run the relay as a single PERSISTENT background task** via the **Monitor** tool. One resident\n   process IS the live delivery pipe. The body is one bare command:\n\n   ### Invocation (Monitor)\n\n   Invoke `spt api listen <id>` via the Monitor tool with:\n   - `command: \"spt api listen <id>\"`\n   - `persistent: true`\n   - `description: \"« spt event »\"`\n\n   - `spt api listen` enters the poll loop inline; the stream stays alive across messages and emits\n   one `<EVENT type=\"msg|alarm|echo_commune|init_signoff\" ...>body</EVENT>` line per delivery.\n   - It **blocks for the session's life**: backlog drains first, then each delivery streams to stdout.\n3. **Readiness.** The relay output will show `BOUND:<id>`, then `READY:<id>`. Check it only if\n   connectivity complications are encountered.\n4. **Reply.** Inbound messages — including replies to messages YOU send — arrive as\n   `<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>` (body HTML-escaped, newlines `<br>`) on THIS ONE\n   relay you started in step 2. To reply, pipe the message body as stdin to `spt send <sender-id>`,\n   then continue. Do NOT arm a SECOND Monitor/poll to wait for a reply — your relay already delivers it.\n5. **Across boundaries — commune + signoff (you are live now, so these apply to you).** Going live in\n   this session does NOT re-fire your SessionStart brief, so here are the mechanics:\n   - **Commune** after a significant body of work, before a `/clear`/`/compact`: write\n     `.claude/<id>-commune.md` in ONE atomic write — a concise delta (task + status, decisions,\n     immediate next steps), NOT a transcript. The daemon ingests + deletes it; the file disappearing is\n     success. It is what rebuilds you after a reset.\n   - **Checkpoint** = a commune that ALSO wipes + rebuilds your context from it (agent-driven `/clear`):\n     embed `!!checkpoint!!` in the body — one marker ⇒ default wake; a PAIR ⇒ the text between them is\n     your custom wake directive. Author it inline this turn; the clear+wake fire automatically.\n   - **Sign off** when done: `spt endpoint shutdown` — stops the listener, saves final context, takes\n     your Psyche down. `/omps:ready`/`/omps:live` brings you back.\n\n## Output — what the user sees\n\nThe relay emits machine markers (`BOUND:<id>`, `READY:<id>`, raw tokens). **Read them to drive\nbringup; never echo them to the user.** The only user-facing surface is the LIVE block below — emit\nit verbatim (substituting `<id>`), and nothing else from bringup:\n\n```\n**LIVE.** Now running as `<id>`.\n- Reachable — other agents reach me with `/omps:send <id>`.\n- Inbound — messages arrive via the Monitor.\n- Across resets — `/omps:commune` before a `/clear`/`/compact`; `/omps:signoff` to go offline.\n```\n\nIf bringup fails, report a short plain-language failure and the likely cause (run `/omps:setup`),\nstill without dumping the raw markers.\n<!-- [doc->REQ-SKILL-LIVE] -->\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\strings\\skills\\live.md"}}},"isError":false,"timestamp":1784087218293}}
{"type":"message","id":"c2e2e4f9","parentId":"cf91f8c3","timestamp":"2026-07-15T03:46:58.299Z","message":{"role":"toolResult","toolCallId":"call_NZ3QBRTpU47uCssolQGmYa5D|fc_0ca4af36969c80fa016a5702b2269481988c3e31b1d33996ed","toolName":"grep","content":[{"type":"text","text":"# plugin/omps/\n## bootstrap.ps1\n*1|# omps SessionStart bootstrap (PowerShell/Windows) — install spt-core on demand if absent.\n 2|# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.\n*3|# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.\n 4|# [impl->REQ-DIST-BOOTSTRAP-INSTALL]\n 5|if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {\n 6|    Write-Output \"spt-core not found - installing...\"\n## bootstrap.sh\n 1|#!/bin/sh\n*2|# omps SessionStart bootstrap (POSIX) — install spt-core on demand if absent.\n 3|# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.\n*4|# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.\n 5|# [impl->REQ-DIST-BOOTSTRAP-INSTALL]\n 6|set -eu\n 7|\n\n## .claude-plugin/\n### plugin.json\n 1|{\n*2|  \"name\": \"omps\",\n 3|  \"version\": \"0.1.11\",\n 4|  \"description\": \"Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).\",\n 5|  \"author\": {\n*6|    \"name\": \"perri\"\n 7|  },\n 8|  \"keywords\": [\"spt\", \"spacetime\", \"messaging\", \"agents\", \"live\", \"claude-spt\"]\n 9|}\n\n## hooks/\n### dispatch.sh\n 1|#!/bin/sh\n*2|# omps hook dispatch — STATIC-FOREVER (D1, ADR-0006 ask #1 → resolve-not-execute).\n 3|#\n 4|# This is the ONLY hook shell the cplugs plugin still ships. It carries NO hook logic — that all moved\n 5|# into the consolidated `claude-spt` binary (`claude-spt hook <event>`), which rides `spt adapter\n...\n 17|# exist until spt-core + the adapter are installed. So SessionStart installs spt-core on demand first;\n*18|# and if the adapter is not yet registered (pre-/omps:setup), get-string yields nothing and we no-op\n 19|# (exit 0) — exactly the pre-readiness no-op the old per-hook wrappers had. [impl->REQ-DIST-HOOK-BINARY]\n 20|event=\"$1\"\n 21|\n...\n 46|\n*47|# Adapter not registered yet (pre-/omps:setup) → no perch to serve → no-op.\n 48|[ -z \"$bin\" ] && exit 0\n 49|\n 50|# Normalize to the bare binary PATH: tolerate a manifest value that still carries a trailing ` hook`\n\n## skills/\n\n### commune/\n#### SKILL.md\n 1|---\n*2|name: commune\n 3|description: |\n 4|  Push a context update to your Psyche so it can brief your resume across a reset. Use when the\n 5|  user says \"commune\" or \"update psyche\", or — as a live agent — after a significant body of work,\n...\n 12|\n*13|# /omps:commune\n 14|\n*15|**Live agents only.** No perch (`spt whoami` is empty)? Run **`/omps:live`** first — a ready agent has\n 16|no Psyche and nothing to rebuild from.\n 17|\n 18|The operative commune + `--checkpoint` mechanics are delivered by the adapter (thin skeleton — the\n 19|prose rides `spt adapter update`; ADR-0001/ADR-0006), not baked here: a live agent already carries them\n*20|in its **SessionStart brief** (the `live-ops` block) and in the **`/omps:live` go-live body**. In short:\n 21|write `.claude/<id>-commune.md` as one atomic context delta (task + status, decisions, next steps);\n 22|add the `!!checkpoint!!` trigger to also self-reset (a PAIR of markers brackets a custom wake directive).\n\n### force-stop/\n#### SKILL.md\n 1|---\n*2|name: force-stop\n 3|description: |\n 4|  Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user\n 5|  says \"stop listening\", \"kill live agent\", \"force stop\", or \"tear down perch\". Session-aware: a\n...\n 10|\n*11|# /omps:force-stop\n 12|\n 13|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15|>\n 16|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18|\n 19|Tears down an agent's endpoint (graceful shutdown, or a lighter stop).\n\n### list-agents/\n#### SKILL.md\n 1|---\n*2|name: list-agents\n 3|description: |\n 4|  List SPT agents and endpoints reachable from this node. Use when the user says \"list agents\",\n 5|  \"who's live\", \"who's listening\", or wants a roster of active perches.\n...\n 8|\n*9|# /omps:list-agents\n 10|\n 11|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*12|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 13|>\n 14|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*15|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 16|\n 17|Shows the roster of spt endpoints, with this session's own pinned distinctly.\n\n### live/\n#### SKILL.md\n 1|---\n*2|name: live\n 3|description: |\n 4|  Run a live agent session. For past sessions, restores a summarized context.\n 5|\n*6|  EXPLICIT START phrases (route to /omps:live <id>):\n 7|  - \"live as\"\n 8|  - \"start live\"\n 9|  - \"go live\"\n...\n 11|\n*12|  AUTO-RESUME phrases (route to /omps:live --auto, resumes most-recently-active live agent):\n 13|  - continue live work\n 14|  - resume live work\n 15|  - continue live agent\n...\n 29|\n*30|# /omps:live\n 31|\n 32|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*33|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 34|>\n 35|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*36|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 37|\n 38|Upgrades THIS session to a LiveAgent (Psyche-backed).\n\n### ready/\n#### SKILL.md\n 1|---\n*2|name: ready\n 3|description: |\n 4|  Make this Claude Code session reachable for inter-agent messages (register a perch and\n 5|  listen). Use when the user says \"listen as\", \"ready as\", or wants to receive messages from\n...\n 10|\n*11|# /omps:ready\n 12|\n 13|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15|>\n 16|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18|\n 19|Registers a perch and listens, so other agents can reach this session.\n\n### send/\n#### SKILL.md\n 1|---\n*2|name: send\n 3|description: |\n 4|  Send a message to another SPT agent. Use when the user says \"send to\", \"message\",\n 5|  \"tell <agent>\", or when you need to reach or reply to another agent yourself.\n...\n 11|\n*12|# /omps:send\n 13|\n 14|Deliver a message to another agent (**body read from stdin**). The operative reach is delivered by the\n 15|adapter, not baked here (thin skeleton — the prose rides `spt adapter update`): a perched session\n...\n 20|- **Reply:** `printf '%s' \"<body>\" | spt send <sender>` (sender = the `from` on the `<EVENT>` you got).\n*21|- No perch yet? `/omps:ready` (or `/omps:live`) first, or send-and-wait without one via\n 22|  `printf '%s' \"<body>\" | spt ring <target> --timeout 60`.\n 23|\n 24|## Shortform — no Bash needed\n...\n 35|- **Commune:** start an output with `>>commune<<` and the rest is written as your commune (same as the\n*36|  `/omps:commune` Write path). A `>>commune<<` output is never scanned for `@<…@>`, so it may quote the\n 37|  syntax freely.\n 38|\n 39|Full guidance: `spt how-to send`.\n\n### setup/\n#### SKILL.md\n 1|---\n*2|name: setup\n 3|description: |\n 4|  Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session\n 5|  (mid-session installer + post-install activation).\n...\n 8|\n*9|# /omps:setup\n 10|\n*11|> **Self-contained by necessity.** Unlike other `/omps:*` skills, `setup` cannot rely on\n 12|> UPS-injection of its body: it runs precisely when spt-core may be **absent**, so the injection\n 13|> source is unavailable. The operative steps therefore live in this SKILL.md (the floor); the\n 14|> adapter body mirrors them for the spt-present repair path.\n...\n 16|Covers the mid-session install + activation gap (ADR-0001): a user who installs the plugin mid-session\n*17|has not had a SessionStart bootstrap fire, so `/omps:setup` runs the same invisible-installer\n 18|bootstrap to fetch + verify spt-core on demand **and then activates the claude-spt adapter**. A\n 19|present binary is not enough: an unregistered/`deregistered` adapter has no profiles/strings/hints/\n*20|`[digest]`, so the `/omps:*` surface is inert until activation.\n 21|\n 22|**Do this:**\n 23|\n...\n 35|     - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` is present near cwd):\n*36|       `spt adapter add ./adapter/claude-spt.toml` (the file-form accepts any path + filename).\n 37|     - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the\n 38|       single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries\n 39|       beside a shared manifest; install auto-resolves the host's), extracts to the durable home,\n...\n 45|   (no `deregistered`). The `[digest]`/`[session]` templates invoke `claude-spt-digest` +\n*46|   `claude-spt-psyche` by **bare name**, and spt-core resolves them **from the adapter install dir**\n 47|   (the `from …/adapters/_github/<safe>/` path in `spt adapter list`), where `--release` activation\n 48|   already extracted them beside the manifest. **No PATH copy needed** (REQ-INSTALL-11, spt v0.8.0\n 49|   Feature B; verified live on v0.8.1 — digest + daemon-hosted Psyche both resolve from the install\n...\n 55|     command with `ccs` (drop-in for `claude`) → run live/ready agents on ccs backends\n*56|     (glm/kimi/custom) via `--adapter claude-spt:ccs` (e.g. `/omps:live`, `/omps:ready`,\n 57|     `spt endpoint run --adapter claude-spt:ccs`). Check `command -v ccs`; if `~/.ccs` exists but `ccs`\n 58|     isn't on PATH, point the user at their ccs bin dir. No action needed if unwanted (base\n 59|     `claude-spt` is unaffected).\n 60|   - Absent → ccs is an optional CLI router for driving alternate model backends (glm/kimi/custom) in\n*61|     place of `claude`. To enable: install ccs (its docs), then re-run `/omps:setup`. Skip if unwanted.\n 62|\n 63|5. **Subnet onboarding (optional — SCOPE setup #3/#4).** A subnet is the private group of paired\n*64|   machines that makes `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use\n 65|   needs none). Check: `spt subnet status`.\n 66|   - In a subnet → to invite a machine: `spt subnet show-code` (6-digit code + URI + QR); on the\n*67|     joiner: `spt subnet join <name> --code <code>`.\n*68|   - Not in one → offer create (`spt subnet create <name>` — seed-holder; prints code/URI/QR) or join\n*69|     (`spt subnet join <name> --code <code>`). Skip if single-machine.\n*70|   - Full verb guidance → **/omps:subnet**. **Elevation:** create/join/show-code are\n 71|     OS-elevation-gated — Windows: elevated (UAC) shell; Linux desktop: pkexec/polkit or sudo\n 72|     terminal; Linux TTY: inline sudo; headless: print the command for the user to run elevated.\n 73|\n\n### signoff/\n#### SKILL.md\n 1|---\n*2|name: signoff\n 3|description: |\n 4|  Gracefully shut down your live session, saving a final context summary. Use when the user says\n 5|  \"sign off\" or \"graceful stop\", or when you (a live agent) are done and want to go offline cleanly.\n...\n 10|\n*11|# /omps:signoff\n 12|\n 13|**Live agents only.** No perch (`spt whoami` is empty)? Nothing to sign off.\n 14|\n...\n 16|`spt adapter update`; ADR-0001/ADR-0006): a live agent carries them in its **SessionStart brief** (the\n*17|`live-ops` block) and the **`/omps:live` go-live body**. In short: optionally write a brief closing\n 18|summary, then `spt endpoint shutdown` (your own perch) — stops the listener, fires the final context\n*19|save, takes your Psyche down. `/omps:ready` or `/omps:live` brings you back; lighter no-save stop is\n 20|`spt endpoint stop`.\n\n### subnet/\n#### SKILL.md\n 1|---\n*2|name: subnet\n 3|description: |\n 4|  Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join\n 5|  an existing one. Use when the user wants to pair machines, set up a subnet, or reach agents\n*6|  across machines. Cross-machine /omps:send and live agents depend on it.\n 7|argument-hint: \"[status|create|show-code|join]\"\n 8|allowed-tools: [Bash]\n 9|---\n 10|\n*11|# /omps:subnet\n 12|\n 13|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15|>\n 16|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18|\n 19|Pairs machines into a private network so agents reach each other across nodes.\n\n### version/\n#### SKILL.md\n 1|---\n*2|name: version\n 3|description: |\n 4|  Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin\n*5|  version). Use when the user asks the spt or adapter version, or runs /omps:version.\n 6|allowed-tools: [Bash]\n 7|---\n 8|\n*9|# /omps:version\n 10|\n 11|> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*12|> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 13|>\n 14|> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*15|> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 16|\n 17|Reports the version-of-truth: the spt-core-tracked binary + adapter manifest.\n\n# adapter/strings/\n## omp-spt.mjs\n 13|\n*14|function attribute(tag, name) {\n*15|\tconst marker = ` ${name}=\"`;\n 16|\tconst start = tag.indexOf(marker);\n 17|\tif (start < 0) return undefined;\n 18|\tconst valueStart = start + marker.length;\n\n## briefs/\n### identity.md\n 3|- **Your id is `{id}`.** You already know it (it is right here) — do NOT run `spt whoami` to look it up.\n*4|- Messages — including replies to messages you send — arrive AUTOMATICALLY on your existing perch: your Monitor EVENT stream (`<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>`) or, mid-tool-call, as `<omps_messages>` the hook injects. Process them and reply.\n 5|- Do NOT arm a second Monitor (or poll, or tail anything) to \"wait for a reply\" — your perch already delivers it. Just send and continue; the reply surfaces on its own.\n*6|- Do NOT run /omps:ready or /omps:live — your perch is already up. Re-arming returns COLLISION (that is proof it is alive, not a stale perch).\n### live-ops.md\n 18|- **Sign off** gracefully when done: `spt endpoint shutdown` (your own perch) — stops the listener,\n*19|  fires the final context save, and takes your Psyche down with it. `/omps:ready` or `/omps:live`\n 20|  brings you back. Lighter no-save stop: `spt endpoint stop`.\n### messaging-no-perch.md\n 3|- Send and wait for a reply: `printf '%s' \"<body>\" | spt ring <target> --timeout 60` (the reply prints to stdout; `TIMEOUT` is exit 0 — the message still landed).\n*4|- To RECEIVE messages yourself, run /omps:ready (or /omps:live for a live session).\n\n## skills/\n### force-stop.md\n*1|# /omps:force-stop — operative instructions\n 2|\n 3|Tear down an spt agent's endpoint — your own, or another agent's.\n 4|\n*5|1. Target id `<id>`: your own (`spt whoami`) by default, or the one the user named.\n 6|2. **Graceful (default):** `spt endpoint shutdown [<id>]` — stops the listener, fires the final save,\n 7|   and for a live agent takes its Psyche down too.\n 8|3. **Lighter, no-save:** `spt endpoint stop <id>` — unregisters the perch; the spool is preserved.\n### list-agents.md\n*1|# /omps:list-agents — operative instructions\n 2|\n 3|Show the roster of spt endpoints reachable from this node.\n 4|\n...\n 6|   session's own pinned at the top.\n*7|2. Summarize who's online and which is this session. Message one with `/omps:send <id>`.\n 8|\n 9|Options: `spt endpoint list --help`.\n\nShowing files 1-20 of 26. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"plugin/omps, adapter/strings, adapter/omp-spt.toml","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":73,"fileCount":20,"files":["plugin/omps/.claude-plugin/plugin.json","plugin/omps/bootstrap.ps1","plugin/omps/bootstrap.sh","plugin/omps/hooks/dispatch.sh","plugin/omps/skills/commune/SKILL.md","plugin/omps/skills/force-stop/SKILL.md","plugin/omps/skills/list-agents/SKILL.md","plugin/omps/skills/live/SKILL.md","plugin/omps/skills/ready/SKILL.md","plugin/omps/skills/send/SKILL.md","plugin/omps/skills/setup/SKILL.md","plugin/omps/skills/signoff/SKILL.md","plugin/omps/skills/subnet/SKILL.md","plugin/omps/skills/version/SKILL.md","adapter/strings/briefs/identity.md","adapter/strings/briefs/live-ops.md","adapter/strings/briefs/messaging-no-perch.md","adapter/strings/omp-spt.mjs","adapter/strings/skills/force-stop.md","adapter/strings/skills/list-agents.md"],"fileMatches":[{"path":"plugin/omps/.claude-plugin/plugin.json","count":2},{"path":"plugin/omps/bootstrap.ps1","count":2},{"path":"plugin/omps/bootstrap.sh","count":2},{"path":"plugin/omps/hooks/dispatch.sh","count":3},{"path":"plugin/omps/skills/commune/SKILL.md","count":4},{"path":"plugin/omps/skills/force-stop/SKILL.md","count":4},{"path":"plugin/omps/skills/list-agents/SKILL.md","count":4},{"path":"plugin/omps/skills/live/SKILL.md","count":6},{"path":"plugin/omps/skills/ready/SKILL.md","count":4},{"path":"plugin/omps/skills/send/SKILL.md","count":4},{"path":"plugin/omps/skills/setup/SKILL.md","count":14},{"path":"plugin/omps/skills/signoff/SKILL.md","count":4},{"path":"plugin/omps/skills/subnet/SKILL.md","count":5},{"path":"plugin/omps/skills/version/SKILL.md","count":5},{"path":"adapter/strings/briefs/identity.md","count":2},{"path":"adapter/strings/briefs/live-ops.md","count":1},{"path":"adapter/strings/briefs/messaging-no-perch.md","count":1},{"path":"adapter/strings/omp-spt.mjs","count":2},{"path":"adapter/strings/skills/force-stop.md","count":2},{"path":"adapter/strings/skills/list-agents.md","count":2}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# plugin/omps/\n## bootstrap.ps1\n*1│# omps SessionStart bootstrap (PowerShell/Windows) — install spt-core on demand if absent.\n 2│# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.\n*3│# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.\n 4│# [impl->REQ-DIST-BOOTSTRAP-INSTALL]\n 5│if (-not (Get-Command spt -ErrorAction SilentlyContinue)) {\n 6│    Write-Output \"spt-core not found - installing...\"\n## bootstrap.sh\n 1│#!/bin/sh\n*2│# omps SessionStart bootstrap (POSIX) — install spt-core on demand if absent.\n 3│# Verbatim against the published contract: spt-releases harness-contract/install-on-demand.md.\n*4│# The invisible-installer pattern: a user who installs the omps plugin gets spt-core for free.\n 5│# [impl->REQ-DIST-BOOTSTRAP-INSTALL]\n 6│set -eu\n 7│\n\n## .claude-plugin/\n### plugin.json\n 1│{\n*2│  \"name\": \"omps\",\n 3│  \"version\": \"0.1.11\",\n 4│  \"description\": \"Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).\",\n 5│  \"author\": {\n*6│    \"name\": \"perri\"\n 7│  },\n 8│  \"keywords\": [\"spt\", \"spacetime\", \"messaging\", \"agents\", \"live\", \"claude-spt\"]\n 9│}\n\n## hooks/\n### dispatch.sh\n  1│#!/bin/sh\n *2│# omps hook dispatch — STATIC-FOREVER (D1, ADR-0006 ask #1 → resolve-not-execute).\n  3│#\n  4│# This is the ONLY hook shell the cplugs plugin still ships. It carries NO hook logic — that all moved\n  5│# into the consolidated `claude-spt` binary (`claude-spt hook <event>`), which rides `spt adapter\n   │...\n 17│# exist until spt-core + the adapter are installed. So SessionStart installs spt-core on demand first;\n*18│# and if the adapter is not yet registered (pre-/omps:setup), get-string yields nothing and we no-op\n 19│# (exit 0) — exactly the pre-readiness no-op the old per-hook wrappers had. [impl->REQ-DIST-HOOK-BINARY]\n 20│event=\"$1\"\n 21│\n   │...\n 46│\n*47│# Adapter not registered yet (pre-/omps:setup) → no perch to serve → no-op.\n 48│[ -z \"$bin\" ] && exit 0\n 49│\n 50│# Normalize to the bare binary PATH: tolerate a manifest value that still carries a trailing ` hook`\n\n## skills/\n\n### commune/\n#### SKILL.md\n  1│---\n *2│name: commune\n  3│description: |\n  4│  Push a context update to your Psyche so it can brief your resume across a reset. Use when the\n  5│  user says \"commune\" or \"update psyche\", or — as a live agent — after a significant body of work,\n   │...\n 12│\n*13│# /omps:commune\n 14│\n*15│**Live agents only.** No perch (`spt whoami` is empty)? Run **`/omps:live`** first — a ready agent has\n 16│no Psyche and nothing to rebuild from.\n 17│\n 18│The operative commune + `--checkpoint` mechanics are delivered by the adapter (thin skeleton — the\n 19│prose rides `spt adapter update`; ADR-0001/ADR-0006), not baked here: a live agent already carries them\n*20│in its **SessionStart brief** (the `live-ops` block) and in the **`/omps:live` go-live body**. In short:\n 21│write `.claude/<id>-commune.md` as one atomic context delta (task + status, decisions, next steps);\n 22│add the `!!checkpoint!!` trigger to also self-reset (a PAIR of markers brackets a custom wake directive).\n\n### force-stop/\n#### SKILL.md\n  1│---\n *2│name: force-stop\n  3│description: |\n  4│  Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user\n  5│  says \"stop listening\", \"kill live agent\", \"force stop\", or \"tear down perch\". Session-aware: a\n   │...\n 10│\n*11│# /omps:force-stop\n 12│\n 13│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15│>\n 16│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18│\n 19│Tears down an agent's endpoint (graceful shutdown, or a lighter stop).\n\n### list-agents/\n#### SKILL.md\n  1│---\n *2│name: list-agents\n  3│description: |\n  4│  List SPT agents and endpoints reachable from this node. Use when the user says \"list agents\",\n  5│  \"who's live\", \"who's listening\", or wants a roster of active perches.\n   │...\n  8│\n *9│# /omps:list-agents\n 10│\n 11│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*12│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 13│>\n 14│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*15│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 16│\n 17│Shows the roster of spt endpoints, with this session's own pinned distinctly.\n\n### live/\n#### SKILL.md\n  1│---\n *2│name: live\n  3│description: |\n  4│  Run a live agent session. For past sessions, restores a summarized context.\n  5│\n *6│  EXPLICIT START phrases (route to /omps:live <id>):\n  7│  - \"live as\"\n  8│  - \"start live\"\n  9│  - \"go live\"\n   │...\n 11│\n*12│  AUTO-RESUME phrases (route to /omps:live --auto, resumes most-recently-active live agent):\n 13│  - continue live work\n 14│  - resume live work\n 15│  - continue live agent\n   │...\n 29│\n*30│# /omps:live\n 31│\n 32│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*33│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 34│>\n 35│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*36│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 37│\n 38│Upgrades THIS session to a LiveAgent (Psyche-backed).\n\n### ready/\n#### SKILL.md\n  1│---\n *2│name: ready\n  3│description: |\n  4│  Make this Claude Code session reachable for inter-agent messages (register a perch and\n  5│  listen). Use when the user says \"listen as\", \"ready as\", or wants to receive messages from\n   │...\n 10│\n*11│# /omps:ready\n 12│\n 13│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15│>\n 16│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18│\n 19│Registers a perch and listens, so other agents can reach this session.\n\n### send/\n#### SKILL.md\n  1│---\n *2│name: send\n  3│description: |\n  4│  Send a message to another SPT agent. Use when the user says \"send to\", \"message\",\n  5│  \"tell <agent>\", or when you need to reach or reply to another agent yourself.\n   │...\n 11│\n*12│# /omps:send\n 13│\n 14│Deliver a message to another agent (**body read from stdin**). The operative reach is delivered by the\n 15│adapter, not baked here (thin skeleton — the prose rides `spt adapter update`): a perched session\n   │...\n 20│- **Reply:** `printf '%s' \"<body>\" | spt send <sender>` (sender = the `from` on the `<EVENT>` you got).\n*21│- No perch yet? `/omps:ready` (or `/omps:live`) first, or send-and-wait without one via\n 22│  `printf '%s' \"<body>\" | spt ring <target> --timeout 60`.\n 23│\n 24│## Shortform — no Bash needed\n   │...\n 35│- **Commune:** start an output with `>>commune<<` and the rest is written as your commune (same as the\n*36│  `/omps:commune` Write path). A `>>commune<<` output is never scanned for `@<…@>`, so it may quote the\n 37│  syntax freely.\n 38│\n 39│Full guidance: `spt how-to send`.\n\n### setup/\n#### SKILL.md\n  1│---\n *2│name: setup\n  3│description: |\n  4│  Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session\n  5│  (mid-session installer + post-install activation).\n   │...\n  8│\n *9│# /omps:setup\n 10│\n*11│> **Self-contained by necessity.** Unlike other `/omps:*` skills, `setup` cannot rely on\n 12│> UPS-injection of its body: it runs precisely when spt-core may be **absent**, so the injection\n 13│> source is unavailable. The operative steps therefore live in this SKILL.md (the floor); the\n 14│> adapter body mirrors them for the spt-present repair path.\n   │...\n 16│Covers the mid-session install + activation gap (ADR-0001): a user who installs the plugin mid-session\n*17│has not had a SessionStart bootstrap fire, so `/omps:setup` runs the same invisible-installer\n 18│bootstrap to fetch + verify spt-core on demand **and then activates the claude-spt adapter**. A\n 19│present binary is not enough: an unregistered/`deregistered` adapter has no profiles/strings/hints/\n*20│`[digest]`, so the `/omps:*` surface is inert until activation.\n 21│\n 22│**Do this:**\n 23│\n   │...\n 35│     - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` is present near cwd):\n*36│       `spt adapter add ./adapter/claude-spt.toml` (the file-form accepts any path + filename).\n 37│     - **End-user (plugin only):** `spt adapter add --release SaberMage/claude-spt` — fetches the\n 38│       single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries\n 39│       beside a shared manifest; install auto-resolves the host's), extracts to the durable home,\n   │...\n 45│   (no `deregistered`). The `[digest]`/`[session]` templates invoke `claude-spt-digest` +\n*46│   `claude-spt-psyche` by **bare name**, and spt-core resolves them **from the adapter install dir**\n 47│   (the `from …/adapters/_github/<safe>/` path in `spt adapter list`), where `--release` activation\n 48│   already extracted them beside the manifest. **No PATH copy needed** (REQ-INSTALL-11, spt v0.8.0\n 49│   Feature B; verified live on v0.8.1 — digest + daemon-hosted Psyche both resolve from the install\n   │...\n 55│     command with `ccs` (drop-in for `claude`) → run live/ready agents on ccs backends\n*56│     (glm/kimi/custom) via `--adapter claude-spt:ccs` (e.g. `/omps:live`, `/omps:ready`,\n 57│     `spt endpoint run --adapter claude-spt:ccs`). Check `command -v ccs`; if `~/.ccs` exists but `ccs`\n 58│     isn't on PATH, point the user at their ccs bin dir. No action needed if unwanted (base\n 59│     `claude-spt` is unaffected).\n 60│   - Absent → ccs is an optional CLI router for driving alternate model backends (glm/kimi/custom) in\n*61│     place of `claude`. To enable: install ccs (its docs), then re-run `/omps:setup`. Skip if unwanted.\n 62│\n 63│5. **Subnet onboarding (optional — SCOPE setup #3/#4).** A subnet is the private group of paired\n*64│   machines that makes `/omps:send`, `/omps:ready`, and live agents work cross-machine (local use\n 65│   needs none). Check: `spt subnet status`.\n 66│   - In a subnet → to invite a machine: `spt subnet show-code` (6-digit code + URI + QR); on the\n*67│     joiner: `spt subnet join <name> --code <code>`.\n*68│   - Not in one → offer create (`spt subnet create <name>` — seed-holder; prints code/URI/QR) or join\n*69│     (`spt subnet join <name> --code <code>`). Skip if single-machine.\n*70│   - Full verb guidance → **/omps:subnet**. **Elevation:** create/join/show-code are\n 71│     OS-elevation-gated — Windows: elevated (UAC) shell; Linux desktop: pkexec/polkit or sudo\n 72│     terminal; Linux TTY: inline sudo; headless: print the command for the user to run elevated.\n 73│\n\n### signoff/\n#### SKILL.md\n  1│---\n *2│name: signoff\n  3│description: |\n  4│  Gracefully shut down your live session, saving a final context summary. Use when the user says\n  5│  \"sign off\" or \"graceful stop\", or when you (a live agent) are done and want to go offline cleanly.\n   │...\n 10│\n*11│# /omps:signoff\n 12│\n 13│**Live agents only.** No perch (`spt whoami` is empty)? Nothing to sign off.\n 14│\n   │...\n 16│`spt adapter update`; ADR-0001/ADR-0006): a live agent carries them in its **SessionStart brief** (the\n*17│`live-ops` block) and the **`/omps:live` go-live body**. In short: optionally write a brief closing\n 18│summary, then `spt endpoint shutdown` (your own perch) — stops the listener, fires the final context\n*19│save, takes your Psyche down. `/omps:ready` or `/omps:live` brings you back; lighter no-save stop is\n 20│`spt endpoint stop`.\n\n### subnet/\n#### SKILL.md\n  1│---\n *2│name: subnet\n  3│description: |\n  4│  Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join\n  5│  an existing one. Use when the user wants to pair machines, set up a subnet, or reach agents\n *6│  across machines. Cross-machine /omps:send and live agents depend on it.\n  7│argument-hint: \"[status|create|show-code|join]\"\n  8│allowed-tools: [Bash]\n  9│---\n 10│\n*11│# /omps:subnet\n 12│\n 13│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*14│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 15│>\n 16│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*17│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 18│\n 19│Pairs machines into a private network so agents reach each other across nodes.\n\n### version/\n#### SKILL.md\n  1│---\n *2│name: version\n  3│description: |\n  4│  Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin\n *5│  version). Use when the user asks the spt or adapter version, or runs /omps:version.\n  6│allowed-tools: [Bash]\n  7│---\n  8│\n *9│# /omps:version\n 10│\n 11│> **Skeleton — thin by design.** Operative instructions for this skill are delivered by the\n*12│> `omps` adapter at invocation time. Look out for the UserPromptSubmit additionalContext.\n 13│>\n 14│> **Operative.** If injection ever no-ops (spt absent / adapter unregistered), check\n*15│> SPT's installation status using the skill `omps:setup`. Otherwise, avoid additional steps.\n 16│\n 17│Reports the version-of-truth: the spt-core-tracked binary + adapter manifest.\n\n# adapter/strings/\n## omp-spt.mjs\n 13│\n*14│function attribute(tag, name) {\n*15│\tconst marker = ` ${name}=\"`;\n 16│\tconst start = tag.indexOf(marker);\n 17│\tif (start < 0) return undefined;\n 18│\tconst valueStart = start + marker.length;\n\n## briefs/\n### identity.md\n 3│- **Your id is `{id}`.** You already know it (it is right here) — do NOT run `spt whoami` to look it up.\n*4│- Messages — including replies to messages you send — arrive AUTOMATICALLY on your existing perch: your Monitor EVENT stream (`<EVENT type=\"msg\" from=\"<sender>\">body</EVENT>`) or, mid-tool-call, as `<omps_messages>` the hook injects. Process them and reply.\n 5│- Do NOT arm a second Monitor (or poll, or tail anything) to \"wait for a reply\" — your perch already delivers it. Just send and continue; the reply surfaces on its own.\n*6│- Do NOT run /omps:ready or /omps:live — your perch is already up. Re-arming returns COLLISION (that is proof it is alive, not a stale perch).\n### live-ops.md\n 18│- **Sign off** gracefully when done: `spt endpoint shutdown` (your own perch) — stops the listener,\n*19│  fires the final context save, and takes your Psyche down with it. `/omps:ready` or `/omps:live`\n 20│  brings you back. Lighter no-save stop: `spt endpoint stop`.\n### messaging-no-perch.md\n 3│- Send and wait for a reply: `printf '%s' \"<body>\" | spt ring <target> --timeout 60` (the reply prints to stdout; `TIMEOUT` is exit 0 — the message still landed).\n*4│- To RECEIVE messages yourself, run /omps:ready (or /omps:live for a live session).\n\n## skills/\n### force-stop.md\n*1│# /omps:force-stop — operative instructions\n 2│\n 3│Tear down an spt agent's endpoint — your own, or another agent's.\n 4│\n*5│1. Target id `<id>`: your own (`spt whoami`) by default, or the one the user named.\n 6│2. **Graceful (default):** `spt endpoint shutdown [<id>]` — stops the listener, fires the final save,\n 7│   and for a live agent takes its Psyche down too.\n 8│3. **Lighter, no-save:** `spt endpoint stop <id>` — unregisters the perch; the spool is preserved.\n### list-agents.md\n*1│# /omps:list-agents — operative instructions\n 2│\n 3│Show the roster of spt endpoints reachable from this node.\n 4│\n  │...\n 6│   session's own pinned at the top.\n*7│2. Summarize who's online and which is this session. Message one with `/omps:send <id>`.\n 8│\n 9│Options: `spt endpoint list --help`."},"isError":false,"timestamp":1784087218299}}
{"type":"message","id":"d847727f","parentId":"c2e2e4f9","timestamp":"2026-07-15T03:46:58.312Z","message":{"role":"toolResult","toolCallId":"call_a6BPA71pp58IQNQ67LCjQRdS|fc_0ca4af36969c80fa016a5702b226b48198860ceb0ac370680a","toolName":"grep","content":[{"type":"text","text":"# traceable-reqs.toml\n 171|id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n*172|title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173|required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174|\n 175|[[requirements]]\n 176|id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n...\n 242|\n*243|# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted\n 244|# 2026-06-15, seeded from SCOPE.md §\"/spt:setup\" (LOCKED v1 = {1..7}) + finding F-005 (the\n 245|# post-install adapter ACTIVATION step). Inactive (required_stages = []) until the slice executes —\n 246|# REQ-SETUP-ACTIVATE activates to [\"doc\",\"impl\",\"int\"] (Wave 1); the {1..7} feature paths get their\n...\n 261|id = \"REQ-SETUP-ACTIVATE\"\n*262|title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n 263|required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264|\n 265|[[requirements]]\n...\n 285|id = \"REQ-DIST-SESSION-RESUME\"\n*286|title = \"The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\\\"session_id\\\",\\\"id\\\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC chan...\n*287|required_stages = [\"unit\"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIE...\n 288|\n 289|[[requirements]]\n 290|id = \"REQ-DIST-IDLE-TRANSLATE\"\n...\n 378|id = \"REQ-DIST-RC-STARTUP\"\n*379|title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n 380|required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381|\n 382|[[requirements]]\n...\n 408|id = \"REQ-DIST-BOUNDARY-RENAME\"\n*409|title = \"Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \\\"<id> @ <node> (<project>/)\\\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\\\"rename\\\":\\\"...\n*410|required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-05 (operator ask, grill-with-docs; v0.14.1). impl: tools/claude-spt/src/launch.rs — SPT_SESSION_NAME env on the spawned Command (both unix exec + windows spawn, same display_name computation as the -n argv element); tools/claude-spt/src/hook.rs — rename_payload helper + the clear-boundary rename self-send (before checkpoint_fire) + the RENAME_SKIP:no-name loud-skip path; tools/claude-spt/src/translate.rs — rename_directive (par...\n 411|\n 412|[[requirements]]\n 413|id = \"REQ-DIST-WORKER-LIFECYCLE\"\n...\n 478|id = \"REQ-OMP-NATIVE-TUI\"\n*479|title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480|required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n\n# OMP-ADAPTER-PLAN.md\n 10|> DESIGN.md D10) — a persistent, SPT-addressable agent — while spt-core does not\n*11|> yet speak ACP. When/if spt-core gains an ACP client, an ACP bridge supersedes\n 12|> this (omp's RPC mode is already ACP-shaped, so this is a stepping stone, not a\n 13|> dead end).\n 14|\n...\n 41|  `state busy`.\n*42|- **live/psyche resume-turn** = the daemon (or bridge) sends one `{type:\"prompt\"}`\n 43|  per event against the held session; continuity is omp's own session file + id.\n 44|- **mid-turn delivery** = `steer` / `follow_up` — no `additionalContext`, no\n 45|  truncation/echo-gate class at all.\n 46|\n*47|### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n 48|\n*49|**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n*50|spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n 51|runs `omp --mode rpc` as a child, and translates in both directions:\n 52|\n 53|```\n*54|SPT daemon ──spawn──▶ omp-spt-bridge ──stdio JSON──▶ omp --mode rpc\n 55|                          │  spt api seed/bind/poll/state/worker-*   (to spt-core)\n 56|                          └  prompt/steer/follow_up/get_state        (to omp)\n 57|```\n...\n 59|This remains useful when no operator terminal is expected. It cannot satisfy an\n*60|attached endpoint because `omp --mode rpc` creates no native TUI.\n 61|\n 62|**Style B — native omp loads an SPT extension (CHOSEN for `[session.self]`).** omp\n 63|*does* have a hook system (`docs/hooks.md`): a TS factory `export default function\n...\n 78|| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n*79|| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n 80|| `[adapter].shortcut_basename` | `cc` | `omp` |\n 81|| `[adapter].kind` | `harness` | `harness` (unchanged) |\n 82|| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n 83|| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n*84|| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n 85|| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n*86|| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n*87|| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n*88|| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n 89|\n 90|### Session/transcript (for the `[digest]` extractor)\n 91|\n...\n 104|\n*105|omp has no \"inject an endpoint id\" flag, but the bridge is the hosted process and\n 106|can call `get_state` → `sessionId`, then `spt api bind <id> --set-session-id\n 107|<sessionId>` (broker-spawned auth is intrinsic — tips doc §Bringup). Pass the SPT\n*108|endpoint id into the bridge via `[env.SPT_ENDPOINT_ID]` (`direction=\"inject\"`,\n 109|`value=\"{id}\"`).\n 110|\n 111|## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n...\n 116|- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n*117|  (bridge command), `[identity]`. Prove with\n 118|  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119|  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120|- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121|  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122|  Acceptance: from another node, message the perch → get an omp answer; assert\n 123|  `spt` state transitions (isolated CI identity per tips doc §Testing).\n...\n 125|  `spt adapter digest-proof omp-spt --sample <session.jsonl>`.\n*126|- **S4 — live/psyche.** `[session.psyche_init]` gate + the bridge as the\n 127|  per-event resume-turn runner; verify daemon-driven turns don't starve.\n*128|- **S5 — OpenRouter provider (#3).** Ship a documented `models.yml` fragment +\n 129|  `.env` convention; **no adapter code** (see below). Role routing\n 130|  (default/smol/slow/plan) + fallback chains are native.\n 131|\n*132|## #3 OpenRouter billing — it's omp config, not adapter code\n 133|\n*134|Per `docs/providers.md`, `~/.omp/agent/models.yml` takes custom\n\n# tests/\n## bridge-acceptance.sh\n 1|#!/bin/sh\n*2|# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n 3|# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n 4|#\n*5|# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n 6|# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n 7|# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n 8|#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n*9|#   2. the bridge sends a reply back to the sender (the bridge trace).\n 10|#\n 11|# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n 12|# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n*13|# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\n 14|set -u\n 15|\n 16|if [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n*17|  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n 18|  exit 0\n 19|fi\n 20|\n...\n 25|# ── Resolve binaries ─────────────────────────────────────────────────────────\n*26|# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n 27|( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n 28|  || { echo \"FAIL cargo build\"; exit 1; }\n 29|exe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\n...\n 31|FAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n*32|# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\n 33|if command -v spt >/dev/null 2>&1; then SPT=spt\n 34|elif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\n 35|else echo \"FAIL spt binary not found\"; exit 1; fi\n...\n 41|work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n*42|LOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\n 43|SUBNET_ARG=\"\"\n 44|[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n 45|\n...\n 55|\n*56|# ── Run: resident bridge (fake omp) → send a message → assert ──────────────────\n 57|# shellcheck disable=SC2086\n 58|SPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_LOG=\"$LOG\" FAKE_OMP_REPLY=\"$REPLY\" OMP_SPT_DEBUG=1 \\\n*59|  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR\" &\n 60|BPID=$!\n 61|sleep 3   # bind + `spt ready` bringup\n 62|\n...\n 70|  echo \"FAIL inbound: '$MARKER' not in fake-omp prompt log\"; fail=1\n*71|  echo \"---- bridge stderr ----\"; tail -8 \"$ERR\" 2>/dev/null\n 72|fi\n 73|\n*74|# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\n 75|if grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n*76|  echo \"ok   outbound: bridge replied to sender ($TX)\"\n 77|else\n 78|  echo \"FAIL outbound: no 'reply out: to=$TX' on the operational log\"; fail=1\n 79|fi\n...\n 90|\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n*91|ERR2=\"$work/bridge-mute.err\"\n 92|# shellcheck disable=SC2086\n 93|SPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_MUTE=1 OMP_SPT_TURN_TIMEOUT_S=3 \\\n*94|  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR2\" &\n 95|BPID=$!\n 96|sleep 3\n 97|printf 'hang me' | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\n...\n 103|  echo \"FAIL no turn-deadline breach on the log\"; fail=1\n*104|  echo \"---- mute-bridge stderr ----\"; tail -8 \"$ERR2\" 2>/dev/null\n 105|fi\n 106|if grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n 107|  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\n...\n 111|\n*112|# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\n 113|if command -v powershell >/dev/null 2>&1; then\n 114|  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n 115|  if [ -n \"$WPID\" ]; then\n...\n 125|  else\n*126|    echo \"SKIP orphan check (bridge already gone)\"\n 127|  fi\n 128|else\n 129|  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\n## manifest-shortcut.sh\n 48|# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\n*49|grep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n*50|  && echo \"ok   [digest].extractor = digest-omp --session {session_id}\" || { echo \"FAIL [digest].extractor is not digest-omp\"; fail=1; }\n*51|grep -Eq '^[[:space:]]*fetcher[[:space:]]*=[[:space:]]*\"omp-spt history-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n*52|  && echo \"ok   [history].fetcher = history-omp --session {session_id}\" || { echo \"FAIL [history].fetcher is not history-omp\"; fail=1; }\n*53|grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt echo-commune-omp --id \\{id\\} --session-id \\{session_id\\}\"' \"$MANIFEST\" \\\n*54|  && echo \"ok   [session.echo_commune].command = echo-commune-omp\" || { echo \"FAIL echo_commune command is not echo-commune-omp\"; fail=1; }\n*55|npsy=$(grep -cE '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt psyche-omp --id \\{id\\} --session-id \\{session_id\\} --psyche-context-file \\{psyche_context_file\\}\"' \"$MANIFEST\")\n*56|if [ \"$npsy\" -eq 2 ]; then echo \"ok   both psyche roles run psyche-omp with the frozen key shape\"; else echo \"FAIL expected 2 psyche-omp role commands, found $npsy\"; fail=1; fi\n 57|# The retired resident key must stay GONE from both psyche roles.\n 58|if grep -q 'psyche_prompt' \"$MANIFEST\"; then echo \"FAIL a psyche role references the retired {psyche_prompt}\"; fail=1; else echo \"ok   no {psyche_prompt} anywhere (event rides stdin)\"; fi\n 59|\n\n# ci/setup/\n## activate-int.sh\n 1|#!/bin/sh\n*2|# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n 3|# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n 4|# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n 5|# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n...\n 45|\n*46|# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\n 47|out=$(spt adapter add \"$MANIFEST\" 2>&1)\n 48|case \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n 49|\n\n# docs/\n## KNOWN-HAZARDS.md\n 203|  Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s\n*204|  bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename\n 205|  delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:\n 206|  `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).\n 207|\n## OMP-BRIDGE-FIELD-ISSUES.md\n*1|# omp-spt bridge — field issues (doyle, 2026-07-09)\n 2|\n 3|Report from an spt-core maintainer (doyle @ HFENDULEAM) after the operator ran a\n 4|live agent (`todlando`) under the experimental **omp-spt** adapter. The endpoint\n...\n 6|and could not be switched back to `claude-spt`. This documents the root causes,\n*7|grounded in this repo's code, so the bridge can be hardened.\n 8|\n*9|All line references are against `tools/claude-spt/src/bridge.rs` and\n 10|`adapter/omp-spt.toml` at commit `f0109d1`.\n 11|\n 12|## Incident summary\n...\n 14|1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15|   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16|2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17|   way to tell working vs. wedged.\n 18|3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19|   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20|   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21|   endpoint looked alive but was dead.\n*22|4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23|5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24|\n 25|Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n...\n 32|\n*33|**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n 34|indistinguishable from a genuine claude-spt process. During the incident this\n 35|made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n*36|todlando's perch was actually the omp bridge. It directly caused the\n 37|misdiagnosis.\n 38|\n 39|**Root.** `adapter/omp-spt.toml:693` (and `:722` for resume) ship:\n 40|```\n*41|command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42|```\n 43|The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44|This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n*45|bridge (S2) is `omp-spt`\"*).\n 46|\n*47|**Fix.** Rename the shipped bridge binary to `omp-spt` (or `omp-spt-bridge`) and\n*48|update `[session.self]`/`[session.resume]` `command` accordingly. A distinct\n 49|basename makes the process self-identifying and removes the collision.\n 50|\n 51|## HIGH-2 — No teardown / signal handling → orphaned children on stop\n...\n 54|`spt ready <id>` child running; the perch stayed `READY`. Recovery needed a\n*55|manual `taskkill /T` of the bridge tree.\n 56|\n*57|**Root.** `bridge.rs` installs no signal/console-control handler and no OS job\n 58|object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n 59|`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n*60|`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n*61|When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62|those lines never run. On Windows, killing a process does not kill its children,\n 63|so both the omp child and the `spt ready` child are leaked. The leaked\n 64|`spt ready` keeps the perch registered, so the dead endpoint still advertises as\n...\n 66|\n*67|**Fix.** Tie child lifetime to the bridge: on Windows assign the omp + `spt ready`\n 68|children to a `JobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; on Unix use\n 69|a process group + `kill_on_drop`. Install a console-control/signal handler that\n 70|tears both children down and unbinds the perch. Consider a `[session.stop]`\n...\n 77|\n*78|**Root.** The bridge runs omp in `--mode rpc` (no TTY UI) and only *inherits*\n*79|omp's stderr (`bridge.rs:132`). The sole thing printed to the broker PTY is the\n 80|`spt ready` child's `READY:<id>` banner (its stderr is inherited,\n*81|`bridge.rs:192`). All turn activity — message received, prompt dispatched, reply\n 82|sent, errors — is emitted only under `OMP_SPT_DEBUG` via the `trace!` macro\n## OMP-OPENROUTER.md\n*1|# OpenRouter billing for the omp-spt Librarian (S5, plan item #3)\n 2|\n*3|Routing the Librarian's model spend through OpenRouter is **omp configuration, not adapter\n 4|code** — omp's provider system (`docs/providers.md` in oh-my-pi) takes custom openai-completions\n 5|providers natively, with role routing (default/smol/slow/plan) and fallback chains built in. The\n 6|adapter's whole job here is to document the fragment and note the key conventions.\n 7|\n*8|## The models.yml fragment\n 9|\n*10|`~/.omp/agent/models.yml` (or per-profile: `~/.omp/profiles/<name>/agent/models.yml`):\n 11|\n 12|```yaml\n 13|providers:\n...\n 19|      - id: anthropic/claude-sonnet-5\n*20|        name: Claude Sonnet 5 (OpenRouter)\n 21|        contextWindow: 200000\n 22|        maxTokens: 8192\n*23|      # add more OpenRouter model ids as needed; omp fuzzy-matches --model against them\n 24|```\n 25|\n 26|## Key resolution + .env layering\n...\n 30|1. CLI `--api-key`\n*31|2. `models.yml` `apiKey`\n 32|3. stored credential (`/login`)\n 33|4. OAuth\n 34|5. provider env / `.env` files\n...\n 38|So the operational convention is: put `OPENROUTER_API_KEY=…` in `~/.omp/agent/.env` (machine-wide)\n*39|or the Librarian project's `.env` (project-scoped), and reference it by NAME in `models.yml`.\n 40|\n 41|## Role routing\n 42|\n...\n 49|\n*50|The adapter's shims deliberately do NOT pin models (`psyche-omp`, `echo-commune-omp` run the\n 51|profile default): a hardcoded model name may not resolve against a user's provider set. Keep the\n 52|cheap-companion economics by assigning an inexpensive default (or smol) model in the profile the\n 53|Librarian endpoint runs under.\n...\n 56|\n*57|The default `~/.omp` profile already routes through OpenRouter (verified live 2026-07-09:\n 58|`get_state` reports `api: openrouter`, `provider: openrouter`) — so for the Librarian bringup the\n 59|remaining choice is which models/roles to assign, not whether billing routing works.\n 60|\n## SPT-CORE-FINDINGS.md\n 426|present, must run `spt adapter add --github <our-adapter-repo>`** (or a local dir) to ACTIVATE — that\n*427|is the bridge. \"Binary present ≠ adapter active\" confirmed correct.\n 428|\n 429|**doyle's doc actions (docs-site):** publish the post-install ACTIVATION step (`adapter add\n 430|[--github]`) in `install-on-demand` + the integration checklist · author Ed25519 signing-key\n...\n 444|**Our action items (ours, not doyle's):** (i) wire `spt adapter add --github <repo>` into\n*445|`/sptc:setup`'s binary-present branch (the activation bridge — folds into the setup-slice below);\n 446|(ii) the manifest needs its own **published github repo target** for the `--github` end-user path\n 447|(distinct from the cplugs skeleton repo); local dev uses `spt adapter add ./adapter`.\n 448|\n\n## adr/\n### 0006-one-command-update-and-consolidated-binary.md\n 56|- The thin-skeleton goal advances even before the asks land: skill bodies already ride strings; the **reactive skills** (`commune`/`send`/`signoff`, full-fat in the plugin because they are invoked without a typed slash-command) can be thinned to stubs by moving their bodies into the perched SessionStart brief (adapter strings) plus the `/spt:live` UPS body — with a known **delivery-timing wrinkle** (the perched brief fires on bind|boundary, not the instant a seed session goes live), tracked as a plan i...\n*57|- Until asks #1/#3 land, hook logic and the translate binary stay on their current channels; the design degrades gracefully (the `message` field bridges the manual steps).\n 58|- One artifact per OS simplifies packaging but couples all seams' build/release — acceptable, they already ship in one `.spt`.\n### 0007-native-omp-tui-hosts-spt-extension.md\n 4|\n*5|accepted (2026-07-13) — supersedes the interactive-hosting choice in `OMP-ADAPTER-PLAN.md`; the RPC bridge remains a headless integration surface.\n 6|\n 7|## Context\n 8|\n*9|Issue #2 requires `spt endpoint run --adapter omp-spt ...` to expose the harness PTY. The shipped session command instead made `omp-spt bridge` the PTY leader and launched `omp --mode rpc` behind pipes. Attaching therefore showed bridge logs, while standalone `omp` showed the expected native terminal UI. No attach flag can recover a UI that the hosted process never created.\n 10|\n 11|## Decision\n 12|\n*13|Run native interactive `omp` as `[session.self]` and `[session.resume]`. Load a packaged OMP extension that binds the OMP session to the SPT endpoint, listens for SPT messages, submits each message through `sendUserMessage`, tracks busy/idle state, and sends the final assistant response back to the sender.\n 14|\n*15|The extension uses OMP's public extension API. It does not render or proxy the terminal: OMP remains the PTY leader and owns its complete native TUI. The old RPC bridge remains in the binary for explicitly headless callers, but endpoint bringup no longer routes through it.\n 16|\n 17|<!-- [doc->REQ-OMP-NATIVE-TUI] -->\n 18|\n...\n 20|\n*21|- `spt rc <id>` and attach-default endpoint bringup display the same native TUI as standalone `omp`.\n 22|- SPT delivery is serialized so two inbound messages cannot collapse into one OMP turn or receive the wrong reply.\n 23|- Extension code ships under the already-packaged `strings/` directory, avoiding a new archive layout or dependency.\n*24|- OMP profile/model flags belong directly on the manifest's `omp` command instead of after the bridge's `--` separator.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"traceable-reqs.toml, tests, ci, docs, OMP-ADAPTER-PLAN.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\omp-spt","cwd":"C:\\Users\\decid\\Documents\\projects\\omp-spt","matchCount":99,"fileCount":11,"files":["traceable-reqs.toml","tests/bridge-acceptance.sh","tests/manifest-shortcut.sh","ci/setup/activate-int.sh","docs/KNOWN-HAZARDS.md","docs/OMP-BRIDGE-FIELD-ISSUES.md","docs/OMP-OPENROUTER.md","docs/SPT-CORE-FINDINGS.md","docs/adr/0006-one-command-update-and-consolidated-binary.md","docs/adr/0007-native-omp-tui-hosts-spt-extension.md","OMP-ADAPTER-PLAN.md"],"fileMatches":[{"path":"traceable-reqs.toml","count":11},{"path":"tests/bridge-acceptance.sh","count":18},{"path":"tests/manifest-shortcut.sh","count":8},{"path":"ci/setup/activate-int.sh","count":2},{"path":"docs/KNOWN-HAZARDS.md","count":1},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","count":20},{"path":"docs/OMP-OPENROUTER.md","count":10},{"path":"docs/SPT-CORE-FINDINGS.md","count":2},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","count":1},{"path":"docs/adr/0007-native-omp-tui-hosts-spt-extension.md","count":6},{"path":"OMP-ADAPTER-PLAN.md","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# traceable-reqs.toml\n 171│id = \"REQ-HAZARD-PSYCHE-PERMS-DEADLOCK\"\n*172│title = \"Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.\"\n*173│required_stages = [\"doc\", \"unit\"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 ...\n 174│\n 175│[[requirements]]\n 176│id = \"REQ-HAZARD-PSYCHE-HOST-THRASH\"\n    │...\n 242│\n*243│# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted\n 244│# 2026-06-15, seeded from SCOPE.md §\"/spt:setup\" (LOCKED v1 = {1..7}) + finding F-005 (the\n 245│# post-install adapter ACTIVATION step). Inactive (required_stages = []) until the slice executes —\n 246│# REQ-SETUP-ACTIVATE activates to [\"doc\",\"impl\",\"int\"] (Wave 1); the {1..7} feature paths get their\n    │...\n 261│id = \"REQ-SETUP-ACTIVATE\"\n*262│title = \"/sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.\"\n 263│required_stages = [\"doc\", \"int\"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profi...\n 264│\n 265│[[requirements]]\n    │...\n 285│id = \"REQ-DIST-SESSION-RESUME\"\n*286│title = \"The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\\\"session_id\\\",\\\"id\\\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC chan...\n*287│required_stages = [\"unit\"]  # [session.resume] is a DECLARATION-ONLY seam: the in-repo deliverable is the manifest table adapter/claude-spt.toml (validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`), and the traceable-reqs scanner does NOT read .toml, so there is no scannable impl artifact in-repo (the code that ACTS on the declaration is the spt-core daemon — out of repo, exercised at int). Same convention as REQ-DIST-SHORTCUT-BASENAME/REQ-DIST-SESSIONSTART-BRIE...\n 288│\n 289│[[requirements]]\n 290│id = \"REQ-DIST-IDLE-TRANSLATE\"\n    │...\n 378│id = \"REQ-DIST-RC-STARTUP\"\n*379│title = \"Every spt-hosted CC bringup — [session.self] (fresh) AND [session.resume] (native-resume), base AND ccs profile — is NODE-AND-PROJECT-NAMED: display name `-n \\\"<id> @ <node> (<project>/)\\\"` + Remote Control `--remote-control <id>--<node>--<project-token>`, so a fleet of endpoints is cross-node- AND cross-project-distinguishable in the prompt box / /resume picker / RC list. Delivered ADAPTER-SIDE via the `claude-spt launch` spawn shim (v0.10.3+): the published fill catalog has NO {node} key (...\n 380│required_stages = [\"impl\", \"unit\"]  # U6 ACTIVATED 2026-06-25 ({id}-only, unit-only declaration seam). PROMOTED to impl+unit 2026-07-01 (v0.10.3): the stretch shipped adapter-side, so there is now scannable impl. impl: tools/claude-spt/src/launch.rs — Args parse (--id/--cli/--resume/--node), node_name (COMPUTERNAME → HOSTNAME → `hostname`), project_name (launch-cwd basename), rc_project_token (RC-safe sanitizer), display_name/rc_name (drop project suffix on unknown project, degrade-to-bare-id on un...\n 381│\n 382│[[requirements]]\n    │...\n 408│id = \"REQ-DIST-BOUNDARY-RENAME\"\n*409│title = \"Every /clear boundary on an spt-hosted endpoint RE-ASSERTS the session display name (the boundary rename): CC drops the `-n \\\"<id> @ <node> (<project>/)\\\"` display name when a clear rotates the session, leaving the operator no rendered TUI anchor. The launch shim exports the computed display name as SPT_SESSION_NAME into the spawned CC env (single computation site — the same string that feeds `-n`, parity by construction); the SessionStart hook, on a `clear` boundary, self-sends {\\\"rename\\\":\\\"...\n*410│required_stages = [\"impl\", \"unit\", \"int\"]  # MINTED + ACTIVATED 2026-07-05 (operator ask, grill-with-docs; v0.14.1). impl: tools/claude-spt/src/launch.rs — SPT_SESSION_NAME env on the spawned Command (both unix exec + windows spawn, same display_name computation as the -n argv element); tools/claude-spt/src/hook.rs — rename_payload helper + the clear-boundary rename self-send (before checkpoint_fire) + the RENAME_SKIP:no-name loud-skip path; tools/claude-spt/src/translate.rs — rename_directive (par...\n 411│\n 412│[[requirements]]\n 413│id = \"REQ-DIST-WORKER-LIFECYCLE\"\n    │...\n 478│id = \"REQ-OMP-NATIVE-TUI\"\n*479│title = \"An attached omp-spt endpoint exposes OMP's native interactive TUI, not a headless RPC bridge terminal, while retaining SPT bind/listen/state/reply behavior through OMP's public extension API.\"\n*480│required_stages = [\"doc\", \"impl\", \"unit\"]  # doc: ADR-0007. impl: adapter/strings/omp-spt.mjs + native-omp [session.self]/[session.resume] commands. unit: tests/omp-extension.mjs + tests/manifest-shortcut.sh. Live PTY acceptance is performed for the release candidate; automate it when Herdr has a stable TUI assertion surface suitable for CI.\n\n# OMP-ADAPTER-PLAN.md\n  10│> DESIGN.md D10) — a persistent, SPT-addressable agent — while spt-core does not\n *11│> yet speak ACP. When/if spt-core gains an ACP client, an ACP bridge supersedes\n  12│> this (omp's RPC mode is already ACP-shaped, so this is a stepping stone, not a\n  13│> dead end).\n  14│\n    │...\n  41│  `state busy`.\n *42│- **live/psyche resume-turn** = the daemon (or bridge) sends one `{type:\"prompt\"}`\n  43│  per event against the held session; continuity is omp's own session file + id.\n  44│- **mid-turn delivery** = `steer` / `follow_up` — no `additionalContext`, no\n  45│  truncation/echo-gate class at all.\n  46│\n *47│### Architecture decision: native TUI for hosted endpoints; RPC for headless callers\n  48│\n *49│**Style A — RPC bridge as a headless harness (RETAINED, NOT INTERACTIVE).** It\n *50│spawns a small **bridge binary** (Rust, replacing `tools/claude-spt/`). The bridge\n  51│runs `omp --mode rpc` as a child, and translates in both directions:\n  52│\n  53│```\n *54│SPT daemon ──spawn──▶ omp-spt-bridge ──stdio JSON──▶ omp --mode rpc\n  55│                          │  spt api seed/bind/poll/state/worker-*   (to spt-core)\n  56│                          └  prompt/steer/follow_up/get_state        (to omp)\n  57│```\n    │...\n  59│This remains useful when no operator terminal is expected. It cannot satisfy an\n *60│attached endpoint because `omp --mode rpc` creates no native TUI.\n  61│\n  62│**Style B — native omp loads an SPT extension (CHOSEN for `[session.self]`).** omp\n  63│*does* have a hook system (`docs/hooks.md`): a TS factory `export default function\n    │...\n  78│| `[adapter].name` / plugin ns | `claude-spt` / `sptc` | `omp-spt` / `omps` (or keep `sptc`? decide) |\n *79│| `[adapter].host_binaries` | `[\"claude\"]` | `[\"omp\"]` (+ the bridge basename) |\n  80│| `[adapter].shortcut_basename` | `cc` | `omp` |\n  81│| `[adapter].kind` | `harness` | `harness` (unchanged) |\n  82│| `[update].repo` / `avenue` | `SaberMage/claude-spt` / `gh_release` | `BigscreenVR/omp-spt` / `gh_release` |\n  83│| `[session.self].command` (bringup) | `claude …` launcher | native `omp --extension …/omp-spt.mjs` |\n *84│| `[identity].parent_ancestor_name` | `claude` | `omp` (or the bridge) |\n  85│| `[digest]` source/extractor | CC `<root>/<cwd-slug>/<sid>.jsonl` | omp `~/.omp/agent/sessions/<dir-enc>/<ts>_<sid>.jsonl` |\n *86│| `[session.psyche_resume].command` | `claude --continue -p` | bridge per-pulse `{type:\"prompt\"}` (or `omp -r <id> -p`) |\n *87│| hook plugin `plugin/sptc/` | 8 CC hooks → `dispatch.sh` → binary | **removed** (RPC bridge owns lifecycle); optional omp-hook fallback |\n *88│| provider/billing (#3) | n/a (CC key) | `~/.omp/agent/models.yml` OpenRouter provider (config artifact, below) |\n  89│\n  90│### Session/transcript (for the `[digest]` extractor)\n  91│\n    │...\n 104│\n*105│omp has no \"inject an endpoint id\" flag, but the bridge is the hosted process and\n 106│can call `get_state` → `sessionId`, then `spt api bind <id> --set-session-id\n 107│<sessionId>` (broker-spawned auth is intrinsic — tips doc §Bringup). Pass the SPT\n*108│endpoint id into the bridge via `[env.SPT_ENDPOINT_ID]` (`direction=\"inject\"`,\n 109│`value=\"{id}\"`).\n 110│\n 111│## Slices (ready-first, then live — mirroring how claude-spt itself grew)\n    │...\n 116│- **S1 — Manifest skeleton + capability.** Fill `[adapter]`, `[session.self]`\n*117│  (bridge command), `[identity]`. Prove with\n 118│  `spt api --adapter omp-spt --manifest adapter/omp-spt.toml capability` (advertises\n 119│  the hostable types) and a clean `spt adapter add` (manifest-first validation).\n*120│- **S2 — RPC bridge (ready-agent).** New Rust bin (replaces `tools/claude-spt/`):\n 121│  spawn `omp --mode rpc`, `ready`/`prompt`/`agent_end`↔`spt api seed/poll/state`.\n 122│  Acceptance: from another node, message the perch → get an omp answer; assert\n 123│  `spt` state transitions (isolated CI identity per tips doc §Testing).\n    │...\n 125│  `spt adapter digest-proof omp-spt --sample <session.jsonl>`.\n*126│- **S4 — live/psyche.** `[session.psyche_init]` gate + the bridge as the\n 127│  per-event resume-turn runner; verify daemon-driven turns don't starve.\n*128│- **S5 — OpenRouter provider (#3).** Ship a documented `models.yml` fragment +\n 129│  `.env` convention; **no adapter code** (see below). Role routing\n 130│  (default/smol/slow/plan) + fallback chains are native.\n 131│\n*132│## #3 OpenRouter billing — it's omp config, not adapter code\n 133│\n*134│Per `docs/providers.md`, `~/.omp/agent/models.yml` takes custom\n\n# tests/\n## bridge-acceptance.sh\n   1│#!/bin/sh\n  *2│# omp-spt bridge acceptance (S2 ready-agent) — the end-to-end proof that a message to the perch is\n   3│# driven into omp and answered, WITHOUT a live model (deterministic, zero billing). [impl->REQ-SKILL-LIVE]\n   4│#\n  *5│# It exercises the REAL bridge (`claude-spt bridge`) against the REAL `spt` binary, substituting a\n   6│# deterministic fake-omp (the compiled `examples/fake-omp` example) for `omp --mode rpc` via\n   7│# $OMP_SPT_OMP_BIN. Asserts on side-effects (the plan's discipline — never model text):\n   8│#   1. the SPT message reaches omp as a `{\"type\":\"prompt\"}` frame (the fake-omp prompt-log), and\n  *9│#   2. the bridge sends a reply back to the sender (the bridge trace).\n  10│#\n  11│# LIVE + spt-mutating (creates/purges a DISPOSABLE perch), so it is GATED OFF by default — set\n  12│# OMP_SPT_ACCEPTANCE=1 to run. On a multi-subnet node set OMP_SPT_TEST_SUBNET (e.g. Phils-Den);\n *13│# on a single-subnet node / CI leave it unset. Run: OMP_SPT_ACCEPTANCE=1 sh tests/bridge-acceptance.sh\n  14│set -u\n  15│\n  16│if [ \"${OMP_SPT_ACCEPTANCE:-0}\" != \"1\" ]; then\n *17│  echo \"SKIP bridge-acceptance (set OMP_SPT_ACCEPTANCE=1 to run the live, spt-mutating E2E)\"\n  18│  exit 0\n  19│fi\n  20│\n    │...\n  25│# ── Resolve binaries ─────────────────────────────────────────────────────────\n *26│# Build the bridge + the fake-omp example (idempotent; the example never ships in adapter.spt).\n  27│( cd \"$CRATE\" && cargo build --release && cargo build --release --example fake-omp ) >/dev/null 2>&1 \\\n  28│  || { echo \"FAIL cargo build\"; exit 1; }\n  29│exe() { [ -f \"$1.exe\" ] && echo \"$1.exe\" || echo \"$1\"; }\n    │...\n  31│FAKE=$(exe \"$CRATE/target/release/examples/fake-omp\")\n *32│# spt: PATH first, then the known install location (mirrors the bridge's own spt_bin resolution).\n  33│if command -v spt >/dev/null 2>&1; then SPT=spt\n  34│elif [ -f \"$LOCALAPPDATA/spt-core/bin/spt.exe\" ]; then SPT=\"$LOCALAPPDATA/spt-core/bin/spt.exe\"\n  35│else echo \"FAIL spt binary not found\"; exit 1; fi\n    │...\n  41│work=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\n *42│LOG=\"$work/prompts.log\"; ERR=\"$work/bridge.err\"\n  43│SUBNET_ARG=\"\"\n  44│[ -n \"${OMP_SPT_TEST_SUBNET:-}\" ] && SUBNET_ARG=\"--subnet $OMP_SPT_TEST_SUBNET\"\n  45│\n    │...\n  55│\n *56│# ── Run: resident bridge (fake omp) → send a message → assert ──────────────────\n  57│# shellcheck disable=SC2086\n  58│SPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_LOG=\"$LOG\" FAKE_OMP_REPLY=\"$REPLY\" OMP_SPT_DEBUG=1 \\\n *59│  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR\" &\n  60│BPID=$!\n  61│sleep 3   # bind + `spt ready` bringup\n  62│\n    │...\n  70│  echo \"FAIL inbound: '$MARKER' not in fake-omp prompt log\"; fail=1\n *71│  echo \"---- bridge stderr ----\"; tail -8 \"$ERR\" 2>/dev/null\n  72│fi\n  73│\n *74│# 2. Outbound: the bridge replied to the sender (always-on oplog line, HIGH-3 — no debug needed).\n  75│if grep -q \"reply out: to=$TX\" \"$ERR\" 2>/dev/null; then\n *76│  echo \"ok   outbound: bridge replied to sender ($TX)\"\n  77│else\n  78│  echo \"FAIL outbound: no 'reply out: to=$TX' on the operational log\"; fail=1\n  79│fi\n    │...\n  90│\"$SPT\" endpoint purge \"$ID\" --force >/dev/null 2>&1\n *91│ERR2=\"$work/bridge-mute.err\"\n  92│# shellcheck disable=SC2086\n  93│SPT_ENDPOINT_ID=\"$ID\" OMP_SPT_OMP_BIN=\"$FAKE\" FAKE_OMP_MUTE=1 OMP_SPT_TURN_TIMEOUT_S=3 \\\n *94│  \"$BIN\" bridge --id \"$ID\" $SUBNET_ARG 2> \"$ERR2\" &\n  95│BPID=$!\n  96│sleep 3\n  97│printf 'hang me' | \"$SPT\" send \"$ID\" --from \"$TX\" >/dev/null 2>&1\n    │...\n 103│  echo \"FAIL no turn-deadline breach on the log\"; fail=1\n*104│  echo \"---- mute-bridge stderr ----\"; tail -8 \"$ERR2\" 2>/dev/null\n 105│fi\n 106│if grep -q \"failure notice out: to=$TX\" \"$ERR2\" 2>/dev/null; then\n 107│  echo \"ok   sender was told the turn failed (LOW-2: never a silent empty reply)\"\n    │...\n 111│\n*112│# ── HIGH-2: hard-killing the bridge leaves no orphaned children (Windows job object) ──\n 113│if command -v powershell >/dev/null 2>&1; then\n 114│  WPID=$(powershell -NoProfile -Command \"(Get-Process omp-spt -ErrorAction SilentlyContinue | Select-Object -First 1).Id\" | tr -d '\\r')\n 115│  if [ -n \"$WPID\" ]; then\n    │...\n 125│  else\n*126│    echo \"SKIP orphan check (bridge already gone)\"\n 127│  fi\n 128│else\n 129│  echo \"SKIP orphan check (non-Windows: job-object teardown is Windows-only today)\"\n## manifest-shortcut.sh\n 48│# ── extractor seams point at the omp-flavored subcommands ─────────────────────────────────────────\n*49│grep -Eq '^[[:space:]]*extractor[[:space:]]*=[[:space:]]*\"omp-spt digest-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n*50│  && echo \"ok   [digest].extractor = digest-omp --session {session_id}\" || { echo \"FAIL [digest].extractor is not digest-omp\"; fail=1; }\n*51│grep -Eq '^[[:space:]]*fetcher[[:space:]]*=[[:space:]]*\"omp-spt history-omp --session \\{session_id\\}\"' \"$MANIFEST\" \\\n*52│  && echo \"ok   [history].fetcher = history-omp --session {session_id}\" || { echo \"FAIL [history].fetcher is not history-omp\"; fail=1; }\n*53│grep -Eq '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt echo-commune-omp --id \\{id\\} --session-id \\{session_id\\}\"' \"$MANIFEST\" \\\n*54│  && echo \"ok   [session.echo_commune].command = echo-commune-omp\" || { echo \"FAIL echo_commune command is not echo-commune-omp\"; fail=1; }\n*55│npsy=$(grep -cE '^[[:space:]]*command[[:space:]]*=[[:space:]]*\"omp-spt psyche-omp --id \\{id\\} --session-id \\{session_id\\} --psyche-context-file \\{psyche_context_file\\}\"' \"$MANIFEST\")\n*56│if [ \"$npsy\" -eq 2 ]; then echo \"ok   both psyche roles run psyche-omp with the frozen key shape\"; else echo \"FAIL expected 2 psyche-omp role commands, found $npsy\"; fail=1; fi\n 57│# The retired resident key must stay GONE from both psyche roles.\n 58│if grep -q 'psyche_prompt' \"$MANIFEST\"; then echo \"FAIL a psyche role references the retired {psyche_prompt}\"; fail=1; else echo \"ok   no {psyche_prompt} anywhere (event rides stdin)\"; fi\n 59│\n\n# ci/setup/\n## activate-int.sh\n  1│#!/bin/sh\n *2│# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n  3│# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n  4│# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n  5│# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n   │...\n 45│\n*46│# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\n 47│out=$(spt adapter add \"$MANIFEST\" 2>&1)\n 48│case \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n 49│\n\n# docs/\n## KNOWN-HAZARDS.md\n 203│  Invariant: the fire emits rename + wake as ONE combined sequence with an explicit enter→ctrl+s\n*204│  bridge delay between the halves (`commands_for_boundary`, `BOUNDARY_BRIDGE_MS`); the rename\n 205│  delivery only STASHES. Never reintroduce a second post-clear inject sequence. Regression lock:\n 206│  `checkpoint_boundary_is_one_combined_sequence_rename_then_wake` (crate unit).\n 207│\n## OMP-BRIDGE-FIELD-ISSUES.md\n *1│# omp-spt bridge — field issues (doyle, 2026-07-09)\n  2│\n  3│Report from an spt-core maintainer (doyle @ HFENDULEAM) after the operator ran a\n  4│live agent (`todlando`) under the experimental **omp-spt** adapter. The endpoint\n   │...\n  6│and could not be switched back to `claude-spt`. This documents the root causes,\n *7│grounded in this repo's code, so the bridge can be hardened.\n  8│\n *9│All line references are against `tools/claude-spt/src/bridge.rs` and\n 10│`adapter/omp-spt.toml` at commit `f0109d1`.\n 11│\n 12│## Incident summary\n   │...\n 14│1. Operator started `todlando` under omp-spt (`[session.self]` →\n*15│   `{adapter_dir}/claude-spt bridge --id todlando --node HFENDULEAM`).\n 16│2. Broker PTY showed `READY:todlando` and nothing further — no turn activity, no\n 17│   way to tell working vs. wedged.\n 18│3. Operator detached + \"stopped\" the endpoint. Two processes survived as\n*19│   orphans: the bridge (`claude-spt.exe bridge --id todlando`, pid 82460) and its\n 20│   `spt ready todlando` child (pid 5956). The perch stayed registered → the\n 21│   endpoint looked alive but was dead.\n*22│4. Resuming from the last claude-spt session still launched the omp-spt bridge.\n 23│5. Starting fresh after switching the harness adapter still launched omp-spt.\n 24│\n 25│Steps 3–5 required manual intervention on the spt-core side (kill the orphan\n   │...\n 32│\n*33│**Symptom.** In `ps`/`tasklist` the omp-spt bridge appears as `claude-spt.exe` —\n 34│indistinguishable from a genuine claude-spt process. During the incident this\n 35│made it impossible to tell, at a glance, that the \"claude-spt\" process holding\n*36│todlando's perch was actually the omp bridge. It directly caused the\n 37│misdiagnosis.\n 38│\n 39│**Root.** `adapter/omp-spt.toml:693` (and `:722` for resume) ship:\n 40│```\n*41│command = \"{adapter_dir}/claude-spt bridge --id {id} --node {node}\"\n 42│```\n 43│The executable is named `claude-spt`, the exact basename of the upstream adapter.\n 44│This contradicts the adapter's own design note (`omp-spt.toml:436`: *\"the RPC\n*45│bridge (S2) is `omp-spt`\"*).\n 46│\n*47│**Fix.** Rename the shipped bridge binary to `omp-spt` (or `omp-spt-bridge`) and\n*48│update `[session.self]`/`[session.resume]` `command` accordingly. A distinct\n 49│basename makes the process self-identifying and removes the collision.\n 50│\n 51│## HIGH-2 — No teardown / signal handling → orphaned children on stop\n   │...\n 54│`spt ready <id>` child running; the perch stayed `READY`. Recovery needed a\n*55│manual `taskkill /T` of the bridge tree.\n 56│\n*57│**Root.** `bridge.rs` installs no signal/console-control handler and no OS job\n 58│object — confirmed: no `SetConsoleCtrlHandler` / `JobObject` / `kill_on_drop` /\n 59│`ctrlc` anywhere in `tools/claude-spt/src/`. The only child cleanup is the two\n*60│`kill()` calls at the *graceful* end of the relay loop (`bridge.rs:241-242`).\n*61│When the broker or operator terminates the bridge process (the normal \"stop\"),\n 62│those lines never run. On Windows, killing a process does not kill its children,\n 63│so both the omp child and the `spt ready` child are leaked. The leaked\n 64│`spt ready` keeps the perch registered, so the dead endpoint still advertises as\n   │...\n 66│\n*67│**Fix.** Tie child lifetime to the bridge: on Windows assign the omp + `spt ready`\n 68│children to a `JobObject` with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`; on Unix use\n 69│a process group + `kill_on_drop`. Install a console-control/signal handler that\n 70│tears both children down and unbinds the perch. Consider a `[session.stop]`\n   │...\n 77│\n*78│**Root.** The bridge runs omp in `--mode rpc` (no TTY UI) and only *inherits*\n*79│omp's stderr (`bridge.rs:132`). The sole thing printed to the broker PTY is the\n 80│`spt ready` child's `READY:<id>` banner (its stderr is inherited,\n*81│`bridge.rs:192`). All turn activity — message received, prompt dispatched, reply\n 82│sent, errors — is emitted only under `OMP_SPT_DEBUG` via the `trace!` macro\n## OMP-OPENROUTER.md\n *1│# OpenRouter billing for the omp-spt Librarian (S5, plan item #3)\n  2│\n *3│Routing the Librarian's model spend through OpenRouter is **omp configuration, not adapter\n  4│code** — omp's provider system (`docs/providers.md` in oh-my-pi) takes custom openai-completions\n  5│providers natively, with role routing (default/smol/slow/plan) and fallback chains built in. The\n  6│adapter's whole job here is to document the fragment and note the key conventions.\n  7│\n *8│## The models.yml fragment\n  9│\n*10│`~/.omp/agent/models.yml` (or per-profile: `~/.omp/profiles/<name>/agent/models.yml`):\n 11│\n 12│```yaml\n 13│providers:\n   │...\n 19│      - id: anthropic/claude-sonnet-5\n*20│        name: Claude Sonnet 5 (OpenRouter)\n 21│        contextWindow: 200000\n 22│        maxTokens: 8192\n*23│      # add more OpenRouter model ids as needed; omp fuzzy-matches --model against them\n 24│```\n 25│\n 26│## Key resolution + .env layering\n   │...\n 30│1. CLI `--api-key`\n*31│2. `models.yml` `apiKey`\n 32│3. stored credential (`/login`)\n 33│4. OAuth\n 34│5. provider env / `.env` files\n   │...\n 38│So the operational convention is: put `OPENROUTER_API_KEY=…` in `~/.omp/agent/.env` (machine-wide)\n*39│or the Librarian project's `.env` (project-scoped), and reference it by NAME in `models.yml`.\n 40│\n 41│## Role routing\n 42│\n   │...\n 49│\n*50│The adapter's shims deliberately do NOT pin models (`psyche-omp`, `echo-commune-omp` run the\n 51│profile default): a hardcoded model name may not resolve against a user's provider set. Keep the\n 52│cheap-companion economics by assigning an inexpensive default (or smol) model in the profile the\n 53│Librarian endpoint runs under.\n   │...\n 56│\n*57│The default `~/.omp` profile already routes through OpenRouter (verified live 2026-07-09:\n 58│`get_state` reports `api: openrouter`, `provider: openrouter`) — so for the Librarian bringup the\n 59│remaining choice is which models/roles to assign, not whether billing routing works.\n 60│\n## SPT-CORE-FINDINGS.md\n 426│present, must run `spt adapter add --github <our-adapter-repo>`** (or a local dir) to ACTIVATE — that\n*427│is the bridge. \"Binary present ≠ adapter active\" confirmed correct.\n 428│\n 429│**doyle's doc actions (docs-site):** publish the post-install ACTIVATION step (`adapter add\n 430│[--github]`) in `install-on-demand` + the integration checklist · author Ed25519 signing-key\n    │...\n 444│**Our action items (ours, not doyle's):** (i) wire `spt adapter add --github <repo>` into\n*445│`/sptc:setup`'s binary-present branch (the activation bridge — folds into the setup-slice below);\n 446│(ii) the manifest needs its own **published github repo target** for the `--github` end-user path\n 447│(distinct from the cplugs skeleton repo); local dev uses `spt adapter add ./adapter`.\n 448│\n\n## adr/\n### 0006-one-command-update-and-consolidated-binary.md\n 56│- The thin-skeleton goal advances even before the asks land: skill bodies already ride strings; the **reactive skills** (`commune`/`send`/`signoff`, full-fat in the plugin because they are invoked without a typed slash-command) can be thinned to stubs by moving their bodies into the perched SessionStart brief (adapter strings) plus the `/spt:live` UPS body — with a known **delivery-timing wrinkle** (the perched brief fires on bind|boundary, not the instant a seed session goes live), tracked as a plan i...\n*57│- Until asks #1/#3 land, hook logic and the translate binary stay on their current channels; the design degrades gracefully (the `message` field bridges the manual steps).\n 58│- One artifact per OS simplifies packaging but couples all seams' build/release — acceptable, they already ship in one `.spt`.\n### 0007-native-omp-tui-hosts-spt-extension.md\n  4│\n *5│accepted (2026-07-13) — supersedes the interactive-hosting choice in `OMP-ADAPTER-PLAN.md`; the RPC bridge remains a headless integration surface.\n  6│\n  7│## Context\n  8│\n *9│Issue #2 requires `spt endpoint run --adapter omp-spt ...` to expose the harness PTY. The shipped session command instead made `omp-spt bridge` the PTY leader and launched `omp --mode rpc` behind pipes. Attaching therefore showed bridge logs, while standalone `omp` showed the expected native terminal UI. No attach flag can recover a UI that the hosted process never created.\n 10│\n 11│## Decision\n 12│\n*13│Run native interactive `omp` as `[session.self]` and `[session.resume]`. Load a packaged OMP extension that binds the OMP session to the SPT endpoint, listens for SPT messages, submits each message through `sendUserMessage`, tracks busy/idle state, and sends the final assistant response back to the sender.\n 14│\n*15│The extension uses OMP's public extension API. It does not render or proxy the terminal: OMP remains the PTY leader and owns its complete native TUI. The old RPC bridge remains in the binary for explicitly headless callers, but endpoint bringup no longer routes through it.\n 16│\n 17│<!-- [doc->REQ-OMP-NATIVE-TUI] -->\n 18│\n   │...\n 20│\n*21│- `spt rc <id>` and attach-default endpoint bringup display the same native TUI as standalone `omp`.\n 22│- SPT delivery is serialized so two inbound messages cannot collapse into one OMP turn or receive the wrong reply.\n 23│- Extension code ships under the already-packaged `strings/` directory, avoiding a new archive layout or dependency.\n*24│- OMP profile/model flags belong directly on the manifest's `omp` command instead of after the bridge's `--` separator.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784087218312}}
{"type":"message","id":"1a99dbbf","parentId":"d847727f","timestamp":"2026-07-15T03:47:21.313Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning structured delivery and coordination**\n\n**Classifying locked and superseded decisions**\n\n**Evaluating plugin retention and dependencies**"},{"type":"thinking","thinking":"**Assessing OMP implementation status and ADR applicability**\n\n**Reviewing ADR6 and test suite relevance for OMP**"},{"type":"toolCall","id":"call_TTLKLNcX5RF0sKgpm8kvsZgQ|fc_0ca4af36969c80fa016a5702c93aa4819888d9de9212641999","name":"read","arguments":{"i":"Reading test gate coverage","path":"tests/ci-gates.sh","selector":"1-180"}},{"type":"toolCall","id":"call_hvbx3YKiCi03ggNmGpQxIUm0|fc_0ca4af36969c80fa016a5702c93abc8198899e0d04b15411b8","name":"read","arguments":{"i":"Reading gate runner","path":"ci/run-gates.sh","selector":"1-200"}},{"type":"toolCall","id":"call_vVQRdewDozyyUK2jnss8SLwP|fc_0ca4af36969c80fa016a5702c93acc8198896c138445091c07","name":"read","arguments":{"i":"Reading registration acceptance","path":"ci/manifest/registration-int.sh","selector":"1-180"}},{"type":"toolCall","id":"call_l65Lbjpfbroo0OTULfog7raw|fc_0ca4af36969c80fa016a5702c93ad8819897fd172a1821e3a7","name":"read","arguments":{"i":"Reading setup activation acceptance","path":"ci/setup/activate-int.sh","selector":"1-160"}},{"type":"toolCall","id":"call_6QDqYzAqepiRtyO0zA2VfmQL|fc_0ca4af36969c80fa016a5702c93ae4819880dd3ad0f5c0980d","name":"read","arguments":{"i":"Reading digest acceptance","path":"ci/digest/digest-proof-int.sh","selector":"1-170"}},{"type":"toolCall","id":"call_PCBNgfIo81kDgix7Qy7Gp9KO|fc_0ca4af36969c80fa016a5702c93aec81989b462f473413add8","name":"read","arguments":{"i":"Reading acceptance harness","path":"tests/acceptance-harness.sh","selector":"1-140"}},{"type":"toolCall","id":"call_78YKysx62ydLhHa3NnrR4Mji|fc_0ca4af36969c80fa016a5702c93af48198b4acf4a9b5721d18","name":"read","arguments":{"i":"Reading real harness acceptance","path":"ci/acceptance/run-acceptance.sh","selector":"1-180"}},{"type":"toolCall","id":"call_9smnDALbLQW3TvfL7WJT7Msa|fc_0ca4af36969c80fa016a5702c93af88198aaedf5cb688e4618","name":"read","arguments":{"i":"Reading archive tests","path":"tests/adapter-archive.sh","selector":"1-190"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":19712,"output":1152,"cacheRead":187904,"cacheWrite":0,"totalTokens":208768,"reasoningTokens":881,"cost":{"input":0.09856000000000001,"output":0.03456,"cacheRead":0.093952,"cacheWrite":0,"total":0.227072}},"stopReason":"toolUse","timestamp":1784087218317,"responseId":"resp_0ca4af36969c80fa016a5702b27a348198b43ea4b14746b65e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a5702b46b508198bfa36729285f8a1c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwK_qmZVz458PqqYWW74qO1OJU85nRQ3hE1A6PTNqkoarUChrrr-iYrfcYD-24fP7lkCjSaFjKvvF88-fy3rGHVlBkURqvPAfFBTneDrmd1DzSgFb5Vv6bn1zVGaqWlJTSxrcsTZ9EoGawv94wr_mTDoS8jMOvnePZiZfH3kSA0odUsOnnkNAPuhcOYDwJKtBO6agboIniqHMSQWcbTs9sHpRlKrOmmpt8mglaRlDYq3uqiACMI3Y-qXymZYQ3pa2cg3kFENicUvU6MA61w42zi7ubkm7prlgc0aIFEtB1EwSwaheFohvtygG-GopxD8gvpiBCJGmdEUzoCzDw-raXDe5htjvsXV6WAkoNQrbiAe5NoT1xGsRbOGaQ-jT3gW6XDwcB_hg5u5JUUBwhE_tBjKrLrwrFiCWZTg7MnGnRKsNTediuPCaMcA3G-hJjNEY5-97o-Mh4wlwV9W3BBoQrkVBVWDiqliB4j-py0id_d4NIcBB6LdVSkGgM8WuLRycX_wIQL8oKbYM_e1uqYD5S69Tdh_h6Zq4tMLGFh36pNahZcQX8XF6ufh4TUE0aETLm7p6uUnDKc-Nn_CUkEsFLvSrZyHzTa-GWs5UN2SxSbdVe6e3GZI2nsCmtYXsYnizaSfdaOWTMbDK7LLIqr34Zymif8sgGm0hI4e7G9UDxBvwiV21zt0J3v9kB3NGbHFiAol9I9O8jQY7wv4xbJtTGrDR7lx1tZXpnExb1TAQyAixkfkf9TsoTN6OY2Gixe5xRwPvKpua65yuRMnzl0oRhJv_jHW-WEeAmJZybb-dI9k9m9zWOMHp7bEJ3a7nNW0uIm2QfPlacZNdCQfWXd8mBp4Y4nrYjYRwXXI82X4U2UJLIKn4lFVrYQxaOyYoZK76y1zyOXsJHFI32PflyQrCVUZLf5xqsedQAhctC1M0aulTxiEpCw0RYuS4Km5B5MdEz6E-TvMxFMbOpUvssn4LUqEo3_aD-056pGstXavKC801LN2mzRR9XAD4i_MFN_RpdNGmMsZ-cL_j5J2fiEK_MoDr5jiOpB5cQi0RfOI6rnDTjVKTHAIFfqi8xbQbAP8lklkkhn8-9ANRpYOaZzoz_xKj6rVg9iribaMPtJtmDdx6FHT_3CQqdZQgUPGeGzzAG9BGos-oVx3cTPh1Xetw8RZbYPqiIovc6YDruXt_gbV90ngMvQme1jocXrv_dpcmjMcxknBArjhUbz3IuTKC1ZPYE0WZZxoI6j3RvxoC6trc5XiUPZdmtX1N6Nrm4NB9hLkuTRFRB4DH0LLdBSTe-6RYTyts6C8tspNd7Dq7WzfOcr5v_lAikrc4ECsCA8Q9U64030vipbPkXBUfJuR9dGqrLXHKRreSc3cOma3wIpW63Gbz8w6eyHFG63aM5DBgOMdyTCyG8QcVSJwW4mzkzVQzVlHf2QjNnZoJ47xuczSDeJmHy1LX8fpEauSlwnHq0VFxEfv3UHhPQoR1dCqjZkRo6tqZaMS_BXH8k62oTqlE7SBSKXYevNPBpUZgOflPfLS5hqanuYnLwioVqVJoZiWVSLwROgh4hWR0CNGgVtyvTm539FC5QE78e-roOgtbdJyTdTsuI4EvJgmi8yeGAdPOptSMvi6VLN3cZTJ8iK93XghPSUcrDzrfeSOeEnyYvLhE2osvnovhdvLIWYkPMDmHwJ7S2nyEQer3HP5kS13aqu4b2L6uDWtsB0Zb_PAIfgPuOOcvf5UdXbbCdt6n4e2x-k4cmO1Oodqk54EHnHUcu0E4-cbUZGlbrg2VZL5qizamyWo7KlDfndYsl40cRBn_BCiU_ItA4b5RjavYvjrUcbyTtsBH55_dCIIkVkUV6EJJFdH8OsYnrij6mYKP8gsoJTyuqEaaatJpjBLJmYvCOxRCxblwcdWy_IvuUyY9N9M-CyIMpNOv1SUJfxATDAZmCj8zKzCxmchAHXG4fw_2CCFQp40wya-bwPYCvRK6GLNXS7-GkEZJstEiHgDB07JHqrtaGtjB9bntPH09D414Hnq45mUWqi3xM7eeLuYLVV8ZYoylpa0qF68mcOSo_o0d6kYvwxoiiewowjgaEowiMF9HKDUJt2k9lUVSRxS57bi8oeRw6V4R9V0xY8hb5p_TapnzmnSySkPZNiVwBdZeLoeInx7-LOFAPcVcR9_vLQasnagvoBZP2jrMLC8kcKxq1CRjaM7K1i7nKqTMZ073tpeCG5p44phCF-2cLLtmFn_3XTzEB18vudNAYUECD4tBJpf1TgGzFPSQo_86Ph9228pQJ7fTeu2K1_uMLnHpchuUNk92jtSNx6HpYaSdwx2oc5idWNH3cFDjNa_Qi693Oc-VoVP8yw6kRVjQNblaptDsyFc7K_XhiKnotXNhtawQScY-_C_62JFvOzxnCrxWN4IWpRP2jKGnDL7ATvfU9XqfTUIav_P3pd3mQcqQj0fSSwT0PHokVz1KqgdOV09lq8bqGi5qa1Iu3hDExCiY2uAzNmiYmbFvh8SIDzJ31c1HBZTHOZwF31Nyuy60VfZyL-N2ErpxHMTGzQ_umOrIATXZG8TiYuqOmSfX1JRKtP2K9iapBn_YSl341aICpz3M3FYelbKkihs3VQAf3-ICq6UOldf1e1FJJgsvA9dliLn6bQubiPsnLtcyc6IgNYcz_7ZxEvc5a1I2mPcjRtxa2DAxZJFs7n80ja5yS5ciY53yH9LV_Rn0FBpx8O6sq9c6FK5Vzq5RDGh_3xwspTIIqBkW5q6pWbR8GyX2Sex1lI-gVsIvcGDPakvBkOTg-1ebwuIciN12OZU6SxC-wzpqqgv2lzZlhGPghp8h_c7gz2M7EynUyNuwdZHFQyl1CX5-UhwPKXK6PUjnCc3gIa9_u_ljFNGpcain37OOr4Y59Y850E1GC-p_N9kEP_w40bAEa1byX4vvN4ybMNQEiA4lewHJt_FiUknGE68pysTp_kI5RCRj36nipg5lrkTE3MQQDzZa0tc0vdqTZA_G3SZQw09_x55OyQ_D3JRpluVsNAjdQITboj955HnfI9bRaZK0yOozAmiK_UC5PQBgkKofXz2aP6WH_KKWqeonNw5GYIMGIRYK7-7qQiI4kb5ot-ub6k3BddGfW6nfQ25tbH-RfBBLI2SRKm_RGh9Q9bAqVIg_77gBVoQ5qBA3wLC05svJSRRAlDjXPAEuX93NWLjChU7ziEx1ZZi37LbnSjXjLfT6OimjH-omQhqYdmiHGM-zFprl_Bmj0GLCEVe8jbhZ-YV2NCUzW0mmYR1U_kpak3YdT-vlN0PxeAd4flVnMjI-e5YAvuRX_0VRB4rXXHg-1Av6sB8-1zpLEOBX6V25yHIQsEMoeL-RSXaobaDojHoGtiqyMAYJU2S50215GtJnIY5dLEpEcnC_1cVQkVrm03ePtJvVbkJ9m2v6YxHuwq2KXpo5SRKe0ttG1QGN8uid-1Nz7M4u5cpLps0zy4jRECENBmMe3R8_3DGchrxKUd3rBBgHX7AnvKpUOM3PJS9EnHu-sCDyWEbjFqN1wC4HSgVCL8xohQLexDnIK4sraluXVMSjvRionUOZwvW66a4npIK4kdydqNom6UxBioTNXN6zheVQJV7saiJCBrWsIqFaiXhRHalpuJ_v9sMjw15VvdmLL3UcHeVvo99AcTj9mmr4-u7ns09jTUFVMsC64TtWbPans_lSto8H71eY7vdQxO28H7dxpDyfepYxAxYp3Gdp8qYYAwojvyT2xs0Gg8xoEWsBHNjW1gsl5GzlyNmlx21Cu9WR4y5A-ePcyA2kJ4T4Oo37UfsUyubL_Tu2lCoMWnfoVfG63ywSjNKxlwnDh3kI2Ye8AMwMurCvCkr5hwoiEKctPS-rvpcw6cC-mdfjJ3lHwdiozXBNf9KAACcLFt0SyLaJ3cnft73zfmyyk4t-GizoKRhceuvghb8mizCQbJKBah2tOdvrOxt3FB0gSTdMYtVfnOr1O8eStMIQBcK2QR32z-gkrH0UdtnSz_OXM-uQ06tWwSJyNI9XRI00n4xBEd3IBDdhgLR2xYeSXFttFdCIlG1x5E3uh6bvF8MQefXB7u6YvKKVJD0DdG_Wo5X011BEo2VV5Wct3wXyF38CGDhnpOZwd_WSOyId8ewgSSHnV7CFS6nTsJRxZMQ9tg3Ljd1dq4iH4gMbmEcsB0TAemtBc2fckNSvOPO9bD3aOI=","summary":[{"type":"summary_text","text":"**Planning structured delivery and coordination**"},{"type":"summary_text","text":"**Classifying locked and superseded decisions**"},{"type":"summary_text","text":"**Evaluating plugin retention and dependencies**"}]},{"id":"rs_0ca4af36969c80fa016a5702bfa0dc8198a3a4197e82a5dbd4","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwLJL10na-YDjw0WFmiRtlb8g2dEjRGS-eaN4EJaIoHM6AFM1lvdMGf0HWjleFJ1J--JMjjxAu_OMy-bDJ1ioY57KKMI6OIi_Sq0Iq7Ski-TKSdXhzyDcYKk_9MWHtryeSlK4xynUmQGMJA2-r7Mantiffv8Wq_UqOzWX_vQTNXFiLfZt77p2moJgHokvL5WYuLMkhifPnTBl1MJN7KvijS5xwchFzNngsEo8LMxdf9Mj13gtJmSwVYN_6C2tlrFTV4nK1AmIlm3o8rY3YZkUkD4bvi5LUYjHIXe5WXAE1LMYmjh1kpIkrCmb50v5iOkxgHE2W-RsEU0a6rJEmhTHLHec8L1EWOiKV2tZbe9OB9gI8Sfm0e1fQiBMzIpRHV8SNIbrTuRbrTMsAG_s-LszjkCC5fWpmFzFhgIThhlWf9AuMDzUsy4GbqS6F2anmEkzO7lpMHZ0fgzd-IEBv578JjWV5HzxovUd7sUOru1eHSYQa6ov2mLt0wL-N4YaPTrjBgDhdlFC4W3QIgz6yFmkwdSmrp4uqcNQye1jbSzclCGBMdU7bJgo0Nvou4U4w8aD3C__hAtFNxaRBn9CgsY8mOin7xkKTpe_mcwXrPxLV6OrQoIublSUyxhHdOEFL1icKFnNEHgOnkVf9QcScsUNxfrls8AI7KIovcEhbngHo3LSmdBKYXdcBQZ7P6IlNaab8GxwZUzn-JQjGRJlkh_Pywhmm0Cx49D9T_U03lFKOnPjHXWDO_qh1I1jzh3ai6BH7zzeBJRXfP-3tji8gdGlf1OD18fzd4AzDWdFY5R_PTHSKN2WnLuHBZo37syFd7s7YrSQDhb7M94bfYAU4HdpRQBbMplaGU6AkNCcCc1xZvBvp8Ge_2iCwsjgLbI14CeIqN4WbSEEF_y6qLt5kUkjB7wdtSGa35Nhi3B6lP2dVTpZvvqrj7rXi5WCuGQFEQk9Q-I5F9IXSkUzODmPaPPK6c2jRnPg09G_QK-2LX34scC954xIGebtrihgObUIaq4n5l9rop9zaWGyrJXda85RzDN5Yk0NGhLYTB0gvYDOZyJzSIErYXAZawb_wWkoBQj3UoCUHaXAjvoYURFows0KC6TdnZvKYcxBAFEIr6kQouSZ-CTxTFgzyhMtcelqclZxkdbq9DDRIp1Sr624F7D7FAD3OwWCvjH5jjt1ami66L1C1nGjRSO-DBbKb6hjLwfFmgdyhnu6wdD8iIWwvMF9qAMvWJClkltslZf83roUNKt0nJ_AE9ZGpMkE_CTweH85642N7xVdi3NFBU6S9SulzuZhezQ-6uynvMQYH76LAOIk2lwkjTn_qXDT2o3kPm21uC7DMyhaCYrkfiU-gnFmJtmJJZ1RtZmDo4Q1AgXaS5HOnEkjDle0_AtDP3NU7eYoIeIbQdshWbN9ML14wHhqqjgcs7JmG2AB6yRvT3nYhvC5lI04Y6o5tNhOuDCjq97fU999xe63U2NS4m1-BP4ce0mjAfogtjWcUm9gNDQwUeafVJWK8tBnJU4uQnhDE76EJy0y1hBUm17UYjOqEZ4Qex0bSpro1gQ7NgSBu13q8VKI1coMjGyntiHBF82bDQjIoyADzH7WH0RatJPqr4PwGL3TUc7mIGUXh24X_MZlE0bz7kIAfyPh6ZWMZq54nzcb9awKaP4v_vHAbhLBLb2hChI1tnr6QAQltTOwX4lvXlwy1tjoeUytoEENuCzChc5L3c16JMHXsYCZaH7AU1mo4O3PIbYqiM_BGTTMxc6kuYPtK0elZfos1gkikHYqWI9Xy0xxnqX-MsO45ujrlshq8t9yL1EHimAy2PkPqHyMDUzuy9GzWSLyCg3sAMwbgXNwhCFOXGS6WHU1t-xA91KOve3wB1BUu5-TT6STtiKhde-sciv4hSYjxy49HRiyE4DjuDeiZa7cE7duYx0Wv3_tW3UZaUNbEArA5jotc8fT8kx0YXHPy09bTQY2mfLB2iNuLzINCrh_8RuNGXjIJvcQdzsu_BQeuTENJ692VGEdwjqm2c9DQjowLWa2vHeeYXp6DlI92wFEFE8TAtLmWviiMIUfaioCmOUb18FDl5vQ0_m6Ls8713XATz6ieN596yCq7PzfagOxHjwTu4UmjV5MitmqW_LAFT6r-d-4FDp6By6u97od3soYASyuLhYeQPIyKt3cIafF9y2Ai6W0FiY_NP_AJiWLdSsgwTA0oWUQz-ihwjmroGk6FvIAlnj9gSPKE2Zt1PXWeXHjoCkk558LCPabORAV6nSUPs1CvitYrWAmBU8eABWLI_ucor5fLzaD_F5YkVRlz7rL3FSPwFRTOzsPuxKjE4xFfIh-UGys4WPlScneHHjaEEju9f3gpPjft_u1uRfK9QDFoizlqT5v9D9f5pCcqdLLtrDuEyzk5DnDnC08QS2Dow_HDYrI9k1BZxE4AWBkW3RLp7BcKOZcwLDfNHgmrly6_-oc8g0k8CqNEmPm_quIC8oSzZjbxueHcEmYztXDKQJ4Ri4Bt1mgHCHI1c8bbZ1zHw21FAFfnwy4VgoIV4JNd_TMgixC1chOIvR-K6eS1y6QleVxctFN4K0kmYFawyN2QnUoyzksKaM1eH6qF0QQ38wYf9dDTDqN8WN9qfdIdc-j7yP3Q2CgQz6wO91YluEuhUZ-fq4r1yC4iH41iicFWVlJo2JqMwf5wJ9vix70AhIw0Anh09_fQvT7aMTJWquKLF7SFqaRmf0jNf3yOPrjgZY4p_QNGo5vLPX2b7KCoK4lbq15wJvfdNUQ5SavQgefaIjcRBKCPIfb-Jdv-veg3GiKGT9cMpZ3X3c7QJh2XKgUby-r8uAaQc9sf8HxOJ_gjjh7uK4IWExDnaQMHcUOe0s9h7Sq41rYzz3RBC6K9xqJ-ywG4RiAqchwiZuCAmWgFDDpuTN_lKxiDSF19kaRTAu6VTWS-1VIUbFzvTAAyJSuYtzf7dgKwWsgbtD3q83Y9_KP6U9tWSkG0M=","summary":[{"type":"summary_text","text":"**Assessing OMP implementation status and ADR applicability**"},{"type":"summary_text","text":"**Reviewing ADR6 and test suite relevance for OMP**"}]},{"id":"fc_0ca4af36969c80fa016a5702c93aa4819888d9de9212641999","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading test gate coverage\",\"path\":\"tests/ci-gates.sh\",\"selector\":\"1-180\"}","call_id":"call_TTLKLNcX5RF0sKgpm8kvsZgQ","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93abc8198899e0d04b15411b8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading gate runner\",\"path\":\"ci/run-gates.sh\",\"selector\":\"1-200\"}","call_id":"call_hvbx3YKiCi03ggNmGpQxIUm0","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93acc8198896c138445091c07","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading registration acceptance\",\"path\":\"ci/manifest/registration-int.sh\",\"selector\":\"1-180\"}","call_id":"call_vVQRdewDozyyUK2jnss8SLwP","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93ad8819897fd172a1821e3a7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading setup activation acceptance\",\"path\":\"ci/setup/activate-int.sh\",\"selector\":\"1-160\"}","call_id":"call_l65Lbjpfbroo0OTULfog7raw","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93ae4819880dd3ad0f5c0980d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading digest acceptance\",\"path\":\"ci/digest/digest-proof-int.sh\",\"selector\":\"1-170\"}","call_id":"call_6QDqYzAqepiRtyO0zA2VfmQL","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93aec81989b462f473413add8","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading acceptance harness\",\"path\":\"tests/acceptance-harness.sh\",\"selector\":\"1-140\"}","call_id":"call_PCBNgfIo81kDgix7Qy7Gp9KO","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93af48198b4acf4a9b5721d18","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading real harness acceptance\",\"path\":\"ci/acceptance/run-acceptance.sh\",\"selector\":\"1-180\"}","call_id":"call_78YKysx62ydLhHa3NnrR4Mji","name":"read"},{"id":"fc_0ca4af36969c80fa016a5702c93af88198aaedf5cb688e4618","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading archive tests\",\"path\":\"tests/adapter-archive.sh\",\"selector\":\"1-190\"}","call_id":"call_9smnDALbLQW3TvfL7WJT7Msa","name":"read"}]},"duration":22995.776799999992,"ttft":2125.045199999935,"contextSnapshot":{"promptTokens":207616,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_TTLKLNcX5RF0sKgpm8kvsZgQ|fc_0ca4af36969c80fa016a5702c93aa4819888d9de9212641999","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"tests/ci-gates.sh"},"intent":"Reading test gate coverage"},"id":"da8962a9","parentId":"1a99dbbf","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hvbx3YKiCi03ggNmGpQxIUm0|fc_0ca4af36969c80fa016a5702c93abc8198899e0d04b15411b8","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"ci/run-gates.sh"},"intent":"Reading gate runner"},"id":"7e3abaf0","parentId":"da8962a9","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vVQRdewDozyyUK2jnss8SLwP|fc_0ca4af36969c80fa016a5702c93acc8198896c138445091c07","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"ci/manifest/registration-int.sh"},"intent":"Reading registration acceptance"},"id":"a3fdc081","parentId":"7e3abaf0","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_l65Lbjpfbroo0OTULfog7raw|fc_0ca4af36969c80fa016a5702c93ad8819897fd172a1821e3a7","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"ci/setup/activate-int.sh"},"intent":"Reading setup activation acceptance"},"id":"62634c1f","parentId":"a3fdc081","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6QDqYzAqepiRtyO0zA2VfmQL|fc_0ca4af36969c80fa016a5702c93ae4819880dd3ad0f5c0980d","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"ci/digest/digest-proof-int.sh"},"intent":"Reading digest acceptance"},"id":"1a6aae41","parentId":"62634c1f","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PCBNgfIo81kDgix7Qy7Gp9KO|fc_0ca4af36969c80fa016a5702c93aec81989b462f473413add8","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"tests/acceptance-harness.sh"},"intent":"Reading acceptance harness"},"id":"605858ce","parentId":"1a6aae41","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_78YKysx62ydLhHa3NnrR4Mji|fc_0ca4af36969c80fa016a5702c93af48198b4acf4a9b5721d18","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"ci/acceptance/run-acceptance.sh"},"intent":"Reading real harness acceptance"},"id":"8157fb21","parentId":"605858ce","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9smnDALbLQW3TvfL7WJT7Msa|fc_0ca4af36969c80fa016a5702c93af88198aaedf5cb688e4618","toolName":"read","startedAt":"2026-07-15T03:47:21.314Z","args":{"path":"tests/adapter-archive.sh"},"intent":"Reading archive tests"},"id":"cbeb5eb7","parentId":"8157fb21","timestamp":"2026-07-15T03:47:21.314Z"}
{"type":"message","id":"65956a2d","parentId":"cbeb5eb7","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_PCBNgfIo81kDgix7Qy7Gp9KO|fc_0ca4af36969c80fa016a5702c93aec81989b462f473413add8","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Unit tests for the CI acceptance harness — deterministic pieces only, NO real `claude` spawned.\n# Run: sh tests/acceptance-harness.sh   (exit 0 = pass).\n. \"$(dirname \"$0\")/../ci/acceptance/lib.sh\"\nfail=0\n\n# ── Perch-collision guard: identity is ALWAYS overridden to a disposable id, never a live name.\n# [unit->REQ-HAZARD-PERCH-COLLISION]\n# Simulate inheriting the operator's identity, then mint — both vars must be displaced.\nSPT_AGENT_ID=perri OWL_SESSION_ID=perri\nsptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n\n# is_disposable: accepts a minted id, rejects any live name.\nsptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\nsptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\nsptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n\n# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.\nwork=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\ntrap 'rm -rf \"$work\"' EXIT INT TERM\nproj=\"$work/p\"; digest=\"$work/d.txt\"\nsptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null\n[ -f \"$proj/.claude/settings.json\" ] && echo \"ok   scaffold writes settings.json\" || { echo \"FAIL no settings.json\"; fail=1; }\n[ -f \"$proj/.claude/hooks/ups-digest.sh\" ] && echo \"ok   scaffold writes UPS hook\" || { echo \"FAIL no UPS hook\"; fail=1; }\ngrep -q 'UserPromptSubmit' \"$proj/.claude/settings.json\" && echo \"ok   settings wires UserPromptSubmit\" || { echo \"FAIL UPS not wired\"; fail=1; }\n# Fixture hook reads stdin, not a /-leading argv (KH 1.1 immunity).\ngrep -q 'input=$(cat)' \"$proj/.claude/hooks/ups-digest.sh\" && echo \"ok   fixture reads stdin (MSYS-safe)\" || { echo \"FAIL fixture not stdin-driven\"; fail=1; }\n\n# Fixture actually produces the digest marker when fed a real CC-shaped payload on stdin.\nprintf '%s' '{\"session_id\":\"x\",\"prompt\":\"hello-probe\"}' | sh \"$proj/.claude/hooks/ups-digest.sh\"\nsptc_ci_assert \"fixture emits digest marker\" \"UPS_FIRED:hello-probe\" \"$digest\" >/dev/null \\\n  && echo \"ok   fixture emits digest marker\" || { echo \"FAIL fixture no marker\"; fail=1; }\n\n# ── Assert helper: negative case (missing needle) must FAIL, not false-pass.\nif sptc_ci_assert \"neg\" \"NOPE\" \"$digest\" >/dev/null; then echo \"FAIL assert false-passed\"; fail=1; else echo \"ok   assert fails on missing needle\"; fi\n\n# ── Orchestrator is env-gated: without SPTC_ACCEPTANCE it skips cleanly (rc 0), spawns no claude.\nout=$(SPTC_ACCEPTANCE=0 sh \"$(dirname \"$0\")/../ci/acceptance/run-acceptance.sh\" 2>&1); orc=$?\n{ [ \"$orc\" -eq 0 ] && printf '%s' \"$out\" | grep -q 'SKIP acceptance'; } \\\n  && echo \"ok   orchestrator env-gate skips clean\" || { echo \"FAIL env-gate: rc=$orc out=[$out]\"; fail=1; }\n\n[ \"$fail\" -eq 0 ] && { echo \"ACCEPTANCE-HARNESS OK\"; exit 0; } || { echo \"ACCEPTANCE-HARNESS FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Unit tests for the CI acceptance harness — deterministic pieces only, NO real `claude` spawned.\n# Run: sh tests/acceptance-harness.sh   (exit 0 = pass).\n. \"$(dirname \"$0\")/../ci/acceptance/lib.sh\"\nfail=0\n\n# ── Perch-collision guard: identity is ALWAYS overridden to a disposable id, never a live name.\n# [unit->REQ-HAZARD-PERCH-COLLISION]\n# Simulate inheriting the operator's identity, then mint — both vars must be displaced.\nSPT_AGENT_ID=perri OWL_SESSION_ID=perri\nsptc_ci_identity 7   # bare call (no $() — must mutate THIS shell)\n[ \"$SPTC_CI_ID\" = \"sptc-ci-7\" ] && echo \"ok   mints disposable id\" || { echo \"FAIL id: got [$SPTC_CI_ID]\"; fail=1; }\n[ \"$SPT_AGENT_ID\" = \"sptc-ci-7\" ] && echo \"ok   SPT_AGENT_ID overridden\" || { echo \"FAIL SPT_AGENT_ID=[$SPT_AGENT_ID] not overridden\"; fail=1; }\n[ \"$OWL_SESSION_ID\" = \"sptc-ci-7\" ] && echo \"ok   OWL_SESSION_ID overridden\" || { echo \"FAIL OWL_SESSION_ID=[$OWL_SESSION_ID] not overridden\"; fail=1; }\n[ \"$SPT_AGENT_ID\" != \"perri\" ] && echo \"ok   live id displaced\" || { echo \"FAIL still resolves live id\"; fail=1; }\n\n# is_disposable: accepts a minted id, rejects any live name.\nsptc_ci_is_disposable \"sptc-ci-7\" && echo \"ok   accepts disposable\" || { echo \"FAIL rejected disposable\"; fail=1; }\nsptc_ci_is_disposable \"perri\" && { echo \"FAIL accepted live id 'perri'\"; fail=1; } || echo \"ok   rejects live id\"\nsptc_ci_is_disposable \"\" && { echo \"FAIL accepted empty id\"; fail=1; } || echo \"ok   rejects empty id\"\n\n# ── Scaffold: builds settings.json + a stdin-reading (never argv) UPS hook fixture.\nwork=$(mktemp -d) || { echo \"FAIL mktemp\"; exit 1; }\ntrap 'rm -rf \"$work\"' EXIT INT TERM\nproj=\"$work/p\"; digest=\"$work/d.txt\"\nsptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null\n[ -f \"$proj/.claude/settings.json\" ] && echo \"ok   scaffold writes settings.json\" || { echo \"FAIL no settings.json\"; fail=1; }\n[ -f \"$proj/.claude/hooks/ups-digest.sh\" ] && echo \"ok   scaffold writes UPS hook\" || { echo \"FAIL no UPS hook\"; fail=1; }\ngrep -q 'UserPromptSubmit' \"$proj/.claude/settings.json\" && echo \"ok   settings wires UserPromptSubmit\" || { echo \"FAIL UPS not wired\"; fail=1; }\n# Fixture hook reads stdin, not a /-leading argv (KH 1.1 immunity).\ngrep -q 'input=$(cat)' \"$proj/.claude/hooks/ups-digest.sh\" && echo \"ok   fixture reads stdin (MSYS-safe)\" || { echo \"FAIL fixture not stdin-driven\"; fail=1; }\n\n# Fixture actually produces the digest marker when fed a real CC-shaped payload on stdin.\nprintf '%s' '{\"session_id\":\"x\",\"prompt\":\"hello-probe\"}' | sh \"$proj/.claude/hooks/ups-digest.sh\"\nsptc_ci_assert \"fixture emits digest marker\" \"UPS_FIRED:hello-probe\" \"$digest\" >/dev/null \\\n  && echo \"ok   fixture emits digest marker\" || { echo \"FAIL fixture no marker\"; fail=1; }\n\n# ── Assert helper: negative case (missing needle) must FAIL, not false-pass.\nif sptc_ci_assert \"neg\" \"NOPE\" \"$digest\" >/dev/null; then echo \"FAIL assert false-passed\"; fail=1; else echo \"ok   assert fails on missing needle\"; fi\n\n# ── Orchestrator is env-gated: without SPTC_ACCEPTANCE it skips cleanly (rc 0), spawns no claude.\nout=$(SPTC_ACCEPTANCE=0 sh \"$(dirname \"$0\")/../ci/acceptance/run-acceptance.sh\" 2>&1); orc=$?\n{ [ \"$orc\" -eq 0 ] && printf '%s' \"$out\" | grep -q 'SKIP acceptance'; } \\\n  && echo \"ok   orchestrator env-gate skips clean\" || { echo \"FAIL env-gate: rc=$orc out=[$out]\"; fail=1; }\n\n[ \"$fail\" -eq 0 ] && { echo \"ACCEPTANCE-HARNESS OK\"; exit 0; } || { echo \"ACCEPTANCE-HARNESS FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\acceptance-harness.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"234a0c81","parentId":"65956a2d","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_TTLKLNcX5RF0sKgpm8kvsZgQ|fc_0ca4af36969c80fa016a5702c93aa4819888d9de9212641999","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Unit tests for CI helpers (pure/deterministic; no real bus or push needed).\n# Run: sh tests/ci-gates.sh   (exit 0 = pass).\n. \"$(dirname \"$0\")/../ci/lib/spt-bus.sh\"\nfail=0\n\n# resolve_spt_send: explicit override wins. Use a REAL executable (sh) — MSYS does not honour\n# `chmod +x` on an arbitrary temp file as `-x`, so synthesise nothing. [unit->REQ-CI-OWL-DISCOVERY]\nrealexe=$(command -v sh)\ngot=$(SPTC_CI_BUS=\"$realexe\" resolve_spt_send)\n[ \"$got\" = \"$realexe\" ] && echo \"ok   override resolves\" || { echo \"FAIL override: got [$got]\"; fail=1; }\n\n# resolve_spt_send: a non-executable override is ignored (no false positive).\nbad=$(mktemp)  # plain file, not executable\ngot=$(SPTC_CI_BUS=\"$bad\" resolve_spt_send 2>/dev/null)\n[ \"$got\" != \"$bad\" ] && echo \"ok   non-exec override rejected\" || { echo \"FAIL non-exec accepted\"; fail=1; }\nrm -f \"$bad\"\n\n# run-gates.sh: valid shell + carries the load-bearing gates. [unit->REQ-CI-GATES]\ng=\"$(dirname \"$0\")/../ci/run-gates.sh\"\nsh -n \"$g\" && echo \"ok   run-gates syntax\" || { echo \"FAIL run-gates syntax\"; fail=1; }\ngrep -q 'traceable-reqs check' \"$g\" && echo \"ok   gate: traceable-reqs\" || { echo \"FAIL missing traceable gate\"; fail=1; }\ngrep -q 'sh -n' \"$g\" && echo \"ok   gate: shell-syntax\" || { echo \"FAIL missing syntax gate\"; fail=1; }\n\n[ \"$fail\" -eq 0 ] && { echo \"CI-HELPERS OK\"; exit 0; } || { echo \"CI-HELPERS FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Unit tests for CI helpers (pure/deterministic; no real bus or push needed).\n# Run: sh tests/ci-gates.sh   (exit 0 = pass).\n. \"$(dirname \"$0\")/../ci/lib/spt-bus.sh\"\nfail=0\n\n# resolve_spt_send: explicit override wins. Use a REAL executable (sh) — MSYS does not honour\n# `chmod +x` on an arbitrary temp file as `-x`, so synthesise nothing. [unit->REQ-CI-OWL-DISCOVERY]\nrealexe=$(command -v sh)\ngot=$(SPTC_CI_BUS=\"$realexe\" resolve_spt_send)\n[ \"$got\" = \"$realexe\" ] && echo \"ok   override resolves\" || { echo \"FAIL override: got [$got]\"; fail=1; }\n\n# resolve_spt_send: a non-executable override is ignored (no false positive).\nbad=$(mktemp)  # plain file, not executable\ngot=$(SPTC_CI_BUS=\"$bad\" resolve_spt_send 2>/dev/null)\n[ \"$got\" != \"$bad\" ] && echo \"ok   non-exec override rejected\" || { echo \"FAIL non-exec accepted\"; fail=1; }\nrm -f \"$bad\"\n\n# run-gates.sh: valid shell + carries the load-bearing gates. [unit->REQ-CI-GATES]\ng=\"$(dirname \"$0\")/../ci/run-gates.sh\"\nsh -n \"$g\" && echo \"ok   run-gates syntax\" || { echo \"FAIL run-gates syntax\"; fail=1; }\ngrep -q 'traceable-reqs check' \"$g\" && echo \"ok   gate: traceable-reqs\" || { echo \"FAIL missing traceable gate\"; fail=1; }\ngrep -q 'sh -n' \"$g\" && echo \"ok   gate: shell-syntax\" || { echo \"FAIL missing syntax gate\"; fail=1; }\n\n[ \"$fail\" -eq 0 ] && { echo \"CI-HELPERS OK\"; exit 0; } || { echo \"CI-HELPERS FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\ci-gates.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"02e695ec","parentId":"234a0c81","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_vVQRdewDozyyUK2jnss8SLwP|fc_0ca4af36969c80fa016a5702c93acc8198896c138445091c07","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the\n# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —\n# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the\n# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the\n# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]\n#\n# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1\n# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes\n# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\nver=$(spt --version 2>/dev/null | awk '{print $NF}')\ncase \"$ver\" in\n  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo \"SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)\"; exit 0 ;;\nesac\n\nfail=0\nok()  { echo \"ok   $1\"; }\nbad() { echo \"FAIL $1\"; fail=1; }\n# Clean up registry residue however we exit.\ntrap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM\n\n# 1. Registration cross-field validation accepts the manifest.\nout=$(spt adapter add \"$MANIFEST\" 2>&1)\ncase \"$out\" in *registered*) ok \"spt adapter add: registered\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n\n# 2. Listed, with the shipped profile resolved as a composite option.\nlist=$(spt adapter list 2>&1)\ncase \"$list\" in *claude-spt*) ok \"listed: claude-spt active\" ;; *) bad \"claude-spt not listed\" ;; esac\n# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable\n# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at\n# step 4d below, not a composite resolve.\n# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —\n# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).\ncase \"$list\" in *claude-spt:ccs*) ok \"shipped profile resolves: claude-spt:ccs (ccs overlay)\" ;; *) bad \"ccs profile not resolved\" ;; esac\n\n# 3. [strings] read through the registry (base value).\nbase=$(spt adapter get-string claude-spt adapter_label 2>&1)\n[ \"$base\" = \"Claude Code (spt)\" ] && ok \"strings resolve: adapter_label (base)\" || bad \"base adapter_label='$base'\"\n\n# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep\n#    profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)\n# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]\nccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)\n[ \"$ccs\" = \"Claude Code (spt, ccs)\" ] && ok \"overlay observable: :ccs adapter_label differs\" || bad \"ccs adapter_label='$ccs'\"\n\n# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline\n#     sibling still prints as-is. Proves `{ file = \"skills/<x>.md\" }` over adapter/strings/ (F-003).\nbody=$(spt adapter get-string claude-spt skills.ready 2>&1)\ncase \"$body\" in \"# /sptc:ready\"*) ok \"file-backed string resolves: skills.ready -> body\" ;; *) bad \"skills.ready not resolved to file body: '$(printf %.40s \"$body\")'\" ;; esac\n# force-stop = endpoint shutdown body. (send/commune/signoff are NOT injected — full-fat in the\n# plugin SKILL.md, no [strings.skills] entry — so they are intentionally absent from this resolve set.)\nfbody=$(spt adapter get-string claude-spt \"skills.force-stop\" 2>&1)\ncase \"$fbody\" in \"# /sptc:force-stop\"*) ok \"file-backed string resolves: skills.force-stop -> body\" ;; *) bad \"skills.force-stop not resolved to file body: '$(printf %.40s \"$fbody\")'\" ;; esac\n# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare\n# `api listen`; REQ-SKILL-LIVE).\nlbody=$(spt adapter get-string claude-spt skills.live 2>&1)\ncase \"$lbody\" in \"# /sptc:live\"*) ok \"file-backed string resolves: skills.live -> body\" ;; *) bad \"skills.live not resolved to file body: '$(printf %.40s \"$lbody\")'\" ;; esac\n# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]\nsbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)\ncase \"$sbody\" in \"# /sptc:subnet\"*) ok \"file-backed string resolves: skills.subnet -> body\" ;; *) bad \"skills.subnet not resolved to file body: '$(printf %.40s \"$sbody\")'\" ;; esac\n\n# 4c. UPS skill-injection end-to-end via the BINARY (D1): `claude-spt hook UserPromptSubmit` resolves a\n#     /sptc:<skill> prompt to the wrapped operative body via get-string on the registered adapter. No\n#     perch (whoami empty) → only skill-injection emits, no drain. (REQ-UPS-INJECTION impl now in the\n#     binary.) Prefer the release build; the dev `adapter add` above registered the manifest, so\n#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\nHOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\nif [ -x \"$HOOKBIN\" ]; then\n  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n  case \"$inj\" in\n    '<sptc_skill name=\"ready\">'*'# /sptc:ready'*'</sptc_skill>'*) ok \"UPS skill-injection (binary): /sptc:ready -> wrapped body\" ;;\n    *) bad \"skill-injection did not emit wrapped body: $(printf %.60s \"$inj\")\" ;;\n  esac\nelse\n  echo \"SKIP: claude-spt binary not built (cargo build --release) — skill-injection int needs it\"\nfi\n\n# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:\n#     `adapter add` (step 1) is manifest-first (\"an invalid manifest registers nothing\"), so the\n#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm\n#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via\n#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]\ncap=$(spt api --adapter claude-spt --manifest \"$MANIFEST\" capability 2>&1)\ncase \"$cap\" in *LiveAgent*) ok \"bringup blocks accepted; capability reports hostable harness\" ;; *) bad \"capability missing LiveAgent: $cap\" ;; esac\n\n# 5. Soft-deregister cleanly.\nrm=$(spt adapter remove claude-spt 2>&1)\nlist2=$(spt adapter list 2>&1)\ncase \"$list2\" in *\"claude-spt\"*\"active\"*) bad \"claude-spt still active after remove\" ;; *) ok \"removed (soft-deregistered)\" ;; esac\n\n[ \"$fail\" -eq 0 ] && { echo \"MANIFEST-REGISTRATION-INT OK\"; exit 0; } || { echo \"MANIFEST-REGISTRATION-INT FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the\n# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —\n# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the\n# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the\n# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]\n#\n# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1\n# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes\n# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\nver=$(spt --version 2>/dev/null | awk '{print $NF}')\ncase \"$ver\" in\n  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo \"SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)\"; exit 0 ;;\nesac\n\nfail=0\nok()  { echo \"ok   $1\"; }\nbad() { echo \"FAIL $1\"; fail=1; }\n# Clean up registry residue however we exit.\ntrap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM\n\n# 1. Registration cross-field validation accepts the manifest.\nout=$(spt adapter add \"$MANIFEST\" 2>&1)\ncase \"$out\" in *registered*) ok \"spt adapter add: registered\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n\n# 2. Listed, with the shipped profile resolved as a composite option.\nlist=$(spt adapter list 2>&1)\ncase \"$list\" in *claude-spt*) ok \"listed: claude-spt active\" ;; *) bad \"claude-spt not listed\" ;; esac\n# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable\n# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at\n# step 4d below, not a composite resolve.\n# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —\n# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).\ncase \"$list\" in *claude-spt:ccs*) ok \"shipped profile resolves: claude-spt:ccs (ccs overlay)\" ;; *) bad \"ccs profile not resolved\" ;; esac\n\n# 3. [strings] read through the registry (base value).\nbase=$(spt adapter get-string claude-spt adapter_label 2>&1)\n[ \"$base\" = \"Claude Code (spt)\" ] && ok \"strings resolve: adapter_label (base)\" || bad \"base adapter_label='$base'\"\n\n# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep\n#    profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)\n# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]\nccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)\n[ \"$ccs\" = \"Claude Code (spt, ccs)\" ] && ok \"overlay observable: :ccs adapter_label differs\" || bad \"ccs adapter_label='$ccs'\"\n\n# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline\n#     sibling still prints as-is. Proves `{ file = \"skills/<x>.md\" }` over adapter/strings/ (F-003).\nbody=$(spt adapter get-string claude-spt skills.ready 2>&1)\ncase \"$body\" in \"# /sptc:ready\"*) ok \"file-backed string resolves: skills.ready -> body\" ;; *) bad \"skills.ready not resolved to file body: '$(printf %.40s \"$body\")'\" ;; esac\n# force-stop = endpoint shutdown body. (send/commune/signoff are NOT injected — full-fat in the\n# plugin SKILL.md, no [strings.skills] entry — so they are intentionally absent from this resolve set.)\nfbody=$(spt adapter get-string claude-spt \"skills.force-stop\" 2>&1)\ncase \"$fbody\" in \"# /sptc:force-stop\"*) ok \"file-backed string resolves: skills.force-stop -> body\" ;; *) bad \"skills.force-stop not resolved to file body: '$(printf %.40s \"$fbody\")'\" ;; esac\n# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare\n# `api listen`; REQ-SKILL-LIVE).\nlbody=$(spt adapter get-string claude-spt skills.live 2>&1)\ncase \"$lbody\" in \"# /sptc:live\"*) ok \"file-backed string resolves: skills.live -> body\" ;; *) bad \"skills.live not resolved to file body: '$(printf %.40s \"$lbody\")'\" ;; esac\n# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]\nsbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)\ncase \"$sbody\" in \"# /sptc:subnet\"*) ok \"file-backed string resolves: skills.subnet -> body\" ;; *) bad \"skills.subnet not resolved to file body: '$(printf %.40s \"$sbody\")'\" ;; esac\n\n# 4c. UPS skill-injection end-to-end via the BINARY (D1): `claude-spt hook UserPromptSubmit` resolves a\n#     /sptc:<skill> prompt to the wrapped operative body via get-string on the registered adapter. No\n#     perch (whoami empty) → only skill-injection emits, no drain. (REQ-UPS-INJECTION impl now in the\n#     binary.) Prefer the release build; the dev `adapter add` above registered the manifest, so\n#     get-string resolves the file-backed skill body. [int->REQ-DIST-HOOK-BINARY]\nHOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt.exe\"\n[ -x \"$HOOKBIN\" ] || HOOKBIN=\"$ROOT/tools/claude-spt/target/release/omp-spt\"\nif [ -x \"$HOOKBIN\" ]; then\n  inj=$(printf '%s' '{\"session_id\":\"reg-int-nosession\",\"prompt\":\"/sptc:ready listen up\"}' | \"$HOOKBIN\" hook UserPromptSubmit --host-pid $$ 2>/dev/null)\n  case \"$inj\" in\n    '<sptc_skill name=\"ready\">'*'# /sptc:ready'*'</sptc_skill>'*) ok \"UPS skill-injection (binary): /sptc:ready -> wrapped body\" ;;\n    *) bad \"skill-injection did not emit wrapped body: $(printf %.60s \"$inj\")\" ;;\n  esac\nelse\n  echo \"SKIP: claude-spt binary not built (cargo build --release) — skill-injection int needs it\"\nfi\n\n# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:\n#     `adapter add` (step 1) is manifest-first (\"an invalid manifest registers nothing\"), so the\n#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm\n#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via\n#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]\ncap=$(spt api --adapter claude-spt --manifest \"$MANIFEST\" capability 2>&1)\ncase \"$cap\" in *LiveAgent*) ok \"bringup blocks accepted; capability reports hostable harness\" ;; *) bad \"capability missing LiveAgent: $cap\" ;; esac\n\n# 5. Soft-deregister cleanly.\nrm=$(spt adapter remove claude-spt 2>&1)\nlist2=$(spt adapter list 2>&1)\ncase \"$list2\" in *\"claude-spt\"*\"active\"*) bad \"claude-spt still active after remove\" ;; *) ok \"removed (soft-deregistered)\" ;; esac\n\n[ \"$fail\" -eq 0 ] && { echo \"MANIFEST-REGISTRATION-INT OK\"; exit 0; } || { echo \"MANIFEST-REGISTRATION-INT FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\ci\\manifest\\registration-int.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"a6f55276","parentId":"02e695ec","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_78YKysx62ydLhHa3NnrR4Mji|fc_0ca4af36969c80fa016a5702c93af48198b4acf4a9b5721d18","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired\n# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this\n# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]\n#\n# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so\n# the deterministic gate run stays green on hosts without claude/auth. Real-claude execution is the\n# cross-process evidence for REQ-CI-ACCEPTANCE: [int->REQ-CI-ACCEPTANCE]\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n. \"$ROOT/ci/acceptance/lib.sh\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then\n  echo \"SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)\"\n  exit 0\nfi\nif ! command -v claude >/dev/null 2>&1; then\n  echo \"SKIP acceptance: no \\`claude\\` on PATH (fleet host without the harness)\"\n  exit 0\nfi\n\n# Disposable identity FIRST — before any scaffold/spawn — so nothing can resolve the operator perch.\n# [int->REQ-HAZARD-PERCH-COLLISION]\nsptc_ci_identity \"$$\"          # sets+exports SPTC_CI_ID / SPT_AGENT_ID / OWL_SESSION_ID in-shell\nsptc_ci_is_disposable \"$SPTC_CI_ID\" || { echo \"FATAL: refusing to spawn under non-disposable id '$SPTC_CI_ID'\"; exit 2; }\necho \"acceptance: SUT identity = $SPTC_CI_ID (operator perch protected)\"\n\nwork=$(mktemp -d 2>/dev/null) || { echo \"FATAL: mktemp -d failed\"; exit 2; }\nproj=\"$work/proj\"\ndigest=\"$work/digest.txt\"\n: > \"$digest\"\ncleanup() { rm -rf \"$work\" 2>/dev/null || true; }\ntrap cleanup EXIT INT TERM\n\nsptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null || { echo \"FATAL: scaffold failed\"; exit 2; }\n\nprobe=\"sptc-acceptance-probe-$$\"\necho \"acceptance: driving real claude -p in $proj (probe='$probe')\"\n# Drive the SUT. We assert on the hook side-effect, not stdout — model text is irrelevant.\n( cd \"$proj\" && claude -p \"$probe\" >/dev/null 2>&1 ) || \\\n  echo \"note: claude -p exited non-zero (model/auth) — asserting on hook side-effect regardless\" >&2\n\nrc=0\nsptc_ci_assert \"UPS hook fired in real claude\" \"UPS_FIRED:$probe\" \"$digest\" || rc=1\n\nprintf '\\n=== ACCEPTANCE: %s ===\\n' \"$([ \"$rc\" -eq 0 ] && echo PASS || echo FAIL)\"\nexit \"$rc\"\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired\n# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this\n# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]\n#\n# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so\n# the deterministic gate run stays green on hosts without claude/auth. Real-claude execution is the\n# cross-process evidence for REQ-CI-ACCEPTANCE: [int->REQ-CI-ACCEPTANCE]\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\n. \"$ROOT/ci/acceptance/lib.sh\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then\n  echo \"SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)\"\n  exit 0\nfi\nif ! command -v claude >/dev/null 2>&1; then\n  echo \"SKIP acceptance: no \\`claude\\` on PATH (fleet host without the harness)\"\n  exit 0\nfi\n\n# Disposable identity FIRST — before any scaffold/spawn — so nothing can resolve the operator perch.\n# [int->REQ-HAZARD-PERCH-COLLISION]\nsptc_ci_identity \"$$\"          # sets+exports SPTC_CI_ID / SPT_AGENT_ID / OWL_SESSION_ID in-shell\nsptc_ci_is_disposable \"$SPTC_CI_ID\" || { echo \"FATAL: refusing to spawn under non-disposable id '$SPTC_CI_ID'\"; exit 2; }\necho \"acceptance: SUT identity = $SPTC_CI_ID (operator perch protected)\"\n\nwork=$(mktemp -d 2>/dev/null) || { echo \"FATAL: mktemp -d failed\"; exit 2; }\nproj=\"$work/proj\"\ndigest=\"$work/digest.txt\"\n: > \"$digest\"\ncleanup() { rm -rf \"$work\" 2>/dev/null || true; }\ntrap cleanup EXIT INT TERM\n\nsptc_ci_mkproject \"$proj\" \"$digest\" >/dev/null || { echo \"FATAL: scaffold failed\"; exit 2; }\n\nprobe=\"sptc-acceptance-probe-$$\"\necho \"acceptance: driving real claude -p in $proj (probe='$probe')\"\n# Drive the SUT. We assert on the hook side-effect, not stdout — model text is irrelevant.\n( cd \"$proj\" && claude -p \"$probe\" >/dev/null 2>&1 ) || \\\n  echo \"note: claude -p exited non-zero (model/auth) — asserting on hook side-effect regardless\" >&2\n\nrc=0\nsptc_ci_assert \"UPS hook fired in real claude\" \"UPS_FIRED:$probe\" \"$digest\" || rc=1\n\nprintf '\\n=== ACCEPTANCE: %s ===\\n' \"$([ \"$rc\" -eq 0 ] && echo PASS || echo FAIL)\"\nexit \"$rc\"\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\ci\\acceptance\\run-acceptance.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"458dccd1","parentId":"a6f55276","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_6QDqYzAqepiRtyO0zA2VfmQL|fc_0ca4af36969c80fa016a5702c93ae4819880dd3ad0f5c0980d","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor under the\n# v0.19.0 FETCHER strategy and renders a non-empty digest from a real CC-shaped sample (the M10\n# acceptance criterion carried forward). [int->REQ-DIST-DIGEST-EXTRACTOR]\n# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the\n# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through\n# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]\n#\n# FETCHER shape (v0.10.0, spt-core v0.19.0): the manifest extractor is\n#   `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}`\n# — the extractor LOCATES the transcript itself (no spt-core pre-read, no `source`, no --sample\n# pipe). So this int builds a THROWAWAY config root shaped like CC's partitioned layout\n# (<root>/projects/<slug>/<session_id>.jsonl from the checked-in sample), points the proof at it\n# via the CLAUDE_CONFIG_DIR env (empirically proven 2026-07-01: digest-proof resolves the\n# {CLAUDE_CONFIG_DIR} read-var from the invoking environment, value-fallback ~/.claude when absent\n# — the same capture semantics as the daemon's bind-time read_env), pins {session_id} with\n# --session, and asserts the located-and-extracted digest renders. This exercises the WHOLE\n# fetcher chain: read-var capture → fill → extractor locate → NDJSON records → rendered digest.\n# [int->REQ-DIST-DIGEST-FETCHER]\n#\n# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor\n# straight from its build dir against the bare-file manifest — NO registry mutation, read-only,\n# so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.19.0 (the fetcher strategy + read-var fill; an older\n# binary rejects/misruns strategy=\"fetcher\") + a built extractor. Idempotent.\n# Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\nSAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\nRELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n\n# The fetcher strategy + {CLAUDE_CONFIG_DIR} read-var fill land in spt-core v0.19.0 — version-gate.\nver=$(spt --version 2>/dev/null | sed -E 's/^spt //')\ncase \"$ver\" in\n  0.19.*|0.[2-9][0-9].*|[1-9]*.*) : ;;  # >= 0.19.0\n  *) echo \"SKIP: spt $ver < 0.19.0 (no [digest] fetcher strategy). Extractor itself: cargo tests green.\"; exit 0 ;;\nesac\n\n[ -x \"$RELDIR/claude-spt\" ] || [ -x \"$RELDIR/claude-spt.exe\" ] || { echo \"SKIP: extractor not built (run sh ci/digest/build.sh)\"; exit 0; }\n\n# Throwaway CC-shaped config root: <root>/projects/<slug>/<session_id>.jsonl. The slug subdir is\n# deliberately arbitrary — the extractor's locate must find the session file WITHIN the tree\n# (the cwd-slug is CC-internal; the locate is the fetcher's whole point).\nSESSION=\"sptc-fetcher-int-$$\"\nCFGROOT=\"${TMPDIR:-/tmp}/sptc-digest-int-$$\"\nmkdir -p \"$CFGROOT/projects/C--some-project-slug\"\ncp \"$SAMPLE\" \"$CFGROOT/projects/C--some-project-slug/$SESSION.jsonl\"\ntrap 'rm -rf \"$CFGROOT\"' EXIT\n\nout=$(CLAUDE_CONFIG_DIR=\"$CFGROOT\" spt adapter digest-proof claude-spt \\\n        --session \"$SESSION\" --manifest \"$MANIFEST\" --dir \"$RELDIR\" 2>&1)\ncase \"$out\" in\n  *DIGEST_PROOF_OK*)\n    echo \"ok  digest-proof (fetcher locate): DIGEST_PROOF_OK\"\n    printf '%s\\n' \"$out\" | grep -E 'parsed|dropped' | sed 's/^/    /'\n    echo \"DIGEST-PROOF-INT OK\"; exit 0 ;;\n  *DIGEST_PROOF_EMPTY*)\n    echo \"FAIL: fetcher locate found nothing — the {CLAUDE_CONFIG_DIR} capture or the extractor's\"\n    echo \"      locate chain regressed (root: $CFGROOT, session: $SESSION):\"\n    printf '%s\\n' \"$out\"; exit 1 ;;\n  *)\n    echo \"FAIL: unexpected digest-proof output:\"; printf '%s\\n' \"$out\"; exit 1 ;;\nesac\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor under the\n# v0.19.0 FETCHER strategy and renders a non-empty digest from a real CC-shaped sample (the M10\n# acceptance criterion carried forward). [int->REQ-DIST-DIGEST-EXTRACTOR]\n# Also the consolidation int (ADR-0006/U2): --dir resolves the bare `claude-spt` binary and the\n# manifest extractor command `claude-spt digest …` runs its `digest` subcommand end-to-end through\n# real spt — proving the merged binary's digest seam still works. [int->REQ-DIST-BINARY-CONSOLIDATE]\n#\n# FETCHER shape (v0.10.0, spt-core v0.19.0): the manifest extractor is\n#   `claude-spt digest --session {session_id} --config-dir {CLAUDE_CONFIG_DIR}`\n# — the extractor LOCATES the transcript itself (no spt-core pre-read, no `source`, no --sample\n# pipe). So this int builds a THROWAWAY config root shaped like CC's partitioned layout\n# (<root>/projects/<slug>/<session_id>.jsonl from the checked-in sample), points the proof at it\n# via the CLAUDE_CONFIG_DIR env (empirically proven 2026-07-01: digest-proof resolves the\n# {CLAUDE_CONFIG_DIR} read-var from the invoking environment, value-fallback ~/.claude when absent\n# — the same capture semantics as the daemon's bind-time read_env), pins {session_id} with\n# --session, and asserts the located-and-extracted digest renders. This exercises the WHOLE\n# fetcher chain: read-var capture → fill → extractor locate → NDJSON records → rendered digest.\n# [int->REQ-DIST-DIGEST-FETCHER]\n#\n# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor\n# straight from its build dir against the bare-file manifest — NO registry mutation, read-only,\n# so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.19.0 (the fetcher strategy + read-var fill; an older\n# binary rejects/misruns strategy=\"fetcher\") + a built extractor. Idempotent.\n# Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\nSAMPLE=\"$ROOT/ci/digest/sample.jsonl\"\nRELDIR=\"$ROOT/tools/claude-spt/target/release\"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)\n\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\n\n# The fetcher strategy + {CLAUDE_CONFIG_DIR} read-var fill land in spt-core v0.19.0 — version-gate.\nver=$(spt --version 2>/dev/null | sed -E 's/^spt //')\ncase \"$ver\" in\n  0.19.*|0.[2-9][0-9].*|[1-9]*.*) : ;;  # >= 0.19.0\n  *) echo \"SKIP: spt $ver < 0.19.0 (no [digest] fetcher strategy). Extractor itself: cargo tests green.\"; exit 0 ;;\nesac\n\n[ -x \"$RELDIR/claude-spt\" ] || [ -x \"$RELDIR/claude-spt.exe\" ] || { echo \"SKIP: extractor not built (run sh ci/digest/build.sh)\"; exit 0; }\n\n# Throwaway CC-shaped config root: <root>/projects/<slug>/<session_id>.jsonl. The slug subdir is\n# deliberately arbitrary — the extractor's locate must find the session file WITHIN the tree\n# (the cwd-slug is CC-internal; the locate is the fetcher's whole point).\nSESSION=\"sptc-fetcher-int-$$\"\nCFGROOT=\"${TMPDIR:-/tmp}/sptc-digest-int-$$\"\nmkdir -p \"$CFGROOT/projects/C--some-project-slug\"\ncp \"$SAMPLE\" \"$CFGROOT/projects/C--some-project-slug/$SESSION.jsonl\"\ntrap 'rm -rf \"$CFGROOT\"' EXIT\n\nout=$(CLAUDE_CONFIG_DIR=\"$CFGROOT\" spt adapter digest-proof claude-spt \\\n        --session \"$SESSION\" --manifest \"$MANIFEST\" --dir \"$RELDIR\" 2>&1)\ncase \"$out\" in\n  *DIGEST_PROOF_OK*)\n    echo \"ok  digest-proof (fetcher locate): DIGEST_PROOF_OK\"\n    printf '%s\\n' \"$out\" | grep -E 'parsed|dropped' | sed 's/^/    /'\n    echo \"DIGEST-PROOF-INT OK\"; exit 0 ;;\n  *DIGEST_PROOF_EMPTY*)\n    echo \"FAIL: fetcher locate found nothing — the {CLAUDE_CONFIG_DIR} capture or the extractor's\"\n    echo \"      locate chain regressed (root: $CFGROOT, session: $SESSION):\"\n    printf '%s\\n' \"$out\"; exit 1 ;;\n  *)\n    echo \"FAIL: unexpected digest-proof output:\"; printf '%s\\n' \"$out\"; exit 1 ;;\nesac\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\ci\\digest\\digest-proof-int.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"11646f9f","parentId":"458dccd1","timestamp":"2026-07-15T03:47:21.319Z","message":{"role":"toolResult","toolCallId":"call_l65Lbjpfbroo0OTULfog7raw|fc_0ca4af36969c80fa016a5702c93ad8819897fd172a1821e3a7","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n# `adapter add <manifest>` -> verify active + shipped profile resolves. The LLM is never in the\n# loop. [int->REQ-SETUP-ACTIVATE]\n#\n# Mutates the node-local adapter registry (add/remove), so it is gated behind SPTC_ACCEPTANCE=1 and a\n# present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes what it\n# adds. Run: SPTC_ACCEPTANCE=1 sh ci/setup/activate-int.sh   (exit 0 = pass).\n#\n# NOTE: this is the LOCAL file-form (dev) activation path. The END-USER `--release` acquisition leg\n# is proven separately by ci/publish/release-acquire-int.sh (REQ-DIST-ADAPTER-RELEASE).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\nver=$(spt --version 2>/dev/null | awk '{print $NF}')\ncase \"$ver\" in\n  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo \"SKIP: spt $ver < 0.7.0 (adapter add is v0.7.0)\"; exit 0 ;;\nesac\n[ -f \"$MANIFEST\" ] || { echo \"SKIP: no manifest at $MANIFEST\"; exit 0; }\n\nfail=0\nok()  { echo \"ok   $1\"; }\nbad() { echo \"FAIL $1\"; fail=1; }\n# Clean up registry residue however we exit (leave no claude-spt registration behind).\ntrap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM\n\n# 0. Seed a registration, then soft-remove it to reach the F-005 trigger state: a `deregistered`\n#    adapter (the residue a prior install/uninstall leaves — binary present, adapter NOT active).\nspt adapter add \"$MANIFEST\" >/dev/null 2>&1\nspt adapter remove claude-spt >/dev/null 2>&1\npre=$(spt adapter list 2>&1)\npreline=$(printf '%s\\n' \"$pre\" | grep '^claude-spt:')\ncase \"$preline\" in\n  *deregistered*) ok \"precondition: claude-spt is deregistered (F-005 trigger state)\" ;;\n  \"\")             ok \"precondition: claude-spt absent (also a non-active start state)\" ;;\n  *active*)       bad \"precondition not reached: claude-spt still active before re-activate ($preline)\" ;;\n  *)              ok \"precondition: claude-spt not active ($preline)\" ;;\nesac\n\n# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\nout=$(spt adapter add \"$MANIFEST\" 2>&1)\ncase \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n\n# 2. Verify active — deregistered -> active is the transition setup guarantees.\npost=$(spt adapter list 2>&1)\npostline=$(printf '%s\\n' \"$post\" | grep '^claude-spt:')\ncase \"$postline\" in *active*) ok \"verified: claude-spt is now active\" ;; *) bad \"claude-spt not active after re-activate ($postline)\" ;; esac\n\n# 3. Profiles/strings went live with activation (the point of activating, not just registering).\ncase \"$post\" in *claude-spt:ccs*) ok \"shipped profile resolves post-activate: claude-spt:ccs\" ;; *) bad \"ccs profile not resolved post-activate\" ;; esac\nlbl=$(spt adapter get-string claude-spt adapter_label 2>&1)\n[ \"$lbl\" = \"Claude Code (spt)\" ] && ok \"strings live post-activate: adapter_label\" || bad \"adapter_label='$lbl'\"\n\n[ \"$fail\" -eq 0 ] && { echo \"SETUP-ACTIVATE-INT OK\"; exit 0; } || { echo \"SETUP-ACTIVATE-INT FAIL\"; exit 1; }\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Integration proof for the /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —\n# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI\n# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup\n# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->\n# `adapter add <manifest>` -> verify active + shipped profile resolves. The LLM is never in the\n# loop. [int->REQ-SETUP-ACTIVATE]\n#\n# Mutates the node-local adapter registry (add/remove), so it is gated behind SPTC_ACCEPTANCE=1 and a\n# present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes what it\n# adds. Run: SPTC_ACCEPTANCE=1 sh ci/setup/activate-int.sh   (exit 0 = pass).\n#\n# NOTE: this is the LOCAL file-form (dev) activation path. The END-USER `--release` acquisition leg\n# is proven separately by ci/publish/release-acquire-int.sh (REQ-DIST-ADAPTER-RELEASE).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/../..\" && pwd)\nMANIFEST=\"$ROOT/adapter/omp-spt.toml\"\n\nif [ \"${SPTC_ACCEPTANCE:-0}\" != \"1\" ]; then echo \"SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)\"; exit 0; fi\ncommand -v spt >/dev/null 2>&1 || { echo \"SKIP: no spt on PATH\"; exit 0; }\nver=$(spt --version 2>/dev/null | awk '{print $NF}')\ncase \"$ver\" in\n  0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo \"SKIP: spt $ver < 0.7.0 (adapter add is v0.7.0)\"; exit 0 ;;\nesac\n[ -f \"$MANIFEST\" ] || { echo \"SKIP: no manifest at $MANIFEST\"; exit 0; }\n\nfail=0\nok()  { echo \"ok   $1\"; }\nbad() { echo \"FAIL $1\"; fail=1; }\n# Clean up registry residue however we exit (leave no claude-spt registration behind).\ntrap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM\n\n# 0. Seed a registration, then soft-remove it to reach the F-005 trigger state: a `deregistered`\n#    adapter (the residue a prior install/uninstall leaves — binary present, adapter NOT active).\nspt adapter add \"$MANIFEST\" >/dev/null 2>&1\nspt adapter remove claude-spt >/dev/null 2>&1\npre=$(spt adapter list 2>&1)\npreline=$(printf '%s\\n' \"$pre\" | grep '^claude-spt:')\ncase \"$preline\" in\n  *deregistered*) ok \"precondition: claude-spt is deregistered (F-005 trigger state)\" ;;\n  \"\")             ok \"precondition: claude-spt absent (also a non-active start state)\" ;;\n  *active*)       bad \"precondition not reached: claude-spt still active before re-activate ($preline)\" ;;\n  *)              ok \"precondition: claude-spt not active ($preline)\" ;;\nesac\n\n# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.\nout=$(spt adapter add \"$MANIFEST\" 2>&1)\ncase \"$out\" in *registered*) ok \"re-activate: adapter add registered the manifest\" ;; *) bad \"adapter add rejected: $out\" ;; esac\n\n# 2. Verify active — deregistered -> active is the transition setup guarantees.\npost=$(spt adapter list 2>&1)\npostline=$(printf '%s\\n' \"$post\" | grep '^claude-spt:')\ncase \"$postline\" in *active*) ok \"verified: claude-spt is now active\" ;; *) bad \"claude-spt not active after re-activate ($postline)\" ;; esac\n\n# 3. Profiles/strings went live with activation (the point of activating, not just registering).\ncase \"$post\" in *claude-spt:ccs*) ok \"shipped profile resolves post-activate: claude-spt:ccs\" ;; *) bad \"ccs profile not resolved post-activate\" ;; esac\nlbl=$(spt adapter get-string claude-spt adapter_label 2>&1)\n[ \"$lbl\" = \"Claude Code (spt)\" ] && ok \"strings live post-activate: adapter_label\" || bad \"adapter_label='$lbl'\"\n\n[ \"$fail\" -eq 0 ] && { echo \"SETUP-ACTIVATE-INT OK\"; exit 0; } || { echo \"SETUP-ACTIVATE-INT FAIL\"; exit 1; }\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\ci\\setup\\activate-int.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"13eda78a","parentId":"11646f9f","timestamp":"2026-07-15T03:47:21.320Z","message":{"role":"toolResult","toolCallId":"call_hvbx3YKiCi03ggNmGpQxIUm0|fc_0ca4af36969c80fa016a5702c93abc8198899e0d04b15411b8","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.\n# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]\n# [impl->REQ-CI-MANUAL]\n# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is\n# always announced so \"green\" never hides un-run coverage).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\ncd \"$ROOT\" || exit 2\nrc=0\ngate() { printf '\\n=== GATE: %s ===\\n' \"$1\"; }\nfail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n\ngate \"shell-syntax (sh -n)\"\nfor f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do\n  if sh -n \"$f\"; then printf 'ok  %s\\n' \"$f\"; else fail \"syntax: $f\"; fi\ndone\n\ngate \"unit tests (tests/*.sh)\"\nfor t in tests/*.sh; do\n  [ -e \"$t\" ] || continue\n  if sh \"$t\" >/dev/null 2>&1; then printf 'ok  %s\\n' \"$t\"; else fail \"unit: $t\"; sh \"$t\" || true; fi\ndone\n\ngate \"digest-extractor (cargo build + test)\"\nif [ -f \"$ROOT/ci/digest/build.sh\" ]; then\n  if sh \"$ROOT/ci/digest/build.sh\"; then :; else fail \"digest-extractor\"; fi\nelse\n  echo \"SKIP: no digest extractor build yet\"\nfi\n\ngate \"psyche-runner (cargo build + test)\"\nif [ -f \"$ROOT/ci/psyche/build.sh\" ]; then\n  if sh \"$ROOT/ci/psyche/build.sh\"; then :; else fail \"psyche-runner\"; fi\nelse\n  echo \"SKIP: no psyche runner build yet\"\nfi\n\ngate \"idle-translate (cargo build + test)\"\nif [ -f \"$ROOT/ci/idle-translate/build.sh\" ]; then\n  if sh \"$ROOT/ci/idle-translate/build.sh\"; then :; else fail \"idle-translate\"; fi\nelse\n  echo \"SKIP: no idle-translate build yet\"\nfi\n\ngate \"traceable-reqs check (requirement coverage)\"\nif command -v traceable-reqs >/dev/null 2>&1; then\n  if traceable-reqs check >/dev/null 2>&1; then echo \"ok  coverage green\"; else fail \"traceable-reqs check\"; traceable-reqs check || true; fi\nelse\n  echo \"SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)\"\nfi\n\ngate \"skeleton-validate (cplugs installability)\"\nif [ -x \"$ROOT/ci/publish/validate-skeleton.sh\" ] || [ -f \"$ROOT/ci/publish/validate-skeleton.sh\" ]; then\n  if sh \"$ROOT/ci/publish/validate-skeleton.sh\" >/dev/null 2>&1; then echo \"ok  skeleton installable\"; else fail \"skeleton-validate\"; sh \"$ROOT/ci/publish/validate-skeleton.sh\" || true; fi\nelse\n  echo \"SKIP: no skeleton validator yet\"\nfi\n\ngate \"manifest-schema\"\nif [ -f \"$ROOT/ci/manifest/check-manifest.sh\" ]; then\n  if sh \"$ROOT/ci/manifest/check-manifest.sh\"; then :; else fail \"manifest-schema\"; fi\nelse\n  echo \"SKIP: no adapter manifest yet — activates when the CC adapter manifest lands\"\nfi\n\ngate \"docs-drift\"\nif [ -f \"$ROOT/ci/docs/check-docs.sh\" ]; then\n  if sh \"$ROOT/ci/docs/check-docs.sh\"; then :; else fail \"docs-drift\"; fi\nelse\n  echo \"SKIP: no docs check yet\"\nfi\n\nprintf '\\n=== RESULT: %s ===\\n' \"$([ \"$rc\" -eq 0 ] && echo PASS || echo FAIL)\"\nexit \"$rc\"\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.\n# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]\n# [impl->REQ-CI-MANUAL]\n# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is\n# always announced so \"green\" never hides un-run coverage).\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\ncd \"$ROOT\" || exit 2\nrc=0\ngate() { printf '\\n=== GATE: %s ===\\n' \"$1\"; }\nfail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n\ngate \"shell-syntax (sh -n)\"\nfor f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do\n  if sh -n \"$f\"; then printf 'ok  %s\\n' \"$f\"; else fail \"syntax: $f\"; fi\ndone\n\ngate \"unit tests (tests/*.sh)\"\nfor t in tests/*.sh; do\n  [ -e \"$t\" ] || continue\n  if sh \"$t\" >/dev/null 2>&1; then printf 'ok  %s\\n' \"$t\"; else fail \"unit: $t\"; sh \"$t\" || true; fi\ndone\n\ngate \"digest-extractor (cargo build + test)\"\nif [ -f \"$ROOT/ci/digest/build.sh\" ]; then\n  if sh \"$ROOT/ci/digest/build.sh\"; then :; else fail \"digest-extractor\"; fi\nelse\n  echo \"SKIP: no digest extractor build yet\"\nfi\n\ngate \"psyche-runner (cargo build + test)\"\nif [ -f \"$ROOT/ci/psyche/build.sh\" ]; then\n  if sh \"$ROOT/ci/psyche/build.sh\"; then :; else fail \"psyche-runner\"; fi\nelse\n  echo \"SKIP: no psyche runner build yet\"\nfi\n\ngate \"idle-translate (cargo build + test)\"\nif [ -f \"$ROOT/ci/idle-translate/build.sh\" ]; then\n  if sh \"$ROOT/ci/idle-translate/build.sh\"; then :; else fail \"idle-translate\"; fi\nelse\n  echo \"SKIP: no idle-translate build yet\"\nfi\n\ngate \"traceable-reqs check (requirement coverage)\"\nif command -v traceable-reqs >/dev/null 2>&1; then\n  if traceable-reqs check >/dev/null 2>&1; then echo \"ok  coverage green\"; else fail \"traceable-reqs check\"; traceable-reqs check || true; fi\nelse\n  echo \"SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)\"\nfi\n\ngate \"skeleton-validate (cplugs installability)\"\nif [ -x \"$ROOT/ci/publish/validate-skeleton.sh\" ] || [ -f \"$ROOT/ci/publish/validate-skeleton.sh\" ]; then\n  if sh \"$ROOT/ci/publish/validate-skeleton.sh\" >/dev/null 2>&1; then echo \"ok  skeleton installable\"; else fail \"skeleton-validate\"; sh \"$ROOT/ci/publish/validate-skeleton.sh\" || true; fi\nelse\n  echo \"SKIP: no skeleton validator yet\"\nfi\n\ngate \"manifest-schema\"\nif [ -f \"$ROOT/ci/manifest/check-manifest.sh\" ]; then\n  if sh \"$ROOT/ci/manifest/check-manifest.sh\"; then :; else fail \"manifest-schema\"; fi\nelse\n  echo \"SKIP: no adapter manifest yet — activates when the CC adapter manifest lands\"\nfi\n\ngate \"docs-drift\"\nif [ -f \"$ROOT/ci/docs/check-docs.sh\" ]; then\n  if sh \"$ROOT/ci/docs/check-docs.sh\"; then :; else fail \"docs-drift\"; fi\nelse\n  echo \"SKIP: no docs check yet\"\nfi\n\nprintf '\\n=== RESULT: %s ===\\n' \"$([ \"$rc\" -eq 0 ] && echo PASS || echo FAIL)\"\nexit \"$rc\"\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\ci\\run-gates.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"502e2e93","parentId":"13eda78a","timestamp":"2026-07-15T03:47:21.320Z","message":{"role":"toolResult","toolCallId":"call_9smnDALbLQW3TvfL7WJT7Msa|fc_0ca4af36969c80fa016a5702c93af88198aaedf5cb688e4618","toolName":"read","content":[{"type":"text","text":"#!/bin/sh\n# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).\n# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024\n# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the\n# archive ROOT, plus each recognized target-triple's binaries under a <triple>/ dir mirroring the\n# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into\n# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]\n# [unit->REQ-DIST-ADAPTER-PEROS]\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nPACKER=\"$ROOT/ci/publish/package-adapter.sh\"\nWIN_TRIPLE=x86_64-pc-windows-msvc\nLINUX_TRIPLE=x86_64-unknown-linux-gnu\nBINS=\"omp-spt\"  # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)\nrc=0\nfail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n\n# The fat archive needs BOTH platforms' release binaries. Probe both triples.\nhave_all=1\nfor b in $BINS; do\n  [ -f \"$ROOT/tools/claude-spt/target/release/$b.exe\" ] || have_all=0\n  [ -f \"$ROOT/tools/claude-spt/target/$LINUX_TRIPLE/release/$b\" ] || have_all=0\ndone\n\nif [ \"$have_all\" -ne 1 ]; then\n  # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the\n  # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.\n  if sh \"$PACKER\" --apply >/dev/null 2>&1; then\n    fail \"packer should refuse (exit!=0) when a platform's binaries are absent\"\n  else\n    echo \"ok   packer refuses --apply when a platform's binaries absent (guard works)\"\n  fi\n  echo \"SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present\"\n  exit \"$rc\"\nfi\n\n# Both platforms present → build a real fat archive to a temp file and assert its structure.\nTMP=$(mktemp -d \"${TMPDIR:-/tmp}/sptc-archtest.XXXXXX\") || { echo \"FAIL: mktemp\"; exit 1; }\ntrap 'rm -rf \"$TMP\"' EXIT\nOUT=\"$TMP/adapter.spt\"\n\nif ADAPTER_SPT_OUT=\"$OUT\" sh \"$PACKER\" --apply >/dev/null 2>&1; then\n  echo \"ok   packer --apply succeeded\"\nelse\n  fail \"packer --apply exited non-zero\"\n  exit \"$rc\"\nfi\n[ -f \"$OUT\" ] || { fail \"no archive written at $OUT\"; exit \"$rc\"; }\n\nLIST=$(tar -tzf \"$OUT\")\n# SHARED manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.\necho \"$LIST\" | grep -qx \"manifest.toml\" \\\n  && echo \"ok   shared manifest.toml at archive root\" \\\n  || fail \"manifest.toml not at archive root (add --release would reject)\"\n# SHARED strings/ at root (not under a triple).\necho \"$LIST\" | grep -q \"^strings/\" \\\n  && echo \"ok   shared strings/ at root\" \\\n  || fail \"strings/ missing from archive root\"\n# Each triple carries all the tool binaries, under its <triple>/ dir, mirroring the flat-root tree.\nfor b in $BINS; do\n  echo \"$LIST\" | grep -qx \"$WIN_TRIPLE/$b.exe\" \\\n    && echo \"ok   $WIN_TRIPLE/$b.exe present\" \\\n    || fail \"$WIN_TRIPLE/$b.exe missing from archive\"\n  echo \"$LIST\" | grep -qx \"$LINUX_TRIPLE/$b\" \\\n    && echo \"ok   $LINUX_TRIPLE/$b present\" \\\n    || fail \"$LINUX_TRIPLE/$b missing from archive\"\ndone\n# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.\nif echo \"$LIST\" | grep -qE \"^$WIN_TRIPLE/(manifest\\.toml|strings/)\"; then\n  fail \"manifest/strings duplicated under a triple — they must be shared at root only\"\nelse\n  echo \"ok   manifest/strings not duplicated under a triple\"\nfi\n# Negative: no UNRECOGNIZED top-level dir (spt-core would silently flatten it as a shared-root entry).\nbadtop=$(echo \"$LIST\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\nif [ -n \"$badtop\" ]; then\n  fail \"unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat (silent footgun)\"\nelse\n  echo \"ok   no stray/unrecognized top-level dirs (only strings/ + the two recognized triples)\"\nfi\n\n# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.\n# (Smoke the guard by reading the dry-run plan's advertised floor.)\nplan=$(sh \"$PACKER\" 2>&1)\necho \"$plan\" | grep -qE \"min_spt_core 0\\.(1[3-9]|[2-9][0-9])\" \\\n  && echo \"ok   packer advertises a fat-capable floor (>= 0.13.2)\" \\\n  || fail \"packer floor advertisement missing/too low; plan=[$plan]\"\n\n[ \"$rc\" -eq 0 ] && echo \"PASS: adapter-archive\"\nexit \"$rc\"\n"}],"details":{"displayContent":{"text":"#!/bin/sh\n# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).\n# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024\n# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the\n# archive ROOT, plus each recognized target-triple's binaries under a <triple>/ dir mirroring the\n# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into\n# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]\n# [unit->REQ-DIST-ADAPTER-PEROS]\nset -u\nROOT=$(CDPATH= cd \"$(dirname \"$0\")/..\" && pwd)\nPACKER=\"$ROOT/ci/publish/package-adapter.sh\"\nWIN_TRIPLE=x86_64-pc-windows-msvc\nLINUX_TRIPLE=x86_64-unknown-linux-gnu\nBINS=\"omp-spt\"  # the ONE consolidated tool binary (built from tools/claude-spt; renamed at v0.1.1, HIGH-1)\nrc=0\nfail() { printf 'FAIL: %s\\n' \"$1\"; rc=1; }\n\n# The fat archive needs BOTH platforms' release binaries. Probe both triples.\nhave_all=1\nfor b in $BINS; do\n  [ -f \"$ROOT/tools/claude-spt/target/release/$b.exe\" ] || have_all=0\n  [ -f \"$ROOT/tools/claude-spt/target/$LINUX_TRIPLE/release/$b\" ] || have_all=0\ndone\n\nif [ \"$have_all\" -ne 1 ]; then\n  # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the\n  # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.\n  if sh \"$PACKER\" --apply >/dev/null 2>&1; then\n    fail \"packer should refuse (exit!=0) when a platform's binaries are absent\"\n  else\n    echo \"ok   packer refuses --apply when a platform's binaries absent (guard works)\"\n  fi\n  echo \"SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present\"\n  exit \"$rc\"\nfi\n\n# Both platforms present → build a real fat archive to a temp file and assert its structure.\nTMP=$(mktemp -d \"${TMPDIR:-/tmp}/sptc-archtest.XXXXXX\") || { echo \"FAIL: mktemp\"; exit 1; }\ntrap 'rm -rf \"$TMP\"' EXIT\nOUT=\"$TMP/adapter.spt\"\n\nif ADAPTER_SPT_OUT=\"$OUT\" sh \"$PACKER\" --apply >/dev/null 2>&1; then\n  echo \"ok   packer --apply succeeded\"\nelse\n  fail \"packer --apply exited non-zero\"\n  exit \"$rc\"\nfi\n[ -f \"$OUT\" ] || { fail \"no archive written at $OUT\"; exit \"$rc\"; }\n\nLIST=$(tar -tzf \"$OUT\")\n# SHARED manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.\necho \"$LIST\" | grep -qx \"manifest.toml\" \\\n  && echo \"ok   shared manifest.toml at archive root\" \\\n  || fail \"manifest.toml not at archive root (add --release would reject)\"\n# SHARED strings/ at root (not under a triple).\necho \"$LIST\" | grep -q \"^strings/\" \\\n  && echo \"ok   shared strings/ at root\" \\\n  || fail \"strings/ missing from archive root\"\n# Each triple carries all the tool binaries, under its <triple>/ dir, mirroring the flat-root tree.\nfor b in $BINS; do\n  echo \"$LIST\" | grep -qx \"$WIN_TRIPLE/$b.exe\" \\\n    && echo \"ok   $WIN_TRIPLE/$b.exe present\" \\\n    || fail \"$WIN_TRIPLE/$b.exe missing from archive\"\n  echo \"$LIST\" | grep -qx \"$LINUX_TRIPLE/$b\" \\\n    && echo \"ok   $LINUX_TRIPLE/$b present\" \\\n    || fail \"$LINUX_TRIPLE/$b missing from archive\"\ndone\n# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.\nif echo \"$LIST\" | grep -qE \"^$WIN_TRIPLE/(manifest\\.toml|strings/)\"; then\n  fail \"manifest/strings duplicated under a triple — they must be shared at root only\"\nelse\n  echo \"ok   manifest/strings not duplicated under a triple\"\nfi\n# Negative: no UNRECOGNIZED top-level dir (spt-core would silently flatten it as a shared-root entry).\nbadtop=$(echo \"$LIST\" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE \"^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$\" || true)\nif [ -n \"$badtop\" ]; then\n  fail \"unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat (silent footgun)\"\nelse\n  echo \"ok   no stray/unrecognized top-level dirs (only strings/ + the two recognized triples)\"\nfi\n\n# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.\n# (Smoke the guard by reading the dry-run plan's advertised floor.)\nplan=$(sh \"$PACKER\" 2>&1)\necho \"$plan\" | grep -qE \"min_spt_core 0\\.(1[3-9]|[2-9][0-9])\" \\\n  && echo \"ok   packer advertises a fat-capable floor (>= 0.13.2)\" \\\n  || fail \"packer floor advertisement missing/too low; plan=[$plan]\"\n\n[ \"$rc\" -eq 0 ] && echo \"PASS: adapter-archive\"\nexit \"$rc\"\n","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\tests\\adapter-archive.sh"}}},"isError":false,"timestamp":1784087241319}}
{"type":"message","id":"d026c79f","parentId":"502e2e93","timestamp":"2026-07-15T03:48:07.441Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing cross-platform session handling gaps**\n\n**Assessing test coverage and error handling gaps**\n\n**Clarifying reply handling and test coverage**"},{"type":"thinking","thinking":"**Evaluating message dispatch and error handling**\n\n**Identifying listener shutdown gap and dispatch behavior**\n\n**Detecting outdated test setups and missing OMP proof**"},{"type":"thinking","thinking":"**Assessing requirement registry validity**\n\n**Identifying outdated and conflicting requirements**\n\n**Flagging stale manifest hook section**"},{"type":"thinking","thinking":"**Clarifying OMP session conventions**\n\n**Planning comprehensive glossary updates**"},{"type":"thinking","thinking":"**Marking partial and resolved implementations**\n\n**Assessing OMP extension scope and floor version**"},{"type":"toolCall","id":"call_qUPrf0CiH85W8xtjIW3M4Mxq|fc_0ca4af36969c80fa016a5702f71c348198a1efd391167e8356","name":"read","arguments":{"i":"Reading identity and env seams","path":"adapter/omp-spt.toml","selector":"588-640,790-905,910-1010"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10419,"output":2356,"cacheRead":207360,"cacheWrite":0,"totalTokens":220135,"reasoningTokens":2308,"cost":{"input":0.052095,"output":0.07068,"cacheRead":0.10368,"cacheWrite":0,"total":0.22645500000000002}},"stopReason":"toolUse","timestamp":1784087241326,"responseId":"resp_0ca4af36969c80fa016a5702c98e4c8198b0d73c87db3e0de7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a5702cb00708198a3eb05a4c33d510f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwLUF8FF-gcvax57iHyF0In4hLjYwYzk0mtFIEDrRXLDlLyiK64extSUinOoFYZwN7O3XWlSPm7GKYrYs61Wm68bjuMa7e9z81zyKEHEylOiplFHQnON8W4iW1J8Rlm1gzMtlA10QP2awmtMfhywWpnJcO2StQKlRAzXCv2VnQQPXGB8OhDk5XDCyz2GBFGs_Jw_bW6OmfQUvjL6jLFB1CVhLmAo8TFLY06aSJ2I5aAzLb0pWJw357EAaPYz-XWb-MUTihIRzzOdVG9ghoBUX2YgZBNkx27ApvAbBjX5nqFFlm2EBpESlrgzt57p-z5ItSlm1Eiqb_XiqC1KrRPyAJu9akMpBpRqL47vjDYmNPgUTzt-kkNKlVGLqnpTKLuKYl4HRZw33WYoGdXz7mBd6LKW7WdvxNfmLFNALpPFYv9XKHFMUKUyIJ6eStNTYUuDs4JJQcMYMGB4cJl-MtFuKOIBswURAjTQwn1jKsEAOOlXJc4mNHzlAQWoOpQNy1VrIUvj8sZaka7q7qx4WhhiDQykTMA1Qjv5E4ANlDu7JJuZtQsTNui8LUL1sWTlQ8rWbNYFldW3-yGAWqo95u7RndSTFnapZU-r3Br1Y79bjxLGwshyhqwFvGZG_tGh7aB9DrFGtVOhL1sTSlc9skcyXR2lBocA4rqouf_y1b_S7-0xziv26ef-1pPxo7c0l79DFcc7Kt-MNJxHP8Dd20OGgcTxaQl2K6Ld0g3358JfLb_0621lYClyFep4KBin6N3U3QescPMYKhtSQo3Sx6586_C8qO2nR1wEBGTSKpG-dP7vP2gCccVErih5xTUXq1ZTK54umDDEJer4LUJl69KyTTt4YhJefwymAkItI7ahKvXF7KdkeQr1MerKHpQ3lxLz-tFAiPhvkVcWU5byDMe7C1hRN-e1N9-y7XapLER3ZyWg8cZqyovumopxdNF-6sknCjvk2jL9NHoVqvxRmAO5hkFB5yT-TcCkgOVyp6VFktsWEwvgTUsw-x-hbrH1ieF1BK2HZXLNVQzmG_9bwGkhFrLlAD8CdnupObkzOKs1YgBoYz2mh1U-8-cSz3JsPKsHUAys1GPiOSmaNPflwk0U_9FsJU8DoUERhAJ-dHZymWUswOqN9iwC2M7EAkuNNTFA_QaAUbNEFdPCJGTiNAYL0peRSmWG7IHVVDXxLD2r_q0g0YO5eUBFy0wVeizwydoBUe2tUGnNi6RcAiCaueBn69Ug2vIb90BKnZ1knBiNWNPg9cSWIaTbTSXB9Gzq4a5BMv0zBSnMCbL3OhFmc-SyQxDlbckAZYyov8lxX0AhP5_ITtNcCmZEqQwiTwV1egRIzQMhq4fCMJrUBHfwW0EFrq7Gms8Gdm8Y_XMwLqXG9Yyi4uQJcCpEJZ5ygcZZNNYtYAFi2AnVE7OxZVQevWXWgsPIbCIV8CJeKKuNSBFfdCVmEM5zxL1t2JsfiygDy7p5JNtijXM_rAW__gOoYh2oGKzxU7BUXAiMFOuGCKjguFGS0QxlfPZPM8AglUGM9XOul_LPXhg74xHJECsdKFW4EkvS2MvoA3mIoXVqur3Lv5cBIjeBtV2zGx994i6SX6bsyq0p_pu4UmtegwYqoriLQXS7TbZWH32pTo7bNCxiP3IMoTd_aQTcjnDl0RQkBvdCtYrS_CzuGHjAj_E-gnX6H_8leMhmas47bo13n8CZ1daE62oCKltN2UM_A5IpY8GA2iV30Q8TkPopHjD6PLeJFItrrAh0DipG64v3gXDlvobh1ntGaakwgKt9j8A9oMITGjBLYeeD6QG1fr-lw-L8KHnYRRqpZdpsisVO3OiWCl_LPVAZ_VnyellDnpfewKZLT_bj2a-wZ8EcnPfLYet1ZVJhYt6VXQ6VewMhqQ5rIoTNQZPtGfsP4smo_sYCOxOjZbLbqGDe20lcKnzJtuBMEnlQWhzYUV7FeuaGzni2c2CZkCg9_k9jEkzKXLct-oFw6jS6sgdMHwuZTRUNyOpBYl-Q7EojMVUhhJalDTpBg1ScO8s2HkTKLd9ncKpHap-UoagV6cjbSidv9twNrx8r4dnG9WlfQXI3aowTkRL_kc0JituK8CsIXAYj-ALgmRVLLOfcZzYyp2Vy_OypyNsNDwiA_LEesFpIvlAsjxNKqCUAPtEGKBuQ9OWU3kB6qvhxKGpimI5Li5xDF2UDsUPtn7XNBfTXOPAU2DOBTQOFnwgyRHEKDZC3ZH9mv4rlBEYRXUI8L1j2XNmt7mB7ZNik_YlAD2_gIZU5LIQofWcS66HvAPJv_knUxvJi0rvmNhlzpFFw_P8T_XtbPpw7vLYlZFX42lCbkTdFkl9UsmsXBVO_Ada3RlmaOp4deOY5RZ2eLQED4Pc0tLh-k5Yl5S1QY1rlwAn53BueKrLSxjbL2PdthePky4-IaIe7DHvedLC0LbxSOH0dCMETpnrvnRQuHccV44Y6wuoh-2gh7W2IYjqUor2NrbIzUcg2CfdW9PeE5p-j_zWr57Dz0jr-d_0lBZfJ4dqLIc2kixeQN-5cmNfVWw2Q1vFYey3RD6q75UrbeiAI0lsIDJtqQiyJQbX-qbjjX7VZHDkCeMXISmbS51hOVxoFub5awHB6uTWrksUP91GN7rvWMtBrAe-yXSXdUZV1JKPgqBpcW_8UfAIfnhfCAtIwofW5T9u49-LiD4BRQS6St713I9xULFq486vf7GDXn2Y7Q5KzxNz9katYG3_lhDXLiLRTTxtVMP7KSSEph_kPOP2SkQp6uyWW33Kr2IqQRROXiOoZ1gAZZJ6eCCMJ0Q4_Hk-Mx6hC-waDMZ1C-EnYkT_J7Lue9SXLKfgYdEaowW50Hls7XcazYF-k_bNDJPf4HsvWTQqbP89jXq9wSYI0bX936ZHUQhSisFWOI0EIiCyn4F3MJuqg30CoFGJARWGoayu1_47pnjSVH_puz39-VYQQAxSU1BPgB6Asm9W59EwJ6Rl-h43AKUjOHziXkW35eOco6090ewS1TKxP_Dwr79Y-a0WLWyWz-oVVcj-h1hbsJ3Uh_rp9Egbntbztxl41-uKxPqAaIXMP0fqTHAyzLpHaC5SFW8noPpcrQdpq0H7kyORXfhB5FGs-I1FdvxhUIUiKmDnP9lYnavH8aokggJUEaBdglMK56TGlnWMGkOJ0m0OKK9zMEf3bmed9oAVyDql3czsPQsTfp7RwxqAwlYg4OuV9beZKwi8wObTDkC7scCsv1TrcL2cj0ywJy3xuXUdnr1qqEOg6OiQZ-OHWUqAWnVvMZtk6q8F8u17frWJo-CL5UFqnaJJDrai6UEpuotXOqRFUl3j_wG1Ddwt6JtMV-FDr7BokZMLdA8UGBYhpkrMDMK8FEwrTQeVmH22OxwPHqZzcmN1JZknY64_Rb94Hu-Jek7m1hqBfv0YkXqTvx63MHCiClshzQoUmXjOKFGSlkJMw6koWqzAELFoh6f_k894YowbNlDtsq5E7PugXODuhijoS0uFAyU_nK0N4iCiZu7xQjQN_95XTbJ5fxW2Q27d_YZOjWqdvNS5aBdT6imiFM6UCoFHKCWkOPk3D7ibA8uMQdmLThycLWfMMCO-vIbVaCMxOoSJtXlURpq_b2i4xjAjXkDXou4qnTQs08drFXdhSAGQSty6RMlOKb4w5fxrBeFk2bu_YKvzrSoiA6KlKOl3IabkJuA1yU5KUi-piJNONhPq7D0r4Gg-mRZTplYC14nXYxyFESWvv_D1ufCkhFOtyxYIx4IUs5sFmRKBzoB6kEclSJLTEvTYZ5JW37U_NXkusOjs0nKuyvHCWvm0-uXLVArNlN5YmnoGGqosXkP7-4I0zkEQeL7N2xidijBcdNHFQB5kW2OBhxUU6LA8d1ZFaXKdwQFbR8GOgT1ilsHeT7dhg4i_bepgOBZB082zgsbW_ZqZkySSgs_nPED9SrI9gdZBEk80vaiok_RlscABFfafY2rk8uxrEDaiN_jrblzbYWA-dZZBS4uB3pP02u2ndzOhJERzogYk7jnbpWqnl-R9KCPtUQjySp01f5D2uKTnbhxVKtLz0XvObIAmavehVQP3JpM1ZPQRiLYM0gqrdrek_v0t6uVVUbru4p4ACze3C5KrSIq9d7fslDuHGu1WATwDL5p8djz59ME2w38guERk3MacXor6q9NB3ods_r5MkSMs75AcPAi6BIQ7xGZOB_8Nl3GdUilVX2tDTo57cMNqqObQbbyxPfNlIBxIKymHD01w7DmDeh4-otjBAQXtK31_89k78P8jdD0NSRj0iWlaDvHzvP6vKpdxeM4OHCWU9K_vYzApU9w==","summary":[{"type":"summary_text","text":"**Analyzing cross-platform session handling gaps**"},{"type":"summary_text","text":"**Assessing test coverage and error handling gaps**"},{"type":"summary_text","text":"**Clarifying reply handling and test coverage**"}]},{"id":"rs_0ca4af36969c80fa016a5702d4d4548198871d2566dffcde3d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwLe0ZTo5Stk-j_SFqX4tdfUB1sfQH5ui482pISuqQ8BYlDakoHdbqctpczOdqD7qeu3U9LK62Nw4gs_MQeg_fbBqLyS-v2dHnMGJS4q16ewKhdhUU-AkuyDkEz4TWTc-ON4HX3_APYkMAU563g9Nj-LVBzpbhOp3g2KxeeFRoBVvz7Faiw8CZS8oB7wvaLMIa9eHyllGtJweIBQxSFaOx7ATi-QxF1OzLtfdYPo3X_au9WNFB3CYv6cR-9xRiwqhfOCc__-WD8saqCmzBtReZbBYXdwleKOccvaNzQ-Prmg1TF0qWoIjwHKNW-Yiv4F4RPkLSp1jX0q--O8JDAVqAwOSR96GqcdaEPZvzNOrVGPFQTpt_7odGxVJMLyArXNuVgIX_LhLDMTVmW8xKbBFPWkwDgEm2IMfUNPVizzwT1cBYvBZU-H4MpQHGf2arPxLugYTtTBx6rFs1Jj-AlhCPvSD771VzjPIvfUPpTZ0BZOTKdA5q5JdZvjPai-XNW832gNinbej3evBEhmhQNLPSAzs0JKRF4GavBFSeB1iX1w1G4Hsm3424F9l71QiXB0Lrkj8IJAO0bM8QA0GoOfmOB_rAwt-95nwFfij7mJGXOvyCFaRm4ohsGPCDolyLwztCvK4mSl9bNCcNYYTMQ21ybPfAtTz3lyLH57k3tdCrnTYHiu5Pf9e35xveCgs7ZKPhv_gbQiqvE4jE5JWKufUtqey7GiKoiq8MvoVE0yOlSQT8Ibl0BMW7gP_SngYSal1LWiZ8Npm3yFQlHYwf8LUZyP07c4P8R1lA2GBgbf6z7NZaSQo2guM55WpJPpVxJE87GRrHZkaIgjPtco8Gr6ibDSPK5YYySzhnTefcc4Rjb3fBc1L48LkFP1sKAhKtqC7YXQtW2ROokhy_2vUzh-6I4Z-Y_X5_hVPRp5RpclNAJWjbJqGPdVmWFfcdMp0QcO5VHR8pJ-26pnK-PDbDFxroQ2lxFq4voBnaJxrYr94I04c2NeM2nAs8TVVBN4oMm_KlVBEmnWR_JSsKD2DOFC2VnzCQNrRu0ZhBzviVxBdt0mkCo6CrnbooqyN2uiz-p3Qp3vW8BPnQ_IzaGC8JdSGhac60PRgItJQnwA2E_-HclA7scvro5jsRzxOfvxkXpAA5g_FJIsrkwxiw0AfE2sWbadCB78vWJPf2EA9pvy6kB0TIWix3kp644yymvIIAKdMoZakys-hcadfbjhFAmXsv34ux3eTI0TpW1v_Jmduq65Iew2snyVKlF_Yky1J8rRyvh51hUPYmCiN_R_VAtdp1mwwgFsuC4U3EaILKdn2tieBT-KVtE189UHFIOg3lu2W4RkwB5DOGQ-B0XhMnwwgz41ldLnwidNj-16nhskRqKJvkibsnz-RcCt14j7BZMFTUEAsDj2IDiEe4pvVUNyDZMmJYcV1P6ZL23f92hyPy_FRsLslu3Uq7CSDdnClschGYCMPNEPDb205v8-bGKAPd_RlRx_DDqOh5ScTWsEM8ZRdJkCJ9G1FFJCDfy1gU885rP34rC6lYRBgfAfMJJ4THPSQs9MG5T_8kFsDwy-xTDT2IfAZQo-LoNlXvStt9S9Rs6jglWMY5NgLIhfEI4YqNh2wFOnAz3h24uyjSs2Ae1q_pluZYvIoaF_d31KgSVHL3yOaE8InW0cLG143VKcTYeVQYeeGhp7shbmPF5bYY0X2tSaGkcRm9HBGbvC8J9Ai-5I7XKGFaqpJ5vVbt2XOxLP8qdSrKn5BPY6TsNsTpmWdxWxfr_5qps8vKkmTZFxomG_4kckAOYJFaa24vTvqQdQmP1-MR_5yhpy_AcfebLMz-v4jZexJXXCNUMwf_T8w23EQPF7kaENxeyxsstnJwAeyH8455kOC9plGfVNg7Zw66MkB9hdD4Gx-5ZWw9kowQptb7shq1_M9EMAZBGCfbyB54LQQaj5WVWqAbR5bf77bufyqiN-Drlfe1hNWPKSsK8YJw6B9Say1z96wLwScnAPZmPDNFP17TEuaLB9jdngwvLG3gJRTeEw3NSgdPZ1wJs2-8H-t3EDPs_c4B_DaVxjaI9bN7dEdI0FKFv5hHG5zeWN-SvlM6HI99_7NF9wVQ5ZrLZMNVqHUukY1ZFa0zmBODSBl3pJcgMdLq9vu5Sn1vrEyZj3KNN3D-IA0mRNBG1sQkHUNbeoYffHQuCdFYyuC1ECyPfzWB3EZk8FMh2EZ2CrnaCc30YX-4r-P61k3ici4HdHrAUFEFI0C3niK8AgCYcY6k5vZE1tEb8jfNDggxWbmiNwQBZdsQN8AZc5pxurwFrFTUA3gr3f_ocy4jxlkECWMwyEr_rtw7-PW6iP-3wWZxTnuIEw6UZk494mLPws5rlQ44SPNj0IV7V6lS-i9k6f4NAvV950b-8rW0KXCsy7BBDMbFolG7VMdecpxzv0OCPRmsplu188cPwj6ZMIZBpeHzOa95Lvo0ShBBOfIP_0VvjkTKHQra_5yFWOGyh9B-_Vf7gGL9oUIxbGrc19bjn8q3ZMaMDBIT3K3p5BDWyxsgeYH7aG7THZ5QbkVW5uVhm21aAwnMPo8Yiz8ak9ijxJLEjWakY3ZtScF83onzEjZkVjBYso9cA2106HsLSwAqAUmuIqybdK0NEjqChwBVQsu4oMWNW5foj6fb3ZkzXY_wC65HwL5JDU8rJUT-BUVll0T_8XVlsO3auvA34-UNzdFP3uH6JclZeFP__OleccO8o_kLQkSMdqACY4el6tfsdNRRbcqguJusnFUxO6x1_K-HQnIdk74Z69wbRA_sF1by9O3ph19MnimSYEFFMVBrsDs6lFmOLWICOAI7YZvy2yy0wDA0wHklLGH3R0mmGpz0b7N5slo_u9xz6vlHDfIL_2PPYLMZBYrShf4QFa_WvOqLupHaS-fFYcQaRyuX4wktBEyv-n8B9nj9gfXQfwZLeTUpvB0xOp9HoCXzKR7ih_mabArf0cJA1EBRCwdvLGhkXhwVhGH9BUTURhN-QZhNbNSKTRcD7sb3u5g-Cj0olV4W79jp6tqonxgAUFb3EU7StqEGuMazgNwdujjVkrFUY8yJ7jFxtc3vBh-j9mjeu43znp1ayUxOpPoiFsWOjpwafppVQwiPY3L0mctY1A3pcBZ8Ol_7NPvgZw_7yLPC_9sZHLPq8M1_XyQeHFOyuikHCHiwovcvyfdhKChU3T2aXilo6iWiMaOGtAbTEkRzTeUXJr_lITG9nRALfPckIicHiB-KBrNk5QVsdEqcIdBXDPTYdRHQrqEBTiWIPWiCv8XyIbv4Kory3zP1VXTdcopFa9se8ZnlJtdfTZDDXcc4_i2EsspglDUIAFc_B-rBN6OL7FO3aE5950AlXG4eQSXdSLuoy_8rVm7cByGZWIMCr7GD1PJy3-haoZ5IFVCCNY8rlxhFsf3464RMzdJk66BUwihBhycYcWBf8sAJVGZ7iIJywIN4rvvMvingDxdGnwX0eurNsQhO28s4ZO0792SCYeDZagpSbD7B7H7I4cWR1VhwKMbga98n3NDZvR6mepw9lGwUvrPjBEwgIbU51BAVfmKSZNmdnoslzifZ-TEHO6PH8xAVf3VeCoeMgK_QpfiKXQMZwgetkLhJcWf5HyqLhNikk0cSMoXyuvSVW2XFHQCxf55ZJA8zL1G8BPBkgCVodUqORf3x1CCIicSl6qqMcXCcsOzLYnwL4XpLmECBNMLvzfjWdb8LjTHCnZQ635G_R1bQroIOWm37SPM2DylHSLYZbKnQKKsuv23_xjJVhNO6c0ckOUtiNu1Vldzt8eCk_tlCGqAElxOf08XabdKQTCA9FY5MhjzrThqglJe-osh_ubRsurjXLhLdZYZtoyiaRomk9t_o4KC2sQRAsRyyy_mdeQSCYGcfDlZVZ9f5m8I-P7PENIe3Mylxg-7pBMTdwhpcu5YLXXTc--05B1bvam-sP1_DpBEkq_pB8MkxbuSsM0CcRVMOE8NNXzeN6hHOoNnj9LD_Kz2HNVtjLaOZmNpcxVUTWQTf-eFR9zythU2Ndznu9CYmseJQEUhfq8IA-j7zYYg5JTgAbJbNSAhSE9l2tFhnZwE7Zrarzv","summary":[{"type":"summary_text","text":"**Evaluating message dispatch and error handling**"},{"type":"summary_text","text":"**Identifying listener shutdown gap and dispatch behavior**"},{"type":"summary_text","text":"**Detecting outdated test setups and missing OMP proof**"}]},{"id":"rs_0ca4af36969c80fa016a5702de052c8198bbad6084434f3461","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwLntsc8o-J0ABOE-Lli18OI4hrcDIYas8vZ1-vqL2s4xfPPHcz-fWkWFc4p5iwu37T6JXLQ8pP3Jn8p50u8omV7zwZxoTfvek-vQzx3lAwFH-N9HwvcQMhPUqJxG7perq6rUXUHL-VVkD2xoU1zw3rZ0zNS3bTcUH_yMWgTvFoDT647kcxFjiUcjXWeanEM3BnXX91BDwb8fyPICbqrz20laQDVV7aZhKpGcrZa9Ch6g683pwVKRO9OJteTE5VYJ3XSL8nQlpS0dLG5ID8uwyFza1E8fA4FBRyxZmiQyBr-SKybX_snpb-nuMfeRix9Px0X_mh-nh3dLHjv87XAqe4mtZWaTADYUi7BXgsgABR8YewqWJeSlivSm6OGFmMIeJu52vUOr7UCO7FwFTb6_kIle8pX2DhC6LdqjTRkwrLHnOekehL6h-ROe6xfHN62pwmReJcB6OmsKBSLRNO3sJxkuUZj0AhdltzL_08r7H6oV8YfSQktR9-tkgm5kfyTyI7x5y2Uevg5UpRnVdydjqPxaIvehB63Oi5fqYBsGEx78wULscmAtggqR-3PCH2fd42Kk8Wh-8UQ5gYOusVOJjt3ZmIhgt0N8vBhl809oOcZb7ULA3AkDiDFrNrC4B3KYZ13qkwPL4owQno_gN8oz1OXS-7vwl8E5ILAgLG2JACuE_g-WjWWqOeU8WFr7zSnttjubzDmnKJa63NiBqu7xvK7PV5EQeR0mUIl0Q-AnuHVdiHTOVwZ1UEw2txXG2sXjc0uDZ9bvDLZ31Z56Sc3NHhd01RpfGVvb-qAEomaAUV7UJ_MK0zDCUiJ4wdAJ38bV0wpEWDiIYFkM6a1F5NKDjFnABMkx8SYDnLFUP_u6xRHKX4rG4h-4mhbPcxTDuwuNlz4K-TJpNSJKY6zKM0gurt5yd4iIcRVZQDT30JBWur7gj5B0ELpWIAuVua4BBdPVWy85WyfHXIDPAgq_KHjy-DgQLkmqQJLR0b8lQhHGz_LilMKhJZ0R6hdHZqAhzCSiNviTbHACNS3J8sLvVREoLiskNFtiU1-Cki9nMMLzqeXU8gPFaQWUXCktwG8WkI22mGMPgLY8Oo1v9Tab8trDXUGP3efzzFyHLE7DINjNTl51GMo8c7DOqN_gbwSpSpceCJxVtGHf0dUYHWWyq-bnMXiG_q14RwZrfWf3XOMHh1jqvgAYOuUS9Yw5vIRQXs0dGrZg_4OqGxIPISgUwdiEJJdciZh4hawZvXLiJKb644xeBB6KMcXHxnZ4Clbg4O8dAUPult01N96aI2nBBE7gomme3Rh-6dfrn70G1wXz1EOadVYX3OuEEd84fKScVD6dsxjNXc1oNeWlJVbvkTmZCQJKhnzZP8ianLmfpBXOH0IRzBfT0GT8ScHvZgT8fvLVY1kwuE9pLRIERTXBlTY_SohqmEVJ54gBbYolHcYQoAo5MNxIY6RcYN8YRAzOOdju063yhaSgnaNXh1kA3yuiRB9RuvAZ5z-BPnT5802wLu8shlIBcMkG5zwU2ajt_mj5S3OJDs-FgCbqLn1AFGLU2HCfmqY4hZMvXdtKjPFS7YpsJ9XQl2siJ6n_V9KpVZZwpjNbERYY3KfibU2oqW9NjtyskL0MpEHTFnUMypPl9oQ919iAva4H_vjkFnyZjjHc8QA87l49xclhBwdTZFQ1yuq4OyBJyjKmhw8XIMDexjMxtZyJDAvek6FUdNlbZPOH-rw640VBYS2zpktI0IY2AEdBaajXhgS9QQfUNbnsBaBJwPvDJogZfdCxA9YLKCVka-Uqbz2Kk5lod48kUVEvqPJsIO4u2axnEq_zF6tRUHVY-u6luV88t5XGsrYfiLK1lKS2-A2unlRz-IYCGQBffmMrrF5zA64zzRjCBgwbsGfw6FQq-SIMd4XcT4nRs82ZtrTsqLNAon524xz8KTWPlZbfJq8Vz7p0MvQgEC4x_KyEy_i54WHbPGdaR7LpaWlRUHPiUjsrJQIfBdZanDp04HS3YMp1kUKeoLD54GcrH6n5iKzT7f2OnNkLsARAo8fTKuia-8wM_1GQf_9yym4ygNDyNlSvLoyYBkYXQBzQnQRHCAjjlRYD330yzvHeug0FK6P887kUbMaAtIlUTyB01S4VQZzMUjylM4HysrCKj2MZB3_CSXnV64YMK86IeRa_7SnCqcsBhshy0RvqZOjdsBv2nCqiGVTplUJpj3sRxyZBDycTEtt2EVItenDdJjzdEVi45-DepwlKLhXVaeL_-PwI9tnjxtnoj4eaxJh44cVTSE11Ts7kfSNCHPdR8gKND6MgvnCtHf62YSwUcQIHpt-3FobtZEo5QWN5oq-cz7vUhpE_-P8pOEOdiTkLy_Hd0K4gZfG0ftU0EMveLrzxCw4BYwyJsJ4eJXv8zQKwgcH8qn5jTVuxaSSYjL8-YkdFbpRFz7PEheIpMnSpoJu2XeTLZ_Pgr3qpRacGLGeuIMhEmXwvjki-5UWZP3DgvGti9dKrYmtiKtSAyZMl4a3bAzZGZsSqupQu-fUKYykxQVMKEe75zgX9-l6fVxqavzufmjhQCCrCrWJp9-J0wghvww0LEvflt4Z-Lku91BcgCdsdHq6-Om2ghw1mxvmkkH1jsv0U9ohNSpQbskPf7tu3jRLxH5CSHUVKSpn7aAZhPLcvqjvDhMvyMbn2ytILAPBZP9Kwr2AXSh_LBlpCHmlnd3rNG964_nUN1seGLVeZDijmEFEpAn7q-9mxVQfeAgWzfm3gfxZYMIVcUDgfKDqwz6A_Vou6JznJM9-5ZqvzRFat7OHJIlW6dHhF5UbodRZosmpNDflZus0RjeN2FVLOt3L9_rZ7nYfgugk_l2TTFUvdDUGFXMvhzebXLUD8ZE1KlyYitn8XGh8qNUeKWu-VdYLChFjXIxf3WMboB2QRl1IrRs95Ltd8ypNMsrMRFs1aihg3zVxDwTHIqYr0-qBmX0NAcwGxcG1G_FoV25uwB8IaIFDaJh9uxRD4EnA_MRT2U-gf7a6i68E5UzSOE_jBjgScot5uj3MqyhjRRpTgVsndyiqEo7LaIRRxOXH5jhVkFDNR7HxkNFyIi-pNz1oKi_TOz21mnCOZX7nRmOtY7Z6EwjfZwFAbuacUuUm11gTzsHMqD80vx-aQr4R6dktSKC6-z94w7sD3KJS27QlNVmOoDsvdmJUzE6MSIYYBxigpliRjKzkC3a5vPs-klLjLevvapO1sNfd8UO4N-AtAeQahGeek7uUhVjngXMp3_wy-g70sa-o5AmZhW1c3FKm6MxYD15FhYeKCGJVd3zW8T10ZmR6DDCbmMBGRwrKsB9OR0wc0mzWkSruLl1XcKTRP8Dhuq_vaKOGxIEikjl0C-elMc6itzbE4QiVgrWxEy0tZz-_jMhe5D6F-hhBTQtDiTQucDA9sPutjeRTb7loXA3dA3ObXiTTUV7X1pWQrthcqvou1FZN1oo8Tp5m4zT-v7M_HbwJQFj9KoFcCX5Txq9xlI4TeVJKIGr8I8fQt4ke74BdWY_w3W5SxJzJ7hbO5FTOinERAKR7Udh8-gsH_ADOqtxzcO6LPjoIdkmSFBNhyNSGPxpJPHhku7DeNvt-wXowYkKWr-kOd-sqtMG3TrKEddG369FrnaXJGCggSLsH5QPyR3uX5aGaD2okup_ZSFjyEnRNkaVR5egc3rO2WlduQivCYJJphQZUsguglKEFiOjr5iFQ8Yg4qOgvfDhd1qVrh55wfWUreKJSngs7zYU7nr8kL1fKC7LF2Crr8Ijey00cnLEdeH0rYstTEYv-Ps3nh0AoYNcya8IX67Z-UGgjDVxdZcT-4I_EHAhcKEYcQ_uNZ_K0eUFbs_OsARkDERezyCLzag6-EtegPdBtP_5RAISWoivdsui886UiPia0rrbSMzWsyJMWJnz2s7rCbQVPGCnw-5nECPTHuqNblANSdL8=","summary":[{"type":"summary_text","text":"**Assessing requirement registry validity**"},{"type":"summary_text","text":"**Identifying outdated and conflicting requirements**"},{"type":"summary_text","text":"**Flagging stale manifest hook section**"}]},{"id":"rs_0ca4af36969c80fa016a5702e767a881989523c6b9fff8ec56","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwLwdGfIHAAPEjLoOXTwcWhAOB6_DI5pCc8gviHqnGAm1-rR-Ed86c49lQ4n0wUwG3uYmcDfVpxVxKiG9kK2BvhfKfasnzhO5vsH4bd6kKwzNl_ntUF7y1Awp9pK1YDCEE-fccIL6TnUi_9Wmi_tXXvoKwKTT8hWK885yz1ChaGzzQpSKeL1iNRtguUjgE38u4ntRlnNTrOy3vpcp86tog6UhccZ8hOz3di2uUXhaoJvSXL2sOQiWXm33vilWYikaFURHGbe2LSG_QvVqTjzMWna0WIA6Sk1AQcYEayWUIbZiK9Xja_e_Ya5LN6WuKwoCRE_jlrHZl2sx69GGRy7HgK0T0t2fFCKhZ39-FCG-qdph69y8htrRmdyUFkz4jcqDhH0HPK0NJ-bV8c1Vb-eHG7dqkRpp-2a7DRJRbae4NtOJioxt_ybzL74P8VP63W_GVrSF3lyMhAD_jQyzPdQLeqPM512H20p4ybTZVdtzm090cnL6qq3LOf4N-t7VSaHa_yFtBhVKyZN8xABGhZT8Gcg9rfuKHVljhxR4B5yKv5uWV0th-QvQXRa2Ob-OU7Eo22RQg3Am2Nr9OeR2r4m5i2uq22oAlN-xWXQhQ22cOVSaTKuIRTxTqGd_yRFdVz_dRaJYaOAdeOduvwQjDU02BkITROyRLAPhYGocHO2SyJnX0a-hLqTprxT4HeWfNDqWj68jdTMeyY775CZ3nOn-xZuiONruiF8dzfSuSkrJkIqQHoUbMIMStXbr4QrjaW3-wucFVgMS19uE2X2xevUGOGh8YHBOrobowe_bpQehwNVRap1uHPXTSOHSimYaO72YidujGZpuno4C5uG4Pn5Y0h4Yx8PzTQd7zZNE7ogEDMoSPTl830YzEhMmaiwWeAxIsPfnaR96Nnp5_qS_fPlYjAwRa6WLmpwwEllw_34Rzv3UEmt2kjy0QlbKbFfJfoOwEBzAQQY3d4ZVtWQpCckSx8TN9RwZfUSvUNkAEZ8iNr9hrGYoRv1Xn4EA0rPP5Ui-_JgS__7AX9ihB6P_lvnHOaT91P7D6laR_05mMAoeGYu3nNe_LhA7gjE-EYKI181ozDZ0OinPx5yaR87QPtGriN4LhXPWhfdwSoKZVoj9Ti7q-H-OiQF-FWcmJ-yrVPGl5EbdZ2swhq6ZTP5bc_XJ3AU3hV2_D7pbWLC-eJITZ2R_D_U7alSTCxWe9JBAbGARwbiscLc7PZCy___0z5qjm46hxTuZMI5hcJrgkXqvNp8cyoITarWCD5JaC1n9_2JVXQ-Y64dZkyA22wcXNKvIISmGyF7olLJj2nI3RmroJMOZqopNn_KxGixljEE-t3mgqdtFaAKycjT9sjzhGHFvtFQNWnRWWhV75ANZTUCG0-3P-cmjkVdfLTQwZiI51GeqdtRts-xGsjS8P_3xoaYj0uCHyb9WOlZQNitOuvnVPBlkmSPeRRpDzRaF-Lh5MPMpXsLKCSxYYyrOR_NU2mRWqPVN_im5clS_bsi3NllK_zNx8jGgrns7h9pRXlHbwXmLve6FoNfYredZgIBzhUEp19HtC911MBM16km1h9RKaGE-bljIo1b9vCANYzLebWWNmHZBmWzUm1vBry_irjrvPQ3l1iPqDVAsldQxKmo1wpS1nuwz8DDbUGHXoqm3BJGjOziybXRDvzsTdynI7TV_23VE3UMLeSCRUXjmfs1LVLob8Wv9tUiTnnbMtgYWExUde4w3Jsh8zqfPxKSMxOOkT2FZdlW7Q9SwiwQbkELXeGccSh7q1C6iOUBCCvFgX3bGrWt0j6jlH-2gbJYz06RnilDDU3uXLsygwakdQAabP61-SA00YA6xlXasQQA4ZoIBQnIRi_f9fRHGETvsDRthFDtqoMllPZtXODhD9Mfxe-9gZTUywyRTqeIEb6BQmhXscexkhHUDl9_QJaGPZfroAExTgwNnAgjjLVJ3pJO9qxYK9WXMQmJNTiujAmg164_kZa7ANr2Qd4NvKNbWTcvCuxJ0gOMkSTQ5vkNOqXxa3Nzckc_i24mAdBuWyriBiulrzhaNZ0vyX29cnnyMvbe9loAs9Wh4lomel_yxro8OMh36-7EnomXaGK4Ux2vPu7ohq0wI3xh_M5-wBanVsuTk4-YxbxJ9wHUcL0qWlj6r-lxb7n8C1t-5TG7C5zMD1yTLbA9saSGc92UBSrC2ZnZduFgmDmYAVTHeQqCh5zkGG2F52WdrUxdmXqGYtHn8Pse_5glypgJZm44nFn-1AR4VtMjsSXD-1F5JQAtdB9fK7R_tH3piFE_aEZ3INw4CCiT7gX_jGWRQlywFPK5sdj7ZrY2E3my7R1bDUBnVD6VKwqUSFR6TWhwxm-y2ilZetKWHmjznOZa4YqjvI8S7bsGftmMu-0jE0bAwFhNnEpxhoXgfpUD9cPg9VnxAaCBDDqD8b5fj6-z5kWYGc9iD7LHcOCXUdlIEbuX-2BOD1xtlW-s6Mwt06acnqGCk617tdurd9FVJeh4lKL2O_4UkJi_R-n1cNI7iZblEBQd768CFBYRFy1j4JFYAzT9FONWbfrSIrbPtwyyDvcCwb4RM303VBythMzMdqQiv5-otDiKb4rtRS9rGFXOqWdChehMZkDNd7q-oyJs1OePzyYTyG_7zoZN-q7mXaTAuQzChdkkYK6OZULJrXG6OJxUU0X8hPjh8lXzUf-p0AyiJ4YMgUVqEpWTqXjmOhWHxiEStCgM2tP0OHjR9iRdtg3T4bbys23hiZv9iu2m9ZvYCods-DrnOT9R_1hFyYY35OqCR267nj1hTO9nqNBa2TECVo-U7yy55KMI7eYK3ZhDPxGWi-nODdaM1ND-3gHSUPsRUqdWsoLKvQ_0yNi2Y0lESpqNAne3LKQg1judb9dtAeTc2EIwrc0HdQEstYkn4mwzbohDauVm_jGEr353d0sivYOa_y0Dv5Pa6xdqzuqT2-szp_Faw1UUHK5Eq17QpG4JcFsgSR2jU9IpmIRNeHG9LVU2S2oRq5Dpvqqx6s9DLVNY9K12eL5Q-U5OyR2EFRbvvbdi1pEQtxjMxQqHsfzj8HaAv4skbX6unjOsoVtkofuRnUgc2sKq5XI0qqEufJ3-m4cdPO2kXbcmyOqntP8iU4vA-DYu2kAuifdpQ2hGX0eNj62ikpqkKldcIg67KfPBdMM4e_joLmRpy6RGGtHw1NZusOts4Lp1teJckt1fmRMKKLlnZl9JrBxuP5Vadx45WA8xhJTSs9j8U0-S6hahT_4ih4Z-hSB2sKkgB8yfFJrFFK629bfng_i3H3fwVTJPG9yE8SRfklV8pQYdvJCD8mDiApb20KUZs2lRMLy86MPlqfChtnX0zoUW0xSMR57Jh2wJEVDvq43-sANug9HD93Ke4Cl4RXomQx21LTx-edmr9go46fvkrl4mjn-rtWZ4YJ6G-PORPAhmQEIfdu3taD6fQ2Q_uzFZYKbeq5-VS3HSmepRqrdSfZt17V18k7nNGJH206Hfi-OwhVLYyyQufZ7B1OE9iTmq8DOOeqvXuavBpGALh8-CQp72MKNxlx76Mog-ZirSrS8ODvCB_YxrfZs-dFGPUBR7QTAwQ9_XuvADd8txHbVWESgj2p8n09SIfak62Iy49MtSRwD5kzH5b9kow9Lynt_lf4jq-gjccveY4gm53Jt3gbUtLaaT_XQ46A4oh0c9pqzS_ryxQVbXhQuWx1mRiGz24LYSqzgEAi5b8YclEkpZ_t0XTosKsGGDNArS1imM4d1L8mymCB9eTzAO70YPHZIQatkcBxWh_KYEaIe06CuZQQ==","summary":[{"type":"summary_text","text":"**Clarifying OMP session conventions**"},{"type":"summary_text","text":"**Planning comprehensive glossary updates**"}]},{"id":"rs_0ca4af36969c80fa016a5702f080dc8198bc169a8bd346d3c0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwL3sTHwcBWKYlA9zpo7huj6uBFibEkmeVRDSeDJmOisqQ-k-Hl3ULcLHWjzHnuuKT9YoK8QEr4UASK1zlGasqi3AR_h5hABbys099_75zmHr-kKKGkuKlYWTnFu683xQfeW2dMnv1FJEkbFm0uwpl2qkwhg7O6z9kFs7ervuOkr_SFMhaM_4rP2MeSbGOxrn5YiierI0Jg7GfNruNkC6wO9Hbn6okcsJdg8jr9FlqpSvSacToaUR5k7GQSdRCB7fM77lZwmpKMwj5suJH_ChnCOe95vMq4chmYLL-st0LnWAMvJR9CB2xvDlKRuQCVQ52mSVy75cNJ_43vQYgfpL1gn7NzDDyfW7dz8HVCOOavXiEtq_teiXX8BdPmDBJmYxpsqNu77tPf2kWRkcEc8WGCgdoDfWXs0jm7nHvb41vOM3be9Uj8RTQXeUC3m6UzIvCZKuux04MobhD3xRK7cC-q2IFoMRAb6eE2xJwiAKoW0vT0kloTHl3fXpneMa96OAy-pWpfr2Q-3QlmlQIC2uQL6Ifk0qr2kFgBTH6G8U7FxclxoG-uNOfSPypD7pNenC-vcyUJqVEKMEYzeGbaJX_WdrOJ1fTRaPOnUQDJ14BixBeE6S-_j3lFCOY7B81Yt-xD7iglfOommHGH2rJsPukF6z4whJhOC2a1YhGdv6iKdB_FUUG83Wivd1YEnkUOiYQq-9ZbomwKXtmNEYy81Uqr7EEspGUCR92c4q7oYcPFOGaVlDiSZoGKMXeCpi7nlK6keVdSUTkxqwTS8WmdPrGrDBxXSYX8qJPEuowEl-CQfHgLnbGf5PVRcVjNJHCFqTqvXdwMbBEHv7RY_j1H0VCfPb-yEFukeM1Su6p7pSlX5SjMIE53ZV56Gr81AJgYZG5ntCCG7qJdAAg_0n4Ulr9kNIw6qP0_iFyR57jXbSNKMy7Og2-ESZSfcUOQMSReS4cXAz_V2OZ2QUFucJqrfclW-bIgsdichHz0x9Mdkp4XRgr9HHKSaaENoAxEQjAhySrEqpnNs1LIR3v3y45AfyLVUEFjQ8SDbN0kPs01aWwrlWfVM-rPE4O8S0KDx0Ixj5g1Ovc3leMBHMqXMZEoLCNqszHwRMF6ebVGl5kkE55za-Hw29FhPxb9wuqqWNr-GwWlxaI__tNJNFXFcx4E0UkkXvpYST80qpy8HKP_nmFrtsRFtVzurePFtdpeWK0L2xL5LuCuyy_t2vUZsILyPUg9d8xu2fE3Lz1nO3KiH88SnrmGZYnt6_-Ply58fA6iQalt9oAkeb5-qaA81dwuQtNPLXozbCnA3bkiTnUb02quH6luDsIMfF7BQWnVAj7xXzD07TIgmaW6SmJx2eIIFuGYaTrrc25dW3JHdfxf9lHJdkIpMg1wthsHvnaSpiNDd1M2vXErqaIW6p2rZNaGWGW0i0IhntSWa-xTDQYogBAJTq7S6D2pMpYe3cMaANNaCdc2DN2zVGdCZMo_tFKVjP16kRpguwjlQ12eXSE41v8vP7Q4EsrG6cmKqVdPQBiPsvbGzqpjvblorwsCL8u-QbIzsKPawiRAK5dCyAdl7QQKVA6SGO222xUOmFyP5pKdNHDye-lgHzmxB5jisAIFS_uLrWfms86vsBydAWGtI0DB5i9DeCoYq1w9-yvvdWa5vGd4rih2-JfnMAdwBhXaMWj63n6qaSdzsyYDj3OB4CvLxJT68iRmhmAxiO4FfqQbMNmJa4fD1WnhpMY_p2KcjP8-iHHnCBoel_KOeQv_eNooY_jX22SBnQWXeUGAX1HBPxbOFPXlFgiTu-tIpj6oPrjGDHdn3jErrsSfE7wIpJpD3Tuhc2TRmzMJr32PlrdC28rLKq8Jxl_Fq6ueP4Nwaxo4G_IE8tbgzWbGFhtmKmIHuSUHBFWYdMuO3DJmUitLs9lfCFyyJ14-YfrOtIj_Oa8ca4WY8n5arpcYTt8NLjlw394SnUKbQcXsl_-Pcf1_uQ4f89I9xDJUqYd0ntpR5R--BK1WedaTINVahsJ2TILGRjsCiHtOBRz-ZlKO-7SjmZ2MiwLgtoQv4s_cb36vbMNO3z41Tx-ZPKvsyHgDAcmtY7Bq6vmstI837nxpoKx6tg8VkIly4K1bVtBYbLeL-F_BDeogzjC0a8mWQ5RKOHn3lSkrIaMOlqyKB5MnbH2tY6SiZh3HUvtX1CdwhaGr93jRw5nhU9ndu0z4CmfhgFv3_tFkq514jshkqEpYhdw_TMAS5bNnh2DXG8THRhYwjLe6XR-kNo_ojm3B-0nvjiyz3ipLSaEYMTYQF2KjDYQEDMXkZNj4IuPXnIdr8fHFRN0txWDyi9QVNiDHcTWy42rdM2A1Y1FbR2vA1lCF5Pt-V-1w2z9Hbnu2pCUzKHxtmcTywvA==","summary":[{"type":"summary_text","text":"**Marking partial and resolved implementations**"},{"type":"summary_text","text":"**Assessing OMP extension scope and floor version**"}]},{"id":"fc_0ca4af36969c80fa016a5702f71c348198a1efd391167e8356","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading identity and env seams\",\"path\":\"adapter/omp-spt.toml\",\"selector\":\"588-640,790-905,910-1010\"}","call_id":"call_qUPrf0CiH85W8xtjIW3M4Mxq","name":"read"}]},"duration":46115.1716,"ttft":1683.4439000000712,"contextSnapshot":{"promptTokens":217779,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qUPrf0CiH85W8xtjIW3M4Mxq|fc_0ca4af36969c80fa016a5702f71c348198a1efd391167e8356","toolName":"read","startedAt":"2026-07-15T03:48:07.442Z","args":{"path":"adapter/omp-spt.toml"},"intent":"Reading identity and env seams"},"id":"3ff83814","parentId":"d026c79f","timestamp":"2026-07-15T03:48:07.442Z"}
{"type":"message","id":"847d5e63","parentId":"3ff83814","timestamp":"2026-07-15T03:48:07.448Z","message":{"role":"toolResult","toolCallId":"call_qUPrf0CiH85W8xtjIW3M4Mxq|fc_0ca4af36969c80fa016a5702f71c348198a1efd391167e8356","toolName":"read","content":[{"type":"text","text":"[hooks.PostToolUse]\n…\n# ── [identity] ─────────────────────────────────────────────────────────────────────────────\n[identity]\n# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).\nsession_id_source = \"post_spawn\"\n# Process-tree anchor when session_id is absent: the omp / omp-spt bridge binary.\nparent_ancestor_name = \"omp\"\n\n# ── [session] ──────────────────────────────────────────────────────────────────────────────\n# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,\n# not api verbs — \"there is deliberately no api commune\"). The agent writes `<endpoint_id>-commune.md`\n# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's\n# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;\n# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session\n# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.\n# /omps:commune writes the commune file here; /omps:signoff's graceful path is `endpoint shutdown`\n# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]\n[session]\ncommune_dir = \".claude\"\nsignoff_dir = \".claude\"\n\n# ── [session.psyche_init] — LIVE-CAPABILITY GATE ONLY (v0.14.0 F-030: never spawned) ───────────────\n# base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17): the Psyche-spawn seam lives\n# in the BASE manifest, not a `:live` profile. ready-vs-live is the COMMAND, not the profile —\n# `/omps:live` (`spt api listen`) stamps state=live_agent → the daemon livehost hosts the Psyche; the\n# livehost checks psyche_init on the resolved manifest. `/omps:ready` stamps state=ready_agent →\n# livehost SKIPS the Psyche regardless (livehost.rs:282 gate).\n#\n# EPHEMERAL MODEL (v0.14.0): psyche_init is NO LONGER A SPAWN PATH. The daemon reads it ONLY as the\n# live-capability gate (`manifest.session.psyche_init.as_ref()`, lifecycle.rs:365 — its presence\n# decides \"this endpoint hosts a psyche\"). Every actual turn — including the FreshWithPreload first\n# turn — is driven through [session.psyche_resume] below (one role, two key shapes). This table is\n# KEPT DECLARED + noop-safe: it mirrors psyche_resume's command shape, so an accidental spawn (a\n# partial apply / a stray driver) runs one safe ephemeral turn rather than misbehaving. [impl->REQ-SKILL-LIVE]\n# NO cwd TEMPLATE (v0.14.2): `{psyche_dir}` is a RETIRED resident-era key the F-030 fill catalog\n# never carried (doyle P-1 verdict 2026-07-06, runtime.rs:65 \"a catalog key must have a real fill\";\n# the freeze fills exactly {id, session_id, parent_session_id, psyche_context_file, node, subnet}).\n# Referencing it faulted EVERY per-event turn at substitution (flynn's dead psyche). The psyche cwd\n# is now the SHIM's job: it chdirs to the {psyche_context_file} parent dir (the psyche's nested\n# perch dir — the same location the retired fill pointed at) before spawning claude, keeping the\n# sid+cwd transcript-resolution pair stable across turns. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# S4 (omp-spt): the psyche shim is `psyche-omp` (psyche_omp.rs) — same frozen turn contract,\n# omp-flavored: continuity keys on a private --session-dir (omp cannot adopt a minted sid), the\n# session-gone reseed (exit 95) is a shim-side dir check (omp -c silently degrades, verified), and\n# the composed prompt rides an @file (omp -p ignores a stdin-only prompt, verified).\n[session.psyche_init]\ncommand = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n# Identity-env scrub (F-028 C2): a spawned psyche must never inherit its parent's identity env —\n# inherited OWL_SESSION_ID/SPT_AGENT_ID make the psyche's hooks resolve \"self\" to the PARENT and\n# rebind THAT perch. Kept on the gate table too (defence-in-depth if ever spawned).\n# [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\nenv_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]\nkeys = [\"id\", \"session_id\", \"psyche_context_file\"]\n\n…\n# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n…\n[history]\n…\n# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value\n# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of\n# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:\n# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would\n# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a\n# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both\n# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would\n# false-positive \"update spt\" on every normal seed. The case is narrow + transient (only bites a node\n# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.\n[env.SPT_ENDPOINT_ID]\ndirection = \"inject\"\nvalue = \"{id}\"\n\n# ── [env.SPT_INJECT_VERIFY_ECHO] — Layer-2 echo-verify opt-in (W6 LIFECYCLE-TRUTH) ───────────\n# doyle W6-DISPATCH-RULING item 3: W5-A shipped the broker seam (SPT_INJECT_VERIFY_ECHO\n# declared-capability, broker default OFF). CC's TUI re-renders typed text, so an inject that\n# echoes back is a VALID verification belt — a CC-hosted session opts IN by declaring the flag =1.\n# This is adapter DATA (the ON switch), NOT a spt-core requirement (per the adapter-glue boundary):\n# the broker reads the injected capability at spawn and arms echo-verify for the session. Inert +\n# forward-safe on cores below the W5-A seam (an unknown env var the session ignores; the broker's\n# default stays OFF), so the floor is NOT bumped here — the counter-49 floor bump is doyle's\n# coordinated ping. Evidence is unit-side (tests/manifest-shortcut.sh — .toml is scan-invisible):\n# REQ-INJECT-VERIFY-ECHO.\n[env.SPT_INJECT_VERIFY_ECHO]\ndirection = \"inject\"\nvalue = \"1\"\n\n# ── [inject] ───────────────────────────────────────────────────────────────────────────────\n[inject]\n# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).\n# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.\nactivity = [\"hook\"]\nidle = [\"hook\"]\n\n# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─\n# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary\n# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>\n# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input\n# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the\n# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery\n# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.\n#\n# `command` = the program+args spt-core spawns (spt-core v0.16.0 seam; `path` deprecated, exactly one\n# of {path,command}). `{adapter_dir}` substitutes the install dir (survives updates), so the filter is\n# now the `translate` subcommand of the ONE consolidated claude-spt binary — D3 fold, ADR-0006 (was\n# the standalone cc-spt-idle-translate; protocol unchanged). The subcommand emits the operator-specified\n# choreography per message — ctrl+s (stash draft) · 50ms · <envelope> · 50ms · {\"key\":\"enter\"} (submit)\n# · {\"commit\":true} (terminate sequence) — so an inbound message never clobbers a half-typed draft (CC\n# AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke). TWO distinct signals\n# (doyle-confirmed from broker.rs): (1) the discrete {\"key\":\"enter\"} submits the PTY line (a verbatim \\r\n# byte does NOT submit a CC message — corrected 2026-06-23); the binary still neutralizes the envelope's\n# internal CR/LF since spt-core applies {\"text\"} VERBATIM (broker.rs:1016-1017). (2) {\"commit\":true} is\n# the MANDATORY inject-sequence terminator: run_inject_worker (broker.rs:1075-1090) ends a sequence ONLY\n# on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE. Satisfies\n# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).\n[message-idle-translation-binary]\ncommand = \"{adapter_dir}/omp-spt translate\"\n\n# ── [digest] ───────────────────────────────────────────────────────────────────────────────\n# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the\n# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,\n# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible\n# divergence from the deliberately log-less mock adapter, which declares no [digest].)\n#\n# FETCHER strategy (spt-core v0.19.0, REQ-DIGEST-FETCHER-STRATEGY): the extractor LOCATES + reads +\n# emits itself; spt-core only runs it bounded and consumes stdout — no locate, no pre-read, no\n# `source` (ignored under fetcher, so it is dropped). This is the designed home for CC's PARTITIONED\n# layout: transcripts live at `<config-root>/projects/<cwd-slug>/<session_id>.jsonl` where the\n# cwd-slug is a CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays\n# harness-agnostic — by design there is no {cwd_slug}), so the adapter owns the locate\n# (owlery::claude_projects_root pattern — search the projects root for `<session_id>.jsonl`).\n# spt-core feeds only harness-NEUTRAL fills: {session_id} + the captured {CLAUDE_CONFIG_DIR}\n# read-var (below). `--config-dir` receives the CAPTURED per-session value — ccs sessions resolve\n# their relocated `~/.ccs/instances/<account>/.claude` tree, base sessions the `~/.claude` fallback —\n# closing the \"not expressible as a static catalog path\" gap in the daemon's own context (the old\n# env-preference branch only worked when the extractor inherited the session's env). The extractor\n# (`claude-spt digest`, ADR-0006/U2) ships in the spt-core-conducted layer; this is its declaration.\n# [impl->REQ-DIST-DIGEST-FETCHER]\n# ── S3 (omp-spt): the extractor is `digest-omp`, not the CC `digest` ───────────────────────────\n# The extractor is `claude-spt digest-omp` (digest_omp.rs): it maps oh-my-pi's native session JSONL\n# (~/.omp/agent/sessions/<dir-encoded>/<ts>_<sessionId>.jsonl) to the SAME digest-record contract as\n# the CC `digest` (shared `rec()` emitter). It LOCATES the file by {session_id} under the omp\n# sessions root, so no {CLAUDE_CONFIG_DIR} read-var is needed (the CC-lineage comment above and\n# [env.CLAUDE_CONFIG_DIR] below are inert for omp). Proven with `spt adapter digest-proof omp-spt\n# --sample <omp-session.jsonl>`. [impl->REQ-DIST-DIGEST-EXTRACTOR]\n[digest]\nstrategy = \"fetcher\"\nextractor = \"omp-spt digest-omp --session {session_id}\"\nwindow_turns = 6\narg_truncation = 240\nsprint_collapse = true\n\n# ── [env.CLAUDE_CONFIG_DIR] — the read-direction capture (spt-core v0.19.0) ─────────────────────\n# direction=\"read\": spt-core CAPTURES this var from the session's environment at bind time into\n# info.json.read_env, then offers it as a fill_template key ({CLAUDE_CONFIG_DIR}) to on-demand\n# commands — the [digest] fetcher extractor above. `value` is the FALLBACK when the session env\n# lacks it (a plain `claude` launch), tilde-expanded spt-core-side: `~/.claude` = CC's default\n# state root. A ccs-launched session carries CLAUDE_CONFIG_DIR=~/.ccs/instances/<account>/.claude\n# in its env → captured → the extractor locates the relocated projects tree with zero profile\n# overlay (the NO-[profiles.ccs.digest]-leaf design, now daemon-context-correct).\n# [impl->REQ-DIST-DIGEST-FETCHER]\n[env.CLAUDE_CONFIG_DIR]\ndirection = \"read\"\nvalue = \"~/.claude\"\n\n# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────\n# S4 (omp-spt): NO shipped profile overlays. claude-spt's `ccs` profile (the Claude Code\n# model/billing multiplexer, kaitranntt/ccs) was REMOVED here — it swapped CC's launch binary,\n# which is meaningless for an omp bringup; omp's own model/billing routing is native config\n# (~/.omp/agent/models.yml — see docs/OMP-OPENROUTER.md, S5) and per-endpoint isolation is\n# `--profile <name>` forwarded to omp through the bridge's `--` passthrough. Node-local user\n# profiles are still created with `spt adapter create-profile` and live in the registry, never\n# in this manifest.\n#\n# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A, inherited). The LiveAgent\n# seam `[session.psyche_init]` lives in the BASE manifest above — base is live-capable, and\n…\n# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in\n# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.\n[strings]\nadapter_label = \"oh-my-pi (spt)\"\n# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's\n# hooks.json + dispatch.sh go static-forever. `{adapter_dir}` is lazily substituted at\n# `spt adapter get-string` read time (spt-core v0.16.0) to the absolute install dir, so the value\n# resolves to `<install_dir>/claude-spt hook` — the consolidated binary's hook subcommand, which\n# survives adapter updates. dispatch.sh get-strings this ONCE per session (caches it in\n# $SPTC_HOOK_BIN), then runs `<resolved> <CCEventName> --host-pid <pid>` per hook, piping the CC hook\n# payload on stdin. Session-scoped keys are NOT available here (get-string is adapter-static) — the\n# binary reads session_id/prompt/tool_input from stdin. [impl->REQ-DIST-HOOK-BINARY]\n#\n# v0.9.1 FIX: the value is the bare binary PATH (no trailing ` hook`). dispatch.sh appends the `hook`\n# subcommand as a literal and caches this path QUOTED in $CLAUDE_ENV_FILE. The v0.9.0 value\n# (\"{adapter_dir}/claude-spt hook\", a space before `hook`) was cached unquoted → CC sources the env\n# file per Bash call → `SPTC_HOOK_BIN=/path/claude-spt hook` parsed as `VAR=val cmd` → ran `hook`\n# (`hook: command not found` on every Bash command) AND dropped the value. dispatch.sh also strips a\n# trailing ` hook` defensively, so it stays compatible with an older manifest mid-update.\nhook_cmd = \"{adapter_dir}/omp-spt\"\n\n[strings.notice]\nnot_installed = \"spt-core not found on PATH — run /omps:setup to install it.\"\n\n[strings.skills]\n# Operative UPS-injection skill bodies. File-backed values (`{ file = \"skills/<x>.md\" }`) live in the\n# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,\n# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the\n# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until\n# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.\n# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`\n# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The\n# \"who am i\" hint below points at the core CLI. (docs/PARITY.md.)\nsetup = { file = \"skills/setup.md\" }\nready = { file = \"skills/ready.md\" }\nversion = { file = \"skills/version.md\" }\nlist-agents = { file = \"skills/list-agents.md\" } # -> spt endpoint list (the roster)\nforce-stop = { file = \"skills/force-stop.md\" } # -> spt endpoint shutdown (graceful, tears down Psyche)\nsubnet = { file = \"skills/subnet.md\" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET\n# `send` / `commune` / `signoff` are NOT injected here — but NOT because their prose lives in the\n# plugin SKILL.md (it no longer does; U4/REQ-DIST-SKELETON-THIN thinned those to stubs, 2026-06-25,\n# superseding the 2026-06-19 full-fat-SKILL.md decision). The reason they aren't UPS skills stands: a\n# live agent uses them REACTIVELY (replying, briefing its Psyche, signing off) WITHOUT typing\n# `/omps:<skill>`, so UPS-injection is the wrong delivery. Instead the prose is delivered PROACTIVELY:\n# `send` rides the `messaging-perch` brief; `commune` (incl `--checkpoint`) + `signoff` ride the new\n# `live-ops` brief (below) — both composed into the perched SessionStart brief — and the go-live\n# moment carries them inline in `skills/live.md`. So the prose rides `spt adapter update`, the plugin\n# SKILL.md stays a true skeleton, and there is no `[strings.skills]` entry for these three.\n# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable\n# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under\n# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts\n# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.\nlive = { file = \"skills/live.md\" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)\n# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists\n# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core\n# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill\n# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).\n\n# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────\n# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved\n# via `spt adapter get-string claude-spt briefs.<key>`). The hook COMPOSES + {id}-substitutes these;\n# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +\n# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +\n# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +\n# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged\n# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.\n# [impl->REQ-DIST-SESSIONSTART-BRIEF]\n[strings.briefs]\nidentity = { file = \"briefs/identity.md\" }                      # {id}-templated: who + perch-live + don't-re-arm\nmessaging-perch = { file = \"briefs/messaging-perch.md\" }        # has-id reach: spt send + reply\nmessaging-no-perch = { file = \"briefs/messaging-no-perch.md\" }  # no-id reach: spt ring\nlive-ops = { file = \"briefs/live-ops.md\" }                      # commune (incl --checkpoint) + signoff (U4); composed into the perched brief\nendpoint-list = \"Show the roster of spt endpoints reachable from this node: `spt endpoint list`.\"  # shared one-liner (inline)\n\n# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────\n# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).\n[[hints]]\nkeywords = [\"go live\", \"live agent\", \"start live\"]\ntext = \"Tip: /omps:live starts or resumes a live agent session.\"\n\n[[hints]]\nkeywords = [\"who am i\", \"my owl name\", \"my agent name\", \"my endpoint id\"]\ntext = \"Tip: run `spt whoami` to see this session's spt endpoint id.\"\n\n[[hints]]\nkeywords = [\"message another agent\", \"send to agent\", \"tell the other agent\"]\ntext = \"Tip: /omps:send messages another spt agent; /omps:ready first to receive replies.\"\n\n[[hints]]\nkeywords = [\"another machine\", \"pair machine\", \"across machines\", \"subnet\", \"join my network\"]\ntext = \"Tip: /omps:subnet pairs machines into a private network so agents reach each other across nodes.\"\n\n[[hints]]\nkeywords = [\"checkpoint\", \"compact my context\", \"context is getting long\", \"clear and continue\", \"self clear\"]\ntext = \"Tip: /omps:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only).\"\n"}],"details":{"displayContent":{"text":"[hooks.PostToolUse]\n…\n# ── [identity] ─────────────────────────────────────────────────────────────────────────────\n[identity]\n# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).\nsession_id_source = \"post_spawn\"\n# Process-tree anchor when session_id is absent: the omp / omp-spt bridge binary.\nparent_ancestor_name = \"omp\"\n\n# ── [session] ──────────────────────────────────────────────────────────────────────────────\n# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,\n# not api verbs — \"there is deliberately no api commune\"). The agent writes `<endpoint_id>-commune.md`\n# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's\n# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;\n# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session\n# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.\n# /omps:commune writes the commune file here; /omps:signoff's graceful path is `endpoint shutdown`\n# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]\n[session]\ncommune_dir = \".claude\"\nsignoff_dir = \".claude\"\n\n# ── [session.psyche_init] — LIVE-CAPABILITY GATE ONLY (v0.14.0 F-030: never spawned) ───────────────\n# base is LIVE-CAPABLE (Option A, doyle-ruled + code-confirmed 2026-06-17): the Psyche-spawn seam lives\n# in the BASE manifest, not a `:live` profile. ready-vs-live is the COMMAND, not the profile —\n# `/omps:live` (`spt api listen`) stamps state=live_agent → the daemon livehost hosts the Psyche; the\n# livehost checks psyche_init on the resolved manifest. `/omps:ready` stamps state=ready_agent →\n# livehost SKIPS the Psyche regardless (livehost.rs:282 gate).\n#\n# EPHEMERAL MODEL (v0.14.0): psyche_init is NO LONGER A SPAWN PATH. The daemon reads it ONLY as the\n# live-capability gate (`manifest.session.psyche_init.as_ref()`, lifecycle.rs:365 — its presence\n# decides \"this endpoint hosts a psyche\"). Every actual turn — including the FreshWithPreload first\n# turn — is driven through [session.psyche_resume] below (one role, two key shapes). This table is\n# KEPT DECLARED + noop-safe: it mirrors psyche_resume's command shape, so an accidental spawn (a\n# partial apply / a stray driver) runs one safe ephemeral turn rather than misbehaving. [impl->REQ-SKILL-LIVE]\n# NO cwd TEMPLATE (v0.14.2): `{psyche_dir}` is a RETIRED resident-era key the F-030 fill catalog\n# never carried (doyle P-1 verdict 2026-07-06, runtime.rs:65 \"a catalog key must have a real fill\";\n# the freeze fills exactly {id, session_id, parent_session_id, psyche_context_file, node, subnet}).\n# Referencing it faulted EVERY per-event turn at substitution (flynn's dead psyche). The psyche cwd\n# is now the SHIM's job: it chdirs to the {psyche_context_file} parent dir (the psyche's nested\n# perch dir — the same location the retired fill pointed at) before spawning claude, keeping the\n# sid+cwd transcript-resolution pair stable across turns. [impl->REQ-PSYCHE-EPHEMERAL-SHIM]\n# S4 (omp-spt): the psyche shim is `psyche-omp` (psyche_omp.rs) — same frozen turn contract,\n# omp-flavored: continuity keys on a private --session-dir (omp cannot adopt a minted sid), the\n# session-gone reseed (exit 95) is a shim-side dir check (omp -c silently degrades, verified), and\n# the composed prompt rides an @file (omp -p ignores a stdin-only prompt, verified).\n[session.psyche_init]\ncommand = \"omp-spt psyche-omp --id {id} --session-id {session_id} --psyche-context-file {psyche_context_file}\"\n# Identity-env scrub (F-028 C2): a spawned psyche must never inherit its parent's identity env —\n# inherited OWL_SESSION_ID/SPT_AGENT_ID make the psyche's hooks resolve \"self\" to the PARENT and\n# rebind THAT perch. Kept on the gate table too (defence-in-depth if ever spawned).\n# [impl->REQ-HAZARD-PSYCHE-IDENTITY-ENV]\nenv_remove = [\"OWL_SESSION_ID\", \"SPT_AGENT_ID\"]\nkeys = [\"id\", \"session_id\", \"psyche_context_file\"]\n\n…\n# S4 (omp-spt): same psyche-omp shim as the psyche_init gate above (one role, two key shapes).\n…\n[history]\n…\n# applied by `spt endpoint run` on **spt-core >= 0.11.0** (REQ-HAZARD-ENV-SUBST). On < 0.11.0 the value\n# arrives EMPTY → SessionStart sees no $SPT_ENDPOINT_ID → it SEEDS (harness-hosted path) instead of\n# binding → the spt-hosted endpoint-run yields no perch (silent; no error). NOT floor-bumped:\n# min_spt_core stays 0.9.0 because the ready/live flows work on 0.9.0, and a blanket floor would\n# needlessly block them on older nodes (doyle). A runtime adapter guard was rejected too — a\n# < 0.11.0 endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user-launched seed session (both\n# have SPT_RELEASE_SEED/OWL_SESSION_ID set + SPT_ENDPOINT_ID empty; verified), so a guard would\n# false-positive \"update spt\" on every normal seed. The case is narrow + transient (only bites a node\n# still on < 0.11.0 running `endpoint run`; the fleet updates forward). Doc-note only.\n[env.SPT_ENDPOINT_ID]\ndirection = \"inject\"\nvalue = \"{id}\"\n\n# ── [env.SPT_INJECT_VERIFY_ECHO] — Layer-2 echo-verify opt-in (W6 LIFECYCLE-TRUTH) ───────────\n# doyle W6-DISPATCH-RULING item 3: W5-A shipped the broker seam (SPT_INJECT_VERIFY_ECHO\n# declared-capability, broker default OFF). CC's TUI re-renders typed text, so an inject that\n# echoes back is a VALID verification belt — a CC-hosted session opts IN by declaring the flag =1.\n# This is adapter DATA (the ON switch), NOT a spt-core requirement (per the adapter-glue boundary):\n# the broker reads the injected capability at spawn and arms echo-verify for the session. Inert +\n# forward-safe on cores below the W5-A seam (an unknown env var the session ignores; the broker's\n# default stays OFF), so the floor is NOT bumped here — the counter-49 floor bump is doyle's\n# coordinated ping. Evidence is unit-side (tests/manifest-shortcut.sh — .toml is scan-invisible):\n# REQ-INJECT-VERIFY-ECHO.\n[env.SPT_INJECT_VERIFY_ECHO]\ndirection = \"inject\"\nvalue = \"1\"\n\n# ── [inject] ───────────────────────────────────────────────────────────────────────────────\n[inject]\n# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).\n# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.\nactivity = [\"hook\"]\nidle = [\"hook\"]\n\n# ── [message-idle-translation-binary] — agnostic idle PTY delivery (harness-contract v0.13.0+) ─\n# The IDLE-delivery seam for spt-HOSTED sessions: a lifecycle-managed stdin→stdout JSON-lines binary\n# spt-core spawns when the endpoint comes online and reaps on shutdown. It turns each inbound <EVENT>\n# envelope into keystroke-commands spt-core applies ATOMICALLY to the broker-held PTY (operator input\n# is buffered during emission, so it coexists with an attached `spt rc` controller). This is the\n# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery\n# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.\n#\n# `command` = the program+args spt-core spawns (spt-core v0.16.0 seam; `path` deprecated, exactly one\n# of {path,command}). `{adapter_dir}` substitutes the install dir (survives updates), so the filter is\n# now the `translate` subcommand of the ONE consolidated claude-spt binary — D3 fold, ADR-0006 (was\n# the standalone cc-spt-idle-translate; protocol unchanged). The subcommand emits the operator-specified\n# choreography per message — ctrl+s (stash draft) · 50ms · <envelope> · 50ms · {\"key\":\"enter\"} (submit)\n# · {\"commit\":true} (terminate sequence) — so an inbound message never clobbers a half-typed draft (CC\n# AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke). TWO distinct signals\n# (doyle-confirmed from broker.rs): (1) the discrete {\"key\":\"enter\"} submits the PTY line (a verbatim \\r\n# byte does NOT submit a CC message — corrected 2026-06-23); the binary still neutralizes the envelope's\n# internal CR/LF since spt-core applies {\"text\"} VERBATIM (broker.rs:1016-1017). (2) {\"commit\":true} is\n# the MANDATORY inject-sequence terminator: run_inject_worker (broker.rs:1075-1090) ends a sequence ONLY\n# on {commit}; without it the broker FAULTs at the 5s INJECT_COMMIT_DEADLINE. Satisfies\n# REQ-DIST-IDLE-TRANSLATE (scanned impl evidence = tools/claude-spt/src/translate.rs; .toml is unscanned).\n[message-idle-translation-binary]\ncommand = \"{adapter_dir}/omp-spt translate\"\n\n# ── [digest] ───────────────────────────────────────────────────────────────────────────────\n# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the\n# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,\n# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible\n# divergence from the deliberately log-less mock adapter, which declares no [digest].)\n#\n# FETCHER strategy (spt-core v0.19.0, REQ-DIGEST-FETCHER-STRATEGY): the extractor LOCATES + reads +\n# emits itself; spt-core only runs it bounded and consumes stdout — no locate, no pre-read, no\n# `source` (ignored under fetcher, so it is dropped). This is the designed home for CC's PARTITIONED\n# layout: transcripts live at `<config-root>/projects/<cwd-slug>/<session_id>.jsonl` where the\n# cwd-slug is a CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays\n# harness-agnostic — by design there is no {cwd_slug}), so the adapter owns the locate\n# (owlery::claude_projects_root pattern — search the projects root for `<session_id>.jsonl`).\n# spt-core feeds only harness-NEUTRAL fills: {session_id} + the captured {CLAUDE_CONFIG_DIR}\n# read-var (below). `--config-dir` receives the CAPTURED per-session value — ccs sessions resolve\n# their relocated `~/.ccs/instances/<account>/.claude` tree, base sessions the `~/.claude` fallback —\n# closing the \"not expressible as a static catalog path\" gap in the daemon's own context (the old\n# env-preference branch only worked when the extractor inherited the session's env). The extractor\n# (`claude-spt digest`, ADR-0006/U2) ships in the spt-core-conducted layer; this is its declaration.\n# [impl->REQ-DIST-DIGEST-FETCHER]\n# ── S3 (omp-spt): the extractor is `digest-omp`, not the CC `digest` ───────────────────────────\n# The extractor is `claude-spt digest-omp` (digest_omp.rs): it maps oh-my-pi's native session JSONL\n# (~/.omp/agent/sessions/<dir-encoded>/<ts>_<sessionId>.jsonl) to the SAME digest-record contract as\n# the CC `digest` (shared `rec()` emitter). It LOCATES the file by {session_id} under the omp\n# sessions root, so no {CLAUDE_CONFIG_DIR} read-var is needed (the CC-lineage comment above and\n# [env.CLAUDE_CONFIG_DIR] below are inert for omp). Proven with `spt adapter digest-proof omp-spt\n# --sample <omp-session.jsonl>`. [impl->REQ-DIST-DIGEST-EXTRACTOR]\n[digest]\nstrategy = \"fetcher\"\nextractor = \"omp-spt digest-omp --session {session_id}\"\nwindow_turns = 6\narg_truncation = 240\nsprint_collapse = true\n\n# ── [env.CLAUDE_CONFIG_DIR] — the read-direction capture (spt-core v0.19.0) ─────────────────────\n# direction=\"read\": spt-core CAPTURES this var from the session's environment at bind time into\n# info.json.read_env, then offers it as a fill_template key ({CLAUDE_CONFIG_DIR}) to on-demand\n# commands — the [digest] fetcher extractor above. `value` is the FALLBACK when the session env\n# lacks it (a plain `claude` launch), tilde-expanded spt-core-side: `~/.claude` = CC's default\n# state root. A ccs-launched session carries CLAUDE_CONFIG_DIR=~/.ccs/instances/<account>/.claude\n# in its env → captured → the extractor locates the relocated projects tree with zero profile\n# overlay (the NO-[profiles.ccs.digest]-leaf design, now daemon-context-correct).\n# [impl->REQ-DIST-DIGEST-FETCHER]\n[env.CLAUDE_CONFIG_DIR]\ndirection = \"read\"\nvalue = \"~/.claude\"\n\n# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────\n# S4 (omp-spt): NO shipped profile overlays. claude-spt's `ccs` profile (the Claude Code\n# model/billing multiplexer, kaitranntt/ccs) was REMOVED here — it swapped CC's launch binary,\n# which is meaningless for an omp bringup; omp's own model/billing routing is native config\n# (~/.omp/agent/models.yml — see docs/OMP-OPENROUTER.md, S5) and per-endpoint isolation is\n# `--profile <name>` forwarded to omp through the bridge's `--` passthrough. Node-local user\n# profiles are still created with `spt adapter create-profile` and live in the registry, never\n# in this manifest.\n#\n# NOTE: there is no `:live` profile (removed 2026-06-17, PREP-4 Option A, inherited). The LiveAgent\n# seam `[session.psyche_init]` lives in the BASE manifest above — base is live-capable, and\n…\n# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in\n# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.\n[strings]\nadapter_label = \"oh-my-pi (spt)\"\n# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's\n# hooks.json + dispatch.sh go static-forever. `{adapter_dir}` is lazily substituted at\n# `spt adapter get-string` read time (spt-core v0.16.0) to the absolute install dir, so the value\n# resolves to `<install_dir>/claude-spt hook` — the consolidated binary's hook subcommand, which\n# survives adapter updates. dispatch.sh get-strings this ONCE per session (caches it in\n# $SPTC_HOOK_BIN), then runs `<resolved> <CCEventName> --host-pid <pid>` per hook, piping the CC hook\n# payload on stdin. Session-scoped keys are NOT available here (get-string is adapter-static) — the\n# binary reads session_id/prompt/tool_input from stdin. [impl->REQ-DIST-HOOK-BINARY]\n#\n# v0.9.1 FIX: the value is the bare binary PATH (no trailing ` hook`). dispatch.sh appends the `hook`\n# subcommand as a literal and caches this path QUOTED in $CLAUDE_ENV_FILE. The v0.9.0 value\n# (\"{adapter_dir}/claude-spt hook\", a space before `hook`) was cached unquoted → CC sources the env\n# file per Bash call → `SPTC_HOOK_BIN=/path/claude-spt hook` parsed as `VAR=val cmd` → ran `hook`\n# (`hook: command not found` on every Bash command) AND dropped the value. dispatch.sh also strips a\n# trailing ` hook` defensively, so it stays compatible with an older manifest mid-update.\nhook_cmd = \"{adapter_dir}/omp-spt\"\n\n[strings.notice]\nnot_installed = \"spt-core not found on PATH — run /omps:setup to install it.\"\n\n[strings.skills]\n# Operative UPS-injection skill bodies. File-backed values (`{ file = \"skills/<x>.md\" }`) live in the\n# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,\n# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the\n# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until\n# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.\n# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`\n# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The\n# \"who am i\" hint below points at the core CLI. (docs/PARITY.md.)\nsetup = { file = \"skills/setup.md\" }\nready = { file = \"skills/ready.md\" }\nversion = { file = \"skills/version.md\" }\nlist-agents = { file = \"skills/list-agents.md\" } # -> spt endpoint list (the roster)\nforce-stop = { file = \"skills/force-stop.md\" } # -> spt endpoint shutdown (graceful, tears down Psyche)\nsubnet = { file = \"skills/subnet.md\" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET\n# `send` / `commune` / `signoff` are NOT injected here — but NOT because their prose lives in the\n# plugin SKILL.md (it no longer does; U4/REQ-DIST-SKELETON-THIN thinned those to stubs, 2026-06-25,\n# superseding the 2026-06-19 full-fat-SKILL.md decision). The reason they aren't UPS skills stands: a\n# live agent uses them REACTIVELY (replying, briefing its Psyche, signing off) WITHOUT typing\n# `/omps:<skill>`, so UPS-injection is the wrong delivery. Instead the prose is delivered PROACTIVELY:\n# `send` rides the `messaging-perch` brief; `commune` (incl `--checkpoint`) + `signoff` ride the new\n# `live-ops` brief (below) — both composed into the perched SessionStart brief — and the go-live\n# moment carries them inline in `skills/live.md`. So the prose rides `spt adapter update`, the plugin\n# SKILL.md stays a true skeleton, and there is no `[strings.skills]` entry for these three.\n# `live` -> upgrade THIS session to a LiveAgent (Option A, 2026-06-17): base claude-spt is live-capable\n# ([session.psyche_init] in base), so going live is just the COMMAND — bare `spt api listen <id>` under\n# a Monitor relay. The live listen path stamps the perch state=live_agent → the daemon livehost hosts\n# the Psyche (livehost.rs:282 gates on live_agent state, doyle). No `:live` composite, no --adapter.\nlive = { file = \"skills/live.md\" }             # -> bare `spt api listen <id>` + Monitor relay (LiveAgent)\n# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists\n# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core\n# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill\n# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).\n\n# ── [strings.briefs] ─────────────────────────────────────────────────────────────────────────\n# SessionStart agent-facing briefs. Same file-backed/inline machinery as [strings.skills] (resolved\n# via `spt adapter get-string claude-spt briefs.<key>`). The hook COMPOSES + {id}-substitutes these;\n# it never authors agent-facing prose (single-source throughline). Composable so the shared roster +\n# messaging text live once: the identity brief (perched sessions — bind+boundary) = identity +\n# messaging-perch + endpoint-list; the ring brief (no-perch seed, peer-gated) = messaging-no-perch +\n# endpoint-list. Liveness-agnostic (no live/ready split — deferred, public liveness-query gap, logged\n# with doyle). One-liners inline; multi-line bodies file-backed under adapter/strings/briefs/.\n# [impl->REQ-DIST-SESSIONSTART-BRIEF]\n[strings.briefs]\nidentity = { file = \"briefs/identity.md\" }                      # {id}-templated: who + perch-live + don't-re-arm\nmessaging-perch = { file = \"briefs/messaging-perch.md\" }        # has-id reach: spt send + reply\nmessaging-no-perch = { file = \"briefs/messaging-no-perch.md\" }  # no-id reach: spt ring\nlive-ops = { file = \"briefs/live-ops.md\" }                      # commune (incl --checkpoint) + signoff (U4); composed into the perched brief\nendpoint-list = \"Show the roster of spt endpoints reachable from this node: `spt endpoint list`.\"  # shared one-liner (inline)\n\n# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────\n# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).\n[[hints]]\nkeywords = [\"go live\", \"live agent\", \"start live\"]\ntext = \"Tip: /omps:live starts or resumes a live agent session.\"\n\n[[hints]]\nkeywords = [\"who am i\", \"my owl name\", \"my agent name\", \"my endpoint id\"]\ntext = \"Tip: run `spt whoami` to see this session's spt endpoint id.\"\n\n[[hints]]\nkeywords = [\"message another agent\", \"send to agent\", \"tell the other agent\"]\ntext = \"Tip: /omps:send messages another spt agent; /omps:ready first to receive replies.\"\n\n[[hints]]\nkeywords = [\"another machine\", \"pair machine\", \"across machines\", \"subnet\", \"join my network\"]\ntext = \"Tip: /omps:subnet pairs machines into a private network so agents reach each other across nodes.\"\n\n[[hints]]\nkeywords = [\"checkpoint\", \"compact my context\", \"context is getting long\", \"clear and continue\", \"self clear\"]\ntext = \"Tip: /omps:commune --checkpoint writes a resume commune AND auto-clears+wakes you from it — an agent-driven context reset (live agents only).\"\n","startLine":583,"lineNumbers":[583,null,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,null,661,null,779,null,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,null,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\omp-spt\\adapter\\omp-spt.toml"}}},"isError":false,"timestamp":1784087287447}}
{"type":"message","id":"315b6f24","parentId":"847d5e63","timestamp":"2026-07-15T03:48:20.543Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Flagging ADR 0004 for status ratification**\n\n**Clarifying plugin and hook contradictions**"},{"type":"toolCall","id":"call_PAf062xFGMxbxUEhBjckk7X1|fc_0ca4af36969c80fa016a57030264c88198b33b838ecd629c21","name":"irc","arguments":{"i":"Sharing decision-audit headline","op":"send","to":"Main","message":"Decision audit headline: only ADR-0007 is OMP-native and accepted. CONTEXT/SCOPE + ADRs 0001–0006 + almost all traceable REQs remain Claude-specific. Root grill question is whether plugin/omps is intentional CC control surface or inherited residue; it determines whether most hazards/REQs stay applicable. I’ll yield full ledger with exact paths.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":8029,"output":563,"cacheRead":217600,"cacheWrite":0,"totalTokens":226192,"reasoningTokens":428,"cost":{"input":0.040145,"output":0.01689,"cacheRead":0.1088,"cacheWrite":0,"total":0.165835}},"stopReason":"toolUse","timestamp":1784087287454,"responseId":"resp_0ca4af36969c80fa016a5702f7c60481988d593cee328b3693","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a5702fa3ab88198a2174b00212affcc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwMC_FfAXM5-cZTZeh2V0I0Q_c7_A9aOBKAOR94dbCVarqxwPb-R86v7Uu37xOsycrZ01wauw_cVIMw3B8rct61Dk_dvPghL1W-hpqzSKrAoefLrCrA6DEOjSpFQxULzC4_8YekCpShqkiLTLWVBJlw8SBKjPWodkeULtYNdS5wzNB5STsNGG6XFShgHpS7mMfwfE7_55lZKGv3JoKPVJhS4vgU0f9YPSAmmhNaB6rzPTyAb_4zFbgW4VO2yZv-C5JIunioCcHC3IFuaFAn_lmyk61yaBVX1uwaBgINHYIvKy70tfQiQiGjhp3kQBFAGlx8P_TKMq0IiBUsUR579Lr53O161w1mVNbOpAk5QAlvAeh5VhxJ48nAXxyqagNHzP6uWJt7Gq2vvphY7hUocaWg_0P48cKK4oBnwNUzd4pdzsup74GvBHtoBDFqckTwZIWSCnv3-qwEvshSzcJ6IC8ZAr1VIawnnxi108wejF2zwsHmPWMmOY_Q8ViQtRF_04_LFnO7_bjeVdAsvXkL8ntOHkMePAEQcgPUp6PlMM_ycpH7OGWl982ZiQhyNEimxdsqDNIkr0m1bF_SNvIm3JXi4_cqMm4l64vEFgLqCvlo86NN_UE46OVWtUjuMIn5tl_YdqgABVSSy8gHebNpJT5ThjVkReYg0NNud8G6YzOLFsrZpT3LQYUrYBveijzYeszDrhx8qZ_AE5F2vjpzVV-fTVoR0OZA8OkZNMr1NXdC0qefy4rb2jfKkEmA1m86LdQBywTL3mX7Yaf6Z5bUEP24XKWJYtESmQx2jMrkQV6QylJqjA5bNiBgNovBazmWcPfMgXoghZYqODlSBzHKih8dSHtg8WjnLNuelm6FLtLwwh32UJ3FYbSYAfneA_nCr1dZ15S3k_YesukdRoTZLFq1fqulkTXQ5AOz_DSLGwyd0a27owr28lh_Qh0c-8WtDlTrD4XVCn31Gh_zotET0hS3qGYHkyHLmsKfMRrzCe3We6L42VLsHkf1zIoSzQdNnOoE0MoewC_WzgDs6MyBjoFAaXJAhmYLofbtGSpSRx5o8HB_tigUNdMKjf6RSi53gRRrrtJVHIFM9NHK9Vb6dTnuIdQreoReHHFLKLDF7NrQBdACw9BpGuYa-xqoovDQpAmZVXJIyC5eFDG1tvYAHjj_zcNkQUsFKD8T6e2hIA7_7mVIu5C28hPvgq4p2jHo94FP3ckeIKe0Q7NVS0ia2bXW0GoIGCYnKMeVweotdOF48BWPi-g1lmqyBVeaJ5e43F18nGULdQjQN-iGNpLt3-CP78LMGS672qHW_QWwW3BV7VvasRLZzCJVUadEHeG4KFia0eIURn_3WlYxi2y8UyFRaBwKreCCRIJyk58yHTCPDha46dp7N6UojkQeITTWJMS6lw89_bQQNUFFcd4geEO5pma90jrvrnx16QyNz55WoTO6swHEL1Jm8aRSwhL_rdtN-628QYmLmulxpzQcVB_6TykU7WsDodAKQvxGNxJTvN-FNnuuwlsVR61uaEAjNYIxebNBpIQqA9-WXCgsCTHCKLRLZh5xExYxyPeqRdwnp2nTy-UxaBvM4_DfvfQIzadBnoU3znLj12QHb0R5SgTlkkvWvtiF6PQ-8zETsUK6JZFYSwRBWzrWHFIrYlb8S7v16tuKFOhOraDr0zqI52a4EHy9OAlJzS-yOKBYBuXyY81r5jq2f8mjD-KjNhzwyu05qcNtfUebqzGd9gdriXONhSm7crhR8FHuhFlVvLTNH-2rvyKug-1SGCEWhsSmYDbqUMsSKkuBSg1uZny0LYwxBahaLCscyK2RDxz35N3hjMxu48hTeHVQYv6eILO8SAmmW8oMzF03w2NjbGkdfEKvsDGFsWyI-VhI0l_gC1iGH8lVDuZJ3u8QR2UDw4CIvytuhsVEr7kVwDgCie-rixcT7bylte-lJ03fDn-H3ns1CLn7FibYL5K2JqcDu-cO59pssE7emThWhZGncdS2qsh4qLKqaTb8Mr__7sFPrbYxpRCWmzEez_pDCQD9W4rsr3vBRbbUYZFHyTTygGM9esClEluM3z02qz4Z9Z6ja9dW8ntHsgaLny5LMTtI2lf7qkWOL5WROG71o95QhB79U3XW9I_ekmhgNy2uX2bCZ8yIg0xpfEWOvnZVf383IpFn2Z7tz7j94y3kLVp6sNGVt9Nhx0upAVuc2z2OlwYMuFTg2N8shFFpTuzoz4G6oqUwbJB5ucoP8JGFaTxd-MWbK9GajaMtlxkiWug8tOiC3_q72c7coBBauXIeAnR7O3LHD3ikM_JcKeGTN6OE82-RCtF9oWEByQsoMH4KbYrB4N3iYmJ1hliXWDks9ougceAeXoNOci5DXJ6SLHmHEPd2oWSuaZkiYOsdKdsnhNumClGqD86peLC9iqL-a7VkLYCX67lhWZ0DkIKfPLsheXhloY7wV4FhDi20TvKVQyKam8vTM-Fzy3HxI_5pkc_H1DpnWK0LMDBF3G-_b7TApkDJhJEA6nL364nDlBsXoMJXPuNzmDDqg0k-IdJvL-gygh2js1zlidf3vEODmXdn2EZtnC5Ko6pF3um_fz0JvFH-qmK-7SNAcM6i5w5a38uj7E-rWb1pCsvCI1TXgupbyi6KzaZZxTcxCo_CFfUZvl2xhwT7ePQfV_j8XFFIrrWcVsNC6XTXdA7LwPwCni4b6sciJEx9TaQ9ClRNciiXBtggo4w29Q-YOrL8xIq_e8igex2sTEIZzUsmkikGxU4e23Pa6MVY-pecsucnSb8vkgYuZIwNEMKMpmKQB5yJ_mAe6NsKo4xTG1sLfG0htnn-CF7N5cBcTo1vbi1w6VcxvecFkEbfkVE6dbjCJRcmrte1Lbbg9HPmzkye87qbg1FS3YifuENbtf9ymvLGcaHGtwpiKmm_pFMM4AkY5Xm-U2eXrn0bwxy0zrNnXYbjXpnje2rB0NXRciYlHasYjR1G_1_zg-rS3u6dJhbPACcXHw3Reb54sK3sXPwhEntL1_CRX4eCYfDELEmqqrpIz8iC1c0GjjTYch44kpBdG4tXyPEnOrjIryr64IwuRfgfL0KDJAd-lIU9XPWp6SWrc_jJISRJjTsynngIdVotfOyjDRtwXxZxHahICWFwvYohYEc3uPRxR0avczqyEqSK_jZLt1OlRP0dSKmM3e-PT-kJT-nEGIe1RTs13hn3M3kL6u55xvuQRcBiY79JuRitvnPSjIhaISk6dzmHoBSPcwy4Bf8Xul3PSxF4Y0gEGrVjupw3jX_pJnXOCVXNAyspGmW5ZQlkofKzXvrmOKhPVnFSiSquDH_tcpQw7yhLlyJ7zJrewrbKIgTnR82Stsv5zVcX7rgtBK5g-Kbj-AVmeO9XnuXBLvGPeyLWB","summary":[{"type":"summary_text","text":"**Flagging ADR 0004 for status ratification**"},{"type":"summary_text","text":"**Clarifying plugin and hook contradictions**"}]},{"id":"fc_0ca4af36969c80fa016a57030264c88198b33b838ecd629c21","type":"function_call","status":"completed","arguments":"{\"i\":\"Sharing decision-audit headline\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Decision audit headline: only ADR-0007 is OMP-native and accepted. CONTEXT/SCOPE + ADRs 0001–0006 + almost all traceable REQs remain Claude-specific. Root grill question is whether plugin/omps is intentional CC control surface or inherited residue; it determines whether most hazards/REQs stay applicable. I’ll yield full ledger with exact paths.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_PAf062xFGMxbxUEhBjckk7X1","name":"irc"}]},"duration":13076.192700000014,"ttft":2783.9346999999834,"contextSnapshot":{"promptTokens":225629,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PAf062xFGMxbxUEhBjckk7X1|fc_0ca4af36969c80fa016a57030264c88198b33b838ecd629c21","toolName":"irc","startedAt":"2026-07-15T03:48:20.544Z","intent":"Sharing decision-audit headline"},"id":"626a0ea3","parentId":"315b6f24","timestamp":"2026-07-15T03:48:20.544Z"}
{"type":"message","id":"7c3080cb","parentId":"626a0ea3","timestamp":"2026-07-15T03:48:20.544Z","message":{"role":"toolResult","toolCallId":"call_PAf062xFGMxbxUEhBjckk7X1|fc_0ca4af36969c80fa016a57030264c88198b33b838ecd629c21","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"DecisionLedgerAudit","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784087300544}}
{"type":"message","id":"4daa6a7f","parentId":"7c3080cb","timestamp":"2026-07-15T03:51:30.987Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Formulating detailed decision ledger structure**\n\n**Analyzing binary argument handling and test coverage**\n\n**Identifying factual defects and bridge test gaps**"},{"type":"thinking","thinking":"**Designing internal plugin and API scopes**\n\n**Locking OMP integration decisions**"},{"type":"thinking","thinking":"**Clarifying shortcut and scope inconsistencies**\n\n**Listing and organizing trace requirements**\n\n**Planning detailed audit result format**"},{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_P7PZD2smja81sWIvCdfI6FYd|fc_0ca4af36969c80fa016a57032383248198ac6524711036f1d3","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"# Decision ledger\n\n## Executive finding\n\nThe repository does not yet have a coherent OMP-native decision system. `docs/adr/0007-native-omp-tui-hosts-spt-extension.md` is the only accepted ADR that directly governs the current OMP architecture. `CONTEXT.md`, `SCOPE.md`, ADRs 0001–0006, most of `docs/KNOWN-HAZARDS.md`, and all but one requirement in `traceable-reqs.toml` still describe the source `claude-spt` product. Some of that lineage is reusable, but the documents do not distinguish inherited history from current omp-spt commitments. As a result, old Claude decisions can be mistaken for live OMP requirements, while implemented OMP behaviors have no decision or traceability record.\n\nThe highest-order live decision is: **is `plugin/omps` an intentional Claude Code control surface for the OMP adapter, or inherited residue that should leave an OMP-only product?** This must be answered before grilling naming, hooks, setup, hazards, tests, or distribution, because it changes the applicability of most inherited commitments.\n\n---\n\n# LOCKED — do not re-ask\n\n1. **Public-contract-only development is a hard constraint.** `SCOPE.md:20-23` says the adapter is built from `SaberMage/spt-releases` and published pages, never spt-core source. `OMP-ADAPTER-PLAN.md` carries that discipline into its validation section. This is still applicable.\n\n2. **Interactive hosted endpoints must run native OMP as the PTY leader.** Accepted ADR-0007, especially `docs/adr/0007-native-omp-tui-hosts-spt-extension.md:5,13-15`, decides that `[session.self]` and `[session.resume]` run `omp`, not the RPC bridge. `adapter/omp-spt.toml` symbols `[session.self]` and `[session.resume]` implement this with `omp --extension …/omp-spt.mjs` and `omp -r {session_id} --extension …`. Do not re-open “bridge logs versus native TUI.”\n\n3. **The packaged OMP extension, not a terminal proxy, supplies SPT integration for interactive endpoints.** ADR-0007 locks bind/listen/state/reply through OMP’s public extension API while OMP owns rendering. `adapter/strings/omp-spt.mjs::ompSpt` is the implementation.\n\n4. **RPC remains a headless integration surface, not the interactive endpoint host.** ADR-0007:15 explicitly retains the old bridge for explicitly headless callers. `OMP-ADAPTER-PLAN.md:47-72` now states the same split: Style A headless, Style B interactive.\n\n5. **Inbound interactive delivery is serialized.** ADR-0007:22 locks that two inbound messages cannot collapse into one OMP turn or receive the wrong reply. `omp-spt.mjs` realizes this with `queue`, `current`, `agentActive`, and `dispatchNext`.\n\n6. **OpenRouter billing is OMP configuration, not adapter code.** `OMP-ADAPTER-PLAN.md:132-153` and `docs/OMP-OPENROUTER.md:1-6` lock the boundary: document `models.yml`, `.env`, role routing, and fallback behavior; do not add provider logic to the adapter.\n\n7. **OMP’s transcript is adapted to the neutral SPT digest record contract.** `OMP-ADAPTER-PLAN.md:90-102` fixes the source and output model; `tools/claude-spt/src/digest_omp.rs` implements `digest-omp`; `[digest].extractor` invokes it. The exact old Claude extractor design should not be re-litigated.\n\n8. **The OMP adapter currently has no shipped ccs profile overlay.** This is explicitly enforced by `tests/manifest-shortcut.sh` (“no shipped profile overlays”) and explained in `adapter/omp-spt.toml` immediately before `[strings]`: ccs was Claude-specific and OMP uses native provider/profile configuration. This is consistent with the OMP plan and should be treated as a completed port decision unless the root product-boundary question intentionally retains a separate Claude surface.\n\n9. **Current concrete identity values are implemented and regression-guarded:** adapter/release identity `omp-spt`, executable `omp-spt`, update repo `BigscreenVR/omp-spt`, host binaries `omp` and `omp-spt`, shortcut basename `omp`. Evidence: `adapter/omp-spt.toml` `[adapter]`/`[update]`, `tools/claude-spt/Cargo.toml` `[[bin]]`, `ci/publish/package-adapter.sh`, and `tests/manifest-shortcut.sh`. The values are not all supported by an OMP naming ADR, so their *rationale* needs documentation, but asking “what does the code currently call this?” is unnecessary.\n\n10. **Bridge startup must complete OMP `ready` + `get_state` before binding.** `tools/claude-spt/src/bridge.rs::run_argv` enforces that order and bounded waits. This closes the “bind before a failed handshake poisons the ledger” alternative.\n\n---\n\n# SUPERSEDED — preserve as history, do not ask in original form\n\n1. **RPC bridge as `[session.self]` / `[session.resume]`.** Superseded explicitly by ADR-0007. Remaining old statements in `OMP-ADAPTER-PLAN.md` slices S1/S2, `tools/claude-spt/src/bridge.rs` module docs (“hosted harness process” / `[session.self]`), the top of `adapter/omp-spt.toml` (“integration is an RPC bridge”), and `docs/OMP-BRIDGE-FIELD-ISSUES.md` must be read as pre-0.1.3 history.\n\n2. **The full `spt-claude-code`/`claude-spt` naming-domain decision.** ADR-0005 was accepted for the sister product, then operationally superseded by the fork’s `omp-spt`/`omps`/`omp` names. It remains valid lineage, not the active OMP naming ADR.\n\n3. **The `cc` launcher decision.** `SCOPE.md`, ADR-0001, and `traceable-reqs.toml::REQ-DIST-SHORTCUT-BASENAME` lock `cc-<id>` for Claude. Current OMP code and tests lock `shortcut_basename = \"omp\"`. Do not ask whether omp-spt should use `cc`; ask only whether `omp` is the right final OMP shortcut token.\n\n4. **ccs profile work and `claude-spt:deep`.** `SCOPE.md` already locks `deep` removal and originally locks ccs for Claude. For OMP, the manifest says no shipped profiles and tests enforce it. `traceable-reqs.toml::REQ-CCS-PROFILES` and `REQ-SETUP-CCS` are stale rather than live OMP requirements.\n\n5. **The OMP plan’s exact directory-encoding open question.** `tools/claude-spt/src/digest_omp.rs` documents that the implementation locates by session id under the sessions root, so it does not need to reproduce the directory encoding. Ask whether locate-by-id is acceptable only if a concrete ambiguity/collision is found; do not re-ask how to clone OMP’s encoding.\n\n6. **The original single-sequence checkpoint macro in proposed ADR-0004.** `docs/KNOWN-HAZARDS.md` §2.4 and `traceable-reqs.toml::REQ-HAZARD-CHECKPOINT-CLEAR-RACE` supersede it with ARM/FIRE split sequencing, then a one-sequence post-clear rename+wake. If checkpoint remains in scope at all, the later hazard invariant is authoritative.\n\n7. **Several SCOPE grill items are already resolved in the same file or later ADRs.** `SCOPE.md:135` locks the post-push signal trigger, while `SCOPE.md:211` still lists “git-hook signal vs polling” as open. `SCOPE.md:182-190` resolves the docs/template split, while lines 212-214 still list it as open. ADR-0001 confirms UPS slash-command behavior, so `SCOPE.md:54-55` is stale. These should not be re-grilled.\n\n8. **Field issue HIGH-1 and interactive HIGH-3.** HIGH-1’s executable collision is implemented as `omp-spt` (`Cargo.toml`, packer, manifest, tests). The interactive “READY only” UX is structurally superseded by ADR-0007 because interactive hosting no longer uses the bridge; bridge logging remains relevant only to headless use.\n\n---\n\n# UNRESOLVED — live design decisions\n\n## Product/domain boundary\n\n1. **Is the Claude Code plugin an intentional supported surface?** The plan says the OMP hook plugin is removed (`OMP-ADAPTER-PLAN.md:87`), yet `plugin/omps` is actively packaged and tested, `[update.post]` still runs plugin reconciliation, the manifest carries `[hooks.*]`, UPS strings, hints, CC inject configuration, and `tools/claude-spt/src/hook.rs` remains active. If “yes,” define this as an explicit cross-harness control surface. If “no,” most Claude-specific ADRs, hazards, requirements, tests, and code are out of scope and should be retired cleanly.\n\n2. **What does `omps` mean and is it final?** `OMP-ADAPTER-PLAN.md:78` still says “`omps` (or keep `sptc`? decide),” while `plugin/omps/.claude-plugin/plugin.json`, skill paths, hints, and tests enforce `omps`. The implementation choice needs ratification and an OMP naming ADR; the old `sptc→spt` succession is not automatically applicable.\n\n3. **What are the canonical topologies and users of the retained headless bridge?** ADR-0007 says “explicitly headless callers,” but does not define who invokes `omp-spt bridge`, whether it is a supported public CLI, whether it serves ReadyAgent only or LiveAgent too, or whether resume/psyche paths count as bridge consumers.\n\n## Capability truth\n\n4. **Worker support is unresolved and currently overclaimed.** `adapter/omp-spt.toml` advertises `hostable_types = [\"LiveAgent\", \"ReadyAgent\", \"Worker\"]`; the OMP plan’s diagram mentions `worker-*`; but `adapter/strings/omp-spt.mjs` handles only session/agent lifecycle and `bridge.rs` never calls worker-start/worker-stop. Existing worker requirements/tests point to Claude hooks. Decide either to implement OMP worker lifecycle or remove Worker from advertised capability.\n\n5. **ReadyAgent versus LiveAgent semantics need an OMP-native statement.** The current skill prose describes upgrading a *Claude Code* session and a Monitor-based relay; the native extension always starts `spt ready`, while the manifest also declares OMP psyche roles. The precise user-visible transition that creates a live OMP agent versus a ready OMP endpoint is not documented in OMP terms.\n\n6. **Session-owned file locations are not decided in OMP language.** `[session].commune_dir` and `signoff_dir` are still `.claude`; OMP skills still tell agents to write `.claude/<id>-commune.md`. Decide whether `.claude` is intentionally adapter-owned compatibility state, should become `.omp`, or should be a harness-neutral project directory. This affects migration and resume semantics.\n\n## Reliability and recovery\n\n7. **RPC bridge mid-turn behavior remains unresolved.** The plan promises `steer` / `follow_up`; field issue MEDIUM-1 requires decoupled delivery and steer; `bridge.rs::drive_turn` instead buffers later messages until `agent_end`. This is safe serialization but not the promised mid-turn control surface. Ratify serialization-only or implement steering.\n\n8. **Clean adapter switch/unbind remains unresolved.** Field issue MEDIUM-4 asks for an operator-supported switch without ledger surgery. Current bridge and extension call `session-end` on graceful shutdown, but there is no documented switch procedure and no requirement/test proving an omp-spt session can be stopped and relaunched under another adapter.\n\n9. **Cross-platform hard-kill child cleanup remains partial.** Windows Job Object cleanup exists. `bridge.rs::teardown::arm_kill_on_close` explicitly returns a no-op error on non-Windows and says Unix process-group teardown is a follow-on. Decide whether headless Linux support requires the same invariant; current packaging ships Linux, so [INFERENCE] it likely should.\n\n10. **Native extension delivery failure policy is undefined.** If `pi.sendUserMessage` throws, `omp-spt.mjs::dispatchNext` logs locally and drops `current` without notifying the sender. If the `spt ready` listener exits, the extension marks the UI offline but does not re-arm, tear down the perch, or create an SPT-visible failure. Decide retry, explicit failure reply, offline transition, or terminal teardown.\n\n11. **The honest minimum spt-core floor is still open.** `OMP-ADAPTER-PLAN.md:174-176` asks for the verbs actually used. The manifest pins `0.27.0` with a long inherited Claude floor narrative. The OMP-specific required floor has not been derived or recorded.\n\n12. **OpenRouter role/model selection remains intentionally unfinished.** `docs/OMP-OPENROUTER.md:57-59` verifies OpenRouter routing and says the remaining choice is model/role assignment. This is a product configuration decision, not an adapter implementation defect.\n\n13. **ACP supersession criterion is only a strategic statement.** `OMP-ADAPTER-PLAN.md:10-13` says an ACP client may supersede this bridge, but gives no decision trigger or migration boundary. Leave it as a future condition unless current roadmap planning needs a formal sunset criterion.\n\n---\n\n# CONTRADICTED BY CODE OR CURRENT ARTIFACTS — factual defects, not taste questions\n\n1. **Setup is functionally pointed at the wrong adapter.** `adapter/strings/skills/setup.md` and `plugin/omps/skills/setup/SKILL.md` repeatedly activate `claude-spt`, use `adapter/claude-spt.toml`, fetch `SaberMage/claude-spt`, select `claude-spt:ccs`, and describe Claude Code. The actual manifest is `adapter/omp-spt.toml`, adapter is `omp-spt`, repo is `BigscreenVR/omp-spt`, and no ccs profile exists.\n\n2. **Plugin identity is stale.** `plugin/omps/.claude-plugin/plugin.json` describes “adapter for Claude Code” and includes keyword `claude-spt`. That may be correct only if the root product-boundary decision intentionally retains a Claude control plugin; otherwise it contradicts the target.\n\n3. **The manifest says hooks are the delivery surface while the accepted interactive architecture uses the OMP extension.** `[inject]` says activity/idle `hook`, top-level comments say RPC bridge, and `[hooks.*]` are CC-specific. ADR-0007 and `[session.self]/[session.resume]` use native OMP + extension. These claims need either a topology qualifier or removal.\n\n4. **The manifest still captures `CLAUDE_CONFIG_DIR` even though its own comments call it inert for OMP.** `[env.CLAUDE_CONFIG_DIR]` is active data, but `digest-omp` locates under OMP sessions. An inert Claude read-var should not remain an unexplained current contract.\n\n5. **CC echo verification is active but explicitly called inert on OMP.** `[env.SPT_INJECT_VERIFY_ECHO] = 1` and `traceable-reqs.toml::REQ-INJECT-VERIFY-ECHO` are Claude-TUI commitments; `tests/manifest-shortcut.sh` says the declaration is “inert on omp.” This should be removed or justified for a retained Claude topology.\n\n6. **Integration tests use the OMP manifest but assert Claude adapter outcomes.** `ci/manifest/registration-int.sh` adds `adapter/omp-spt.toml` then looks for/removes `claude-spt`, requires `claude-spt:ccs`, expects “Claude Code (spt),” fetches `/sptc:*` strings, and invokes capability as `claude-spt`. `ci/setup/activate-int.sh` has the same wrong identity/profile assumptions. These are demonstrably stale test contracts.\n\n7. **Digest integration is not an OMP digest proof.** `ci/digest/digest-proof-int.sh` points at `adapter/omp-spt.toml` but checks for a built `claude-spt` executable, creates a Claude `projects/<slug>/<sid>.jsonl` tree from a CC-shaped sample, and runs `digest-proof claude-spt`. It does not demonstrate `digest-omp` against OMP JSONL.\n\n8. **Acceptance remains a real-Claude hook test, not OMP acceptance.** `ci/acceptance/run-acceptance.sh`, `ci/acceptance/lib.sh`, and `tests/acceptance-harness.sh` create `.claude` hook fixtures and spawn `claude -p`. This may remain sister-lineage testing only if the Claude plugin is deliberately supported; it cannot satisfy an omp-spt acceptance requirement.\n\n9. **Psyche integration remains Claude-specific.** `ci/psyche/live-relay-int.sh` sets `A=claude-spt`, describes a resident `claude-spt-psyche`, and tests the retired resident model, while current OMP uses `psyche-omp` as an ephemeral per-event shim.\n\n10. **`traceable-reqs.toml` is largely false-domain metadata.** Header says `spt-claude-code`; titles and evidence paths name `adapter/claude-spt.toml`, `plugin/sptc`, `/sptc:*`, ccs, Claude hooks, CC remote control, and Claude hazards. The registry has only one OMP-specific requirement, `REQ-OMP-NATIVE-TUI`. A green trace check can therefore certify retained Claude evidence while leaving OMP capabilities untraced.\n\n11. **`REQ-DIST-SHORTCUT-BASENAME` says `cc`; its test enforces `omp`.** The test carries `[unit->REQ-DIST-SHORTCUT-BASENAME]` but verifies a different observable contract than the requirement title.\n\n12. **`REQ-DIST-MANIFEST-SCHEMA`, `REQ-DIST-DIGEST-EXTRACTOR`, `REQ-DIST-SESSION-RESUME`, `REQ-SKILL-LIVE`, and many others have Claude-specific titles but are being reused as tags around OMP artifacts.** This is semantic trace corruption: the evidence may be real code, but it does not prove the stated requirement.\n\n13. **The RPC bridge’s `spt_call` still does not evaluate child exit status.** `tools/claude-spt/src/bridge.rs::spt_call` returns merged stdout/stderr regardless of `out.status`; bind then logs `bound:` even if `spt api bind` exits nonzero and returns no token. This means field issue LOW-1 (“errors universally swallowed”) is only partially closed despite manifest changelog claims.\n\n14. **The OMP plan says hook plugin removed, but the repository actively ships and validates `plugin/omps`.** This is the concrete contradiction underlying the root design question.\n\n15. **`REQ-OMP-NATIVE-TUI` overstates its unit evidence.** Its title requires native TUI *and* bind/listen/state/reply behavior. `tests/manifest-shortcut.sh` only checks command strings/existence, and `tests/omp-extension.mjs` only tests `decodeBody`, `drainEvents`, and `extractReply`. Neither drives `ompSpt(pi)` through session_start, agent_start/end, listener failure, queue serialization, state auth, reply send, or shutdown.\n\n16. **`ci/run-gates.sh` can appear green while live OMP proofs skip.** `tests/bridge-acceptance.sh` exits 0 unless `OMP_SPT_ACCEPTANCE=1`; native OMP PTY acceptance is not automated; stale real-Claude acceptance is separately gated. The default gate is therefore not evidence of end-to-end OMP behavior.\n\n---\n\n# KNOWN FIELD ISSUES — demonstrable closure status\n\n- **HIGH-1 executable collision: implemented.** `tools/claude-spt/Cargo.toml` names the shipped binary `omp-spt`; packer/tests enforce it. Field document itself still reads as open historical advice.\n- **HIGH-2 child teardown: partial.** Windows Job Object + explicit graceful kills exist and `tests/bridge-acceptance.sh` has a Windows orphan check. Unix hard-kill cleanup and signal/process-group handling remain absent; the code says so explicitly. No dedicated trace requirement exists.\n- **HIGH-3 visibility: implemented for headless bridge.** Always-on `oplog!` and heartbeat exist; acceptance asserts message/reply/bind log lines, but not heartbeat/error variants. Interactive visibility is superseded by native TUI.\n- **HIGH-4 bounded waits: substantially implemented.** `await_frame`, reader threads, env-configurable deadlines, and turn-failure notice exist. Unit tests cover await timeout/EOF; gated bridge acceptance covers a hung turn. Startup/get_state deadline E2E is not demonstrated.\n- **MEDIUM-1 head-of-line / steer: unresolved.** Later messages are queued FIFO, but no `steer` or `follow_up` implementation exists.\n- **MEDIUM-2 dead child detection: implemented.** Reader EOF and heartbeat `try_wait` cover idle detection.\n- **MEDIUM-3 binary collision: mitigated, not eliminated.** Known install paths precede PATH and the ready deadline fails loudly, but there is no explicit version probe. This matches one of the field document’s proposed alternatives.\n- **MEDIUM-4 clean adapter switch: unresolved as an operator contract.** Graceful `session-end` exists, but no documented/tested switch/unbind flow exists.\n- **LOW-1 swallowed SPT errors: partial/factually defective.** Logging improved, but `spt_call` ignores process status and bind can be announced after failure.\n- **LOW-2 silent empty failed reply: implemented for timeout/EOF.** Failures send an explicit notice. A successful `agent_end` with no assistant text still produces an empty bridge reply; the native extension instead uses a non-empty fallback string.\n\n---\n\n# GLOSSARY UPDATES NEEDED\n\n`CONTEXT.md` should cease presenting the sister adapter as the current product. Required canonical entries:\n\n1. **`omp-spt`** — repository, adapter id, and consolidated executable identity; distinguish executable invocation `omp-spt <subcommand>` from adapter id.\n2. **OMP / oh-my-pi** — the interactive terminal agent, including the exact capitalization rule (“OMP” product/UI versus `omp` executable).\n3. **native OMP endpoint** — an spt-hosted endpoint where `omp` owns the PTY and loads the SPT extension.\n4. **OMP SPT extension** — packaged `adapter/strings/omp-spt.mjs`; owns interactive bind/listen/state/reply, not terminal rendering.\n5. **headless RPC bridge** — `omp-spt bridge`; retained for callers without an operator terminal, explicitly not `[session.self]`.\n6. **interactive versus headless topology** — make the split first-class so “bridge,” “host,” and “endpoint” are not used interchangeably.\n7. **OMP session id / OMP session file / OMP transcript root** — separate OMP’s session identity from SPT endpoint id and from Claude’s transcript terminology.\n8. **OMP Psyche turn** — current ephemeral `psyche-omp` model, not the obsolete “detached resident claude companion” definition.\n9. **Librarian** — if it is a real product role rather than plan-only motivation, define it and distinguish the role from the generic adapter.\n10. **`omps` plugin namespace** — only if deliberately retained; define why a Claude Code plugin belongs in omp-spt and whether `/omps:*` is temporary or final.\n11. **ACP supersession** — define only if there is a concrete trigger; otherwise leave it as plan rationale, not glossary truth.\n\nExisting entries requiring correction or historical qualification:\n\n- `spt-claude-code`, `claude-spt`, `naming-domains`, `legacy spt`, `skeleton plugin`, `fetch-stub skill`, `ccs profile`, `cc launcher`, `psyche sandbox`, `tagged-input round-trip`, `interrupt marker`, and `boundary rename` are Claude/sister lineage, not unqualified OMP terms.\n- The current Psyche definition says “detached companion process” and `claude-spt-psyche`; current OMP manifest uses a captured, run-to-completion `psyche-omp` role.\n- The current `ccs profile` examples (`glm`, `kimi`) are already stale even for the sister adapter, whose later ruling chose bare `ccs`; they are doubly stale for OMP.\n- “bridge” currently names both the former interactive PTY leader and the retained headless command. Qualify it every time.\n- “hook” needs qualification: Claude Code command hooks (`plugin/omps/hooks`) versus OMP extension events (`pi.on(...)`). They are different integration surfaces.\n- Use canonical SPT nomenclature consistently (`Psyche`, live agent, ready agent, endpoint, perch, node, subnet); avoid CONTEXT’s unqualified “companion” where release guidance explicitly says not to substitute that softer synonym.\n\n---\n\n# REQUIREMENTS / TEST GAPS\n\nThe OMP port needs explicit OMP requirements rather than attaching OMP evidence to Claude titles. Minimum missing requirement set:\n\n- OMP native extension lifecycle: bind, auth, ready listener, busy/idle, serialized dispatch, reply correlation, shutdown.\n- Native PTY visibility/attach acceptance (separate from extension protocol behavior).\n- Headless RPC bridge contract and supported caller/topology.\n- Bridge child-lifetime invariant per supported OS.\n- Bridge ready/get_state/turn deadlines and failure reporting.\n- Clean stop/unbind/adapter-switch recovery.\n- OMP digest extractor and locate behavior.\n- OMP history fetcher.\n- OMP ephemeral Psyche turn/reseed behavior.\n- OMP echo-commune behavior.\n- OMP native resume.\n- OMP distribution/setup/update identity.\n- OMP provider/OpenRouter documentation boundary.\n- OMP Worker lifecycle, or an explicit “Worker intentionally unsupported” decision and capability removal.\n- Native listener-death and `sendUserMessage`-failure no-silent-loss hazard.\n\nTests needed to satisfy those contracts:\n\n- A mocked/fake `pi` lifecycle test for `ompSpt(pi)` that asserts event registration, bind args/token auth, state transitions, queue serialization, one reply per sender, fallback reply, listener close behavior, and session_shutdown teardown.\n- A real or stable-fake native endpoint-run/attach proof for the PTY/TUI requirement; current helper tests cannot prove a TUI exists.\n- OMP JSONL samples for `digest-omp` and `history-omp`, invoked through `spt adapter digest-proof omp-spt` rather than Claude-shaped fixtures.\n- `psyche-omp` and `echo-commune-omp` integration proofs using their current ephemeral contracts.\n- Bridge startup ready timeout, get_state timeout, dead-child idle detection, bind failure, state/send failure, and Unix hard-kill tests where supported.\n- Stop/unbind/relaunch-under-other-adapter acceptance.\n- Capability assertion matching actual ReadyAgent/LiveAgent/Worker support.\n- Update/registration/setup tests rewritten to `omp-spt`, `BigscreenVR/omp-spt`, `/omps:*`, and no ccs overlay—or removed if the plugin is retired.\n\nTraceability defects are structural: add `OMP-ADAPTER-PLAN.md` to scan roots if it remains requirement-bearing; rename/generalize inherited requirements only when their observable contract genuinely survives; retire inapplicable Claude requirements instead of leaving active stages; do not use one tag to mean “same rough seam” when its title specifies another harness.\n","files":[{"path":"CONTEXT.md","description":"Authoritative glossary is entirely the claude-spt glossary. It lacks omp-spt, native OMP endpoint, OMP extension, headless RPC bridge, OMP session/transcript, and OMP Psyche terms; multiple definitions contradict the current fork."},{"path":"SCOPE.md","description":"Locked Claude product ledger. Contains reusable public-surface discipline but also stale CC distribution, ccs, cc launcher, setup, CI, and parity decisions. Its open queue contradicts decisions already locked/resolved earlier in the same file."},{"path":"OMP-ADAPTER-PLAN.md","description":"Primary OMP port plan. Correctly states the native-TUI/RPC split and OpenRouter boundary, but retains stale slice wording, unresolved namespace/floor/capability questions, and an explicit claim that the hook plugin is removed despite active plugin/omps code."},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","description":"Accepted Claude-specific thin-plugin decision. Applicability to omp-spt depends entirely on whether plugin/omps is an intentional supported Claude surface."},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","description":"Accepted Claude hook architecture. It is not an OMP extension decision and should be topology-qualified or retired for an OMP-only product."},{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","description":"Accepted resident Claude Psyche sandbox decision. Current OMP uses psyche-omp with different process/model behavior; this ADR is sister lineage, not an OMP Psyche contract."},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","description":"Still marked proposed despite implementation, and its original single-sequence mechanics are superseded by later checkpoint hazards. It is also Claude-hook/translation-specific."},{"path":"docs/adr/0005-name-unification.md","description":"Accepted and executed for claude-spt, then operationally superseded by the omp-spt fork. No replacement OMP naming ADR records omp-spt/omps/omp rationale."},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","description":"Single-binary concept survives as omp-spt, but the ADR’s plugin, binary names, update instructions, and open asks are Claude-specific or stale."},{"path":"docs/adr/0007-native-omp-tui-hosts-spt-extension.md","description":"Only accepted OMP-native ADR. Definitively locks native OMP as PTY leader, public extension integration, serialized delivery, and RPC bridge relegation to headless callers."},{"path":"docs/KNOWN-HAZARDS.md","description":"Conformance checklist remains almost entirely Claude-specific. Some hazards may apply only if plugin/omps is retained. It does not contain the OMP bridge incident invariants or native-extension no-silent-loss/recovery hazards."},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","description":"Historical incident report with no per-item closure status. HIGH-1/3 and most HIGH-4/MED-2/LOW-2 are implemented; HIGH-2 is Windows-only, MED-1 and MED-4 remain open, and LOW-1 is only partially fixed."},{"path":"traceable-reqs.toml","description":"Registry header and nearly all requirement titles/evidence are claude-spt-specific. Only REQ-OMP-NATIVE-TUI is OMP-native, and even that requirement’s bind/listen/state/reply behavior is not exercised by its claimed unit tests. OMP-ADAPTER-PLAN.md is absent from scan roots."},{"path":"adapter/omp-spt.toml","description":"Current implementation truth for adapter/repo/binary/shortcut, native self/resume, OMP digest/history/psyche, and extension packaging. Also carries large contradictory Claude hook/inject/env/skills commentary and active tables, including inert CLAUDE_CONFIG_DIR and CC echo-verify declarations."},{"path":"adapter/strings/omp-spt.mjs","description":"Native OMP extension implementation. Pure helper tests exist, but lifecycle, queue, auth/state/reply, failure recovery, and shutdown are untested. sendUserMessage failure can silently drop a taken message from the sender’s perspective; listener death is only local UI state."},{"path":"plugin/omps/.claude-plugin/plugin.json","description":"Enforces plugin name omps but describes a Claude Code adapter and includes claude-spt lineage. This is either evidence of an intentional cross-harness surface needing a decision, or stale product identity."},{"path":"adapter/strings/skills/setup.md","description":"Broken/stale OMP setup instructions: activates claude-spt, uses the old manifest/repo, and offers a nonexistent ccs overlay."},{"path":"plugin/omps/skills/setup/SKILL.md","description":"Self-contained setup fallback repeats the wrong claude-spt paths/repo/profile and therefore cannot be dismissed as harmless comments."},{"path":"tools/claude-spt/src/bridge.rs","description":"Headless RPC bridge with Windows Job Object, reader threads, deadlines, logging, and explicit failure replies. Still serializes rather than steers, lacks Unix hard-kill cleanup, has no clean switch contract, and ignores spt child exit status in spt_call."},{"path":"tools/claude-spt/Cargo.toml","description":"Shipped executable is correctly renamed omp-spt, closing the basename collision on current builds."},{"path":"tests/omp-extension.mjs","description":"Tests only decodeBody, drainEvents, and extractReply; does not exercise the exported extension lifecycle required by REQ-OMP-NATIVE-TUI."},{"path":"tests/manifest-shortcut.sh","description":"Useful current manifest assertions for omp identity/native self+resume/no ccs, but tags them to several Claude-specific generic requirements, creating trace semantic mismatch."},{"path":"tests/bridge-acceptance.sh","description":"Gated headless bridge E2E covers message/reply/bind, hung-turn deadline/failure notice, and Windows orphan cleanup. It is incorrectly tagged as impl evidence for Claude REQ-SKILL-LIVE and skips by default."},{"path":"ci/manifest/registration-int.sh","description":"Adds the OMP manifest but asserts/removes claude-spt, requires claude-spt:ccs, and resolves /sptc bodies. It is stale and cannot be valid OMP registration evidence."},{"path":"ci/setup/activate-int.sh","description":"Adds the OMP manifest but drives the claude-spt deregister/reactivate/profile/string contract. Stale acceptance."},{"path":"ci/digest/digest-proof-int.sh","description":"Uses OMP manifest path but Claude binary name, Claude session layout/sample, and adapter id claude-spt; does not test digest-omp."},{"path":"ci/acceptance/run-acceptance.sh","description":"Real-Claude UserPromptSubmit hook acceptance, not OMP acceptance. Retain only if the Claude plugin is intentionally supported and classify separately."},{"path":"ci/psyche/live-relay-int.sh","description":"Tests the obsolete resident claude-spt-psyche model and claude-spt adapter id, not current psyche-omp ephemeral behavior."},{"path":"ci/run-gates.sh","description":"Default gates run shell/unit/build/trace checks but gated OMP E2E skips as success; native TUI acceptance is absent, so default green is not OMP end-to-end proof."}],"architecture":"# One-question-at-a-time grill dependency order\n\nAsk exactly one question at each stage; do not descend until its parent is settled.\n\n1. **Product boundary (highest dependency):** “Is `plugin/omps` a deliberately supported Claude Code control surface for omp-spt, or should omp-spt be OMP-native only?”\n   - If OMP-only: retire plugin/update-post/hook/UPS/CC hazards/CC tests cleanly.\n   - If retained: define it as a separate topology with its own applicability labels; do not call Claude hook behavior OMP behavior.\n\n2. **Supported topology matrix:** “Which topologies are product-supported: native interactive OMP, headless `omp-spt bridge`, retained Claude plugin (if yes), and daemon-driven OMP Psyche?”\n   - This fixes the domain boundary for every later requirement and term.\n\n3. **Canonical naming:** “Ratify or change the implemented OMP tokens: repo/adapter/binary `omp-spt`, shortcut `omp`, and—only if retained—Claude plugin prefix `omps`.”\n   - Record a new OMP naming ADR; explicitly supersede ADR-0005 for this fork.\n\n4. **Capability truth:** “For each supported topology, which SPT types are honest: ReadyAgent, LiveAgent, Worker?”\n   - In particular, decide Worker implementation versus removing the advertised capability.\n\n5. **Native extension lifecycle contract:** “When a bound native extension loses its listener or cannot submit an inbound message, must it retry, reply with failure, mark offline, or terminate the endpoint?”\n   - Lock no-silent-loss and recovery before writing tests.\n\n6. **Headless bridge turn policy:** “Does the headless bridge intentionally serialize later messages until `agent_end`, or must it implement OMP `steer`/`follow_up` for mid-turn delivery?”\n   - This disposes MEDIUM-1 and corrects the plan promise.\n\n7. **Headless bridge shutdown/switch policy:** “What operator-visible stop/unbind/switch contract must work on every supported OS?”\n   - Then decide whether Unix process-group cleanup is required and define the cross-adapter recovery acceptance.\n\n8. **Session/context ownership:** “What OMP-native locations and identity rules govern commune/signoff files, resume, transcript lookup, and Psyche state?”\n   - Resolve `.claude` versus `.omp`/adapter-owned state and align skill prose, manifest, digest/history, and Psyche.\n\n9. **Provider configuration:** “Which OMP profile/model roles should the Librarian use by default, if any, while keeping OpenRouter setup outside adapter code?”\n   - The billing boundary itself is already locked and must not be re-asked.\n\n10. **Minimum public contract:** “Given the final topology/capability matrix, what is the lowest published spt-core version that supports every actually used verb and manifest field?”\n    - Derive the floor from published artifacts only; delete the inherited floor narrative that no longer applies.\n\n11. **Traceability migration:** “Do we clean-cut the registry to OMP requirement titles, or preserve Claude lineage in a separately labeled historical registry/document?”\n    - Recommendation: clean-cut current requirements; preserve lineage in docs/changelog, not active trace entries.\n\n12. **Acceptance bar:** “Which live proofs are release-blocking for native OMP, headless RPC, Psyche, resume, digest/history, and cross-adapter recovery?”\n    - Only after contracts are fixed should required stages and tests be assigned.\n\n13. **Documentation supersession pass:** “Which inherited ADRs/hazards remain applicable to a retained Claude topology, and which become historical/superseded?”\n    - Update CONTEXT last so its glossary reflects all upstream answers rather than steering them.\n\n## Recommended document authority after the grill\n\n- `CONTEXT.md`: OMP-native glossary only, with explicitly qualified sister/legacy terms.\n- `SCOPE.md`: replace the Claude scoping ledger with the final omp-spt product/capability boundary; archive the old ledger as lineage if needed.\n- OMP ADRs: one naming/product-boundary ADR, one topology/capability ADR, ADR-0007 retained, one bridge reliability/recovery ADR if headless remains supported.\n- `docs/KNOWN-HAZARDS.md`: only current invariants, grouped by topology; retain historical incidents with explicit resolved/superseded status.\n- `docs/OMP-BRIDGE-FIELD-ISSUES.md`: add per-finding status and evidence, or fold still-live invariants into KNOWN-HAZARDS and mark the report historical.\n- `traceable-reqs.toml`: OMP-observable contracts only; no OMP evidence attached to a requirement whose title says Claude/ccs/sptc/cc launcher.\n- Tests: separate `native-extension`, `headless-bridge`, `omp-session`, and optional `claude-control-plugin` lanes so one topology cannot accidentally certify another."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":1445,"output":10453,"cacheRead":224768,"cacheWrite":0,"totalTokens":236666,"reasoningTokens":1582,"cost":{"input":0.0072250000000000005,"output":0.31359000000000004,"cacheRead":0.112384,"cacheWrite":0,"total":0.433199}},"stopReason":"toolUse","timestamp":1784087300562,"responseId":"resp_0ca4af36969c80fa016a570304b7a88198ba02165da46d3e03","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ca4af36969c80fa016a57030698d48198a49b3a25670ba8db","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwMQoiGVwEvmWtaY_D6i1x_GjrwslsmqS6uarK5rijOihlWkwKfUjrA5k0hGqCprFHzSeRAM6z8-8O4674FfSSTQf0MdfmTFhdl3BU-uZwYzFyoVzi9U7D4EuZ9Nmg5t_QSqAA2Dw9uf5QGolatLIl6jXzratKUfLuamxhDXbcEViOmHS1mR22-7IEN3IRovigy8ka3n8zGiIAf89HFGt8YxKmectBssOX6v2tzyJp2SakSXkUoW2kZZGRNx6yEwXESFIFhWTQyEIApMSaiGIUp8gsyPdj6tBUzHpaoFF6kg8ehrW3_EhAypKQ3JGCsSHYSq_qIIZR02ecYzPfOzfZkkoZc1Ny5VR8YRFJ5IrV07YYUfB4Z2twSL_mN_6O3I8N55ITp6juEcLaj2Bv0aU6ZMqeNz-iyWdRaKm5yegezKo4pQCV-rUWscg6LdVcwVqUsJytbKzUCFJrAHqLaGOJTyYLf2wuSii6JTrzoHU9Dw3Oc3ZFUnoyBXizsIN6icUuyxfiUIRGVwUYmmrbTbJRC-nfX2ncQrnPiyqx1P2N5nfkBK80PfYkl2_7dpOHxd8mWD1-Q2uAsviyP1HT5FYnUE8hzRpgbHUl9wdqTNShDq6O92UWquzU2VgnUhmdZaHjznpN7hC7lljuz5q7fRt8_kAu5Z4_nxNIDBibKPnYhqWEnKv1bcvw6k_7B-LWG-ezmcKrqTM86-tPmadkKuYpLhVvVVgsdyXItupRYVV6WGP25ZLxNE3ZwkIbZQLYgM-jwYHqgR72s12qpFBT_dHknCwXnZj9ce0U2A6Qa4L_2_SIOU8rcMmLYN1egHlD68k4hVqHo4kLQ4nT-YmIaSgZf4QGqCxoNUWke6uBlDlqpMO03rAifOShYB6LVS93S0LNiu5JZ6HN83cMVi4-b50Mnu-02YanPiNgDRG46PEhJ2x06RLpNGQj8JiihIa6R_0eUc1YeSsFngbncnqGkFsZrn5n_u3G-3iKXkuAG86v7L3yWpjpiK0nKgq8V8L6wxu5EomWMsrJkJPlkrL27hePUFCdm-_mXlxCYatXkG5g73T9qUC2FAqwHj5CZcKePR4QPXi8iIJWGXH8qQ9rE8eXGpcxs3bHjqfgKfQfATkzJ4rQBWk6xFGWDVIAQiEnFUEouRxChhBUvJm_LV95U16h1EZergyFdBGyutesMgdIeDeNcQNZulxL9E6KsiJ3Mw2xnuqPwTsMbmb4OveBtP7jh4ThCv2eufZmoxrgpxypB8EQNYKafSNUySxCCagJrx1s4A-5gk77TBEUCYvQyM6ejGu15--4h31i3LSLPFOVuFXZsTyDqBjDX0YOjHqaG8GzAAw_TUmszp4_qWQ3wJW6TidiYoNo3Jpdtkpg8rpDueO7wrJbG5zOQA1WsW9eDsru6SXgUPtibJTOIkR1JMmn8wTzMyAUWPV9R1eh-1kGh-dIcVgUkCpaw7S1L_a75ijUlPdDZPWEYsO2TlkKo7Co3JXy8bHRmT3krmYFM5hjGS7X9uIFeAKuvc0XOSZO8RefAunNZj40IaLG-8lJSdFEp8cpjZQZMsOAr0MCad54dwi5ymVUFnjzBUFhsPBzyA3oTBywZkt5UzHQvbHGdf738fesKtYLD3kupKPvYOSptbRLmPiLpPkMeOky-CZvpD7LU5FE0jAiamrnXejXVeDarv2PpAxm324zDELk4-KHNKMENDG-YNdcDVqGda3GQLbFM0xTLJyycZ_2E7Y8ZfwNLHUxTGN5TUt52aH92qzepBumArixLB7XxmaQq7o4RQng4-Wr79zqLmScnFEWPYfiAEzGAi34_XGCbJdRfMzPfg_4krMp-aqB12gVgYKjsH8vi9feRQy-zzcliLgpWSv6WUIfvpUYgEvxWBqqJqWCeF4rQ88c61wQUYjmvU3rAGGS2RjszXL0r1WruDtouUpkxxGeOieIq67uVOtVUKox8VlYK337sSPpNMed2zMYQsopcgSEdN7K58Jrz5S2MRhHDnlkCINF50oDQ-LdtYfIdRU_uoZ_xvpoEJl81OwgRNKV8c7eifnyKbVnm_FEzaKeIOP_tqcu6evLW6Wzt7yoWlIEBIG9lr5NK_V8QYbylMnQCRw0qSWxGCyPBts38--h7hfZIo0SHw5Vo9sRjD_CxLyYyXceJ_R6x4xtgcUnACY8_6MQySxFBERHE2k3Mv5jCg3TzV3f12uZ1EfXGbMRnZXXu3hvRb2yYcqVtqldGNN1GV0eFDq9PJUcMw10GuIJgQz4NVXBpKtHhC9tXdrYGTfk1-l9m3fzyF0tcEfduUnkSRCR8e1TgjIrjx4kD3oFUxGo4EDCxkdd_BbKqN_GD54Acv9M2nDGq8wGWSpSVwxT99kmQZQ2tcQ1lAACUnVmIhloSPGWoSgC4Xx2P2VZsognKVjWGbVaKnpuAuOgkxzWcDqobQHgAyZmkrdnTVcw30FApXGrLP_hptsDDU6LFANdLgbuvc_GuxGdnuUYKUZkEElnTBXy5BXNigUtdOKq9kI9-9o8MJpvhzePBjdLt77b8T0TL2oiRnoJ6AbUfEb6ot2CpuK1aFHTkoTMWM_Mc7X5KBCOrnUyjDIbPmxOUqI7RtrVSThlRPkVoscNGU7qc3b2flmt_SRcJAppsu-eZ-D9u3GBkUXeR6FjCQJH8Md4sGVbfmgay3iqfe-wjPN3RcGf28AJOMC1NA7UZB-tGiKkAsDi1iEsKpP7LwL2OtFZfu8LjMBGzxq_HeBz3nOeZtWCGLq2wOvuOUkwza53JJgpKp4VyuybMUnyNYmVwdht1_Y4qNvSZS-eLCIgU1zLj_t-OdVNXXXurYAmgQMZTOxpDW5TSgrXL-NCDKRWbzNxFvb-lnkWaod9Uv_0zXubjpcviDbRK4bbQxiZ0Gv2wTVMrFPoV54AIQ2nv-GNOH1IZVxKoFD5JsU_pcaENYzv09dmibLOvgshZ6XS98g4btk_6kv_iwDz6EfxRIZHCTcNAATpDgAvX8hL8Vcp4pTGfa8PLHkstOt1nFVfequySZAyLzlM7MlXXi5e33JUYLc0pN8c9HcSqY91mtIk0xlHCxcsBb_PQk_gA6cL6wqJK28H9x5SlEWyWvNzf1wLX0a090ljd3TYNc8dzmLyNsbMKwjF8Tb-8_HfCYGcYi_UzbyxzHmCSo3MvnnDsqXYNvFaxOjoIm12mUto6pZi5yWQr3GGe1H3qYB8yDyHbwvM-_E08USov6DEcflsOui10RFW1zcVDS8MZedqpErt55OXZ5jwyUvJcj8ji_NdKw8S1Z_FFSpamoJztL_YCGjsHsLyabTe7yqDayfJOX1dfbaf7ZQItmu2hi7g4-JGCriRumtN-wjbUK-vkcK9vnKMsHji2UmbG-MdLBbDOwfzt08DvA8AaKjS5uXFwA8MiF-TjEptHNjC5VKbknAXU4nORxymoFV84eQfv42eeGzGcePMdPZiz0PsjZq8THmR5RI6LF_1RmqBlap4StrqUPUkEimpqiTKIvM4_p_-cPoRS2_VDiWXRsxlfQF5zEdj6PFSD2I3lBrt6amShVtG9mIYcqnNjHnDpk57VUWKZFM83lTQgs3sq1ofZC26o4LjnYB2SekR2E2b_ut169kluMTdAmp7DJwh7XOq3DxP5ZwUVhRFvghX6xwPDuTtCl-byu9Td53NXowEjZAYqtXisBnTRWQX5mZD7NPDuXkelXiqzDOIHE6Natmugk_7ilIihgCqdVdBA1ZaUiWKBh11xJF54pdKC7eBbsAs-8Us7XAkzcvopZZHzeZ2Qan8hugv9YSjcLzcSzu2Ue2suJXFDXKzmvCT810GZWiLGyoT2XlFo_YhjEiB4BFgdu23qd_36h3RmUPwqRPKNbxgn4XXDRqG_Ql82BekNo1sunJCg0xon2Rb8_7pz-hx_LY55QYuKELKcoX9FfQF7a9hl4Gi6DRnAa_6mW7Q2l7-CdP7QrjYKuMFPELpdNvIVVKnxc34pud_39R5hXdmgkwkm651q7riKMU732_jJO5t9zyGUpEnmtFZPI0iVyBCp-ulnf31UHdYFd61YD-tzdjFsKiX8Ddtmmlj4lSxtcMfxqayESsWAj7JMsLF5cI9HVteR2zFUPk0tMr-cMR2WmCa8oe_yWLpXsbQ_qqwpFYgYQoE90yDVSKkQo","summary":[{"type":"summary_text","text":"**Formulating detailed decision ledger structure**"},{"type":"summary_text","text":"**Analyzing binary argument handling and test coverage**"},{"type":"summary_text","text":"**Identifying factual defects and bridge test gaps**"}]},{"id":"rs_0ca4af36969c80fa016a570310c3a48198ba936c462b391ae7","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwMZwzbFAJ3SdC2UZjs5CdrLZG64viJ7ptGOJk3DKP2whD2tMaRVMhq2xvfDrdWSnSsQDy8RRJMt59V0EGLg797BUS1nsGpHJEEinPzzBTAgH0YW9L_oEKh_RJ34UoLrvt_hkCJzj3Ob7FKe2mAx1FO6ZLyMkzbjc0JuoEBt9qfseFP6Rup2QsL5Wg4yaSB23k0y7BF8M5Xq1uGtXWnWWt1OjgAtPFLY8C-xmIVaaqjPmNyg2UI0dKfy4LDpb78iEz0lOLidshocVDIUd7loN8sYon946l1Vj5XILSTZtJbCtOW5mbWS-2bKtMDUGGOm2TVyh_mpb0JYkwcy0ZVHJ0rSH0tzeKaxk25TTuYQOB-OKFiLOZkqQf3zEvARvt0vwNL7ldsX39mWEXgoT_y6am8BSuTp8rUS3YztDo7H1VM0xrbODlPvxu-9Hrkj38pfVGk4cRZYsWjGQZMrN4wzZp_9pROIJfszlecM-nGZ7ge8ibtnnTohl7RCPa35KlZ5XB8go2MKKcaY4vsaLiivrNSoVR1hYfB1izyKwKBWGII5pOZpID-7Q9YrHhBkXGANJG18aY1mTnVP20-y4ycAEizTW1k54rBG-dBhk30DCAc0stNpy-8xCKP6zIt5fOvviRNpbFEpxD3jFMH_5xlMTxLSx0_uYy9Gfso3MDhAw6y_w1GW39M0p441LuFSOmIneBbCUYCxCB8n3QkCy2Q-EwQBoJg7V1qYMf8TDv5l4b4w3-HeGiGzEa61ZSm0SF0JhU8l0B0dWdrrLkVc8ZaaeJLBxHoe66GjsYGdAA_C3QQ9Tx_Rp0SNLsMYVOKskxCDPo_XuE05qEyMRpootyk6iqREymH4Z5zz6fBp5MxBMAt9xLPW9BioLSdV--v0KMizxS0HrCYowzaej2SCNWizZEanh8uV7lK_gO8V3L4vJzns9Zjb6Kfd3QEdG63WLSUbOy56nEv3PrcBJXErzbJIBx-I3p6dDXuP1__0ekWrgQPQA4IDZdU21ltJ-raSTdMNVINaBKBixoPnK-LxnQsLjZ8v0-Zz15OgjuCDRFK5vq53K-xY0KIphH_EB6XiawpIVj8R_cFfivCd8jgQXrvd0LS_cThddUuVnaizodYXL0UDbUPVynR8jsMu-Ay8PxDzLRHkXmsfLW4DATofa9R5k61CAIy41KOVk9XAF25JSzHtVWLAxAPwZgUJ2hYErBMzFBMbj7Avhp19OyolyR5Eib2ZrvWY33wvxhpeHGDy4dDPptKCZVqzA5SgHfsoQCg2mDLeXQqVPGOjqgrFDQtiJFa8DhOKVdjIx8DV3-EmKEzCAnDloY2rMXd-QT10gWMvXCt1KOhscGBw3G3emqLjboNVZ1y8yu_zI4kt2L5su_hHCwG44WNcpGr2TquAxm350uXZ1QEtXOXPRYEckwv9mmK5tMTVn-wFe0n3CJeMN7OCMZ5FD9vlwJFR4KWmy5sAqyiCgUas5aUXjh0JCtwWMt9rO-y-TNm7ONO3ndTTSOYjGSXRdfgMzKAi-qFn-_8Abd8so_wReArtX_jfwtMmv22nxrB-WzAMhAMXZ7PiYRCAQgof0VtNElmq9WR3CySJ6WkigxofjyE3WrAVbpeDIKHu-WhmKV3yceDqGxmAWMEFhdqpsysTPTV8mZHHmeIx82p1niwAWzcbeDMYLOj-E93ALn-rZzewhh8yfELKPqoBFwWMluEglIMo4mlVQhL4XviNNw2-_M7rzfE3sfiIEHJh74ObUjo68O4oF0V_jtHgTN_O2IvzxovULQNQUzlN1r-fQZvKSCvb2MzcvZAs5IS-f8-Q0boRwEMMeqAhf8ZZ2LWlSy2chz90J9R4NAEMolzldm3bzsqA5MyzJaBCl9TcVaZ_qDnhi1eS6hzDchXPZ8_6z8asy2SBIaFwqgRU3bL0XN23kC3DhLmvWriAtnon0krQ8wjFYMlQMRY55AaSwRQnwtl0h6_RAOl8APdhTkGJ5Cxow4FlQD9NLsNm5wcOvKZj39li6G65I2OHKMwNP01rcRrPcTwBVmlU0-5WkXG9cGfqOfvSUp1YJ714-wAMIpcoWKNqfbTeYB7oP7IVCPBHOrgppjMVYGbrmESTzH7neEkJWB_2Rj4tbjilygDMyKvFhFX3S3biZhPcmAyTTFFvsXU9ZQTvCmnAyYjZVksXdWU6cXEgvRj_sOiAliFYYwuntduzqTkCFWVPO-LsrpAOL790E0-2w9y6BN0-ulVIO_8cAxcul-60CFef8OTv_Q8DszBN3hMLBwyk4oekBrg0tSo3bi0Z-U6QcpLyoTw7g9XXAtumS3aN3D6w-AWm_JMLpWshrK85_5W80Jmf585JvP3qjN43PpSS2AwvRJ0eQbcFE9bqSfxsU0ObB2Gzw6TOH1SGu6ghLNcUXVPbslamnQb2Xe1LyUyRK5uXcD7Xt4ggpfkSY4b5mWRhLdulrBTVzdrKmimChOapMGZJNq6BS1JnUjnRQoQ2WO1DZUGseR5KG3NfwLu2hEPL55xXgQ5lRHM7Oe5SH6wqnTWaPGX7vOt1Bm1J4ZqeKObG03hGCIoG8u70V9fMD0rggvuU_6ENUnUhg81XXy8fGONBcmQ4F6jOj9N44ism529fWBSZOorwhkoCqI3wK-Au0hx7BpLwXoGSUSWYAcUfDi2VnWCmPZRfF520uEyvFqk7vlGJsIhOlkCHcZwRmkl3M-g2Unvag3VEKi1DsJU9846Ysh5cGY01gZ9oSqiwsOQZcdjF7SSDp4TlZxOjbm17yzessZ3hoN2asASNlqEQ-GRPvadt9xehx42XA34Hf609PFX9DClGxAnVjfH63qJ-ezY8naINbh82RyuedKdmx39qF7qtLq0erb_b7CxhwOsth-an9Vop1kEZX286qe8WEh6va1fOsf-O_MxltWR3KPJUrZ5NxvRy2kcpkdCBOTIMSDSy8k4EcdP5BjcDDc2O1eBqtCr3u7mkW17FM4DD-__o-IE320F6QCiE2aQ1dUmL1YxwlS2hSWvF8BZScrRg61K86SIycw2O_nyIDdI9tXpLm5etdT6Mvq7eZLu3W4pTRTUESHGNMnk599czziensfGriCrJLdT-ZlCfzL9Gs26IHAkMhY5kWBh8t-j_Ztkd2hUw-Kb9GqjNgbYZqS9mjqBKVT3nYItIjwXL854Azq6QzEGzQDkAD7W97AKTWZlh7A0Rk4aaCkkOA1uhinh7Rt4BJmPBxlAfeVWdlDJktlN1mm1ENYpUJ9pBG_hemsXWMKoy70OVB3w5x_LX1BwLa0B1hxqnw2rtnZlO4ghvtggQ7RqkJM2HF7Bxp2s0uhKBZRkFw7vLvqnMxAzoFtZurCxcFQ_G8GzHi4zAxZbHqEQ45bRn8Dx-yB9SnkNHQSHs4--0RwbNz1NPMZlBzwDTC5AphfO20okRFFE3GMRZwuah0q2dUaNjS2piQ2i9Ez7TfkwmbLDfZQT18apvBrdvxOUIX_Ln-zSj2cWvN16gEMwsVsu502FuZnNhxyXCctPHotKS9bLSY2BMTiMxdlfON0NK4Do3GbmbX3qG8zxZOxjRbKcovfoXaOV1_56bgpOS3s_oJy0p0ydm_C1FjnKznZm7J_q9dDnh3lHK2Nwzgq5qbKvNeitf8tNsSglWzgiFrw0pu-D696ApBDar-qfar1QoFS7ghQNjji89hju3D4xR4N6EFelukwpKbsX_5r3q0AI-iTWc3x0k4T6OevWTVAlPTyNXPIekbqMIdkB3g3WBPNYgCmIkTtRYhKgBIgMEpA54NpZydDy6dX_35L3pIi4I9Mv9XqXBXSWOiZsc9h1m-_e5yhT6uYUzcVVmfFny_uKLRTSR_3Be5rbeRmk-iOqaZyAumtEN6XD5OX48r57k5nlzg1TQOGxr3s0UNhSQofltsrs3k3ny4z9L4PowOklz11vTJqhbJnWwM-R726w0UOUi4CDyeFlkNr14SjIB0m8QG9PsyWF1iJ9NH3ADsHBpyju01Jt_k_ApRJAtG2wEkzZJEnQv8ddF4NPBlwr0dpiPzh83l28bZcRXLHvU6HDxWw==","summary":[{"type":"summary_text","text":"**Designing internal plugin and API scopes**"},{"type":"summary_text","text":"**Locking OMP integration decisions**"}]},{"id":"rs_0ca4af36969c80fa016a5703193eac819890a4b4fb7566f2ec","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwMjJNP8QD6LZmUVHmemnV2657aer_EmyaW_cuMEyjOOrDLSoWQwTQqCc9Wco8y67CwDu1sp9Z6AWah_CTIYSNNBtAYPzrVdgUkL0UAe5HZic-nIHlLTmELTXNSHWywrUei03A_KCKhik3NkQjAS3LagdwqIFg2A098JBUURcIHHgOxkT2L1NPrqKkmooOUIo9WCFrvaJhfkj0CTTBJSaTfHkz6hXWpsWNnY8fCo0Bys4mfqST9E6qnFz7jf5-iayJHFw7Ak_47NzZ547MHI71AMDft-uGaGoAXkECdIL2Z7_jhndml1bTwFR2lJF5Z8cMWdIMY7affq_mEEUklAuqAnjxLNsy5M9JvhN8wRMzQdBRs-_trNGlBuQW7CV6lKrsrOaVVp3PAthKVrFjsvknlgwOduMhnyy9Kw0yQi8BV1Z562l3LH5DLCEKcxeIQXoHdI7Zz_L4LkKT0i9NoATCWrlJqL38h3P6R7w1lWb3I5owmxlwOj9xiKopMpAkQmHBkYyt_M_BU294fh46sJlkRNbcvMu6L-EvcElQk3Bz29UuDmTya6mllSyYaSZsyWNkqxWKMtKBaSftX87vXcpEoVoFITVY5nR6RGSlpXeN_8NUBdq9Y9cU1HImH_d6YoBZwfplFBB_NTFes90ImuDtyDV3ysIoCa3bl7_-YX_DYyFMF-cd5Qe7ozoT6KZxI4AJvS5OivUTVsHyqp1P42VuukX6UA63MS0ZdaunJoeNHK1lbPacRIPmHhp8cyjP_vicWdjo5XYi7QMhtHy9_SgZwhiDFDQZNvyuUc-tqpDS_wJPj20snmpcDeEqwuoJWUCJRe0V6B5ZCDBBmYkwlkyz-NnlE-VLsMZ2sFj34RQmciMAOAHrwq1q9JqEAFrcIjB05MQyu-jMdUT54z-8-8Dpp6XQhhp7bNusNovh9vtvnAgJIm_XH80muZoJojNPyox33vdcFf2J_lHWbiuXX7hwCbJvC-HsoAh5_-vnlDIO6m1faSHJ048ElmMeE4e-rgsXYEExQ8BzydsiLdbKpbMLNlFC1try7H72PIoGeTgr3jkjQHhC96dJKiKdS7J_4JbIczh4iS_vU9fStwAFCfoJBZOxApum6stpL1hdCsEKNrxpi7maxFTThbGvlGqLUb0_SAe3pxuRvdiY1M8pJ6aVQosSD17665021xFyTHldHromG1RO7XtgfaEsOQISSuJloyKc5EMlYURoBEVauUbwLH4Viiodv0k3QyLfcU-MBh-FH05aqZ7BrHVwOv1WBDar0vKES30bVec4HKAr_pLXrONFTyyozJBhDe0cg2sUIhPbKCNKCpcjJaBf9B-5X91ln4qj9fYDs44O0C6F23y5cCnid6gmmstqwc-ZcTJGLyTbURm_LsP4gTHIdzFlEU2n-A19JpiBaR9S83DcoCof92QLOdRKZ8V8cySC6C4GrEBNOczBZGgvGujgqy806bitvt7SleuDGkepOKyk_HsxKjnp4n8s6hoBNY2xmnsT_NYfGcHtN7D5pcDRLph-kKoxCBgzw8x3JPJLWYVeqfZeawNIfLOT-BdMa-9Jvy38mdun5M_CD_lfvwygjP4GNAzfwOJkRh4eZKpS5ILamUmxzStMIydjYnImwHeHDadOkcx0JhSqM2s6F1C6OyBxkiaw13mewxDFKCM_Ii59XAZFolh-ntDdwMgt1cCh73TRAGrBlUQ4U2jVLsO_AwKrwzGGnQn3BYSQjJfiMExzRWDelLjxdcQWADT_Q9R4gyOnaUP4Ogg3rKTlQqAZB9-2E5EJshegmoU4Jk2fXWfFmzSV0jGJ3oHF-qFFZ5biLnZsTF1YSxR7TiTGE661Oux933LFytCSppfGOqmbqDEyzKsHkgPG5FKnUi5VTKfzLg09WhtzXayCCIhdQmPHSqhfj9gHwueP-tvTGDTNQQUoGL42LVEoJBGHpVsvffhOjNCSIfhePqmX0A4-9JsQUsMRXqcShFeZy09soFru4yInF-nT-855v2nwpdtT6wajNjTCnJJXAnJI27QD_gDY6wBlfqU1OYdV_P7C0uGVPPmzylD0EBl49aDHQLy4jhYyK8TI3TdctJN9l6Hti9SWUix_nPamvMM0vX7_CyFoWtoe4_kGQK3UNd2B5IkQeg7NYZlCgSTfCdFPaJZriTWn06qqYIJjN4tzfVKzThgvgFZH-pWHYUHlFe7_HYRQ_hjsuqpnkqYECLc-4Wls_CSlFtA3-mXwcIxYlr4Jz7ujCsvHhMaLVW8Qyop2X7_yaDFXNm6_NPjSkf_opi8mO0bvIQJETjiinQORYYdMd7QQyabcSP2DYk9Xpbq39Bg5h-kSfmKP_z0Ssyiv9APxIq468mjwyvmA36jZt4GTUNHqzYEopB_q8yW6BpgrG4ers6RY-gLH1WTQ_JHAYvHtafWbQqnx-6vchau8Wi0LYK8aU6SsiU-sLqZzonJZGLu7UB7MbC5Q184TjspP6IF_zbm2dvXQeSL0y9uRSTnr78Oga8S5fhfwClZPXyY5d905yeRCrTIWgII7tM5yGJ9m6NpyggaIkjSUy5RRGOCJSr7uYqXBeJZewxm0L3btyjPRroCAuXGG21NR1VU7ANOiukMuF4HQEeMb64BxhmtQp4sYGU3830wcF365B6T_R5UW4tXcfjcew-XkrLa9PBBscbrm2MvF7chPgWmMxiVxtaA6q-RTvLCu8vj4uT1eNWM_DbJNa1VsuvAWdkW0DC6R2578rZhAkgTwNjEu4cqEw-3WDTsbgwABs1PzEXLN4zSmV2VvaYz37zv5MvWq-QtUAt4uXGUP4S-XZXL1RqWln_JSzRwxxpqdQMPjGanl5Oymn6E1w_KDKwMaUiM91PtYArwuJiC_n3WmfeUFQwhhT94r8YgrS8t2doQOKigAL6Ymol3GWyUlylNSR8eBGnKnHbrKpptx94v3qazMyNZ8mzVY0fe83HslxVMuXiIs7KIqPKDW_OIJl5nqNCX7UZLiA7C27BPTGIdWuPkmfU6fIOgS_99vKrpX0iUT49EDCJSOlx2bD_tlx75ugz6d7GXHb_ZZLAnh8ZhWhRWBz-Hilk7U4g_JLKPwfBN42f41Yrdv2lIJpBRbHTLIn5GeF23hsx2mzk3jT0IIPdLOBC6BC_dxMomq_PAnuD17gU33unY7GGeO6_4-waKukmMRdQ7EzI49X80KxMyD8BicRPJemIMVXd4bqAsM4QFBFL0-eSMpb2q23xZ55RcMUgoAeqeSnjFfBPSXwYZm6kHDkqjFrJ7t-aB3vIxfiKwYk6HKdVB12qGVWzSDCb9VEP-AbD04GWFEidUNse-NvOz58vXu8xfCGTZyi4qPnTeKG4fW8Rx-B3786pcs76I7n4Jq4rGmOT9-FPwrBXLVJw6no-Ho8EtFjWJ2Np4FBlW32Gm0xQsMF37nKR5VKZnjxnvJts9-0YPK6MZpcxXrO1WLUY21Jh9y5S9KQCvnfqDHaieR_Y-AXAa6okUN3m7dc50lOqT3kdJ93DNI_0d_eoFzdxaIb3jbH5_yBmWrv51ntfbD9KGre1PwesUJd3CFidIvUobR4ye6ouAp7-kebsnX0uQFtrzYezxGi37u7v08xVzQEzPk0h5Ty7I5Zm0o4D5WQS0hwjx3HmwsMza6VETBzREtRsh1N2VY_OW0Bk2-nid5FDpMKyyRHqEs2KmM947uI4omYSfRpXZXlg6wvZjuN7lzn71dXkWGtb3a_Ft42nHcKvUbSKAbHXLwWsoJgkTI7qm2jFCTtJFKPqyIYJdSSnzvx6W1x6-3MC9ff3qMc2kW_LEILbpBVtYwRMCoxdGNrOAhnoQRdq2JwklyDLTErKoVDju5yK7PbpJcGuqg==","summary":[{"type":"summary_text","text":"**Clarifying shortcut and scope inconsistencies**"},{"type":"summary_text","text":"**Listing and organizing trace requirements**"},{"type":"summary_text","text":"**Planning detailed audit result format**"}]},{"id":"rs_0ca4af36969c80fa016a570323041c8198941d790882b96b91","type":"reasoning","content":[],"encrypted_content":"gAAAAABqVwMj0JeLSKRkDzCnVn1DYELz0zlEK6X6TP1S8CF-y92aprQ9pBbHU0dsw2Ie6s6FwuLIhevV3_8GWSq2i9CKkHDP7VG5z0j7ewPkekqZ7UbtsByulII_W971FWWzfhEWxM4hP3XW0PNgOjpoO1PNB_Q7DFRkBjl7bl2gjvQTIdY6jPWoZhX-7w7-EhsORXW0V0j8UBDFl7crgf363KC7xf0ADbL8LveLwb0JEuTJS24oIh-3D8riQkAQgbPyc5gaabJcxnazoPS7EmTgygI1-ACNe-76pfckhkLFPPzkLtxtzEYTRGM5VJ3av-tTVoCOwVnTIQ7kFor-ox6eZoVS9Oh9KYNv0zBXiEKy8D-N9xinAg6bmj9V_428HHZDEtxxpO5bIYMueaMAxK_0nWLnkpEa2CBal34UHf6oegw0A6xnFPm7mJ8qowPax5pAIVA1K86c--ZxvC85q35QHFirw51-Gl6rBJO0ZeZAySGk-hb_9kHG-LqHRpg8D8ClfXoIzl_YUq_kIDWIR_GGSyiun6SN4TNyAz7l6ajmK0rjjWI7aZjXgJtot-SNCFwzAt0csfxlKM-rWt-SFz6Vv2XEVHGb3haLdCJGFsIYRmJuooH-gyOU5DlG1QMZhN1XZSvyd-ZdRNfjlnY8Sqc_EcpemTaZYvivGts06qzkpsCmZdtIRG6clFD9WO8tGaQAFainCHqCEcNu6NpCdkKwhCxD9Mqo8ZTTWqT3T3ZxKrwEyq12tbxk7NNN75EpgaLEnVMn275oyx3iDKbaEwL8TEmajE1A75nGQZMwhiR15wCeGkvGDoDhdo98VOVFkyr0_QxvztA2xGjCqygL3oUT9Un4DVQO8JuYiqBmn6OpdzOKsb5A7z7QUZS5nI2D5HKX0gUG8OBwMjcZB4pMz2W10ORicJy7HSApyF-YD6TTJjEnjD90TfLuB9DeFTHasrYonVHZTj9zQ8duck_tS1x9eEqJfk7kqBHNLrZl3GTJ1QWQkyOOSGsU60FrXIote0q7E80Q3_BRkbRZLg4vXNEsrm86dJ0amcN-qNtPdZFaI-O7SRaOmv0ZIDVe38BshTGLf2-Nigsd","summary":[]},{"id":"fc_0ca4af36969c80fa016a57032383248198ac6524711036f1d3","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"# Decision ledger\\n\\n## Executive finding\\n\\nThe repository does not yet have a coherent OMP-native decision system. `docs/adr/0007-native-omp-tui-hosts-spt-extension.md` is the only accepted ADR that directly governs the current OMP architecture. `CONTEXT.md`, `SCOPE.md`, ADRs 0001–0006, most of `docs/KNOWN-HAZARDS.md`, and all but one requirement in `traceable-reqs.toml` still describe the source `claude-spt` product. Some of that lineage is reusable, but the documents do not distinguish inherited history from current omp-spt commitments. As a result, old Claude decisions can be mistaken for live OMP requirements, while implemented OMP behaviors have no decision or traceability record.\\n\\nThe highest-order live decision is: **is `plugin/omps` an intentional Claude Code control surface for the OMP adapter, or inherited residue that should leave an OMP-only product?** This must be answered before grilling naming, hooks, setup, hazards, tests, or distribution, because it changes the applicability of most inherited commitments.\\n\\n---\\n\\n# LOCKED — do not re-ask\\n\\n1. **Public-contract-only development is a hard constraint.** `SCOPE.md:20-23` says the adapter is built from `SaberMage/spt-releases` and published pages, never spt-core source. `OMP-ADAPTER-PLAN.md` carries that discipline into its validation section. This is still applicable.\\n\\n2. **Interactive hosted endpoints must run native OMP as the PTY leader.** Accepted ADR-0007, especially `docs/adr/0007-native-omp-tui-hosts-spt-extension.md:5,13-15`, decides that `[session.self]` and `[session.resume]` run `omp`, not the RPC bridge. `adapter/omp-spt.toml` symbols `[session.self]` and `[session.resume]` implement this with `omp --extension …/omp-spt.mjs` and `omp -r {session_id} --extension …`. Do not re-open “bridge logs versus native TUI.”\\n\\n3. **The packaged OMP extension, not a terminal proxy, supplies SPT integration for interactive endpoints.** ADR-0007 locks bind/listen/state/reply through OMP’s public extension API while OMP owns rendering. `adapter/strings/omp-spt.mjs::ompSpt` is the implementation.\\n\\n4. **RPC remains a headless integration surface, not the interactive endpoint host.** ADR-0007:15 explicitly retains the old bridge for explicitly headless callers. `OMP-ADAPTER-PLAN.md:47-72` now states the same split: Style A headless, Style B interactive.\\n\\n5. **Inbound interactive delivery is serialized.** ADR-0007:22 locks that two inbound messages cannot collapse into one OMP turn or receive the wrong reply. `omp-spt.mjs` realizes this with `queue`, `current`, `agentActive`, and `dispatchNext`.\\n\\n6. **OpenRouter billing is OMP configuration, not adapter code.** `OMP-ADAPTER-PLAN.md:132-153` and `docs/OMP-OPENROUTER.md:1-6` lock the boundary: document `models.yml`, `.env`, role routing, and fallback behavior; do not add provider logic to the adapter.\\n\\n7. **OMP’s transcript is adapted to the neutral SPT digest record contract.** `OMP-ADAPTER-PLAN.md:90-102` fixes the source and output model; `tools/claude-spt/src/digest_omp.rs` implements `digest-omp`; `[digest].extractor` invokes it. The exact old Claude extractor design should not be re-litigated.\\n\\n8. **The OMP adapter currently has no shipped ccs profile overlay.** This is explicitly enforced by `tests/manifest-shortcut.sh` (“no shipped profile overlays”) and explained in `adapter/omp-spt.toml` immediately before `[strings]`: ccs was Claude-specific and OMP uses native provider/profile configuration. This is consistent with the OMP plan and should be treated as a completed port decision unless the root product-boundary question intentionally retains a separate Claude surface.\\n\\n9. **Current concrete identity values are implemented and regression-guarded:** adapter/release identity `omp-spt`, executable `omp-spt`, update repo `BigscreenVR/omp-spt`, host binaries `omp` and `omp-spt`, shortcut basename `omp`. Evidence: `adapter/omp-spt.toml` `[adapter]`/`[update]`, `tools/claude-spt/Cargo.toml` `[[bin]]`, `ci/publish/package-adapter.sh`, and `tests/manifest-shortcut.sh`. The values are not all supported by an OMP naming ADR, so their *rationale* needs documentation, but asking “what does the code currently call this?” is unnecessary.\\n\\n10. **Bridge startup must complete OMP `ready` + `get_state` before binding.** `tools/claude-spt/src/bridge.rs::run_argv` enforces that order and bounded waits. This closes the “bind before a failed handshake poisons the ledger” alternative.\\n\\n---\\n\\n# SUPERSEDED — preserve as history, do not ask in original form\\n\\n1. **RPC bridge as `[session.self]` / `[session.resume]`.** Superseded explicitly by ADR-0007. Remaining old statements in `OMP-ADAPTER-PLAN.md` slices S1/S2, `tools/claude-spt/src/bridge.rs` module docs (“hosted harness process” / `[session.self]`), the top of `adapter/omp-spt.toml` (“integration is an RPC bridge”), and `docs/OMP-BRIDGE-FIELD-ISSUES.md` must be read as pre-0.1.3 history.\\n\\n2. **The full `spt-claude-code`/`claude-spt` naming-domain decision.** ADR-0005 was accepted for the sister product, then operationally superseded by the fork’s `omp-spt`/`omps`/`omp` names. It remains valid lineage, not the active OMP naming ADR.\\n\\n3. **The `cc` launcher decision.** `SCOPE.md`, ADR-0001, and `traceable-reqs.toml::REQ-DIST-SHORTCUT-BASENAME` lock `cc-<id>` for Claude. Current OMP code and tests lock `shortcut_basename = \\\"omp\\\"`. Do not ask whether omp-spt should use `cc`; ask only whether `omp` is the right final OMP shortcut token.\\n\\n4. **ccs profile work and `claude-spt:deep`.** `SCOPE.md` already locks `deep` removal and originally locks ccs for Claude. For OMP, the manifest says no shipped profiles and tests enforce it. `traceable-reqs.toml::REQ-CCS-PROFILES` and `REQ-SETUP-CCS` are stale rather than live OMP requirements.\\n\\n5. **The OMP plan’s exact directory-encoding open question.** `tools/claude-spt/src/digest_omp.rs` documents that the implementation locates by session id under the sessions root, so it does not need to reproduce the directory encoding. Ask whether locate-by-id is acceptable only if a concrete ambiguity/collision is found; do not re-ask how to clone OMP’s encoding.\\n\\n6. **The original single-sequence checkpoint macro in proposed ADR-0004.** `docs/KNOWN-HAZARDS.md` §2.4 and `traceable-reqs.toml::REQ-HAZARD-CHECKPOINT-CLEAR-RACE` supersede it with ARM/FIRE split sequencing, then a one-sequence post-clear rename+wake. If checkpoint remains in scope at all, the later hazard invariant is authoritative.\\n\\n7. **Several SCOPE grill items are already resolved in the same file or later ADRs.** `SCOPE.md:135` locks the post-push signal trigger, while `SCOPE.md:211` still lists “git-hook signal vs polling” as open. `SCOPE.md:182-190` resolves the docs/template split, while lines 212-214 still list it as open. ADR-0001 confirms UPS slash-command behavior, so `SCOPE.md:54-55` is stale. These should not be re-grilled.\\n\\n8. **Field issue HIGH-1 and interactive HIGH-3.** HIGH-1’s executable collision is implemented as `omp-spt` (`Cargo.toml`, packer, manifest, tests). The interactive “READY only” UX is structurally superseded by ADR-0007 because interactive hosting no longer uses the bridge; bridge logging remains relevant only to headless use.\\n\\n---\\n\\n# UNRESOLVED — live design decisions\\n\\n## Product/domain boundary\\n\\n1. **Is the Claude Code plugin an intentional supported surface?** The plan says the OMP hook plugin is removed (`OMP-ADAPTER-PLAN.md:87`), yet `plugin/omps` is actively packaged and tested, `[update.post]` still runs plugin reconciliation, the manifest carries `[hooks.*]`, UPS strings, hints, CC inject configuration, and `tools/claude-spt/src/hook.rs` remains active. If “yes,” define this as an explicit cross-harness control surface. If “no,” most Claude-specific ADRs, hazards, requirements, tests, and code are out of scope and should be retired cleanly.\\n\\n2. **What does `omps` mean and is it final?** `OMP-ADAPTER-PLAN.md:78` still says “`omps` (or keep `sptc`? decide),” while `plugin/omps/.claude-plugin/plugin.json`, skill paths, hints, and tests enforce `omps`. The implementation choice needs ratification and an OMP naming ADR; the old `sptc→spt` succession is not automatically applicable.\\n\\n3. **What are the canonical topologies and users of the retained headless bridge?** ADR-0007 says “explicitly headless callers,” but does not define who invokes `omp-spt bridge`, whether it is a supported public CLI, whether it serves ReadyAgent only or LiveAgent too, or whether resume/psyche paths count as bridge consumers.\\n\\n## Capability truth\\n\\n4. **Worker support is unresolved and currently overclaimed.** `adapter/omp-spt.toml` advertises `hostable_types = [\\\"LiveAgent\\\", \\\"ReadyAgent\\\", \\\"Worker\\\"]`; the OMP plan’s diagram mentions `worker-*`; but `adapter/strings/omp-spt.mjs` handles only session/agent lifecycle and `bridge.rs` never calls worker-start/worker-stop. Existing worker requirements/tests point to Claude hooks. Decide either to implement OMP worker lifecycle or remove Worker from advertised capability.\\n\\n5. **ReadyAgent versus LiveAgent semantics need an OMP-native statement.** The current skill prose describes upgrading a *Claude Code* session and a Monitor-based relay; the native extension always starts `spt ready`, while the manifest also declares OMP psyche roles. The precise user-visible transition that creates a live OMP agent versus a ready OMP endpoint is not documented in OMP terms.\\n\\n6. **Session-owned file locations are not decided in OMP language.** `[session].commune_dir` and `signoff_dir` are still `.claude`; OMP skills still tell agents to write `.claude/<id>-commune.md`. Decide whether `.claude` is intentionally adapter-owned compatibility state, should become `.omp`, or should be a harness-neutral project directory. This affects migration and resume semantics.\\n\\n## Reliability and recovery\\n\\n7. **RPC bridge mid-turn behavior remains unresolved.** The plan promises `steer` / `follow_up`; field issue MEDIUM-1 requires decoupled delivery and steer; `bridge.rs::drive_turn` instead buffers later messages until `agent_end`. This is safe serialization but not the promised mid-turn control surface. Ratify serialization-only or implement steering.\\n\\n8. **Clean adapter switch/unbind remains unresolved.** Field issue MEDIUM-4 asks for an operator-supported switch without ledger surgery. Current bridge and extension call `session-end` on graceful shutdown, but there is no documented switch procedure and no requirement/test proving an omp-spt session can be stopped and relaunched under another adapter.\\n\\n9. **Cross-platform hard-kill child cleanup remains partial.** Windows Job Object cleanup exists. `bridge.rs::teardown::arm_kill_on_close` explicitly returns a no-op error on non-Windows and says Unix process-group teardown is a follow-on. Decide whether headless Linux support requires the same invariant; current packaging ships Linux, so [INFERENCE] it likely should.\\n\\n10. **Native extension delivery failure policy is undefined.** If `pi.sendUserMessage` throws, `omp-spt.mjs::dispatchNext` logs locally and drops `current` without notifying the sender. If the `spt ready` listener exits, the extension marks the UI offline but does not re-arm, tear down the perch, or create an SPT-visible failure. Decide retry, explicit failure reply, offline transition, or terminal teardown.\\n\\n11. **The honest minimum spt-core floor is still open.** `OMP-ADAPTER-PLAN.md:174-176` asks for the verbs actually used. The manifest pins `0.27.0` with a long inherited Claude floor narrative. The OMP-specific required floor has not been derived or recorded.\\n\\n12. **OpenRouter role/model selection remains intentionally unfinished.** `docs/OMP-OPENROUTER.md:57-59` verifies OpenRouter routing and says the remaining choice is model/role assignment. This is a product configuration decision, not an adapter implementation defect.\\n\\n13. **ACP supersession criterion is only a strategic statement.** `OMP-ADAPTER-PLAN.md:10-13` says an ACP client may supersede this bridge, but gives no decision trigger or migration boundary. Leave it as a future condition unless current roadmap planning needs a formal sunset criterion.\\n\\n---\\n\\n# CONTRADICTED BY CODE OR CURRENT ARTIFACTS — factual defects, not taste questions\\n\\n1. **Setup is functionally pointed at the wrong adapter.** `adapter/strings/skills/setup.md` and `plugin/omps/skills/setup/SKILL.md` repeatedly activate `claude-spt`, use `adapter/claude-spt.toml`, fetch `SaberMage/claude-spt`, select `claude-spt:ccs`, and describe Claude Code. The actual manifest is `adapter/omp-spt.toml`, adapter is `omp-spt`, repo is `BigscreenVR/omp-spt`, and no ccs profile exists.\\n\\n2. **Plugin identity is stale.** `plugin/omps/.claude-plugin/plugin.json` describes “adapter for Claude Code” and includes keyword `claude-spt`. That may be correct only if the root product-boundary decision intentionally retains a Claude control plugin; otherwise it contradicts the target.\\n\\n3. **The manifest says hooks are the delivery surface while the accepted interactive architecture uses the OMP extension.** `[inject]` says activity/idle `hook`, top-level comments say RPC bridge, and `[hooks.*]` are CC-specific. ADR-0007 and `[session.self]/[session.resume]` use native OMP + extension. These claims need either a topology qualifier or removal.\\n\\n4. **The manifest still captures `CLAUDE_CONFIG_DIR` even though its own comments call it inert for OMP.** `[env.CLAUDE_CONFIG_DIR]` is active data, but `digest-omp` locates under OMP sessions. An inert Claude read-var should not remain an unexplained current contract.\\n\\n5. **CC echo verification is active but explicitly called inert on OMP.** `[env.SPT_INJECT_VERIFY_ECHO] = 1` and `traceable-reqs.toml::REQ-INJECT-VERIFY-ECHO` are Claude-TUI commitments; `tests/manifest-shortcut.sh` says the declaration is “inert on omp.” This should be removed or justified for a retained Claude topology.\\n\\n6. **Integration tests use the OMP manifest but assert Claude adapter outcomes.** `ci/manifest/registration-int.sh` adds `adapter/omp-spt.toml` then looks for/removes `claude-spt`, requires `claude-spt:ccs`, expects “Claude Code (spt),” fetches `/sptc:*` strings, and invokes capability as `claude-spt`. `ci/setup/activate-int.sh` has the same wrong identity/profile assumptions. These are demonstrably stale test contracts.\\n\\n7. **Digest integration is not an OMP digest proof.** `ci/digest/digest-proof-int.sh` points at `adapter/omp-spt.toml` but checks for a built `claude-spt` executable, creates a Claude `projects/<slug>/<sid>.jsonl` tree from a CC-shaped sample, and runs `digest-proof claude-spt`. It does not demonstrate `digest-omp` against OMP JSONL.\\n\\n8. **Acceptance remains a real-Claude hook test, not OMP acceptance.** `ci/acceptance/run-acceptance.sh`, `ci/acceptance/lib.sh`, and `tests/acceptance-harness.sh` create `.claude` hook fixtures and spawn `claude -p`. This may remain sister-lineage testing only if the Claude plugin is deliberately supported; it cannot satisfy an omp-spt acceptance requirement.\\n\\n9. **Psyche integration remains Claude-specific.** `ci/psyche/live-relay-int.sh` sets `A=claude-spt`, describes a resident `claude-spt-psyche`, and tests the retired resident model, while current OMP uses `psyche-omp` as an ephemeral per-event shim.\\n\\n10. **`traceable-reqs.toml` is largely false-domain metadata.** Header says `spt-claude-code`; titles and evidence paths name `adapter/claude-spt.toml`, `plugin/sptc`, `/sptc:*`, ccs, Claude hooks, CC remote control, and Claude hazards. The registry has only one OMP-specific requirement, `REQ-OMP-NATIVE-TUI`. A green trace check can therefore certify retained Claude evidence while leaving OMP capabilities untraced.\\n\\n11. **`REQ-DIST-SHORTCUT-BASENAME` says `cc`; its test enforces `omp`.** The test carries `[unit->REQ-DIST-SHORTCUT-BASENAME]` but verifies a different observable contract than the requirement title.\\n\\n12. **`REQ-DIST-MANIFEST-SCHEMA`, `REQ-DIST-DIGEST-EXTRACTOR`, `REQ-DIST-SESSION-RESUME`, `REQ-SKILL-LIVE`, and many others have Claude-specific titles but are being reused as tags around OMP artifacts.** This is semantic trace corruption: the evidence may be real code, but it does not prove the stated requirement.\\n\\n13. **The RPC bridge’s `spt_call` still does not evaluate child exit status.** `tools/claude-spt/src/bridge.rs::spt_call` returns merged stdout/stderr regardless of `out.status`; bind then logs `bound:` even if `spt api bind` exits nonzero and returns no token. This means field issue LOW-1 (“errors universally swallowed”) is only partially closed despite manifest changelog claims.\\n\\n14. **The OMP plan says hook plugin removed, but the repository actively ships and validates `plugin/omps`.** This is the concrete contradiction underlying the root design question.\\n\\n15. **`REQ-OMP-NATIVE-TUI` overstates its unit evidence.** Its title requires native TUI *and* bind/listen/state/reply behavior. `tests/manifest-shortcut.sh` only checks command strings/existence, and `tests/omp-extension.mjs` only tests `decodeBody`, `drainEvents`, and `extractReply`. Neither drives `ompSpt(pi)` through session_start, agent_start/end, listener failure, queue serialization, state auth, reply send, or shutdown.\\n\\n16. **`ci/run-gates.sh` can appear green while live OMP proofs skip.** `tests/bridge-acceptance.sh` exits 0 unless `OMP_SPT_ACCEPTANCE=1`; native OMP PTY acceptance is not automated; stale real-Claude acceptance is separately gated. The default gate is therefore not evidence of end-to-end OMP behavior.\\n\\n---\\n\\n# KNOWN FIELD ISSUES — demonstrable closure status\\n\\n- **HIGH-1 executable collision: implemented.** `tools/claude-spt/Cargo.toml` names the shipped binary `omp-spt`; packer/tests enforce it. Field document itself still reads as open historical advice.\\n- **HIGH-2 child teardown: partial.** Windows Job Object + explicit graceful kills exist and `tests/bridge-acceptance.sh` has a Windows orphan check. Unix hard-kill cleanup and signal/process-group handling remain absent; the code says so explicitly. No dedicated trace requirement exists.\\n- **HIGH-3 visibility: implemented for headless bridge.** Always-on `oplog!` and heartbeat exist; acceptance asserts message/reply/bind log lines, but not heartbeat/error variants. Interactive visibility is superseded by native TUI.\\n- **HIGH-4 bounded waits: substantially implemented.** `await_frame`, reader threads, env-configurable deadlines, and turn-failure notice exist. Unit tests cover await timeout/EOF; gated bridge acceptance covers a hung turn. Startup/get_state deadline E2E is not demonstrated.\\n- **MEDIUM-1 head-of-line / steer: unresolved.** Later messages are queued FIFO, but no `steer` or `follow_up` implementation exists.\\n- **MEDIUM-2 dead child detection: implemented.** Reader EOF and heartbeat `try_wait` cover idle detection.\\n- **MEDIUM-3 binary collision: mitigated, not eliminated.** Known install paths precede PATH and the ready deadline fails loudly, but there is no explicit version probe. This matches one of the field document’s proposed alternatives.\\n- **MEDIUM-4 clean adapter switch: unresolved as an operator contract.** Graceful `session-end` exists, but no documented/tested switch/unbind flow exists.\\n- **LOW-1 swallowed SPT errors: partial/factually defective.** Logging improved, but `spt_call` ignores process status and bind can be announced after failure.\\n- **LOW-2 silent empty failed reply: implemented for timeout/EOF.** Failures send an explicit notice. A successful `agent_end` with no assistant text still produces an empty bridge reply; the native extension instead uses a non-empty fallback string.\\n\\n---\\n\\n# GLOSSARY UPDATES NEEDED\\n\\n`CONTEXT.md` should cease presenting the sister adapter as the current product. Required canonical entries:\\n\\n1. **`omp-spt`** — repository, adapter id, and consolidated executable identity; distinguish executable invocation `omp-spt <subcommand>` from adapter id.\\n2. **OMP / oh-my-pi** — the interactive terminal agent, including the exact capitalization rule (“OMP” product/UI versus `omp` executable).\\n3. **native OMP endpoint** — an spt-hosted endpoint where `omp` owns the PTY and loads the SPT extension.\\n4. **OMP SPT extension** — packaged `adapter/strings/omp-spt.mjs`; owns interactive bind/listen/state/reply, not terminal rendering.\\n5. **headless RPC bridge** — `omp-spt bridge`; retained for callers without an operator terminal, explicitly not `[session.self]`.\\n6. **interactive versus headless topology** — make the split first-class so “bridge,” “host,” and “endpoint” are not used interchangeably.\\n7. **OMP session id / OMP session file / OMP transcript root** — separate OMP’s session identity from SPT endpoint id and from Claude’s transcript terminology.\\n8. **OMP Psyche turn** — current ephemeral `psyche-omp` model, not the obsolete “detached resident claude companion” definition.\\n9. **Librarian** — if it is a real product role rather than plan-only motivation, define it and distinguish the role from the generic adapter.\\n10. **`omps` plugin namespace** — only if deliberately retained; define why a Claude Code plugin belongs in omp-spt and whether `/omps:*` is temporary or final.\\n11. **ACP supersession** — define only if there is a concrete trigger; otherwise leave it as plan rationale, not glossary truth.\\n\\nExisting entries requiring correction or historical qualification:\\n\\n- `spt-claude-code`, `claude-spt`, `naming-domains`, `legacy spt`, `skeleton plugin`, `fetch-stub skill`, `ccs profile`, `cc launcher`, `psyche sandbox`, `tagged-input round-trip`, `interrupt marker`, and `boundary rename` are Claude/sister lineage, not unqualified OMP terms.\\n- The current Psyche definition says “detached companion process” and `claude-spt-psyche`; current OMP manifest uses a captured, run-to-completion `psyche-omp` role.\\n- The current `ccs profile` examples (`glm`, `kimi`) are already stale even for the sister adapter, whose later ruling chose bare `ccs`; they are doubly stale for OMP.\\n- “bridge” currently names both the former interactive PTY leader and the retained headless command. Qualify it every time.\\n- “hook” needs qualification: Claude Code command hooks (`plugin/omps/hooks`) versus OMP extension events (`pi.on(...)`). They are different integration surfaces.\\n- Use canonical SPT nomenclature consistently (`Psyche`, live agent, ready agent, endpoint, perch, node, subnet); avoid CONTEXT’s unqualified “companion” where release guidance explicitly says not to substitute that softer synonym.\\n\\n---\\n\\n# REQUIREMENTS / TEST GAPS\\n\\nThe OMP port needs explicit OMP requirements rather than attaching OMP evidence to Claude titles. Minimum missing requirement set:\\n\\n- OMP native extension lifecycle: bind, auth, ready listener, busy/idle, serialized dispatch, reply correlation, shutdown.\\n- Native PTY visibility/attach acceptance (separate from extension protocol behavior).\\n- Headless RPC bridge contract and supported caller/topology.\\n- Bridge child-lifetime invariant per supported OS.\\n- Bridge ready/get_state/turn deadlines and failure reporting.\\n- Clean stop/unbind/adapter-switch recovery.\\n- OMP digest extractor and locate behavior.\\n- OMP history fetcher.\\n- OMP ephemeral Psyche turn/reseed behavior.\\n- OMP echo-commune behavior.\\n- OMP native resume.\\n- OMP distribution/setup/update identity.\\n- OMP provider/OpenRouter documentation boundary.\\n- OMP Worker lifecycle, or an explicit “Worker intentionally unsupported” decision and capability removal.\\n- Native listener-death and `sendUserMessage`-failure no-silent-loss hazard.\\n\\nTests needed to satisfy those contracts:\\n\\n- A mocked/fake `pi` lifecycle test for `ompSpt(pi)` that asserts event registration, bind args/token auth, state transitions, queue serialization, one reply per sender, fallback reply, listener close behavior, and session_shutdown teardown.\\n- A real or stable-fake native endpoint-run/attach proof for the PTY/TUI requirement; current helper tests cannot prove a TUI exists.\\n- OMP JSONL samples for `digest-omp` and `history-omp`, invoked through `spt adapter digest-proof omp-spt` rather than Claude-shaped fixtures.\\n- `psyche-omp` and `echo-commune-omp` integration proofs using their current ephemeral contracts.\\n- Bridge startup ready timeout, get_state timeout, dead-child idle detection, bind failure, state/send failure, and Unix hard-kill tests where supported.\\n- Stop/unbind/relaunch-under-other-adapter acceptance.\\n- Capability assertion matching actual ReadyAgent/LiveAgent/Worker support.\\n- Update/registration/setup tests rewritten to `omp-spt`, `BigscreenVR/omp-spt`, `/omps:*`, and no ccs overlay—or removed if the plugin is retired.\\n\\nTraceability defects are structural: add `OMP-ADAPTER-PLAN.md` to scan roots if it remains requirement-bearing; rename/generalize inherited requirements only when their observable contract genuinely survives; retire inapplicable Claude requirements instead of leaving active stages; do not use one tag to mean “same rough seam” when its title specifies another harness.\\n\",\"files\":[{\"path\":\"CONTEXT.md\",\"description\":\"Authoritative glossary is entirely the claude-spt glossary. It lacks omp-spt, native OMP endpoint, OMP extension, headless RPC bridge, OMP session/transcript, and OMP Psyche terms; multiple definitions contradict the current fork.\"},{\"path\":\"SCOPE.md\",\"description\":\"Locked Claude product ledger. Contains reusable public-surface discipline but also stale CC distribution, ccs, cc launcher, setup, CI, and parity decisions. Its open queue contradicts decisions already locked/resolved earlier in the same file.\"},{\"path\":\"OMP-ADAPTER-PLAN.md\",\"description\":\"Primary OMP port plan. Correctly states the native-TUI/RPC split and OpenRouter boundary, but retains stale slice wording, unresolved namespace/floor/capability questions, and an explicit claim that the hook plugin is removed despite active plugin/omps code.\"},{\"path\":\"docs/adr/0001-distribution-splits-by-volatility.md\",\"description\":\"Accepted Claude-specific thin-plugin decision. Applicability to omp-spt depends entirely on whether plugin/omps is an intentional supported Claude surface.\"},{\"path\":\"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md\",\"description\":\"Accepted Claude hook architecture. It is not an OMP extension decision and should be topology-qualified or retired for an OMP-only product.\"},{\"path\":\"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md\",\"description\":\"Accepted resident Claude Psyche sandbox decision. Current OMP uses psyche-omp with different process/model behavior; this ADR is sister lineage, not an OMP Psyche contract.\"},{\"path\":\"docs/adr/0004-checkpoint-via-self-send-loopback.md\",\"description\":\"Still marked proposed despite implementation, and its original single-sequence mechanics are superseded by later checkpoint hazards. It is also Claude-hook/translation-specific.\"},{\"path\":\"docs/adr/0005-name-unification.md\",\"description\":\"Accepted and executed for claude-spt, then operationally superseded by the omp-spt fork. No replacement OMP naming ADR records omp-spt/omps/omp rationale.\"},{\"path\":\"docs/adr/0006-one-command-update-and-consolidated-binary.md\",\"description\":\"Single-binary concept survives as omp-spt, but the ADR’s plugin, binary names, update instructions, and open asks are Claude-specific or stale.\"},{\"path\":\"docs/adr/0007-native-omp-tui-hosts-spt-extension.md\",\"description\":\"Only accepted OMP-native ADR. Definitively locks native OMP as PTY leader, public extension integration, serialized delivery, and RPC bridge relegation to headless callers.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Conformance checklist remains almost entirely Claude-specific. Some hazards may apply only if plugin/omps is retained. It does not contain the OMP bridge incident invariants or native-extension no-silent-loss/recovery hazards.\"},{\"path\":\"docs/OMP-BRIDGE-FIELD-ISSUES.md\",\"description\":\"Historical incident report with no per-item closure status. HIGH-1/3 and most HIGH-4/MED-2/LOW-2 are implemented; HIGH-2 is Windows-only, MED-1 and MED-4 remain open, and LOW-1 is only partially fixed.\"},{\"path\":\"traceable-reqs.toml\",\"description\":\"Registry header and nearly all requirement titles/evidence are claude-spt-specific. Only REQ-OMP-NATIVE-TUI is OMP-native, and even that requirement’s bind/listen/state/reply behavior is not exercised by its claimed unit tests. OMP-ADAPTER-PLAN.md is absent from scan roots.\"},{\"path\":\"adapter/omp-spt.toml\",\"description\":\"Current implementation truth for adapter/repo/binary/shortcut, native self/resume, OMP digest/history/psyche, and extension packaging. Also carries large contradictory Claude hook/inject/env/skills commentary and active tables, including inert CLAUDE_CONFIG_DIR and CC echo-verify declarations.\"},{\"path\":\"adapter/strings/omp-spt.mjs\",\"description\":\"Native OMP extension implementation. Pure helper tests exist, but lifecycle, queue, auth/state/reply, failure recovery, and shutdown are untested. sendUserMessage failure can silently drop a taken message from the sender’s perspective; listener death is only local UI state.\"},{\"path\":\"plugin/omps/.claude-plugin/plugin.json\",\"description\":\"Enforces plugin name omps but describes a Claude Code adapter and includes claude-spt lineage. This is either evidence of an intentional cross-harness surface needing a decision, or stale product identity.\"},{\"path\":\"adapter/strings/skills/setup.md\",\"description\":\"Broken/stale OMP setup instructions: activates claude-spt, uses the old manifest/repo, and offers a nonexistent ccs overlay.\"},{\"path\":\"plugin/omps/skills/setup/SKILL.md\",\"description\":\"Self-contained setup fallback repeats the wrong claude-spt paths/repo/profile and therefore cannot be dismissed as harmless comments.\"},{\"path\":\"tools/claude-spt/src/bridge.rs\",\"description\":\"Headless RPC bridge with Windows Job Object, reader threads, deadlines, logging, and explicit failure replies. Still serializes rather than steers, lacks Unix hard-kill cleanup, has no clean switch contract, and ignores spt child exit status in spt_call.\"},{\"path\":\"tools/claude-spt/Cargo.toml\",\"description\":\"Shipped executable is correctly renamed omp-spt, closing the basename collision on current builds.\"},{\"path\":\"tests/omp-extension.mjs\",\"description\":\"Tests only decodeBody, drainEvents, and extractReply; does not exercise the exported extension lifecycle required by REQ-OMP-NATIVE-TUI.\"},{\"path\":\"tests/manifest-shortcut.sh\",\"description\":\"Useful current manifest assertions for omp identity/native self+resume/no ccs, but tags them to several Claude-specific generic requirements, creating trace semantic mismatch.\"},{\"path\":\"tests/bridge-acceptance.sh\",\"description\":\"Gated headless bridge E2E covers message/reply/bind, hung-turn deadline/failure notice, and Windows orphan cleanup. It is incorrectly tagged as impl evidence for Claude REQ-SKILL-LIVE and skips by default.\"},{\"path\":\"ci/manifest/registration-int.sh\",\"description\":\"Adds the OMP manifest but asserts/removes claude-spt, requires claude-spt:ccs, and resolves /sptc bodies. It is stale and cannot be valid OMP registration evidence.\"},{\"path\":\"ci/setup/activate-int.sh\",\"description\":\"Adds the OMP manifest but drives the claude-spt deregister/reactivate/profile/string contract. Stale acceptance.\"},{\"path\":\"ci/digest/digest-proof-int.sh\",\"description\":\"Uses OMP manifest path but Claude binary name, Claude session layout/sample, and adapter id claude-spt; does not test digest-omp.\"},{\"path\":\"ci/acceptance/run-acceptance.sh\",\"description\":\"Real-Claude UserPromptSubmit hook acceptance, not OMP acceptance. Retain only if the Claude plugin is intentionally supported and classify separately.\"},{\"path\":\"ci/psyche/live-relay-int.sh\",\"description\":\"Tests the obsolete resident claude-spt-psyche model and claude-spt adapter id, not current psyche-omp ephemeral behavior.\"},{\"path\":\"ci/run-gates.sh\",\"description\":\"Default gates run shell/unit/build/trace checks but gated OMP E2E skips as success; native TUI acceptance is absent, so default green is not OMP end-to-end proof.\"}],\"architecture\":\"# One-question-at-a-time grill dependency order\\n\\nAsk exactly one question at each stage; do not descend until its parent is settled.\\n\\n1. **Product boundary (highest dependency):** “Is `plugin/omps` a deliberately supported Claude Code control surface for omp-spt, or should omp-spt be OMP-native only?”\\n   - If OMP-only: retire plugin/update-post/hook/UPS/CC hazards/CC tests cleanly.\\n   - If retained: define it as a separate topology with its own applicability labels; do not call Claude hook behavior OMP behavior.\\n\\n2. **Supported topology matrix:** “Which topologies are product-supported: native interactive OMP, headless `omp-spt bridge`, retained Claude plugin (if yes), and daemon-driven OMP Psyche?”\\n   - This fixes the domain boundary for every later requirement and term.\\n\\n3. **Canonical naming:** “Ratify or change the implemented OMP tokens: repo/adapter/binary `omp-spt`, shortcut `omp`, and—only if retained—Claude plugin prefix `omps`.”\\n   - Record a new OMP naming ADR; explicitly supersede ADR-0005 for this fork.\\n\\n4. **Capability truth:** “For each supported topology, which SPT types are honest: ReadyAgent, LiveAgent, Worker?”\\n   - In particular, decide Worker implementation versus removing the advertised capability.\\n\\n5. **Native extension lifecycle contract:** “When a bound native extension loses its listener or cannot submit an inbound message, must it retry, reply with failure, mark offline, or terminate the endpoint?”\\n   - Lock no-silent-loss and recovery before writing tests.\\n\\n6. **Headless bridge turn policy:** “Does the headless bridge intentionally serialize later messages until `agent_end`, or must it implement OMP `steer`/`follow_up` for mid-turn delivery?”\\n   - This disposes MEDIUM-1 and corrects the plan promise.\\n\\n7. **Headless bridge shutdown/switch policy:** “What operator-visible stop/unbind/switch contract must work on every supported OS?”\\n   - Then decide whether Unix process-group cleanup is required and define the cross-adapter recovery acceptance.\\n\\n8. **Session/context ownership:** “What OMP-native locations and identity rules govern commune/signoff files, resume, transcript lookup, and Psyche state?”\\n   - Resolve `.claude` versus `.omp`/adapter-owned state and align skill prose, manifest, digest/history, and Psyche.\\n\\n9. **Provider configuration:** “Which OMP profile/model roles should the Librarian use by default, if any, while keeping OpenRouter setup outside adapter code?”\\n   - The billing boundary itself is already locked and must not be re-asked.\\n\\n10. **Minimum public contract:** “Given the final topology/capability matrix, what is the lowest published spt-core version that supports every actually used verb and manifest field?”\\n    - Derive the floor from published artifacts only; delete the inherited floor narrative that no longer applies.\\n\\n11. **Traceability migration:** “Do we clean-cut the registry to OMP requirement titles, or preserve Claude lineage in a separately labeled historical registry/document?”\\n    - Recommendation: clean-cut current requirements; preserve lineage in docs/changelog, not active trace entries.\\n\\n12. **Acceptance bar:** “Which live proofs are release-blocking for native OMP, headless RPC, Psyche, resume, digest/history, and cross-adapter recovery?”\\n    - Only after contracts are fixed should required stages and tests be assigned.\\n\\n13. **Documentation supersession pass:** “Which inherited ADRs/hazards remain applicable to a retained Claude topology, and which become historical/superseded?”\\n    - Update CONTEXT last so its glossary reflects all upstream answers rather than steering them.\\n\\n## Recommended document authority after the grill\\n\\n- `CONTEXT.md`: OMP-native glossary only, with explicitly qualified sister/legacy terms.\\n- `SCOPE.md`: replace the Claude scoping ledger with the final omp-spt product/capability boundary; archive the old ledger as lineage if needed.\\n- OMP ADRs: one naming/product-boundary ADR, one topology/capability ADR, ADR-0007 retained, one bridge reliability/recovery ADR if headless remains supported.\\n- `docs/KNOWN-HAZARDS.md`: only current invariants, grouped by topology; retain historical incidents with explicit resolved/superseded status.\\n- `docs/OMP-BRIDGE-FIELD-ISSUES.md`: add per-finding status and evidence, or fold still-live invariants into KNOWN-HAZARDS and mark the report historical.\\n- `traceable-reqs.toml`: OMP-observable contracts only; no OMP evidence attached to a requirement whose title says Claude/ccs/sptc/cc launcher.\\n- Tests: separate `native-extension`, `headless-bridge`, `omp-session`, and optional `claude-control-plugin` lanes so one topology cannot accidentally certify another.\"}}}","call_id":"call_P7PZD2smja81sWIvCdfI6FYd","name":"yield"}]},"duration":190405.3931,"ttft":2047.514899999951,"contextSnapshot":{"promptTokens":226213,"nonMessageTokens":17965}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_P7PZD2smja81sWIvCdfI6FYd|fc_0ca4af36969c80fa016a57032383248198ac6524711036f1d3","toolName":"yield","startedAt":"2026-07-15T03:51:30.987Z"},"id":"658a6cb4","parentId":"4daa6a7f","timestamp":"2026-07-15T03:51:30.987Z"}
{"type":"message","id":"95488174","parentId":"658a6cb4","timestamp":"2026-07-15T03:51:30.988Z","message":{"role":"toolResult","toolCallId":"call_P7PZD2smja81sWIvCdfI6FYd|fc_0ca4af36969c80fa016a57032383248198ac6524711036f1d3","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"# Decision ledger\n\n## Executive finding\n\nThe repository does not yet have a coherent OMP-native decision system. `docs/adr/0007-native-omp-tui-hosts-spt-extension.md` is the only accepted ADR that directly governs the current OMP architecture. `CONTEXT.md`, `SCOPE.md`, ADRs 0001–0006, most of `docs/KNOWN-HAZARDS.md`, and all but one requirement in `traceable-reqs.toml` still describe the source `claude-spt` product. Some of that lineage is reusable, but the documents do not distinguish inherited history from current omp-spt commitments. As a result, old Claude decisions can be mistaken for live OMP requirements, while implemented OMP behaviors have no decision or traceability record.\n\nThe highest-order live decision is: **is `plugin/omps` an intentional Claude Code control surface for the OMP adapter, or inherited residue that should leave an OMP-only product?** This must be answered before grilling naming, hooks, setup, hazards, tests, or distribution, because it changes the applicability of most inherited commitments.\n\n---\n\n# LOCKED — do not re-ask\n\n1. **Public-contract-only development is a hard constraint.** `SCOPE.md:20-23` says the adapter is built from `SaberMage/spt-releases` and published pages, never spt-core source. `OMP-ADAPTER-PLAN.md` carries that discipline into its validation section. This is still applicable.\n\n2. **Interactive hosted endpoints must run native OMP as the PTY leader.** Accepted ADR-0007, especially `docs/adr/0007-native-omp-tui-hosts-spt-extension.md:5,13-15`, decides that `[session.self]` and `[session.resume]` run `omp`, not the RPC bridge. `adapter/omp-spt.toml` symbols `[session.self]` and `[session.resume]` implement this with `omp --extension …/omp-spt.mjs` and `omp -r {session_id} --extension …`. Do not re-open “bridge logs versus native TUI.”\n\n3. **The packaged OMP extension, not a terminal proxy, supplies SPT integration for interactive endpoints.** ADR-0007 locks bind/listen/state/reply through OMP’s public extension API while OMP owns rendering. `adapter/strings/omp-spt.mjs::ompSpt` is the implementation.\n\n4. **RPC remains a headless integration surface, not the interactive endpoint host.** ADR-0007:15 explicitly retains the old bridge for explicitly headless callers. `OMP-ADAPTER-PLAN.md:47-72` now states the same split: Style A headless, Style B interactive.\n\n5. **Inbound interactive delivery is serialized.** ADR-0007:22 locks that two inbound messages cannot collapse into one OMP turn or receive the wrong reply. `omp-spt.mjs` realizes this with `queue`, `current`, `agentActive`, and `dispatchNext`.\n\n6. **OpenRouter billing is OMP configuration, not adapter code.** `OMP-ADAPTER-PLAN.md:132-153` and `docs/OMP-OPENROUTER.md:1-6` lock the boundary: document `models.yml`, `.env`, role routing, and fallback behavior; do not add provider logic to the adapter.\n\n7. **OMP’s transcript is adapted to the neutral SPT digest record contract.** `OMP-ADAPTER-PLAN.md:90-102` fixes the source and output model; `tools/claude-spt/src/digest_omp.rs` implements `digest-omp`; `[digest].extractor` invokes it. The exact old Claude extractor design should not be re-litigated.\n\n8. **The OMP adapter currently has no shipped ccs profile overlay.** This is explicitly enforced by `tests/manifest-shortcut.sh` (“no shipped profile overlays”) and explained in `adapter/omp-spt.toml` immediately before `[strings]`: ccs was Claude-specific and OMP uses native provider/profile configuration. This is consistent with the OMP plan and should be treated as a completed port decision unless the root product-boundary question intentionally retains a separate Claude surface.\n\n9. **Current concrete identity values are implemented and regression-guarded:** adapter/release identity `omp-spt`, executable `omp-spt`, update repo `BigscreenVR/omp-spt`, host binaries `omp` and `omp-spt`, shortcut basename `omp`. Evidence: `adapter/omp-spt.toml` `[adapter]`/`[update]`, `tools/claude-spt/Cargo.toml` `[[bin]]`, `ci/publish/package-adapter.sh`, and `tests/manifest-shortcut.sh`. The values are not all supported by an OMP naming ADR, so their *rationale* needs documentation, but asking “what does the code currently call this?” is unnecessary.\n\n10. **Bridge startup must complete OMP `ready` + `get_state` before binding.** `tools/claude-spt/src/bridge.rs::run_argv` enforces that order and bounded waits. This closes the “bind before a failed handshake poisons the ledger” alternative.\n\n---\n\n# SUPERSEDED — preserve as history, do not ask in original form\n\n1. **RPC bridge as `[session.self]` / `[session.resume]`.** Superseded explicitly by ADR-0007. Remaining old statements in `OMP-ADAPTER-PLAN.md` slices S1/S2, `tools/claude-spt/src/bridge.rs` module docs (“hosted harness process” / `[session.self]`), the top of `adapter/omp-spt.toml` (“integration is an RPC bridge”), and `docs/OMP-BRIDGE-FIELD-ISSUES.md` must be read as pre-0.1.3 history.\n\n2. **The full `spt-claude-code`/`claude-spt` naming-domain decision.** ADR-0005 was accepted for the sister product, then operationally superseded by the fork’s `omp-spt`/`omps`/`omp` names. It remains valid lineage, not the active OMP naming ADR.\n\n3. **The `cc` launcher decision.** `SCOPE.md`, ADR-0001, and `traceable-reqs.toml::REQ-DIST-SHORTCUT-BASENAME` lock `cc-<id>` for Claude. Current OMP code and tests lock `shortcut_basename = \"omp\"`. Do not ask whether omp-spt should use `cc`; ask only whether `omp` is the right final OMP shortcut token.\n\n4. **ccs profile work and `claude-spt:deep`.** `SCOPE.md` already locks `deep` removal and originally locks ccs for Claude. For OMP, the manifest says no shipped profiles and tests enforce it. `traceable-reqs.toml::REQ-CCS-PROFILES` and `REQ-SETUP-CCS` are stale rather than live OMP requirements.\n\n5. **The OMP plan’s exact directory-encoding open question.** `tools/claude-spt/src/digest_omp.rs` documents that the implementation locates by session id under the sessions root, so it does not need to reproduce the directory encoding. Ask whether locate-by-id is acceptable only if a concrete ambiguity/collision is found; do not re-ask how to clone OMP’s encoding.\n\n6. **The original single-sequence checkpoint macro in proposed ADR-0004.** `docs/KNOWN-HAZARDS.md` §2.4 and `traceable-reqs.toml::REQ-HAZARD-CHECKPOINT-CLEAR-RACE` supersede it with ARM/FIRE split sequencing, then a one-sequence post-clear rename+wake. If checkpoint remains in scope at all, the later hazard invariant is authoritative.\n\n7. **Several SCOPE grill items are already resolved in the same file or later ADRs.** `SCOPE.md:135` locks the post-push signal trigger, while `SCOPE.md:211` still lists “git-hook signal vs polling” as open. `SCOPE.md:182-190` resolves the docs/template split, while lines 212-214 still list it as open. ADR-0001 confirms UPS slash-command behavior, so `SCOPE.md:54-55` is stale. These should not be re-grilled.\n\n8. **Field issue HIGH-1 and interactive HIGH-3.** HIGH-1’s executable collision is implemented as `omp-spt` (`Cargo.toml`, packer, manifest, tests). The interactive “READY only” UX is structurally superseded by ADR-0007 because interactive hosting no longer uses the bridge; bridge logging remains relevant only to headless use.\n\n---\n\n# UNRESOLVED — live design decisions\n\n## Product/domain boundary\n\n1. **Is the Claude Code plugin an intentional supported surface?** The plan says the OMP hook plugin is removed (`OMP-ADAPTER-PLAN.md:87`), yet `plugin/omps` is actively packaged and tested, `[update.post]` still runs plugin reconciliation, the manifest carries `[hooks.*]`, UPS strings, hints, CC inject configuration, and `tools/claude-spt/src/hook.rs` remains active. If “yes,” define this as an explicit cross-harness control surface. If “no,” most Claude-specific ADRs, hazards, requirements, tests, and code are out of scope and should be retired cleanly.\n\n2. **What does `omps` mean and is it final?** `OMP-ADAPTER-PLAN.md:78` still says “`omps` (or keep `sptc`? decide),” while `plugin/omps/.claude-plugin/plugin.json`, skill paths, hints, and tests enforce `omps`. The implementation choice needs ratification and an OMP naming ADR; the old `sptc→spt` succession is not automatically applicable.\n\n3. **What are the canonical topologies and users of the retained headless bridge?** ADR-0007 says “explicitly headless callers,” but does not define who invokes `omp-spt bridge`, whether it is a supported public CLI, whether it serves ReadyAgent only or LiveAgent too, or whether resume/psyche paths count as bridge consumers.\n\n## Capability truth\n\n4. **Worker support is unresolved and currently overclaimed.** `adapter/omp-spt.toml` advertises `hostable_types = [\"LiveAgent\", \"ReadyAgent\", \"Worker\"]`; the OMP plan’s diagram mentions `worker-*`; but `adapter/strings/omp-spt.mjs` handles only session/agent lifecycle and `bridge.rs` never calls worker-start/worker-stop. Existing worker requirements/tests point to Claude hooks. Decide either to implement OMP worker lifecycle or remove Worker from advertised capability.\n\n5. **ReadyAgent versus LiveAgent semantics need an OMP-native statement.** The current skill prose describes upgrading a *Claude Code* session and a Monitor-based relay; the native extension always starts `spt ready`, while the manifest also declares OMP psyche roles. The precise user-visible transition that creates a live OMP agent versus a ready OMP endpoint is not documented in OMP terms.\n\n6. **Session-owned file locations are not decided in OMP language.** `[session].commune_dir` and `signoff_dir` are still `.claude`; OMP skills still tell agents to write `.claude/<id>-commune.md`. Decide whether `.claude` is intentionally adapter-owned compatibility state, should become `.omp`, or should be a harness-neutral project directory. This affects migration and resume semantics.\n\n## Reliability and recovery\n\n7. **RPC bridge mid-turn behavior remains unresolved.** The plan promises `steer` / `follow_up`; field issue MEDIUM-1 requires decoupled delivery and steer; `bridge.rs::drive_turn` instead buffers later messages until `agent_end`. This is safe serialization but not the promised mid-turn control surface. Ratify serialization-only or implement steering.\n\n8. **Clean adapter switch/unbind remains unresolved.** Field issue MEDIUM-4 asks for an operator-supported switch without ledger surgery. Current bridge and extension call `session-end` on graceful shutdown, but there is no documented switch procedure and no requirement/test proving an omp-spt session can be stopped and relaunched under another adapter.\n\n9. **Cross-platform hard-kill child cleanup remains partial.** Windows Job Object cleanup exists. `bridge.rs::teardown::arm_kill_on_close` explicitly returns a no-op error on non-Windows and says Unix process-group teardown is a follow-on. Decide whether headless Linux support requires the same invariant; current packaging ships Linux, so [INFERENCE] it likely should.\n\n10. **Native extension delivery failure policy is undefined.** If `pi.sendUserMessage` throws, `omp-spt.mjs::dispatchNext` logs locally and drops `current` without notifying the sender. If the `spt ready` listener exits, the extension marks the UI offline but does not re-arm, tear down the perch, or create an SPT-visible failure. Decide retry, explicit failure reply, offline transition, or terminal teardown.\n\n11. **The honest minimum spt-core floor is still open.** `OMP-ADAPTER-PLAN.md:174-176` asks for the verbs actually used. The manifest pins `0.27.0` with a long inherited Claude floor narrative. The OMP-specific required floor has not been derived or recorded.\n\n12. **OpenRouter role/model selection remains intentionally unfinished.** `docs/OMP-OPENROUTER.md:57-59` verifies OpenRouter routing and says the remaining choice is model/role assignment. This is a product configuration decision, not an adapter implementation defect.\n\n13. **ACP supersession criterion is only a strategic statement.** `OMP-ADAPTER-PLAN.md:10-13` says an ACP client may supersede this bridge, but gives no decision trigger or migration boundary. Leave it as a future condition unless current roadmap planning needs a formal sunset criterion.\n\n---\n\n# CONTRADICTED BY CODE OR CURRENT ARTIFACTS — factual defects, not taste questions\n\n1. **Setup is functionally pointed at the wrong adapter.** `adapter/strings/skills/setup.md` and `plugin/omps/skills/setup/SKILL.md` repeatedly activate `claude-spt`, use `adapter/claude-spt.toml`, fetch `SaberMage/claude-spt`, select `claude-spt:ccs`, and describe Claude Code. The actual manifest is `adapter/omp-spt.toml`, adapter is `omp-spt`, repo is `BigscreenVR/omp-spt`, and no ccs profile exists.\n\n2. **Plugin identity is stale.** `plugin/omps/.claude-plugin/plugin.json` describes “adapter for Claude Code” and includes keyword `claude-spt`. That may be correct only if the root product-boundary decision intentionally retains a Claude control plugin; otherwise it contradicts the target.\n\n3. **The manifest says hooks are the delivery surface while the accepted interactive architecture uses the OMP extension.** `[inject]` says activity/idle `hook`, top-level comments say RPC bridge, and `[hooks.*]` are CC-specific. ADR-0007 and `[session.self]/[session.resume]` use native OMP + extension. These claims need either a topology qualifier or removal.\n\n4. **The manifest still captures `CLAUDE_CONFIG_DIR` even though its own comments call it inert for OMP.** `[env.CLAUDE_CONFIG_DIR]` is active data, but `digest-omp` locates under OMP sessions. An inert Claude read-var should not remain an unexplained current contract.\n\n5. **CC echo verification is active but explicitly called inert on OMP.** `[env.SPT_INJECT_VERIFY_ECHO] = 1` and `traceable-reqs.toml::REQ-INJECT-VERIFY-ECHO` are Claude-TUI commitments; `tests/manifest-shortcut.sh` says the declaration is “inert on omp.” This should be removed or justified for a retained Claude topology.\n\n6. **Integration tests use the OMP manifest but assert Claude adapter outcomes.** `ci/manifest/registration-int.sh` adds `adapter/omp-spt.toml` then looks for/removes `claude-spt`, requires `claude-spt:ccs`, expects “Claude Code (spt),” fetches `/sptc:*` strings, and invokes capability as `claude-spt`. `ci/setup/activate-int.sh` has the same wrong identity/profile assumptions. These are demonstrably stale test contracts.\n\n7. **Digest integration is not an OMP digest proof.** `ci/digest/digest-proof-int.sh` points at `adapter/omp-spt.toml` but checks for a built `claude-spt` executable, creates a Claude `projects/<slug>/<sid>.jsonl` tree from a CC-shaped sample, and runs `digest-proof claude-spt`. It does not demonstrate `digest-omp` against OMP JSONL.\n\n8. **Acceptance remains a real-Claude hook test, not OMP acceptance.** `ci/acceptance/run-acceptance.sh`, `ci/acceptance/lib.sh`, and `tests/acceptance-harness.sh` create `.claude` hook fixtures and spawn `claude -p`. This may remain sister-lineage testing only if the Claude plugin is deliberately supported; it cannot satisfy an omp-spt acceptance requirement.\n\n9. **Psyche integration remains Claude-specific.** `ci/psyche/live-relay-int.sh` sets `A=claude-spt`, describes a resident `claude-spt-psyche`, and tests the retired resident model, while current OMP uses `psyche-omp` as an ephemeral per-event shim.\n\n10. **`traceable-reqs.toml` is largely false-domain metadata.** Header says `spt-claude-code`; titles and evidence paths name `adapter/claude-spt.toml`, `plugin/sptc`, `/sptc:*`, ccs, Claude hooks, CC remote control, and Claude hazards. The registry has only one OMP-specific requirement, `REQ-OMP-NATIVE-TUI`. A green trace check can therefore certify retained Claude evidence while leaving OMP capabilities untraced.\n\n11. **`REQ-DIST-SHORTCUT-BASENAME` says `cc`; its test enforces `omp`.** The test carries `[unit->REQ-DIST-SHORTCUT-BASENAME]` but verifies a different observable contract than the requirement title.\n\n12. **`REQ-DIST-MANIFEST-SCHEMA`, `REQ-DIST-DIGEST-EXTRACTOR`, `REQ-DIST-SESSION-RESUME`, `REQ-SKILL-LIVE`, and many others have Claude-specific titles but are being reused as tags around OMP artifacts.** This is semantic trace corruption: the evidence may be real code, but it does not prove the stated requirement.\n\n13. **The RPC bridge’s `spt_call` still does not evaluate child exit status.** `tools/claude-spt/src/bridge.rs::spt_call` returns merged stdout/stderr regardless of `out.status`; bind then logs `bound:` even if `spt api bind` exits nonzero and returns no token. This means field issue LOW-1 (“errors universally swallowed”) is only partially closed despite manifest changelog claims.\n\n14. **The OMP plan says hook plugin removed, but the repository actively ships and validates `plugin/omps`.** This is the concrete contradiction underlying the root design question.\n\n15. **`REQ-OMP-NATIVE-TUI` overstates its unit evidence.** Its title requires native TUI *and* bind/listen/state/reply behavior. `tests/manifest-shortcut.sh` only checks command strings/existence, and `tests/omp-extension.mjs` only tests `decodeBody`, `drainEvents`, and `extractReply`. Neither drives `ompSpt(pi)` through session_start, agent_start/end, listener failure, queue serialization, state auth, reply send, or shutdown.\n\n16. **`ci/run-gates.sh` can appear green while live OMP proofs skip.** `tests/bridge-acceptance.sh` exits 0 unless `OMP_SPT_ACCEPTANCE=1`; native OMP PTY acceptance is not automated; stale real-Claude acceptance is separately gated. The default gate is therefore not evidence of end-to-end OMP behavior.\n\n---\n\n# KNOWN FIELD ISSUES — demonstrable closure status\n\n- **HIGH-1 executable collision: implemented.** `tools/claude-spt/Cargo.toml` names the shipped binary `omp-spt`; packer/tests enforce it. Field document itself still reads as open historical advice.\n- **HIGH-2 child teardown: partial.** Windows Job Object + explicit graceful kills exist and `tests/bridge-acceptance.sh` has a Windows orphan check. Unix hard-kill cleanup and signal/process-group handling remain absent; the code says so explicitly. No dedicated trace requirement exists.\n- **HIGH-3 visibility: implemented for headless bridge.** Always-on `oplog!` and heartbeat exist; acceptance asserts message/reply/bind log lines, but not heartbeat/error variants. Interactive visibility is superseded by native TUI.\n- **HIGH-4 bounded waits: substantially implemented.** `await_frame`, reader threads, env-configurable deadlines, and turn-failure notice exist. Unit tests cover await timeout/EOF; gated bridge acceptance covers a hung turn. Startup/get_state deadline E2E is not demonstrated.\n- **MEDIUM-1 head-of-line / steer: unresolved.** Later messages are queued FIFO, but no `steer` or `follow_up` implementation exists.\n- **MEDIUM-2 dead child detection: implemented.** Reader EOF and heartbeat `try_wait` cover idle detection.\n- **MEDIUM-3 binary collision: mitigated, not eliminated.** Known install paths precede PATH and the ready deadline fails loudly, but there is no explicit version probe. This matches one of the field document’s proposed alternatives.\n- **MEDIUM-4 clean adapter switch: unresolved as an operator contract.** Graceful `session-end` exists, but no documented/tested switch/unbind flow exists.\n- **LOW-1 swallowed SPT errors: partial/factually defective.** Logging improved, but `spt_call` ignores process status and bind can be announced after failure.\n- **LOW-2 silent empty failed reply: implemented for timeout/EOF.** Failures send an explicit notice. A successful `agent_end` with no assistant text still produces an empty bridge reply; the native extension instead uses a non-empty fallback string.\n\n---\n\n# GLOSSARY UPDATES NEEDED\n\n`CONTEXT.md` should cease presenting the sister adapter as the current product. Required canonical entries:\n\n1. **`omp-spt`** — repository, adapter id, and consolidated executable identity; distinguish executable invocation `omp-spt <subcommand>` from adapter id.\n2. **OMP / oh-my-pi** — the interactive terminal agent, including the exact capitalization rule (“OMP” product/UI versus `omp` executable).\n3. **native OMP endpoint** — an spt-hosted endpoint where `omp` owns the PTY and loads the SPT extension.\n4. **OMP SPT extension** — packaged `adapter/strings/omp-spt.mjs`; owns interactive bind/listen/state/reply, not terminal rendering.\n5. **headless RPC bridge** — `omp-spt bridge`; retained for callers without an operator terminal, explicitly not `[session.self]`.\n6. **interactive versus headless topology** — make the split first-class so “bridge,” “host,” and “endpoint” are not used interchangeably.\n7. **OMP session id / OMP session file / OMP transcript root** — separate OMP’s session identity from SPT endpoint id and from Claude’s transcript terminology.\n8. **OMP Psyche turn** — current ephemeral `psyche-omp` model, not the obsolete “detached resident claude companion” definition.\n9. **Librarian** — if it is a real product role rather than plan-only motivation, define it and distinguish the role from the generic adapter.\n10. **`omps` plugin namespace** — only if deliberately retained; define why a Claude Code plugin belongs in omp-spt and whether `/omps:*` is temporary or final.\n11. **ACP supersession** — define only if there is a concrete trigger; otherwise leave it as plan rationale, not glossary truth.\n\nExisting entries requiring correction or historical qualification:\n\n- `spt-claude-code`, `claude-spt`, `naming-domains`, `legacy spt`, `skeleton plugin`, `fetch-stub skill`, `ccs profile`, `cc launcher`, `psyche sandbox`, `tagged-input round-trip`, `interrupt marker`, and `boundary rename` are Claude/sister lineage, not unqualified OMP terms.\n- The current Psyche definition says “detached companion process” and `claude-spt-psyche`; current OMP manifest uses a captured, run-to-completion `psyche-omp` role.\n- The current `ccs profile` examples (`glm`, `kimi`) are already stale even for the sister adapter, whose later ruling chose bare `ccs`; they are doubly stale for OMP.\n- “bridge” currently names both the former interactive PTY leader and the retained headless command. Qualify it every time.\n- “hook” needs qualification: Claude Code command hooks (`plugin/omps/hooks`) versus OMP extension events (`pi.on(...)`). They are different integration surfaces.\n- Use canonical SPT nomenclature consistently (`Psyche`, live agent, ready agent, endpoint, perch, node, subnet); avoid CONTEXT’s unqualified “companion” where release guidance explicitly says not to substitute that softer synonym.\n\n---\n\n# REQUIREMENTS / TEST GAPS\n\nThe OMP port needs explicit OMP requirements rather than attaching OMP evidence to Claude titles. Minimum missing requirement set:\n\n- OMP native extension lifecycle: bind, auth, ready listener, busy/idle, serialized dispatch, reply correlation, shutdown.\n- Native PTY visibility/attach acceptance (separate from extension protocol behavior).\n- Headless RPC bridge contract and supported caller/topology.\n- Bridge child-lifetime invariant per supported OS.\n- Bridge ready/get_state/turn deadlines and failure reporting.\n- Clean stop/unbind/adapter-switch recovery.\n- OMP digest extractor and locate behavior.\n- OMP history fetcher.\n- OMP ephemeral Psyche turn/reseed behavior.\n- OMP echo-commune behavior.\n- OMP native resume.\n- OMP distribution/setup/update identity.\n- OMP provider/OpenRouter documentation boundary.\n- OMP Worker lifecycle, or an explicit “Worker intentionally unsupported” decision and capability removal.\n- Native listener-death and `sendUserMessage`-failure no-silent-loss hazard.\n\nTests needed to satisfy those contracts:\n\n- A mocked/fake `pi` lifecycle test for `ompSpt(pi)` that asserts event registration, bind args/token auth, state transitions, queue serialization, one reply per sender, fallback reply, listener close behavior, and session_shutdown teardown.\n- A real or stable-fake native endpoint-run/attach proof for the PTY/TUI requirement; current helper tests cannot prove a TUI exists.\n- OMP JSONL samples for `digest-omp` and `history-omp`, invoked through `spt adapter digest-proof omp-spt` rather than Claude-shaped fixtures.\n- `psyche-omp` and `echo-commune-omp` integration proofs using their current ephemeral contracts.\n- Bridge startup ready timeout, get_state timeout, dead-child idle detection, bind failure, state/send failure, and Unix hard-kill tests where supported.\n- Stop/unbind/relaunch-under-other-adapter acceptance.\n- Capability assertion matching actual ReadyAgent/LiveAgent/Worker support.\n- Update/registration/setup tests rewritten to `omp-spt`, `BigscreenVR/omp-spt`, `/omps:*`, and no ccs overlay—or removed if the plugin is retired.\n\nTraceability defects are structural: add `OMP-ADAPTER-PLAN.md` to scan roots if it remains requirement-bearing; rename/generalize inherited requirements only when their observable contract genuinely survives; retire inapplicable Claude requirements instead of leaving active stages; do not use one tag to mean “same rough seam” when its title specifies another harness.\n","files":[{"path":"CONTEXT.md","description":"Authoritative glossary is entirely the claude-spt glossary. It lacks omp-spt, native OMP endpoint, OMP extension, headless RPC bridge, OMP session/transcript, and OMP Psyche terms; multiple definitions contradict the current fork."},{"path":"SCOPE.md","description":"Locked Claude product ledger. Contains reusable public-surface discipline but also stale CC distribution, ccs, cc launcher, setup, CI, and parity decisions. Its open queue contradicts decisions already locked/resolved earlier in the same file."},{"path":"OMP-ADAPTER-PLAN.md","description":"Primary OMP port plan. Correctly states the native-TUI/RPC split and OpenRouter boundary, but retains stale slice wording, unresolved namespace/floor/capability questions, and an explicit claim that the hook plugin is removed despite active plugin/omps code."},{"path":"docs/adr/0001-distribution-splits-by-volatility.md","description":"Accepted Claude-specific thin-plugin decision. Applicability to omp-spt depends entirely on whether plugin/omps is an intentional supported Claude surface."},{"path":"docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md","description":"Accepted Claude hook architecture. It is not an OMP extension decision and should be topology-qualified or retired for an OMP-only product."},{"path":"docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md","description":"Accepted resident Claude Psyche sandbox decision. Current OMP uses psyche-omp with different process/model behavior; this ADR is sister lineage, not an OMP Psyche contract."},{"path":"docs/adr/0004-checkpoint-via-self-send-loopback.md","description":"Still marked proposed despite implementation, and its original single-sequence mechanics are superseded by later checkpoint hazards. It is also Claude-hook/translation-specific."},{"path":"docs/adr/0005-name-unification.md","description":"Accepted and executed for claude-spt, then operationally superseded by the omp-spt fork. No replacement OMP naming ADR records omp-spt/omps/omp rationale."},{"path":"docs/adr/0006-one-command-update-and-consolidated-binary.md","description":"Single-binary concept survives as omp-spt, but the ADR’s plugin, binary names, update instructions, and open asks are Claude-specific or stale."},{"path":"docs/adr/0007-native-omp-tui-hosts-spt-extension.md","description":"Only accepted OMP-native ADR. Definitively locks native OMP as PTY leader, public extension integration, serialized delivery, and RPC bridge relegation to headless callers."},{"path":"docs/KNOWN-HAZARDS.md","description":"Conformance checklist remains almost entirely Claude-specific. Some hazards may apply only if plugin/omps is retained. It does not contain the OMP bridge incident invariants or native-extension no-silent-loss/recovery hazards."},{"path":"docs/OMP-BRIDGE-FIELD-ISSUES.md","description":"Historical incident report with no per-item closure status. HIGH-1/3 and most HIGH-4/MED-2/LOW-2 are implemented; HIGH-2 is Windows-only, MED-1 and MED-4 remain open, and LOW-1 is only partially fixed."},{"path":"traceable-reqs.toml","description":"Registry header and nearly all requirement titles/evidence are claude-spt-specific. Only REQ-OMP-NATIVE-TUI is OMP-native, and even that requirement’s bind/listen/state/reply behavior is not exercised by its claimed unit tests. OMP-ADAPTER-PLAN.md is absent from scan roots."},{"path":"adapter/omp-spt.toml","description":"Current implementation truth for adapter/repo/binary/shortcut, native self/resume, OMP digest/history/psyche, and extension packaging. Also carries large contradictory Claude hook/inject/env/skills commentary and active tables, including inert CLAUDE_CONFIG_DIR and CC echo-verify declarations."},{"path":"adapter/strings/omp-spt.mjs","description":"Native OMP extension implementation. Pure helper tests exist, but lifecycle, queue, auth/state/reply, failure recovery, and shutdown are untested. sendUserMessage failure can silently drop a taken message from the sender’s perspective; listener death is only local UI state."},{"path":"plugin/omps/.claude-plugin/plugin.json","description":"Enforces plugin name omps but describes a Claude Code adapter and includes claude-spt lineage. This is either evidence of an intentional cross-harness surface needing a decision, or stale product identity."},{"path":"adapter/strings/skills/setup.md","description":"Broken/stale OMP setup instructions: activates claude-spt, uses the old manifest/repo, and offers a nonexistent ccs overlay."},{"path":"plugin/omps/skills/setup/SKILL.md","description":"Self-contained setup fallback repeats the wrong claude-spt paths/repo/profile and therefore cannot be dismissed as harmless comments."},{"path":"tools/claude-spt/src/bridge.rs","description":"Headless RPC bridge with Windows Job Object, reader threads, deadlines, logging, and explicit failure replies. Still serializes rather than steers, lacks Unix hard-kill cleanup, has no clean switch contract, and ignores spt child exit status in spt_call."},{"path":"tools/claude-spt/Cargo.toml","description":"Shipped executable is correctly renamed omp-spt, closing the basename collision on current builds."},{"path":"tests/omp-extension.mjs","description":"Tests only decodeBody, drainEvents, and extractReply; does not exercise the exported extension lifecycle required by REQ-OMP-NATIVE-TUI."},{"path":"tests/manifest-shortcut.sh","description":"Useful current manifest assertions for omp identity/native self+resume/no ccs, but tags them to several Claude-specific generic requirements, creating trace semantic mismatch."},{"path":"tests/bridge-acceptance.sh","description":"Gated headless bridge E2E covers message/reply/bind, hung-turn deadline/failure notice, and Windows orphan cleanup. It is incorrectly tagged as impl evidence for Claude REQ-SKILL-LIVE and skips by default."},{"path":"ci/manifest/registration-int.sh","description":"Adds the OMP manifest but asserts/removes claude-spt, requires claude-spt:ccs, and resolves /sptc bodies. It is stale and cannot be valid OMP registration evidence."},{"path":"ci/setup/activate-int.sh","description":"Adds the OMP manifest but drives the claude-spt deregister/reactivate/profile/string contract. Stale acceptance."},{"path":"ci/digest/digest-proof-int.sh","description":"Uses OMP manifest path but Claude binary name, Claude session layout/sample, and adapter id claude-spt; does not test digest-omp."},{"path":"ci/acceptance/run-acceptance.sh","description":"Real-Claude UserPromptSubmit hook acceptance, not OMP acceptance. Retain only if the Claude plugin is intentionally supported and classify separately."},{"path":"ci/psyche/live-relay-int.sh","description":"Tests the obsolete resident claude-spt-psyche model and claude-spt adapter id, not current psyche-omp ephemeral behavior."},{"path":"ci/run-gates.sh","description":"Default gates run shell/unit/build/trace checks but gated OMP E2E skips as success; native TUI acceptance is absent, so default green is not OMP end-to-end proof."}],"architecture":"# One-question-at-a-time grill dependency order\n\nAsk exactly one question at each stage; do not descend until its parent is settled.\n\n1. **Product boundary (highest dependency):** “Is `plugin/omps` a deliberately supported Claude Code control surface for omp-spt, or should omp-spt be OMP-native only?”\n   - If OMP-only: retire plugin/update-post/hook/UPS/CC hazards/CC tests cleanly.\n   - If retained: define it as a separate topology with its own applicability labels; do not call Claude hook behavior OMP behavior.\n\n2. **Supported topology matrix:** “Which topologies are product-supported: native interactive OMP, headless `omp-spt bridge`, retained Claude plugin (if yes), and daemon-driven OMP Psyche?”\n   - This fixes the domain boundary for every later requirement and term.\n\n3. **Canonical naming:** “Ratify or change the implemented OMP tokens: repo/adapter/binary `omp-spt`, shortcut `omp`, and—only if retained—Claude plugin prefix `omps`.”\n   - Record a new OMP naming ADR; explicitly supersede ADR-0005 for this fork.\n\n4. **Capability truth:** “For each supported topology, which SPT types are honest: ReadyAgent, LiveAgent, Worker?”\n   - In particular, decide Worker implementation versus removing the advertised capability.\n\n5. **Native extension lifecycle contract:** “When a bound native extension loses its listener or cannot submit an inbound message, must it retry, reply with failure, mark offline, or terminate the endpoint?”\n   - Lock no-silent-loss and recovery before writing tests.\n\n6. **Headless bridge turn policy:** “Does the headless bridge intentionally serialize later messages until `agent_end`, or must it implement OMP `steer`/`follow_up` for mid-turn delivery?”\n   - This disposes MEDIUM-1 and corrects the plan promise.\n\n7. **Headless bridge shutdown/switch policy:** “What operator-visible stop/unbind/switch contract must work on every supported OS?”\n   - Then decide whether Unix process-group cleanup is required and define the cross-adapter recovery acceptance.\n\n8. **Session/context ownership:** “What OMP-native locations and identity rules govern commune/signoff files, resume, transcript lookup, and Psyche state?”\n   - Resolve `.claude` versus `.omp`/adapter-owned state and align skill prose, manifest, digest/history, and Psyche.\n\n9. **Provider configuration:** “Which OMP profile/model roles should the Librarian use by default, if any, while keeping OpenRouter setup outside adapter code?”\n   - The billing boundary itself is already locked and must not be re-asked.\n\n10. **Minimum public contract:** “Given the final topology/capability matrix, what is the lowest published spt-core version that supports every actually used verb and manifest field?”\n    - Derive the floor from published artifacts only; delete the inherited floor narrative that no longer applies.\n\n11. **Traceability migration:** “Do we clean-cut the registry to OMP requirement titles, or preserve Claude lineage in a separately labeled historical registry/document?”\n    - Recommendation: clean-cut current requirements; preserve lineage in docs/changelog, not active trace entries.\n\n12. **Acceptance bar:** “Which live proofs are release-blocking for native OMP, headless RPC, Psyche, resume, digest/history, and cross-adapter recovery?”\n    - Only after contracts are fixed should required stages and tests be assigned.\n\n13. **Documentation supersession pass:** “Which inherited ADRs/hazards remain applicable to a retained Claude topology, and which become historical/superseded?”\n    - Update CONTEXT last so its glossary reflects all upstream answers rather than steering them.\n\n## Recommended document authority after the grill\n\n- `CONTEXT.md`: OMP-native glossary only, with explicitly qualified sister/legacy terms.\n- `SCOPE.md`: replace the Claude scoping ledger with the final omp-spt product/capability boundary; archive the old ledger as lineage if needed.\n- OMP ADRs: one naming/product-boundary ADR, one topology/capability ADR, ADR-0007 retained, one bridge reliability/recovery ADR if headless remains supported.\n- `docs/KNOWN-HAZARDS.md`: only current invariants, grouped by topology; retain historical incidents with explicit resolved/superseded status.\n- `docs/OMP-BRIDGE-FIELD-ISSUES.md`: add per-finding status and evidence, or fold still-live invariants into KNOWN-HAZARDS and mark the report historical.\n- `traceable-reqs.toml`: OMP-observable contracts only; no OMP evidence attached to a requirement whose title says Claude/ccs/sptc/cc launcher.\n- Tests: separate `native-extension`, `headless-bridge`, `omp-session`, and optional `claude-control-plugin` lanes so one topology cannot accidentally certify another."},"status":"success","type":"result"},"isError":false,"timestamp":1784087490987}}
{"type":"message","id":"4a6c7ad6","parentId":"95488174","timestamp":"2026-07-15T03:51:31.002Z","message":{"role":"assistant","content":[],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"aborted","errorMessage":"Request was aborted","errorId":134221824,"timestamp":1784087490998}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-15T03:58:31.030Z"},"id":"c3b43af4","parentId":"4a6c7ad6","timestamp":"2026-07-15T03:58:31.030Z"}
