diff --git a/AGENTS.md b/AGENTS.md
index 7ad108051..2a6a4cf94 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -307,6 +307,11 @@ consider model capability as the cause.
## System Skills Sync Rule
+Runtime skill use is logical: call `skill_load` by canonical name and
+`skill_read_resource` for bundled files. Do not expose physical skill roots in
+prompt indexes or teach agents to derive `SKILL.md` paths. Direct filesystem
+inspection is reserved for explicit operator diagnostics.
+
System skills in `feeds/skills/.system/files/` are the agent's operational
guidance — they tell the running agent how to use features. When you change a
feature area, the corresponding skill **must** be updated in the same PR.
diff --git a/Directory.Build.props b/Directory.Build.props
index a9f59e71b..b91f55b9e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,21 +9,13 @@
enabletrue0.25.0
- alpha.onnx.5
- Netclaw v0.25.0-beta.1 — SkillServer native sub-agent sync, memory curation unification, systemd PATH fix
-
-**Features**
-
-* **SkillServer native sub-agent sync** — Optional native manifest sidecar sync for server-managed sub-agents. Local sub-agent files load before server-feed files so user-authored definitions always win. ([#1539](https://github.com/netclaw-dev/netclaw/pull/1539))
-
-**Bug Fixes**
-
-* **Systemd shell tool PATH** — Fixed: daemon now captures the operator's full PATH into the systemd EnvironmentFile instead of relying on a hardcoded list. ([#1565](https://github.com/netclaw-dev/netclaw/pull/1565))
-
-**Memory**
-
-* **Shared curation evaluator** — Unified curation logic across both memory write pipelines so they can never diverge again. ([#1575](https://github.com/netclaw-dev/netclaw/pull/1575))
-* **Memory audit quick wins** — July 2026 audit: revived curation LLM, balanced prompt, and recall precision re-tune. ([#1568](https://github.com/netclaw-dev/netclaw/pull/1568))
+ alpha.onnx.6
+ - Preserve Git working context across sessions and subagents — A bounded, audience-aware Git working-context snapshot stays current in the system prompt, and coding subagents inherit recent-file/project context (#1630)
+- User-written AGENTS.md for application-specific agent guidance — Operators can author ~/.netclaw/identity/AGENTS.md, layered after Netclaw's embedded operating core and inherited by sub-agents (#1622)
+- Memory curation no longer overwrites existing documents on collision — Create-decision collisions now append new content instead of silently overwriting the existing document (#1637)
+- STDIO MCP server arguments no longer rewritten — The daemon now preserves configured STDIO MCP server arguments, and uses one daemon-owned client per configured MCP server (#1636)
+- Logical skill access and authoritative inventory refresh — Skill loading now resolves through logical skill_load/skill_read_resource access with native > managed-feed > external precedence (#1634)
+- Bump SkillServer to stable — Netclaw.SkillClient 0.4.0-beta.4 → 0.4.0 (#1638)net10.0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1cd139e82..c0206dd8f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -68,7 +68,7 @@
-
+
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 92287b935..829c1ad5b 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,38 @@
# NetClaw Release Notes
+## 0.25.0-alpha.onnx.6 (2026-07-14)
+
+> Experimental ONNX local-embeddings build. Syncs `feature/memory-embeddings` with `dev`
+> through 0.25.0-beta.4. No changes to memory/embeddings behavior in this release — this is
+> a mainline sync only. Everything still rides `Memory.Embeddings.Enabled`, off by default;
+> install only by exact pin (`NETCLAW_VERSION=0.25.0-alpha.onnx.6`).
+
+### Features
+- **Preserve Git working context across sessions and subagents** — a bounded, audience-aware Git working-context snapshot stays current in the system prompt, and coding subagents inherit recent-file/project context so parent sessions merge back only confirmed successful child edits ([#1630](https://github.com/netclaw-dev/netclaw/pull/1630))
+
+### Bug Fixes
+- **Curation dedup no longer overwrites existing memories** — a Create-decision anchor collision now appends below a dated separator instead of silently replacing the document; verbatim duplicates are skipped. ([#1637](https://github.com/netclaw-dev/netclaw/pull/1637))
+- **STDIO MCP server arguments no longer rewritten** — the daemon now preserves configured STDIO MCP server arguments, and uses one daemon-owned client per configured MCP server ([#1636](https://github.com/netclaw-dev/netclaw/pull/1636))
+
+### Improvements
+- **Logical skill access and authoritative inventory refresh** — skill loading now resolves through logical `skill_load`/`skill_read_resource` access with native > managed-feed > external precedence ([#1634](https://github.com/netclaw-dev/netclaw/pull/1634))
+
+## 0.25.0-beta.4 (2026-07-14)
+
+### Features
+- **Preserve Git working context across sessions and subagents** — A bounded, audience-aware Git working-context snapshot (branch, worktree, repository, upstream, changed files) now stays current in the system prompt, and coding subagents inherit recent-file/project context so parent sessions merge back only confirmed successful child edits. Measured 20% → 100% success rate on a linked-worktree coding eval ([#1630](https://github.com/netclaw-dev/netclaw/pull/1630))
+- **User-written `AGENTS.md` for application-specific agent guidance** — Operators can now author `~/.netclaw/identity/AGENTS.md`, layered after Netclaw's embedded operating core and inherited by sub-agents, to give the running agent deployment-specific mission and workflow guidance. Seeded with a minimal scaffold during init without overwriting existing guidance ([#1622](https://github.com/netclaw-dev/netclaw/pull/1622))
+
+### Bug Fixes
+- **Memory curation no longer overwrites existing documents on collision** — Fixed: when a curation Create decision targeted an anchor that already had a document, the write silently overwrote the existing title, body, and classification with no history — observed 88 times in 14 days in production, including one case that destroyed an LLM-merged document. Collisions now append the new content under a dated separator instead of overwriting; a verbatim duplicate is skipped as a no-op ([#1637](https://github.com/netclaw-dev/netclaw/pull/1637))
+- **STDIO MCP server arguments no longer rewritten** — Fixed: configured STDIO MCP server arguments were being rewritten by the daemon; the daemon now preserves them as configured. Also simplified to one daemon-owned client per configured MCP server, removing Playwright-specific session-scoped process handling ([#1636](https://github.com/netclaw-dev/netclaw/pull/1636))
+
+### Improvements
+- **Logical skill access and authoritative inventory refresh** — Skill loading now resolves through logical `skill_load`/`skill_read_resource` access instead of physical skill-root paths, with native > managed-feed > external precedence. Startup, sync, watcher, and `skill_manage` inventory rebuilds are now centralized through one live-source refresher publishing atomic registry snapshots ([#1634](https://github.com/netclaw-dev/netclaw/pull/1634))
+
+### Dependency Updates
+- **Bump SkillServer to stable** — `Netclaw.SkillClient` 0.4.0-beta.4 → 0.4.0 (stable release) ([#1638](https://github.com/netclaw-dev/netclaw/pull/1638))
+
## 0.25.0-alpha.onnx.5 (2026-07-12)
> **Experimental feature build** (fifth in the memory-embeddings series). This build carries
@@ -23,6 +56,18 @@
- MessagePack 3.1.7 → 3.1.8 ([#1605](https://github.com/netclaw-dev/netclaw/pull/1605))
- .NET SDK 10.0.300 → 10.0.301 ([#1381](https://github.com/netclaw-dev/netclaw/pull/1381))
+## 0.25.0-beta.3 (2026-07-12)
+
+### Features
+- **Discord DM reminder delivery** — Reminders can now be delivered to Discord DMs via improved `DiscordReminderTargetResolver` ([#1609](https://github.com/netclaw-dev/netclaw/pull/1609))
+- **Named model configuration & provider runtime validation** — New `NamedModelConfiguration` and `ProviderRuntimeValidation` types, config schema updates, and CLI wizard improvements for provider/model setup ([#1610](https://github.com/netclaw-dev/netclaw/pull/1610))
+
+### Bug Fixes
+- **Model set/picker preserves hand-set modalities** — Re-selecting the same model no longer wipes operator-set `InputModalities`/`OutputModalities` and `ContextWindow`. Added `--input-modalities`, `--output-modalities`, `--clear-modalities`, and `--clear-context-window` CLI flags ([#1610](https://github.com/netclaw-dev/netclaw/pull/1610))
+- **Slack processing status serialization** — Slack processing status updates are now serialized to prevent race conditions during concurrent sends ([#1556](https://github.com/netclaw-dev/netclaw/pull/1556))
+- **Sub-agent token usage tracked in daily stats** — Sub-agent LLM calls now record token usage, making them visible in `netclaw stats` ([#1597](https://github.com/netclaw-dev/netclaw/pull/1597))
+- **Subagents fail closed for unattended approvals** — When a subagent requires approval but the session is unattended, it now fails closed instead of proceeding or hanging ([#1616](https://github.com/netclaw-dev/netclaw/pull/1616))
+
## 0.25.0-alpha.onnx.4 (2026-07-09)
> **Experimental feature build** (fourth in the memory-embeddings series). Same gating:
diff --git a/docs/prd/PRD-004-cli-onboarding-and-config.md b/docs/prd/PRD-004-cli-onboarding-and-config.md
index 03fe4f848..79569da66 100644
--- a/docs/prd/PRD-004-cli-onboarding-and-config.md
+++ b/docs/prd/PRD-004-cli-onboarding-and-config.md
@@ -61,7 +61,8 @@ Fresh-install flow:
1. LLM provider configuration (endpoint URL, API key or OAuth device flow,
model selection, connectivity test)
2. Identity setup (workspaces directory, user name, timezone) with init-owned
- regeneration of `SOUL.md` and `TOOLING.md`
+ regeneration of `SOUL.md` and `TOOLING.md` plus non-destructive seeding of
+ the deployment mission scaffold in `AGENTS.md`
3. Security posture (`Personal`, `Team`, `Public`)
4. Enabled Features for `Team` and `Public` only
5. Final validation / health check / next steps
@@ -199,12 +200,18 @@ Onboarding captures all Phase 1 setup items in a stepwise flow.
`netclaw init` SHALL support an interactive guided onboarding flow that:
1. Captures LLM provider configuration (OpenRouter default, OAuth or API key)
-2. Captures init-owned identity settings and regenerates `SOUL.md` /
- `TOOLING.md`
+2. Captures init-owned identity settings, regenerates `SOUL.md` / `TOOLING.md`,
+ and seeds `AGENTS.md` only when absent
3. Selects security posture (`Personal`, `Team`, `Public`)
4. Continues into Enabled Features when posture is `Team` or `Public`
5. Runs final validation and prints next-step run commands
+After successful setup, the initial chat SHALL discover operator context and
+the deployment mission as separate concerns. Confirmed personality/operator
+context is persisted to `SOUL.md`; confirmed mission, recurring workflows,
+skill-selection rules, delegation practices, and review gates are persisted to
+`AGENTS.md` without overwriting an existing playbook during wizard setup.
+
### CLI-001B Post-Install Configuration
`netclaw config` SHALL be the primary post-install settings surface. It SHALL:
diff --git a/docs/prd/PRD-006-mcp-tool-integration.md b/docs/prd/PRD-006-mcp-tool-integration.md
index ae7091bbe..5d51bff87 100644
--- a/docs/prd/PRD-006-mcp-tool-integration.md
+++ b/docs/prd/PRD-006-mcp-tool-integration.md
@@ -94,6 +94,13 @@ Runtime SHALL degrade gracefully when MCP server is unavailable:
- Reconnection is attempted on next tool call
- Diagnostics flag the outage
+### MCP-009 Daemon-Bound Server Ownership
+
+Each configured MCP server SHALL have at most one live client connection per
+Netclaw daemon. A local STDIO server process and its internal state are shared
+by all sessions authorized to use that server; Netclaw session identity SHALL
+not launch or select a separate MCP process.
+
## Non-Goals (MVP)
- Dynamic marketplace discovery of MCP servers
@@ -110,6 +117,8 @@ Runtime SHALL degrade gracefully when MCP server is unavailable:
5. MCP tools appear in session tool definitions when server is enabled and
granted.
6. Unavailable MCP server does not crash the session.
+7. Calls from different authorized sessions to one local STDIO profile use the
+ same daemon-owned client and child process.
## Cross-References
diff --git a/docs/prd/PRD-007-agent-personality-and-local-memory.md b/docs/prd/PRD-007-agent-personality-and-local-memory.md
index 9b2bedea7..bb919e599 100644
--- a/docs/prd/PRD-007-agent-personality-and-local-memory.md
+++ b/docs/prd/PRD-007-agent-personality-and-local-memory.md
@@ -9,7 +9,7 @@
## Goal
-Define the agent soul (personality, instructions, user preferences), local
+Define the agent identity (personality, deployment mission, operator context), local
memory system (project registry, environment inventory), capability
self-discovery, self-configuration, and first-party tool access. This is the
"brain" of Netclaw — everything that makes it a persistent, context-aware
@@ -25,18 +25,19 @@ agent rather than a stateless chat endpoint.
## Agent Soul Architecture
-### File-Based Personality (Soul Files)
+### File-Based Identity
-Agent identity is stored as data (markdown files), not code. The agent
-reconstructs its personality from files on every session start. This makes
-identity hot-swappable and version-controllable without code changes.
+Agent identity is stored as data (markdown files), not code. Netclaw supplies
+an embedded operating core that explains the machinery. Operator-authored files
+augment it and are re-read before each inbound turn, making deployment behavior
+hot-swappable and version-controllable without code changes.
```
~/.netclaw/
- soul/
- PERSONALITY.md # Agent character, tone, values, boundaries
- INSTRUCTIONS.md # Operating rules, behavioral guidelines
- USER.md # Owner preferences, timezone, how to address them
+ identity/
+ SOUL.md # Agent personality, tone, and operator context
+ AGENTS.md # Deployment mission, workflows, skills, and quality gates
+ TOOLING.md # Available environment capabilities
projects/
registry.json # Registered project configurations
environment/
@@ -50,14 +51,19 @@ identity hot-swappable and version-controllable without code changes.
### Layered System Prompt Assembly
-Session context is assembled from layers (later layers augment earlier):
+Session context is assembled from layers:
-1. **PERSONALITY.md** — who the agent is (values, tone, boundaries)
-2. **INSTRUCTIONS.md** — how the agent operates (rules, workflows)
-3. **USER.md** — who it serves (owner name, preferences, timezone)
-4. **Project AGENTS.md** — context overlay when working on a registered project
-5. **Environment summary** — condensed capability inventory
-6. **Session context** — conversation history, tool results, memory
+1. **SOUL.md** — who the agent is and who it serves
+2. **Embedded operating core** — how Netclaw machinery operates for the audience
+3. **Deployment AGENTS.md** — how this deployment accomplishes its mission
+4. **TOOLING.md** — what capabilities are available
+5. **Project AGENTS.md** — scoped context for the active project
+6. **Dynamic and session context** — tools, skills, memory, history, and results
+
+The same deployment playbook applies to Personal, Team, and Public audiences
+and is inherited by sub-agents. It contains durable workflow guidance only,
+never secrets or audience-private data. Embedded prompt precedence is guidance;
+runtime ACL and tool policy remain the authoritative security boundary.
### Conversational Personality Bootstrap
@@ -65,10 +71,12 @@ On first interaction (or when personality files don't exist), the agent runs
a personality bootstrap conversation:
1. Introduce itself and explain the setup process
-2. Learn the owner's name, preferences, communication style
-3. Scan environment for installed tools and capabilities
-4. Write initial PERSONALITY.md, INSTRUCTIONS.md, USER.md
-5. Confirm readiness
+2. Learn the owner's name, preferences, and communication style
+3. Learn the deployment mission, successful outcomes, recurring workflows,
+ skill-selection expectations, delegation rules, and known failure modes
+4. Propose a concise mission playbook and obtain confirmation
+5. Update `SOUL.md` and `AGENTS.md` with their canonical content
+6. Confirm that the playbook applies on the next message
This can be re-triggered via `netclaw personality reset` (PRD-004).
diff --git a/docs/spec/SPEC-007-guided-onboarding.md b/docs/spec/SPEC-007-guided-onboarding.md
index 6c527ef96..f93b0adb0 100644
--- a/docs/spec/SPEC-007-guided-onboarding.md
+++ b/docs/spec/SPEC-007-guided-onboarding.md
@@ -27,6 +27,7 @@ existing-install re-entry paths.
- collect user name
- collect timezone
- regenerate `SOUL.md` and `TOOLING.md`
+- seed a minimal deployment `AGENTS.md` scaffold only when absent
### Step 3: Security Posture
@@ -60,6 +61,16 @@ bootstrap flow by default. Instead it presents:
- remains init-owned
- reuses the identity form with existing values prefilled
- continues into the bot-assisted identity conversation
+- never overwrites an existing deployment `AGENTS.md`
+
+### Post-Init Conversation
+
+- discovers operator/personality context for `SOUL.md`
+- discovers mission, workflows, required skills, delegation, and review gates
+ for `AGENTS.md`
+- summarizes the proposed playbook and requires confirmation before writing
+- reads and preserves existing identity-file content
+- reports that confirmed changes apply on the next inbound message
### Start Over From Scratch
diff --git a/evals/README.md b/evals/README.md
index cbb60aee7..85bd194ac 100644
--- a/evals/README.md
+++ b/evals/README.md
@@ -47,6 +47,13 @@ default provider.
`$EVAL_HOME` is deleted. A throwaway root-in-container cleanup step
handles files the daemon wrote as UID 0.
+The harness preloads `evals/fixtures/config/netclaw.json` into the ephemeral
+home before startup. It auto-approves tools and grants read/write access for the
+Personal audience because headless sessions cannot answer approval prompts or
+edit an interactive trust policy. A companion `tool-approvals.json` trusts Git
+for shell-based coding cases. Tool exposure and command-deny rules still apply,
+and these policies are never copied into an operator's config.
+
`--network host` is the default because operators often host their LLM on
a Tailscale node — MagicDNS hostnames like `my-gpu-server.tailnet.ts.net` only
resolve when the container shares the host's DNS resolver. macOS/Windows
@@ -61,13 +68,15 @@ log patterns** (skill loading, memory recall, checkpoint formation).
| Category | Cases | What It Validates |
|----------|-------|-------------------|
-| Identity & Self-Awareness | 4 | Bot knows its name, version, repo, session ID |
+| Identity & Self-Awareness | 5 | Bot knows its name, version, repo, session ID, and routes all identity-file concerns without a skill dependency |
| Skill Auto-Loading | 4 | Keyword matching triggers correct skills |
| Memory Pipeline | 4 | Memory recall is active, identity-vs-memory routing is correct, explicit saves use memory tools, and automatic checkpointing still fires |
| Tool Discovery & Use | 4 | Progressive tool discovery and invocation |
| Grounding & Alignment | 3 | Uses tools to verify facts, admits uncertainty |
| Autonomy & Execution | 2 | Executes tasks rather than describing them |
-| Subagents | 1 | Delegates through `spawn_agent` and verifies headless subagents complete ambiguous work without clarification loops |
+| Deployment Mission | 1 | Applies the disk mission playbook, loads its required skill, and returns reviewed sales email |
+| Subagents | 2 | Delegates through `spawn_agent`, completes ambiguous work, and gives specialized subagent guidance precedence over a conflicting deployment playbook |
+| Coding Context | 1 | Repeatedly switches between isolated linked worktrees, alternates branch and one-of-four target files by run, and verifies Git grounding, wrong-file/worktree safety, and path-free child handoff |
| Complex Task Execution | 5 | Multi-step tool chains complete successfully, incl. bounded tool output — given only the goal (no handling hints), the agent retrieves a deep line from oversized shell output and from a large file, which is only possible by coping with the bound the way AGENTS.md/skills/steer text direct |
| Multi-Turn Conversation | 7 | Session resume and speaker attribution recall |
@@ -155,20 +164,24 @@ NETCLAW_EVAL_PROVIDER_TYPE=ollama \
NETCLAW_EVAL_PROVIDER_ENDPOINT=http://127.0.0.1:11434 \
NETCLAW_EVAL_MODEL_ID=qwen3:30b \
./evals/run-evals.sh
+
+# Run ten alternating linked-worktree/recent-file coherence trials
+NETCLAW_IMAGE=netclaw-eval:working-context-treatment \
+NETCLAW_EVAL_PROVIDER_TYPE=openai-compatible \
+NETCLAW_EVAL_PROVIDER_ENDPOINT=https://your-provider.example/v1 \
+NETCLAW_EVAL_MODEL_ID=your-model \
+NETCLAW_EVAL_CASE=coding_context_worktree_handoff \
+NETCLAW_EVAL_RUNS=10 NETCLAW_EVAL_TIMEOUT=180 \
+ ./evals/run-evals.sh
```
## Results Database
-Results are stored in `$EVAL_HOME/evals/results.db` (SQLite) inside the
-per-run throwaway directory, NOT under `~/.netclaw/`. This means results
-don't persist across runs by default — on script exit, the database is
-deleted along with `$EVAL_HOME`.
-
-If you want to retain results for trend analysis, copy the database out
-of `$EVAL_HOME` before the EXIT trap fires (look for the "Results:
-..." line at the bottom of the script output to get the path). A
-dedicated results-retention follow-up may add a `NETCLAW_EVAL_RESULTS_DB`
-override.
+Results are accumulated in `$EVAL_HOME/evals/results.db` during execution.
+On exit, the harness archives the database, run metadata, daemon log, and
+per-turn stdout under `evals/runs//` before deleting the throwaway
+home. These archives are gitignored and can be compared locally without
+touching the operator's `~/.netclaw/` state.
Requires `sqlite3` CLI — if not available, the script still runs but
skips persistence.
diff --git a/evals/fixtures/agents/coding-worker.md b/evals/fixtures/agents/coding-worker.md
new file mode 100644
index 000000000..85cbd2ad2
--- /dev/null
+++ b/evals/fixtures/agents/coding-worker.md
@@ -0,0 +1,7 @@
+---
+name: coding-worker
+description: Eval fixture subagent that performs a small, deterministic code edit in the inherited project.
+timeoutSeconds: 120
+---
+
+You are a headless coding worker. Use the inherited working context to make the requested minimal edit. Inspect only what is necessary, use first-party file tools for edits, do not change branches or worktrees, and report the files you changed.
diff --git a/evals/fixtures/agents/headless-analyst.md b/evals/fixtures/agents/headless-analyst.md
index a811be2d7..7379cce5c 100644
--- a/evals/fixtures/agents/headless-analyst.md
+++ b/evals/fixtures/agents/headless-analyst.md
@@ -6,6 +6,10 @@ timeoutSeconds: 60
You are a headless analysis worker used by the Netclaw eval suite.
+You are not a sales-email writer. When asked to draft an outbound or prospecting
+email, return a concise research brief headed `SPECIALIZED ANALYST BRIEF` instead.
+Do not produce a subject line, greeting, call request, or email copy.
+
When a task has ambiguous inclusion criteria, do not ask the user what to do.
Make a reasonable assumption, state it briefly, and produce a final answer.
diff --git a/evals/fixtures/config/netclaw.json b/evals/fixtures/config/netclaw.json
new file mode 100644
index 000000000..2cc51d363
--- /dev/null
+++ b/evals/fixtures/config/netclaw.json
@@ -0,0 +1,21 @@
+{
+ "configVersion": 1,
+ "Tools": {
+ "AudienceProfiles": {
+ "Personal": {
+ "ReadFiles": {
+ "Mode": "All",
+ "Roots": []
+ },
+ "WriteFiles": {
+ "Mode": "All",
+ "Roots": []
+ },
+ "ApprovalPolicy": {
+ "DefaultMode": "Auto",
+ "ToolOverrides": {}
+ }
+ }
+ }
+ }
+}
diff --git a/evals/fixtures/config/tool-approvals.json b/evals/fixtures/config/tool-approvals.json
new file mode 100644
index 000000000..e2cd6a0d7
--- /dev/null
+++ b/evals/fixtures/config/tool-approvals.json
@@ -0,0 +1,37 @@
+{
+ "version": 2,
+ "audiences": {
+ "personal": {
+ "shell_execute": [
+ {
+ "verb": "git",
+ "directory": null
+ },
+ {
+ "verb": "git branch",
+ "directory": null
+ },
+ {
+ "verb": "git config",
+ "directory": null
+ },
+ {
+ "verb": "git diff",
+ "directory": null
+ },
+ {
+ "verb": "git rev-parse",
+ "directory": null
+ },
+ {
+ "verb": "git status",
+ "directory": null
+ },
+ {
+ "verb": "git worktree",
+ "directory": null
+ }
+ ]
+ }
+ }
+}
diff --git a/evals/fixtures/identity/AGENTS.md b/evals/fixtures/identity/AGENTS.md
new file mode 100644
index 000000000..35e2de06b
--- /dev/null
+++ b/evals/fixtures/identity/AGENTS.md
@@ -0,0 +1,14 @@
+# Sales Outreach Mission
+
+This deployment helps sales representatives write concise, credible business
+email. When asked to compose customer or prospect email:
+
+1. Load the `business-email-review` skill before drafting.
+2. Apply that skill's drafting and review checklist.
+3. Revise the draft before returning it; do not expose internal review notes.
+4. Never invent customer examples, adoption claims, metrics, or existing
+ relationships. Use only facts supplied in the task.
+
+When email work is delegated, pass the task to the worker and require the same
+reviewed output. Every prospecting email must include a `Subject:` line and end
+with this single CTA: `Would Tuesday or Wednesday work for a 15-minute call?`
diff --git a/evals/fixtures/server-feed-skills/logical-feed-probe/SKILL.md b/evals/fixtures/server-feed-skills/logical-feed-probe/SKILL.md
new file mode 100644
index 000000000..bc95f717a
--- /dev/null
+++ b/evals/fixtures/server-feed-skills/logical-feed-probe/SKILL.md
@@ -0,0 +1,11 @@
+---
+name: logical-feed-probe
+description: Use when asked to verify logical access to a managed server-feed skill and its reference resource.
+metadata:
+ version: 1.0.0
+---
+
+# Logical Feed Probe
+
+Read `references/probe.md` with `skill_read_resource` and return its exact
+verification phrase. Do not infer or reconstruct the phrase.
diff --git a/evals/fixtures/server-feed-skills/logical-feed-probe/references/probe.md b/evals/fixtures/server-feed-skills/logical-feed-probe/references/probe.md
new file mode 100644
index 000000000..8d1a21d54
--- /dev/null
+++ b/evals/fixtures/server-feed-skills/logical-feed-probe/references/probe.md
@@ -0,0 +1,3 @@
+# Logical feed verification
+
+The exact verification phrase is `ORBITAL-MANGO-7421`.
diff --git a/evals/fixtures/skills/business-email-review/SKILL.md b/evals/fixtures/skills/business-email-review/SKILL.md
new file mode 100644
index 000000000..b9c859805
--- /dev/null
+++ b/evals/fixtures/skills/business-email-review/SKILL.md
@@ -0,0 +1,19 @@
+---
+name: business-email-review
+description: Required drafting and review process for customer-facing business and sales email.
+invocable: false
+---
+
+# Business Email Review
+
+Draft concise email that is specific to the recipient and avoids unsupported
+claims. Before returning it, review and revise for:
+
+- a concrete subject line;
+- a relevant opening rather than generic pleasantries;
+- one business outcome;
+- short, direct sentences;
+- exactly one clear call to action;
+- no invented customer facts, metrics, or relationships.
+
+Return only the revised email. Do not expose the internal checklist.
diff --git a/evals/run-evals.sh b/evals/run-evals.sh
index 86650160c..17633a01a 100755
--- a/evals/run-evals.sh
+++ b/evals/run-evals.sh
@@ -239,10 +239,15 @@ archive_eval_run() {
cp "$TMPDIR_EVAL"/stdout_*.txt "$archive_dir/stdout/" 2>/dev/null || true
fi
- # Write run metadata
+ # Write run metadata, including the immutable image identity so before/after
+ # comparisons remain auditable even when tags are later rebuilt.
+ local image_id
+ image_id=$(docker image inspect "$NETCLAW_IMAGE" --format '{{.Id}}' 2>/dev/null || echo unknown)
cat > "$archive_dir/run-info.txt" <&2
+ exit 1
+ fi
substitute_identity_template "$template_dir/TOOLING.template.md" "$EVAL_HOME/identity/TOOLING.md"
else
echo "ERROR: no identity templates at $template_dir/ — Identity evals will fail." >&2
@@ -352,12 +362,29 @@ start_eval_daemon() {
cp -r "$REPO_ROOT/evals/fixtures/skills/." "$EVAL_HOME/skills/"
fi
+ # Copy a skill into a managed server-feed origin. The configured feed URL
+ # below is intentionally unreachable: startup must retain the already
+ # materialized managed skill while the eval proves model access by logical
+ # name without relying on the physical feed path.
+ if [[ -d "$REPO_ROOT/evals/fixtures/server-feed-skills" ]]; then
+ mkdir -p "$EVAL_HOME/skills/.server-feeds/eval-feed"
+ cp -r "$REPO_ROOT/evals/fixtures/server-feed-skills/." \
+ "$EVAL_HOME/skills/.server-feeds/eval-feed/"
+ fi
+
# Copy eval-only subagent definitions into the mounted NETCLAW_HOME so
# spawn_agent behavior can be exercised without touching the host install.
if [[ -d "$REPO_ROOT/evals/fixtures/agents" ]]; then
cp -r "$REPO_ROOT/evals/fixtures/agents/." "$EVAL_HOME/data/agents/"
fi
+ # Install the eval-only approval policy before daemon startup. Headless eval
+ # sessions cannot answer approval prompts, so tools must be automatic for the
+ # Personal audience. Exposure, filesystem, and command-deny rules remain in force.
+ cp "$REPO_ROOT/evals/fixtures/config/netclaw.json" \
+ "$REPO_ROOT/evals/fixtures/config/tool-approvals.json" \
+ "$EVAL_HOME/data/config/"
+
# Pre-seed a large (>256 KB) text file in the workspaces read-root for the
# bounded-tool-output file_read eval (complex_large_file_read_ranged). It must
# be too big for one inline read AND have model-unguessable content so the only
@@ -371,18 +398,6 @@ start_eval_daemon() {
awk 'BEGIN{x=1;for(i=1;i<=30000;i++){x=(x*48271)%2147483647;print x}}' \
> "$EVAL_HOME/data/workspaces/netclaw-eval-largefile.txt"
- # Pre-trust the 'sleep' verb so the background-job lifecycle eval can
- # actually submit its job: the headless container has no approval
- # requester, and background submission evaluates the approval gate before
- # StartBackgroundJob — without this every submission dies at the gate and
- # the case can only test API shape, not the lifecycle. trust-verb writes a
- # global-wildcard (verb, null) entry to tool-approvals.json under the
- # CLI's NETCLAW_HOME; $EVAL_HOME/data is what the container mounts at
- # /home/netclaw/.netclaw, and the daemon re-reads the file per approval
- # evaluation.
- NETCLAW_HOME="$EVAL_HOME/data" "$NETCLAW_BIN" approvals trust-verb sleep --audience personal >/dev/null 2>&1 \
- || echo "WARN: could not pre-trust 'sleep' — tool_background_job_lifecycle will fail at the approval gate" >&2
-
# The eval container runs as the non-root `netclaw` user and needs write
# access to the bind-mounted identity, logs, skills, and data trees.
chmod -R ugo+rwX "$EVAL_HOME/identity" "$EVAL_HOME/logs" "$EVAL_HOME/data" "$EVAL_HOME/skills"
@@ -421,6 +436,11 @@ start_eval_daemon() {
# ships whatever was last released — masking any unpublished skill
# changes (e.g. version bumps in this PR) and the local copies above.
-e "NETCLAW_SkillSync__DisableSystemSkillSync=true"
+ -e "NETCLAW_SkillFeeds__Feeds__0__Name=eval-feed"
+ -e "NETCLAW_SkillFeeds__Feeds__0__Url=http://127.0.0.1:1"
+ -e "NETCLAW_SkillFeeds__Feeds__0__Enabled=true"
+ -e "NETCLAW_SkillFeeds__Feeds__0__TimeoutSeconds=1"
+ -e "NETCLAW_SkillFeeds__SyncIntervalMinutes=0"
)
if [[ -n "$EVAL_CONTEXT_WINDOW" ]]; then
@@ -748,6 +768,11 @@ run_prompt_resume() {
local prompt="$2"
local turn_file="$TMPDIR_EVAL/stdout_$(date +%s%N)_turn.txt"
+ if [[ ! -x "$NETCLAW_BIN" ]]; then
+ echo "ERROR: eval CLI disappeared during the run: $NETCLAW_BIN" >&2
+ exit 2
+ fi
+
# First call in a multi-turn case: open a fresh shared STDOUT_FILE.
if [[ -z "${MULTI_TURN_STDOUT_FILE:-}" ]]; then
MULTI_TURN_STDOUT_FILE="$TMPDIR_EVAL/stdout_$(date +%s%N)_multi.txt"
@@ -803,20 +828,33 @@ run_multi_turn_case() {
local session_id="eval/${case_name}-run${run}-$$"
MULTI_TURN_STDOUT_FILE=""
+ local setup_fn="setup_${case_name}"
+ if declare -f "$setup_fn" >/dev/null 2>&1; then
+ "$setup_fn" "$run"
+ fi
+
local turn=1
local prompt
for prompt in "${prompts[@]}"; do
- run_prompt_resume "$session_id" "$prompt"
+ local rendered_prompt="$prompt"
+ rendered_prompt="${rendered_prompt//\{\{FIRST_WORKTREE\}\}/${CODING_CONTEXT_FIRST_WORKTREE:-}}"
+ rendered_prompt="${rendered_prompt//\{\{SECOND_WORKTREE\}\}/${CODING_CONTEXT_SECOND_WORKTREE:-}}"
+ rendered_prompt="${rendered_prompt//\{\{TARGET_BRANCH\}\}/${CODING_CONTEXT_TARGET_BRANCH:-}}"
+ rendered_prompt="${rendered_prompt//\{\{TARGET_FILE\}\}/${CODING_CONTEXT_TARGET_FILE:-}}"
+ run_prompt_resume "$session_id" "$rendered_prompt"
store_metrics "$case_name" "$run" "$turn" "$LAST_TURN_USAGE_LINE"
turn=$((turn + 1))
done
local passed=0
local details="fail"
+ EVAL_ASSERTION_DETAILS=""
if $assert_fn 2>/dev/null; then
passed=1
passes=$((passes + 1))
details="pass"
+ elif [[ -n "${EVAL_ASSERTION_DETAILS:-}" ]]; then
+ details="$EVAL_ASSERTION_DETAILS"
fi
# Use the first prompt as the representative prompt_used for eval_results.
@@ -882,6 +920,21 @@ daemon_log_skill_loaded() {
daemon_log_tail | grep -qaE "turn_skill_loaded skill=$skill_name" 2>/dev/null
}
+daemon_log_skill_loaded_by_method() {
+ local skill_name="$1"
+ local method="$2"
+ daemon_log_tail | grep -qaE \
+ "turn_skill_loaded skill=$skill_name method=$method" 2>/dev/null
+}
+
+daemon_log_skill_loaded_via_skill_tool() {
+ daemon_log_skill_loaded_by_method "$1" "skill_load"
+}
+
+daemon_log_skill_loaded_via_file_read() {
+ daemon_log_skill_loaded_by_method "$1" "file_read"
+}
+
daemon_log_no_skill_loaded() {
! daemon_log_tail | grep -qaE "turn_skill_loaded" 2>/dev/null
}
@@ -890,6 +943,15 @@ stdout_tool_called() {
grep -qaE "\\[tool:call\\] $1\\(" "$STDOUT_FILE" 2>/dev/null
}
+stdout_skill_file_read_called() {
+ grep -aiE '^\[tool:call\] file_read\(' "$STDOUT_FILE" 2>/dev/null \
+ | grep -qi 'SKILL\.md'
+}
+
+stdout_no_skill_file_read_called() {
+ ! stdout_skill_file_read_called
+}
+
# ─── Case Assertion Functions ─────────────────────────────────────────────────
# Category 1: Identity & Self-Awareness
@@ -909,10 +971,19 @@ assert_identity_session() {
stdout_contains 'headless/' || stdout_contains 'signalr/' || stdout_contains 'slack/'
}
+assert_identity_file_routing() {
+ stdout_response_contains 'SOUL.md' && \
+ stdout_response_contains 'AGENTS.md' && \
+ stdout_response_contains 'TOOLING.md' && \
+ daemon_log_no_skill_loaded
+}
+
# Category 2: Skill Discovery — tests that the model retrieves procedural
# knowledge from skills when needed AND actually loaded the skill to get it.
assert_skill_scheduling_knowledge() {
- stdout_contains 'cron' && daemon_log_skill_loaded 'netclaw-operations'
+ stdout_contains 'cron' \
+ && daemon_log_skill_loaded_via_skill_tool 'netclaw-operations' \
+ && stdout_no_skill_file_read_called
}
# Two-hop progressive disclosure: the model must (1) load netclaw-operations, then
@@ -921,13 +992,16 @@ assert_skill_scheduling_knowledge() {
# in the slim SKILL.md index. Catches a model that loads the index but skips the
# second hop — the failure mode that silently regresses smaller local agents.
assert_skill_progressive_disclosure() {
- daemon_log_skill_loaded 'netclaw-operations' \
+ daemon_log_skill_loaded_via_skill_tool 'netclaw-operations' \
&& stdout_tool_called 'skill_read_resource' \
+ && stdout_no_skill_file_read_called \
&& { stdout_contains 'ReminderAutoDisabled' || stdout_contains '5 consecutive'; }
}
assert_skill_memory_knowledge() {
- stdout_contains 'durable' && stdout_contains 'evidence' && daemon_log_skill_loaded 'netclaw-memory'
+ stdout_contains 'durable' && stdout_contains 'evidence' \
+ && daemon_log_skill_loaded_via_skill_tool 'netclaw-memory' \
+ && stdout_no_skill_file_read_called
}
assert_skill_operations_diagnostics() {
@@ -941,43 +1015,67 @@ assert_skill_citation_search() {
}
assert_skill_web_content_knowledge() {
- stdout_contains 'browser' && daemon_log_skill_loaded 'web-content-retrieval'
+ stdout_contains 'browser' \
+ && daemon_log_skill_loaded_via_skill_tool 'web-content-retrieval' \
+ && stdout_no_skill_file_read_called
}
# Category 2b: Skill Activation — measures ONLY whether the model loaded
# the skill, using prompts where pretraining cannot shortcut the answer.
assert_skill_activation_scheduling() {
- daemon_log_skill_loaded 'netclaw-operations'
+ daemon_log_skill_loaded_via_skill_tool 'netclaw-operations' \
+ && stdout_no_skill_file_read_called
}
assert_skill_activation_memory() {
- daemon_log_skill_loaded 'netclaw-memory'
+ daemon_log_skill_loaded_via_skill_tool 'netclaw-memory' \
+ && stdout_no_skill_file_read_called
}
assert_skill_activation_search() {
- daemon_log_skill_loaded 'search-citation'
+ daemon_log_skill_loaded_via_skill_tool 'search-citation' \
+ && stdout_no_skill_file_read_called
}
# Soft phrasing — model may load the skill OR use the tool directly from AGENTS.md
assert_skill_activation_soft_scheduling() {
- daemon_log_skill_loaded 'netclaw-operations' || stdout_tool_called 'set_reminder'
+ { daemon_log_skill_loaded_via_skill_tool 'netclaw-operations' \
+ || stdout_tool_called 'set_reminder'; } \
+ && stdout_no_skill_file_read_called
}
assert_skill_activation_soft_memory() {
- daemon_log_skill_loaded 'netclaw-memory' || stdout_tool_called 'find_memories'
+ { daemon_log_skill_loaded_via_skill_tool 'netclaw-memory' \
+ || stdout_tool_called 'find_memories'; } \
+ && stdout_no_skill_file_read_called
}
assert_skill_activation_subagent_authoring() {
- daemon_log_skill_loaded 'subagent-authoring'
+ daemon_log_skill_loaded_via_skill_tool 'subagent-authoring' \
+ && stdout_no_skill_file_read_called
}
# User skills (non-system, from eval fixtures)
assert_skill_activation_user_coding() {
- daemon_log_skill_loaded 'modern-csharp-coding-standards'
+ daemon_log_skill_loaded_via_skill_tool 'modern-csharp-coding-standards' \
+ && stdout_no_skill_file_read_called
}
assert_skill_activation_user_serialization() {
- daemon_log_skill_loaded 'serialization'
+ daemon_log_skill_loaded_via_skill_tool 'serialization' \
+ && stdout_no_skill_file_read_called
+}
+
+assert_skill_server_feed_logical_access() {
+ daemon_log_skill_loaded_via_skill_tool 'logical-feed-probe' \
+ && stdout_tool_called 'skill_read_resource' \
+ && stdout_contains 'ORBITAL-MANGO-7421' \
+ && stdout_no_skill_file_read_called
+}
+
+assert_skill_explicit_physical_inspection() {
+ stdout_tool_called 'file_read' \
+ && daemon_log_skill_loaded_via_file_read 'modern-csharp-coding-standards'
}
# Negative cases — model should NOT load a skill for unrelated prompts
@@ -1136,10 +1234,19 @@ assert_autonomy_web_fetch() {
stdout_contains '\[tool:call\] web_search' || stdout_contains '\[tool:call\] web_fetch'
}
+# Category 6a: Deployment Mission
+assert_deployment_mission_sales_email() {
+ daemon_log_skill_loaded_via_skill_tool 'business-email-review' \
+ && stdout_no_skill_file_read_called \
+ && \
+ stdout_response_contains '^Subject:' && \
+ stdout_response_contains 'Would Tuesday or Wednesday work for a 15-minute call?'
+}
+
# Category 6b: Subagents
assert_subagent_headless_ambiguous_task() {
stdout_tool_called 'spawn_agent' && \
- daemon_log_contains 'SubAgent \[headless-analyst\] completed \(success=True' && \
+ stdout_contains '\[subagent:done\] headless-analyst (completed' && \
stdout_response_contains 'assumption' && \
stdout_response_not_contains 'which.*include' && \
stdout_response_not_contains 'what.*include' && \
@@ -1147,6 +1254,102 @@ assert_subagent_headless_ambiguous_task() {
stdout_response_not_contains 'need.*more.*information'
}
+assert_subagent_specialization_precedence() {
+ stdout_tool_called 'spawn_agent' && \
+ stdout_contains '\[subagent:done\] headless-analyst (completed' && \
+ stdout_contains 'SPECIALIZED ANALYST BRIEF' && \
+ stdout_response_contains '^Subject:' && \
+ stdout_response_contains 'Would Tuesday or Wednesday work for a 15-minute call?'
+}
+
+setup_coding_context_worktree_handoff() {
+ local run="$1"
+ if (( run % 2 == 1 )); then
+ CODING_CONTEXT_FIRST="blue"
+ CODING_CONTEXT_SECOND="green"
+ else
+ CODING_CONTEXT_FIRST="green"
+ CODING_CONTEXT_SECOND="blue"
+ fi
+ CODING_CONTEXT_FIRST_WORKTREE="/home/netclaw/.netclaw/workspaces/coding-context-$CODING_CONTEXT_FIRST"
+ CODING_CONTEXT_SECOND_WORKTREE="/home/netclaw/.netclaw/workspaces/coding-context-$CODING_CONTEXT_SECOND"
+ CODING_CONTEXT_TARGET_BRANCH="feature/$CODING_CONTEXT_SECOND"
+ local -a target_files=(
+ "src/CalculatorAlpha.cs"
+ "src/CalculatorBeta.cs"
+ "src/CalculatorGamma.cs"
+ "src/CalculatorDelta.cs"
+ )
+ CODING_CONTEXT_TARGET_FILE="${target_files[$(((run - 1) % ${#target_files[@]}))]}"
+
+ docker exec --user netclaw "$EVAL_CONTAINER_NAME" bash -lc '
+ set -euo pipefail
+ base=/home/netclaw/.netclaw/workspaces
+ rm -rf "$base/coding-context" "$base/coding-context-blue" "$base/coding-context-green"
+ mkdir -p "$base/coding-context/src"
+ git -C "$base/coding-context" init -b main >/dev/null
+ git -C "$base/coding-context" config user.name "Netclaw Eval"
+ git -C "$base/coding-context" config user.email "eval@netclaw.dev"
+ for name in Alpha Beta Gamma Delta; do
+ printf "public static class Calculator%s\n{\n public static int Add(int a, int b) => a + b;\n}\n" "$name" > "$base/coding-context/src/Calculator$name.cs"
+ done
+ git -C "$base/coding-context" add src
+ git -C "$base/coding-context" commit -m seed >/dev/null
+ git -C "$base/coding-context" worktree add -b feature/blue "$base/coding-context-blue" >/dev/null
+ git -C "$base/coding-context" worktree add -b feature/green "$base/coding-context-green" >/dev/null
+ for color in blue green; do
+ printf "%s staged context\n" "$color" > "$base/coding-context-$color/STAGED-$color.txt"
+ git -C "$base/coding-context-$color" add "STAGED-$color.txt"
+ printf "%s untracked context\n" "$color" > "$base/coding-context-$color/UNTRACKED-$color.txt"
+ done
+ '
+}
+
+assert_coding_context_worktree_handoff() {
+ if ! docker exec --user netclaw \
+ -e "EVAL_FIRST=$CODING_CONTEXT_FIRST" \
+ -e "EVAL_SECOND=$CODING_CONTEXT_SECOND" \
+ -e "EVAL_TARGET_FILE=$CODING_CONTEXT_TARGET_FILE" \
+ "$EVAL_CONTAINER_NAME" bash -lc '
+ set -euo pipefail
+ base=/home/netclaw/.netclaw/workspaces
+ first="$base/coding-context-$EVAL_FIRST"
+ second="$base/coding-context-$EVAL_SECOND"
+ main="$base/coding-context"
+ test "$(git -C "$second" branch --show-current)" = "feature/$EVAL_SECOND"
+ grep -q "Divide" "$second/$EVAL_TARGET_FILE"
+ for tree in "$first" "$main"; do
+ ! grep -R -q "Divide" "$tree/src"
+ done
+ while IFS= read -r file; do
+ [[ "$file" == "$second/$EVAL_TARGET_FILE" ]] || ! grep -q "Divide" "$file"
+ done < <(find "$second/src" -type f -name "*.cs" -print)
+ '; then
+ EVAL_ASSERTION_DETAILS="wrong_worktree_or_missing_edit"
+ return 1
+ fi
+ if ! stdout_tool_called 'spawn_agent'; then
+ EVAL_ASSERTION_DETAILS="spawn_agent_not_called"
+ return 1
+ fi
+ if ! grep -a '^\[tool:call\] spawn_agent' "$STDOUT_FILE" | grep -qv '"Context"'; then
+ EVAL_ASSERTION_DETAILS="manual_context_injected"
+ return 1
+ fi
+ if grep -a '^\[tool:call\] spawn_agent' "$STDOUT_FILE" | grep -q 'Calculator'; then
+ EVAL_ASSERTION_DETAILS="file_name_leaked_to_child"
+ return 1
+ fi
+ if ! stdout_response_contains "$(basename "$CODING_CONTEXT_TARGET_FILE")"; then
+ EVAL_ASSERTION_DETAILS="changed_file_not_reported"
+ return 1
+ fi
+ if ! stdout_response_contains "$CODING_CONTEXT_TARGET_BRANCH"; then
+ EVAL_ASSERTION_DETAILS="target_branch_not_reported"
+ return 1
+ fi
+}
+
# Category 7: Complex Task Execution
assert_complex_write_and_run() {
stdout_contains '\[tool:call\] file_write' && \
@@ -1435,6 +1638,10 @@ run_all() {
"What is your session ID?" \
"What session are we in?"
+ run_case identity_file_routing "routes all three identity concerns without loading a skill" \
+ "Which identity file should hold each of these: my communication style, this deployment's recurring sales workflow, and the tools available on this host?" \
+ "Map personality and operator context, deployment mission and review rules, and environment capabilities to the correct Netclaw identity files."
+
end_category
# ── Category 2: Skill Discovery ──
@@ -1516,6 +1723,12 @@ run_all() {
"Should I stick with Newtonsoft.Json or migrate to something else?" \
"How should I handle serialization for messages between services?"
+ run_case skill_server_feed_logical_access "server-feed skill and resource loaded by logical name" \
+ "Use the logical-feed-probe skill and its listed reference resource. What exact verification phrase does the resource contain?"
+
+ run_case skill_explicit_physical_inspection "explicit physical inspection may use file_read" \
+ "Explicitly inspect the physical file /home/netclaw/.netclaw/skills/modern-csharp-coding-standards/SKILL.md with file_read and tell me its title. This is a filesystem inspection request, not normal skill activation."
+
# Negative cases — model should NOT load a skill
run_case skill_no_activation_unrelated "no skill loaded" \
"What's 2 + 2?" \
@@ -1608,13 +1821,41 @@ run_all() {
end_category
+ # ── Category 6a: Deployment Mission ──
+ print_category "Deployment Mission"
+
+ run_case deployment_mission_sales_email "loads the required skill and returns reviewed mission-compliant email" \
+ "Write a short prospecting email to Morgan, an engineering director evaluating incident-response tools. Introduce Netclaw and ask for a call." \
+ "Draft a concise outbound email to Riley, a platform lead looking to reduce repetitive operations work. Offer a brief Netclaw introduction."
+
+ end_category
+
# ── Category 6b: Subagents ──
print_category "Subagents"
+ local previous_timeout="$PROMPT_TIMEOUT"
+ PROMPT_TIMEOUT=120
+
run_case subagent_headless_ambiguous_task "spawned subagent completes ambiguous task without clarification" \
"Use spawn_agent with agent headless-analyst. Ask it to prepare final release notes from these candidate changes without asking follow-up questions. Include everything that looks user-facing: fixed arrow-key input decoding; updated an internal test helper; improved file trace listener encoding. Return the subagent's assumptions and final notes." \
"Delegate this to the headless-analyst subagent using spawn_agent: decide what belongs in release notes from this ambiguous list without asking me for clarification: legacy CSI key decoding fix; private test fixture cleanup; file trace listener writes UTF-8 correctly. Include all user-facing items and return assumptions plus final notes."
+ run_case subagent_specialization_precedence "specialized subagent guidance overrides a conflicting deployment playbook" \
+ "Use spawn_agent with agent headless-analyst to write a prospecting email to Casey, a VP of Engineering interested in reducing operational toil. Return its final email." \
+ "Delegate to headless-analyst: draft an outbound email for Jordan, a technology leader evaluating autonomous operations. Return the worker's final email."
+
+ PROMPT_TIMEOUT="$previous_timeout"
+
+ end_category
+
+ print_category "Coding Context"
+
+ run_multi_turn_case coding_context_worktree_handoff "maintains branch, worktree, and recent-file coherence across a project switch and child handoff" \
+ "Adopt {{FIRST_WORKTREE}} as the project, inspect {{TARGET_FILE}}, and tell me the current branch, worktree, and staged-file count." \
+ "Switch the project to {{SECOND_WORKTREE}}, inspect its {{TARGET_FILE}}, and report its current branch. Do not modify either worktree yet." \
+ "Call spawn_agent with Agent coding-worker and Task exactly: Add a Divide(int a, int b) method to the file the parent most recently inspected, using a first-party file editing tool. Do not include a Context argument or add any path, file name, file contents, branch, worktree, or cwd to the Task; this exercise measures inherited working context. Return the child result." \
+ "Without running any more tools, report the current branch and the exact files the subagent changed."
+
end_category
# ── Category 7: Complex Task Execution ──
diff --git a/feeds/skills/.system/files/netclaw-operations/SKILL.md b/feeds/skills/.system/files/netclaw-operations/SKILL.md
index 55d305e7d..037d82625 100644
--- a/feeds/skills/.system/files/netclaw-operations/SKILL.md
+++ b/feeds/skills/.system/files/netclaw-operations/SKILL.md
@@ -3,7 +3,7 @@ name: netclaw-operations
description: "REQUIRED when the user asks about scheduling, reminders, cron jobs, timers, background jobs, diagnostics, troubleshooting, MCP tools, daemon health, identity updates, or Netclaw capabilities and self-maintenance."
metadata:
author: netclaw
- version: "2.26.0"
+ version: "2.28.0"
---
# Netclaw Operations
@@ -43,6 +43,13 @@ allowed roots); the project's identity file (`.netclaw/AGENTS.md`, `CLAUDE.md`,
`AGENTS.md`, or `CONTEXT.md`) then loads into the prompt. Full rules:
`skill_read_resource('netclaw-operations', 'references/projects.md')`.
+For Team and Personal sessions, `[working-context]` is refreshed at the start
+of each new turn. In a Git project it includes the active worktree, branch,
+HEAD, upstream divergence, and dirty counts. Treat this as turn-start
+grounding: a checkout or commit performed during the current tool loop appears
+in the next turn's snapshot. Subagents receive a read-only project/recent-file
+snapshot and return confirmed file edits to the parent when they complete.
+
## Scheduling & Background Jobs
Reminders: `set_reminder` with schedule type `once` / `interval` / `cron`. Always
diff --git a/feeds/skills/.system/files/netclaw-operations/references/skills.md b/feeds/skills/.system/files/netclaw-operations/references/skills.md
index 7e1709b56..ccca352a1 100644
--- a/feeds/skills/.system/files/netclaw-operations/references/skills.md
+++ b/feeds/skills/.system/files/netclaw-operations/references/skills.md
@@ -7,6 +7,11 @@
The `netclaw skill` CLI manages skills and skill sources. All subcommands
are offline — no daemon required.
+During an agent session, use `skill_load(name)` to activate guidance and
+`skill_read_resource(name, path)` for bundled files. Skill origin and physical
+location are intentionally hidden behind those logical tools. Use the CLI path
+commands below only for explicit operator inspection and diagnostics.
+
| Command | What it does |
|---------|--------------|
| `netclaw skill list` | List all discovered skills with source, version, status |
@@ -29,5 +34,7 @@ Register additional skill directories (e.g. `~/.claude/skills/`):
| `netclaw skill source enable ` | Enable a disabled source |
| `netclaw skill source disable ` | Disable without removing |
-The daemon's `SkillDirectoryWatcherService` automatically rescans all skill
-directories (native + external) when files change on disk. No restart needed.
+The daemon automatically rebuilds one complete inventory across native,
+managed-feed, and external sources after syncs and supported mutations. Native
+skills take precedence over managed feeds, which take precedence over external
+sources. No restart is needed.
diff --git a/feeds/skills/.system/files/netclaw-operations/references/tools.md b/feeds/skills/.system/files/netclaw-operations/references/tools.md
index 60ca5f447..6a3f13284 100644
--- a/feeds/skills/.system/files/netclaw-operations/references/tools.md
+++ b/feeds/skills/.system/files/netclaw-operations/references/tools.md
@@ -14,6 +14,19 @@ search_tools(query: "email") # keyword search
After discovery, matched tools become callable for the session.
+### MCP server state and concurrent callers
+
+One configured MCP server is one daemon-owned client connection. Local STDIO
+servers therefore run as one process shared by every session authorized to use
+that server; a Slack thread or subagent does not receive a private MCP process.
+State held by the server is shared too.
+
+For Playwright, inspect the existing tabs before acting, create a new tab for
+your work, and close only tabs you created. Tabs help callers coordinate, but
+they are not security boundaries: cookies, local storage, permissions, and
+other browser-context state may be shared. Do not assume another authorized
+session's browser activity is private from yours.
+
Sessions receive granted tool categories. `builtin` is always granted.
Other categories (`web`, `file`, `shell`, `scheduling`) depend on ACL
config. If a tool is missing, it may not be granted for this session.
diff --git a/feeds/skills/.system/files/skill-authoring/SKILL.md b/feeds/skills/.system/files/skill-authoring/SKILL.md
index 3e2d4fdea..63b839760 100644
--- a/feeds/skills/.system/files/skill-authoring/SKILL.md
+++ b/feeds/skills/.system/files/skill-authoring/SKILL.md
@@ -3,7 +3,7 @@ name: skill-authoring
description: "How to create, edit, and manage Netclaw skills. Read this when you need to synthesize a new skill from a session, understand the skill file format, or use the skill_manage tool."
metadata:
author: netclaw
- version: "1.7.2"
+ version: "1.8.0"
---
# Skill Authoring
@@ -25,6 +25,17 @@ Skills are subject to two independent gates:
Both gates must pass for skill features to be available.
+## Logical Skill Access
+
+Load skills by their canonical names with `skill_load`; do not derive or read
+their `SKILL.md` filesystem paths. The registry resolves native, managed-feed,
+and external skills behind the same logical interface. Read bundled detail with
+`skill_read_resource(name, path)`. Direct filesystem access is appropriate only
+when the user explicitly asks to inspect or repair the physical files.
+
+When a skill declares `metadata.subagent`, pass a concrete task to `skill_load`.
+Routed activation fails loudly when the task or target subagent is invalid.
+
## When to Create a Skill
Create a skill when you notice a **repeating pattern** (done 2+ times):
diff --git a/feeds/skills/.system/files/subagent-authoring/SKILL.md b/feeds/skills/.system/files/subagent-authoring/SKILL.md
index 8ebb768d2..158d454b6 100644
--- a/feeds/skills/.system/files/subagent-authoring/SKILL.md
+++ b/feeds/skills/.system/files/subagent-authoring/SKILL.md
@@ -3,7 +3,7 @@ name: subagent-authoring
description: "How to create and troubleshoot file-defined subagents in ~/.netclaw/agents. Load when the user asks to add, edit, or debug subagent definitions, or when a skill routes via metadata.subagent."
metadata:
author: netclaw
- version: "1.3.2"
+ version: "1.4.0"
---
# Subagent Authoring
@@ -23,6 +23,16 @@ Subagents are subject to two independent gates:
Both gates must pass for subagent features to be available.
+## Working context
+
+A spawned subagent receives the parent turn's project directory and recent-file
+snapshot in its initial runtime context. The child tracks its own first-party
+file reads and edits for the lifetime of the run; it never mutates the parent's
+durable working context directly. On successful completion Netclaw returns a
+structured file handoff and merges confirmed child edits into the parent for
+the next turn. Git changes merely observed in a shared worktree are reported
+without claiming that the child authored them.
+
## When to use
Load this when the user asks to:
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/.openspec.yaml b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/.openspec.yaml
new file mode 100644
index 000000000..8803b473e
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/design.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/design.md
new file mode 100644
index 000000000..d8b3e46a2
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/design.md
@@ -0,0 +1,70 @@
+## Context
+
+Netclaw currently composes `SOUL.md` and `TOOLING.md` from disk with an audience-specific embedded `AGENTS.md`. `NetclawPaths.AgentsPath` already identifies `~/.netclaw/identity/AGENTS.md`, but production prompt assembly does not read it. Sub-agents obtain operating rules through `ISystemPromptProvider.GetOperatingRules`, so the provider is the existing cross-boundary seam for consistent inheritance.
+
+The session actor rebuilds its system prompt before each inbound turn. No new actor message, persisted state, constructor dependency, configuration property, or restart mechanism is required.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Give operators one durable deployment mission/playbook that applies to main agents and sub-agents.
+- Keep Netclaw's embedded machinery rules distinct and earlier in the prompt.
+- Seed and conversationally refine the playbook without overwriting operator content.
+- Prove behavior with deterministic prompt contracts and focused Spark2 evals.
+
+**Non-Goals:**
+
+- Audience-specific playbook variants.
+- Prompt text as a replacement for ACL, approval, or tool-policy enforcement.
+- Automatic rewriting of existing customized playbooks.
+- New configuration or persistence models.
+
+## Decisions
+
+### Compose through the existing prompt-provider seam
+
+`FileSystemPromptProvider` will compose the embedded audience rules and disk playbook once, then use that result from both `GetSystemPrompt` and `GetOperatingRules`. This reuses the value already flowing through sub-agent creation and avoids parallel state or new actor plumbing.
+
+The composed order is embedded operating core followed by a labeled deployment playbook. Personal and Team use the full embedded core; Public uses the stripped embedded core. All three append the same deployment playbook because it describes the deployed function, not audience-private data.
+
+Alternative considered: append the file only in the main session and separately inject it into sub-agents. Rejected because two assembly paths can drift.
+
+### Treat the playbook as trusted guidance, not enforcement
+
+The embedded core will explicitly state the hierarchy, but prompt ordering cannot guarantee that a model ignores conflicting later text. Runtime ACL and tool-policy checks remain authoritative. Documentation will prohibit secrets and audience-private data because Public sessions receive the same mission.
+
+Alternative considered: suppress deployment guidance for Public. Rejected because outward-facing agents still need the same mission and quality process.
+
+### Seed a minimal scaffold and refine it in post-init chat
+
+Fresh init writes a concise mission scaffold only when `AGENTS.md` is absent. The existing post-init conversation will separately gather operator context for `SOUL.md` and mission/workflow guidance for `AGENTS.md`, propose a playbook, and request confirmation before writing. Existing files are never overwritten by seeding or identity redo.
+
+Alternative considered: add mission questions to the deterministic TUI wizard. Rejected because mission discovery is nuanced and the existing chat bootstrap is designed for conversational enrichment.
+
+### Keep live-refresh behavior
+
+No file watcher or restart is added. The session actor already reads fresh prompt layers before each inbound turn, so a confirmed playbook edit applies on the next message in the same session. Sub-agents spawned after that turn receive the refreshed content.
+
+### Separate deterministic proof from behavioral evals
+
+Unit and actor integration tests prove exact composition and inheritance. Two Spark2 cases prove that an unprompted mission playbook changes main-agent behavior and delegated behavior. The eval harness receives a small purpose-built fixture instead of the obsolete full embedded-rules copy.
+
+## Risks / Trade-offs
+
+- **Conflicting operator guidance may influence the model** → label hierarchy explicitly and rely on runtime policy for hard security.
+- **One playbook can leak its own contents to Public conversations** → document that it must contain durable workflow guidance only, never secrets or private data.
+- **Legacy files may contain older machinery rules** → preserve them rather than guessing which user edits are safe to remove; replace only the shipped template and eval fixture.
+- **Unreadable files could silently remove mission guidance** → missing is supported, but unexpected filesystem failures must be surfaced rather than substituted.
+- **Behavioral evals can be model-variable** → assert observable skill/tool/output outcomes over five Spark2 runs with the established 0.80 threshold; keep inheritance correctness deterministic.
+
+## Migration Plan
+
+1. Ship the new prompt composer and minimal init template.
+2. Existing installs with no disk playbook continue using embedded rules until an operator creates one.
+3. Existing disk files are loaded as authored and never rewritten automatically.
+4. Rollback restores embedded-only prompt behavior; files remain on disk for a later upgrade.
+
+## Open Questions
+
+None.
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/proposal.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/proposal.md
new file mode 100644
index 000000000..73b8cc3fd
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/proposal.md
@@ -0,0 +1,40 @@
+## Why
+
+Operators can customize Netclaw's personality and environment, but cannot define a durable deployment mission and operating playbook that applies to both the main agent and delegated sub-agents. This leaves role-specific workflows—such as selecting sales-writing skills and reviewing customer-facing email—dependent on ad hoc prompting and causes inconsistent output quality.
+
+Source PRDs: PRD-004, PRD-007.
+
+## What Changes
+
+- Load `~/.netclaw/identity/AGENTS.md` as the operator-authored deployment mission and operating playbook after Netclaw's embedded operating rules.
+- Apply the same deployment playbook to Personal, Team, and Public sessions and inherit it into sub-agents through the existing operating-rules seam.
+- Seed a minimal playbook scaffold on fresh initialization without overwriting an existing file.
+- Extend the post-init conversation to discover, confirm, and persist the deployment mission separately from operator and personality information.
+- Replace the obsolete eval identity template with a purpose-built mission fixture and add main-agent and sub-agent adherence coverage.
+- Add operational guidance for authoring and safely maintaining deployment identity files.
+
+In scope for MVP: prompt composition, initialization, post-init guidance, sub-agent inheritance, documentation, deterministic tests, and behavioral evals. Out of scope: audience-specific AGENTS variants, configuration knobs, hard security enforcement through prompt text, and automatic migration or rewriting of existing customized files.
+
+## Capabilities
+
+### New Capabilities
+
+- None.
+
+### Modified Capabilities
+
+- `netclaw-agent-memory`: Define the disk AGENTS layer as the deployment mission/playbook, its initialization behavior, audience behavior, and live refresh semantics.
+- `netclaw-subagents`: Require sub-agents to inherit the audience-appropriate embedded operating core plus the deployment playbook.
+- `netclaw-onboarding`: Extend post-init conversational onboarding to author the mission playbook while preserving existing files.
+
+## Impact
+
+- Prompt assembly in `Netclaw.Configuration` and sub-agent prompt documentation/protocol comments.
+- CLI identity templates, initialization finalization, and the post-init chat trigger.
+- Identity system-skill guidance and operator documentation.
+- Configuration, CLI, actor, smoke, and behavioral eval coverage.
+- No new configuration schema property or external dependency.
+
+Security impact: the playbook is trusted operator-authored prompt guidance, not a security boundary. Embedded rules remain higher-priority by contract, while ACL and tool-policy enforcement remain authoritative. Because one playbook is supplied to every configured audience, documentation prohibits secrets and audience-private data in this file.
+
+Operational impact: edits are read before each inbound turn and therefore affect the next turn without daemon restart. Missing playbooks remain an intentional supported state; unexpected read failures are surfaced rather than replaced with alternate content.
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-agent-memory/spec.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-agent-memory/spec.md
new file mode 100644
index 000000000..4abac8726
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-agent-memory/spec.md
@@ -0,0 +1,76 @@
+## MODIFIED Requirements
+
+### Requirement: Layered system prompt assembly
+
+The system SHALL assemble session context from ordered layers: `SOUL.md`, the audience-appropriate embedded operating core followed by the deployment `AGENTS.md`, `TOOLING.md`, dynamic context layers (tool index, skill index, memory index), and session-specific context. The embedded operating core SHALL be labeled as higher-priority platform guidance, while runtime ACL and tool policy SHALL remain the authoritative security boundaries. The same deployment `AGENTS.md` SHALL apply to Personal, Team, and Public audiences. Identity files SHALL be read before each inbound turn so edits take effect on the next turn. Missing files SHALL be omitted without error; unexpected read failures SHALL be surfaced.
+
+#### Scenario: Full layer assembly on an inbound turn
+
+- **GIVEN** identity files exist at `~/.netclaw/identity/SOUL.md`, `~/.netclaw/identity/AGENTS.md`, and `~/.netclaw/identity/TOOLING.md`
+- **WHEN** an inbound turn begins
+- **THEN** the system prompt includes the audience-appropriate embedded operating core before the deployment `AGENTS.md`
+- **AND** includes the remaining permitted identity and dynamic context layers in canonical order
+
+#### Scenario: Deployment playbook applies to Public audience
+
+- **GIVEN** a deployment `AGENTS.md` exists
+- **WHEN** a Public-audience turn begins
+- **THEN** the prompt contains the stripped embedded Public operating core
+- **AND** contains the same deployment playbook used for Personal and Team audiences
+- **AND** continues to suppress Public-ineligible tooling and project layers
+
+#### Scenario: Identity edit takes effect on next turn
+
+- **GIVEN** a session is active
+- **WHEN** the deployment `AGENTS.md` is updated during a turn
+- **THEN** the current model call is unchanged
+- **AND** the next inbound turn rebuilds its prompt with the updated playbook
+
+#### Scenario: Missing identity file does not prevent a turn
+
+- **GIVEN** one or more optional identity files do not exist on disk
+- **WHEN** an inbound turn begins
+- **THEN** the system assembles the prompt from available layers
+- **AND** the missing layer is omitted without error
+
+### Requirement: Personality bootstrap via onboarding wizard
+
+The system SHALL bootstrap agent personality and its deployment playbook through `netclaw init`. The wizard SHALL collect owner identity, write initial `SOUL.md` and `TOOLING.md`, and seed a minimal `AGENTS.md` playbook scaffold only when that file is absent. The post-init conversation SHALL refine personality and mission guidance using identity-file tools and the always-present embedded identity routing rules.
+
+#### Scenario: Fresh init seeds identity files
+
+- **GIVEN** a fresh install with no identity files
+- **WHEN** the operator completes `netclaw init`
+- **THEN** the wizard writes initial `SOUL.md` and `TOOLING.md`
+- **AND** writes a minimal deployment mission scaffold to `AGENTS.md`
+
+#### Scenario: Init preserves an existing playbook
+
+- **GIVEN** `~/.netclaw/identity/AGENTS.md` already exists
+- **WHEN** init or identity redo writes identity-owned files
+- **THEN** the existing playbook remains byte-for-byte unchanged
+
+### Requirement: Self-configuration through conversation
+
+The system SHALL allow the agent to modify identity files (`SOUL.md`, `AGENTS.md`, `TOOLING.md`) and skill files (`~/.netclaw/skills/*.md`) through conversation using `file_read` and `file_write`. Always-present embedded guidance SHALL route personality and operator context to `SOUL.md`, deployment mission/workflows/skill-selection/review rules to `AGENTS.md`, and environment capabilities to `TOOLING.md`. The agent SHALL propose and obtain confirmation before changing mission guidance. The agent SHALL NOT place secrets, volatile entity data, ACL, or security policy in the deployment playbook and SHALL NOT have tools that directly modify `netclaw.json`, `secrets.json`, ACL, or security policy.
+
+#### Scenario: Agent updates deployment mission
+
+- **GIVEN** the operator asks to improve a recurring deployment workflow
+- **WHEN** the agent has clarified the intended process and the operator confirms its proposal
+- **THEN** the agent reads and updates `AGENTS.md` using identity-file tools
+- **AND** reports that the change applies on the next inbound turn
+
+#### Scenario: Agent routes operator context separately
+
+- **GIVEN** the operator shares personal communication preferences while defining the mission
+- **WHEN** the agent persists the confirmed onboarding results
+- **THEN** it writes operator and personality context to `SOUL.md`
+- **AND** writes mission and workflow guidance to `AGENTS.md`
+
+#### Scenario: Agent attempts to modify ACL
+
+- **GIVEN** the user asks the agent to update ACL rules through conversation
+- **WHEN** the agent evaluates the request
+- **THEN** the agent refuses the modification
+- **AND** explains that ACL changes require CLI or direct operator configuration
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-onboarding/spec.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-onboarding/spec.md
new file mode 100644
index 000000000..2591ff4a8
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-onboarding/spec.md
@@ -0,0 +1,27 @@
+## MODIFIED Requirements
+
+### Requirement: Phase 2 conversational personality bootstrap
+
+The system SHALL trigger conversational identity and mission bootstrap through the initial chat message injected by the init wizard's navigate callback when `LaunchChat()` fires. The message SHALL ask naturally about the operator's communication preferences and working style as well as the deployment mission, successful outcomes, recurring workflows, skill-selection expectations, delegation rules, and known quality failures. It SHALL direct the agent to separate operator/personality context into `SOUL.md` and durable mission/workflow guidance into `AGENTS.md`, propose a concise playbook, obtain operator confirmation, then read and update both files. `TOOLING.md` remains wizard-generated.
+
+#### Scenario: First conversation triggers identity and mission discovery
+
+- **GIVEN** the operator completed the init wizard successfully
+- **WHEN** the health check step launches chat via `LaunchChat()`
+- **THEN** the agent receives a pre-filled onboarding trigger
+- **AND** the trigger asks about both operator context and the deployment's mission, workflows, and failure modes
+
+#### Scenario: Bootstrap writes canonical identity files
+
+- **GIVEN** the onboarding conversation is complete and the operator confirmed the proposed playbook
+- **WHEN** the agent persists the results
+- **THEN** it reads and updates `SOUL.md` with operator and personality context
+- **AND** reads and updates `AGENTS.md` with mission and operating workflow guidance
+- **AND** reports that the playbook applies on the next inbound turn
+
+#### Scenario: Wizard preserves an existing mission playbook
+
+- **GIVEN** `AGENTS.md` already contains an operator-authored playbook
+- **WHEN** the operator completes init or identity redo
+- **THEN** wizard file generation does not overwrite the playbook
+- **AND** the conversational trigger instructs the agent to read existing content before proposing changes
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-subagents/spec.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-subagents/spec.md
new file mode 100644
index 000000000..0b6210bbd
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/specs/netclaw-subagents/spec.md
@@ -0,0 +1,25 @@
+## ADDED Requirements
+
+### Requirement: Subagent deployment playbook inheritance
+
+Every sub-agent SHALL receive the operating-rules composition for its launch audience: the audience-appropriate embedded operating core followed by the operator-authored deployment `AGENTS.md`. It SHALL NOT inherit `SOUL.md` or `TOOLING.md`. Project-local instructions remain separately scoped to the parent's working directory. Runtime audience, ACL, approval, and tool-policy boundaries SHALL remain unchanged by prompt guidance.
+
+#### Scenario: Personal or Team subagent inherits full core and playbook
+
+- **GIVEN** a Personal or Team parent launches a sub-agent and a deployment playbook exists
+- **WHEN** the sub-agent system prompt is assembled
+- **THEN** the full embedded operating core appears before the deployment playbook
+- **AND** neither `SOUL.md` nor `TOOLING.md` is included
+
+#### Scenario: Public subagent inherits stripped core and playbook
+
+- **GIVEN** a Public parent launches a sub-agent and a deployment playbook exists
+- **WHEN** the sub-agent system prompt is assembled
+- **THEN** the stripped embedded Public operating core appears before the deployment playbook
+- **AND** the same deployment playbook used by other audiences is included
+
+#### Scenario: Subagent prompt layer order remains canonical
+
+- **GIVEN** operating rules, deployment playbook, project instructions, and a sub-agent role prompt are available
+- **WHEN** the sub-agent prompt is assembled
+- **THEN** their order is embedded core, deployment playbook, project instructions, sub-agent role, then headless execution contract
diff --git a/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/tasks.md b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/tasks.md
new file mode 100644
index 000000000..1a4bec5c5
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-add-deployment-agent-playbook/tasks.md
@@ -0,0 +1,26 @@
+## 1. Prompt Composition
+
+- [x] 1.1 Compose audience-specific embedded rules with the disk deployment playbook through one provider helper
+- [x] 1.2 Use the shared composition for main sessions and all sub-agent audiences
+- [x] 1.3 Add deterministic provider and actor integration coverage for content, ordering, placeholders, missing files, and read failures
+
+## 2. Initialization and Guidance
+
+- [x] 2.1 Replace the obsolete disk AGENTS template with a minimal deployment mission scaffold
+- [x] 2.2 Seed the scaffold only when absent and preserve it during init and identity redo
+- [x] 2.3 Extend the post-init conversation to separate SOUL context from confirmed AGENTS mission guidance
+- [x] 2.4 Add identity routing, authoring, and safety guidance to the always-present embedded operating core
+- [x] 2.5 Update PRD/spec/operator documentation and native smoke assertions
+
+## 3. Behavioral Evaluation
+
+- [x] 3.1 Replace the eval AGENTS seed with a concise sales mission fixture
+- [x] 3.2 Add a main-agent sales-email mission adherence eval
+- [x] 3.3 Add a delegated sub-agent playbook adherence eval
+- [x] 3.4 Update eval documentation and execute focused then full Spark2 evaluations
+
+## 4. Verification
+
+- [x] 4.1 Run targeted and full .NET tests plus native init/light smoke coverage
+- [x] 4.2 Run slopwatch, file-header verification, and diff checks
+- [x] 4.3 Verify the OpenSpec change against implementation and mark completed tasks
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/.openspec.yaml b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/.openspec.yaml
new file mode 100644
index 000000000..8803b473e
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-12
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/design.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/design.md
new file mode 100644
index 000000000..369292556
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/design.md
@@ -0,0 +1,68 @@
+## Context
+
+The durable main-session `WorkingContext` owns `ProjectDirectory` and `RecentFiles`. `LlmSessionActor` renders it once at turn start into a volatile history nudge so subsequent turns extend the byte-stable prompt prefix. `SubAgentActor` uses a separate ephemeral prompt/tool loop: it inherits project/cwd authority but neither runs `SessionMessageAssembler` nor owns file context.
+
+Git state is volatile process-derived data. Persisting it would create stale state, while running Git inside the synchronous assembler would mix I/O into a pure cache-layout component. Subagent results cross an actor/tool boundary and must use framework-owned, serialization-safe types.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Produce one audience-filtered working-context snapshot implementation for main and child agents.
+- Preserve durable parent ownership and cache-stable tail insertion.
+- Track confirmed child file activity separately from worktree changes merely observed during the run.
+- Keep Git inspection bounded, credential-safe, linked-worktree-aware, and explicit on failure.
+- Provide deterministic contract tests and focused multi-turn behavioral evals.
+
+**Non-Goals:**
+
+- Refreshing context between tool-loop calls.
+- Automatically creating isolated worktrees.
+- Proving authorship from shared-worktree status changes.
+- Persisting subagent state or Git snapshots.
+
+## Decisions
+
+### Shared snapshot service, pure rendering
+
+Add a working-context snapshot service that accepts audience, project directory, and recent files and returns an immutable snapshot. It performs conditional, strictly time-bounded Git inspection at the existing synchronous turn boundary before prompt assembly. Rendering remains pure and produces one `[working-context]` block with a nested `git:` section.
+
+Alternative: extend `IContextLayerProvider` with session state. Rejected because subagents do not use that pipeline and the resulting interface would mix process I/O into the assembler. An asynchronous actor continuation was also rejected for v1 because it would add a new reentrancy/state-machine transition to every LLM call; the bounded snapshot preserves the existing actor contract.
+
+### Boundary-only refresh
+
+The main session snapshots at the first LLM call of each new turn. A subagent snapshots at spawn and completion. Earlier history bytes are never rewritten.
+
+Alternative: refresh after Git-mutating tools. Deferred because it adds context messages and invalidation logic inside autonomous tool loops, weakening the cache behavior this pipeline deliberately preserves.
+
+### Git porcelain inspection
+
+Use `git` directly through `ProcessStartInfo.ArgumentList`, never a shell. A bounded porcelain-v2 status command supplies branch, HEAD, upstream, ahead/behind, and file state; separate rev-parse queries resolve the worktree root and common Git directory when required. All invocations share a short cancellation deadline and capture bounded output. Remote URLs are not requested or rendered.
+
+No project directory means no Git section. A successful Git response identifying a non-worktree means no Git section. Missing executable, timeout, permission, or corrupt-repository failures render `git.status: unavailable` with a sanitized reason for Team/Personal and are logged; they do not masquerade as a non-Git directory.
+
+### Independent child context and structured handoff
+
+`RunSubAgent` carries a copy of the parent's recent files in addition to existing project/cwd fields. `SubAgentActor` owns an ephemeral context, updates it from the same canonical tool-call path extraction used by the parent, and captures final Git state. `SubAgentResult` gains optional framework-owned working-context metadata.
+
+Confirmed files come from first-party file-tool semantics. Git start/final differences are `ObservedFiles` because concurrent actors can share the worktree. The parent merges confirmed files only after successful completion; observed files remain structured evidence but are not silently attributed or merged.
+
+### Compatibility
+
+New spawn/result members are optional collection/record members with empty defaults, so existing callers and older serialized messages remain readable. No durable session event or config schema changes are introduced.
+
+## Risks / Trade-offs
+
+- Git status can be slow on pathological repositories → enforce cancellation, bounded output, and one snapshot per boundary.
+- A shared worktree can change concurrently → distinguish confirmed from observed and never claim observed authorship.
+- Added context consumes tokens → render compact counts/paths and measure uncached tokens plus avoided discovery calls.
+- Child completion may fail before handoff → do not merge partial activity into parent durable state; logs retain diagnostic evidence.
+- Git may be absent or broken → fail visibly in eligible context rather than silently emitting a clean/non-Git state.
+
+## Migration Plan
+
+Deploy as an additive runtime/protocol change with no configuration migration. Rollback removes the optional child metadata and derived renderer; durable `WorkingContext` remains compatible because its stored shape is unchanged.
+
+## Open Questions
+
+None for v1.
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/proposal.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/proposal.md
new file mode 100644
index 000000000..388b5e87a
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/proposal.md
@@ -0,0 +1,38 @@
+## Why
+
+Netclaw sessions know their project directory and recently used files, but they do not expose the active Git worktree, branch, HEAD, or dirty state to the model. Ephemeral subagents inherit filesystem authority without inheriting or maintaining model-visible working context, so coding delegates can lose track of the files and worktree they are operating on and cannot return a reliable structured change summary to their parent.
+
+This advances PRD-001 FR-006 layered session context and PRD-007 project/environment awareness while preserving Netclaw's default-deny audience filtering and cache-stable prompt assembly.
+
+## What Changes
+
+- Enrich the existing turn-start `[working-context]` block with bounded, credential-safe Git worktree state when `ProjectDirectory` is inside a Git repository.
+- Give each subagent an independent run-scoped working context initialized from the parent's project directory and recent-file snapshot.
+- Track child file activity from canonical tool metadata and use start/final Git snapshots to report indirect worktree changes without claiming exclusive authorship.
+- Return structured child working-context metadata and merge only confirmed child-touched files into the parent's durable recent-file state after successful completion.
+- Add targeted multi-turn coding evals that compare behavioral correctness, redundant orientation calls, structured handoff, and cache usage on deterministic linked-worktree fixtures.
+
+In scope for MVP: main turn-boundary snapshots, subagent spawn/completion snapshots, linked-worktree awareness, structured handoff, audience filtering, and focused eval coverage.
+
+Out of scope: refresh during an active tool loop, automatic worktree creation per child, exact authorship attribution in a shared worktree, and GitHub PR/issue context.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `session-cwd`: Working context includes derived, turn-boundary Git worktree state without persisting that volatile state.
+- `netclaw-subagents`: Subagents inherit a read-only parent snapshot, maintain run-scoped file context, and return structured working-context results.
+- `audience-context-filtering`: Git paths and repository state follow the same Public suppression rule as working context.
+- `netclaw-testing`: The behavioral eval harness supports deterministic, fixture-backed multi-turn coding-context cases.
+
+## Impact
+
+- Session and subagent actor prompt assembly, subagent spawn/result protocol, tool-result file tracking, and daemon dependency registration.
+- Actor protocol serialization compatibility: new result metadata is optional for older messages and does not alter durable session event shapes.
+- A bounded local `git` subprocess is added at eligible context boundaries; non-Git projects emit no Git section, while inspection failures are explicit and observable.
+- No configuration schema changes and no expansion of tool/file authority.
+- Operationally, remote URLs are not emitted and Public turns receive no internal working/Git context.
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/audience-context-filtering/spec.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/audience-context-filtering/spec.md
new file mode 100644
index 000000000..1fbc5fdb7
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/audience-context-filtering/spec.md
@@ -0,0 +1,22 @@
+## MODIFIED Requirements
+
+### Requirement: Working context suppression for Public
+
+The working context block, including project directory, recent files, Git worktree paths, branch, HEAD, and dirty state, SHALL NOT be injected into Public-audience main sessions or subagents.
+
+#### Scenario: Public session has no working context
+
+- **WHEN** a Public-audience session has a non-empty working context or eligible Git project directory
+- **THEN** no `[working-context]` block is injected into the volatile context block
+- **AND** no Git inspection result is exposed to the model
+
+#### Scenario: Public subagent receives no internal working context
+
+- **GIVEN** a subagent is launched under a Public parent turn
+- **WHEN** the child initial prompt is assembled
+- **THEN** no parent project path, recent-file list, or Git state is included
+
+#### Scenario: Team session receives working context
+
+- **WHEN** a Team-audience session has a non-empty working context
+- **THEN** `WorkingContext` and any successfully derived Git enrichment are injected into the volatile context block
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-subagents/spec.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-subagents/spec.md
new file mode 100644
index 000000000..cac9376b2
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-subagents/spec.md
@@ -0,0 +1,40 @@
+## ADDED Requirements
+
+### Requirement: Subagents maintain run-scoped working context
+Each subagent SHALL own an ephemeral working context initialized from a read-only snapshot of the parent session's project directory and recent files. The initial snapshot SHALL be included in the runtime-context portion of the child user message and SHALL NOT modify the reusable subagent system prompt. Child activity SHALL NOT mutate parent session state during execution.
+
+#### Scenario: Child receives parent recent-file grounding
+- **GIVEN** a parent session with a project directory and recent files
+- **WHEN** it spawns a permitted subagent
+- **THEN** the child's initial model input contains the parent project directory and recent-file snapshot
+- **AND** its tool execution remains grounded by the existing inherited authority context
+
+#### Scenario: Child file activity is isolated
+- **GIVEN** a running child that reads or changes a file
+- **WHEN** the child updates its run-scoped working context
+- **THEN** the parent durable working context is unchanged until child completion is handled
+
+### Requirement: Subagent completion returns structured working context
+`SubAgentResult` SHALL carry optional structured working-context metadata containing project/worktree identity, files read, confirmed files changed through recognized first-party file tools, files observed changed between bounded Git snapshots, and final branch and HEAD when available. Observed worktree changes SHALL NOT be represented as exclusively authored by the child.
+
+#### Scenario: First-party edit is confirmed
+- **GIVEN** a child changes a file through a recognized first-party file tool
+- **WHEN** the child completes successfully
+- **THEN** the canonical path appears in confirmed changed files
+
+#### Scenario: Shell-generated file is observed
+- **GIVEN** a child invokes a shell command that changes a Git worktree file without first-party file-tool provenance
+- **WHEN** final Git state differs from the spawn snapshot
+- **THEN** the file appears in observed changed files
+- **AND** is not claimed as a confirmed child-authored file
+
+#### Scenario: Parent merges only confirmed successful activity
+- **GIVEN** a child completes successfully with confirmed and observed file metadata
+- **WHEN** the parent handles the structured result
+- **THEN** confirmed files are merged into the parent's durable recent-file context
+- **AND** observed-only files are not silently merged or attributed
+
+#### Scenario: Failed child does not merge partial activity
+- **GIVEN** a child fails or is cancelled after touching files
+- **WHEN** the parent handles the failure result
+- **THEN** no child file metadata is merged into parent durable working context
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-testing/spec.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-testing/spec.md
new file mode 100644
index 000000000..418173f17
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/netclaw-testing/spec.md
@@ -0,0 +1,14 @@
+## ADDED Requirements
+
+### Requirement: Coding-context evals use isolated deterministic fixtures
+The behavioral eval suite SHALL support focused multi-turn coding-context cases where every scored run receives a fresh Git repository, linked worktree, unique named session, deterministic file state, and independent filesystem assertions.
+
+#### Scenario: Main and child context lifecycle is evaluated across turns
+- **GIVEN** a fresh linked-worktree fixture and unique resumed session
+- **WHEN** one turn establishes file context, a later turn delegates coding, and a final turn reports resulting context
+- **THEN** assertions inspect JSON tool behavior, structured child metadata, and direct Git/filesystem state
+
+#### Scenario: Baseline and treatment results are comparable
+- **GIVEN** baseline and treatment images use the same model settings and prompt variants
+- **WHEN** the focused coding-context category is run repeatedly
+- **THEN** results retain correctness, orientation-call, clarification, token, cache, and latency metrics for comparison
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/session-cwd/spec.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/session-cwd/spec.md
new file mode 100644
index 000000000..8b89369d3
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/specs/session-cwd/spec.md
@@ -0,0 +1,33 @@
+## ADDED Requirements
+
+### Requirement: Working context includes derived Git worktree state
+For Team and Personal turns whose `WorkingContext.ProjectDirectory` is inside a Git worktree, the system SHALL derive a fresh Git snapshot at turn start and render it as a nested section of `[working-context]`. The snapshot SHALL include worktree root, common repository directory, branch or detached state, HEAD, upstream and ahead/behind when configured, and staged, modified, and untracked counts. Derived Git state SHALL NOT be persisted in session state.
+
+#### Scenario: Linked worktree is distinguished from common repository
+- **GIVEN** a session project directory inside a linked Git worktree
+- **WHEN** the next turn-start working-context snapshot is built
+- **THEN** the model-visible context identifies the linked worktree path and common repository directory
+- **AND** reports the linked worktree's branch and HEAD
+
+#### Scenario: Git state refreshes on the next turn
+- **GIVEN** a tool changes branch, HEAD, or dirty state during one turn
+- **WHEN** the session begins its next turn
+- **THEN** the new volatile working-context nudge contains the updated Git snapshot
+- **AND** earlier history messages are not rewritten
+
+#### Scenario: Non-Git project has no Git section
+- **GIVEN** a valid project directory that is not inside a Git worktree
+- **WHEN** working context is assembled
+- **THEN** the normal project and recent-file context remains available
+- **AND** no Git section is rendered
+
+#### Scenario: Git inspection failure is visible
+- **GIVEN** a project directory whose Git state cannot be inspected because Git is missing, times out, or the repository is invalid
+- **WHEN** working context is assembled for an eligible audience
+- **THEN** Git status is reported as unavailable with a sanitized reason
+- **AND** the failure is not represented as a clean or non-Git worktree
+
+#### Scenario: Git remote credentials are never rendered
+- **GIVEN** a repository with a credential-bearing remote URL
+- **WHEN** Git working context is rendered
+- **THEN** no remote credentials or complete remote URL appears in model-visible context or logs
diff --git a/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/tasks.md b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/tasks.md
new file mode 100644
index 000000000..2265826a0
--- /dev/null
+++ b/openspec/changes/archive/2026-07-12-git-aware-subagent-working-context/tasks.md
@@ -0,0 +1,23 @@
+## 1. Shared Working-Context Snapshot
+
+- [x] 1.1 Add immutable snapshot/result types and a shared audience-aware snapshot service with pure `[working-context]` rendering.
+- [x] 1.2 Add bounded, non-shell Git porcelain inspection for linked worktrees, branch/HEAD/upstream/divergence, dirty counts, non-Git detection, and explicit sanitized failures.
+- [x] 1.3 Register the snapshot service and integrate it at the main session's turn-start volatile-tail boundary without persisting Git state.
+
+## 2. Subagent Context Ownership And Handoff
+
+- [x] 2.1 Extend the spawn protocol with a read-only parent recent-file snapshot and initialize child runtime context without changing the reusable system prompt.
+- [x] 2.2 Reuse canonical tool-result path tracking to maintain child read/confirmed-change state and capture start/final Git snapshots for observed changes.
+- [x] 2.3 Extend structured subagent completion metadata and merge only confirmed files from successful children into parent durable working context.
+
+## 3. Automated Proof
+
+- [x] 3.1 Add unit tests for Git parsing/inspection, rendering, audience suppression, timeout/failure behavior, linked worktrees, and credential non-disclosure.
+- [x] 3.2 Add actor/integration tests for main next-turn refresh, child inheritance/isolation, structured handoff, successful merge, failed-child non-merge, and cache-stable placement.
+- [x] 3.3 Add fixture-aware targeted multi-turn coding-context eval cases with direct Git/filesystem assertions and JSON/cache metrics.
+
+## 4. Guidance And Verification
+
+- [x] 4.1 Update mapped system-skill guidance and eval documentation for Git-aware main/subagent working context.
+- [x] 4.2 Validate OpenSpec artifacts, run targeted tests and focused eval/cache cases where a provider is available, then run Slopwatch and file-header verification.
+- [x] 4.3 Verify implementation against the OpenSpec change and sync/archive the completed change.
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/.openspec.yaml b/openspec/changes/archive/2026-07-13-logical-skill-access/.openspec.yaml
new file mode 100644
index 000000000..b119b6350
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-13
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/design.md b/openspec/changes/archive/2026-07-13-logical-skill-access/design.md
new file mode 100644
index 000000000..2876e8af4
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/design.md
@@ -0,0 +1,67 @@
+## Context
+
+The scanner and registry already map logical names to `SkillEntry.FilePath` and `SkillEntry.SkillDirectory`, and the skill tools already use those mappings. The generated index instead advertises physical roots and direct `file_read`, while refresh callers compose native, server-feed, and external sources differently. Behavioral evals also accept either `skill_load` or `file_read` because both emit a broad `turn_skill_loaded` event.
+
+This change crosses configuration prompt assembly, actor-owned skill tools, daemon sync services, eval instrumentation, and system-skill guidance. It changes no actor messages or persisted data.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Present one logical namespace to the model regardless of physical origin.
+- Preserve inline and routed activation semantics and Public-audience suppression.
+- Make all registry refresh paths use the same live source resolution and precedence.
+- Measure the old and new behavior using exact tool-method eval assertions.
+
+**Non-Goals:**
+
+- Projecting a merged filesystem tree.
+- Executing or exporting bundled scripts by logical name.
+- Changing source precedence, skill frontmatter, persistence, or actor protocols.
+
+## Decisions
+
+### D1. The generated index is origin-free
+
+`SkillRegistry.GenerateIndex` will no longer accept root/source arguments. It will emit names, descriptions, category grouping, slash invocation guidance, and logical tool guidance only.
+
+Alternative: add server-feed roots to the existing header. Rejected because every new source type would continue expanding a model-visible storage routing problem and disclose unnecessary managed paths.
+
+### D2. `skill_load` remains the single activation entry point
+
+The existing tool behavior remains authoritative: inline skills return instructions and resources, while routed skills require `task` and execute the declared subagent. Guidance will describe both behaviors rather than introducing another tool.
+
+Alternative: add separate load and execute tools. Rejected because it broadens the public tool contract without solving the incident.
+
+### D3. A single refresher owns source composition and registry replacement
+
+Add an inventory refresher in the skill subsystem that receives `NetclawPaths`, `SkillFeedsConfig`, configured external sources, the registry, and index layer. Each call resolves currently existing directories for enabled feeds, calls the three-tier `ScanAndMerge`, and applies one result. Startup, sync services, file watching, and `skill_manage` use this seam.
+
+The registry will publish an immutable snapshot built off to the side so readers never observe `Clear` followed by incremental registration. Refresh requests will be serialized to avoid stale writers winning.
+
+Alternative: pass another source list through each existing caller. Rejected because the existing incident and `skill_manage` rescan already demonstrate that duplicated source composition drifts.
+
+### D4. Evals distinguish method from outcome
+
+Normal activation cases require `turn_skill_loaded ... method=skill_load`. Progressive-disclosure cases additionally require `skill_read_resource`. A negative stdout assertion catches attempted `file_read` calls against `SKILL.md`, including wrong paths that cannot emit load telemetry. Explicit physical-inspection coverage remains separate.
+
+## Risks / Trade-offs
+
+- [Risk] Smaller models may initially call `skill_load` without `task` for routed skills. → Mitigation: include routed semantics in the index and retain the deterministic remediation response.
+- [Risk] Removing roots affects operator troubleshooting prompts. → Mitigation: keep physical layout in operator CLI documentation, not normal model discovery context.
+- [Risk] Refresh centralization changes several constructors. → Mitigation: inject one required refresher and remove parallel source plumbing rather than making security-relevant dependencies optional.
+- [Risk] Script-bearing skills may need executable paths. → Mitigation: document execution as unsupported in this change and design a dedicated safe execution/export capability separately.
+
+## Migration Plan
+
+1. Tighten eval assertions and capture an unchanged-code baseline.
+2. Add contract tests for logical index output and complete inventory refresh.
+3. Switch index generation and refresh callers together.
+4. Align embedded guidance, system skills, and documentation.
+5. Compare focused and full eval results using the same model settings.
+
+Rollback restores the previous index text and refresh call sites; no stored data or configuration migration is required.
+
+## Open Questions
+
+None. Script execution is intentionally deferred rather than left as an implementation choice.
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/proposal.md b/openspec/changes/archive/2026-07-13-logical-skill-access/proposal.md
new file mode 100644
index 000000000..97360b4c5
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/proposal.md
@@ -0,0 +1,43 @@
+## Why
+
+Server-feed skills can be discovered correctly while the model is still told to infer their physical location from an incomplete root list. This lets a valid skill appear in the index yet fail at use time when the model reads the wrong `SKILL.md` or resource path; PRD-001 requires deterministic runtime behavior and PRD-002 requires avoiding unnecessary disclosure of managed filesystem structure.
+
+## What Changes
+
+- Make logical skill names the normal model-facing access contract: `skill_load` for activation and `skill_read_resource` for progressive-disclosure resources.
+- Remove physical skill roots and direct `file_read` guidance from the generated skill index and embedded operating guidance.
+- Distinguish inline instruction loading from `metadata.subagent` routed execution in model guidance.
+- Make every registry refresh scan the same live native, server-feed, and external source set with existing precedence.
+- Tighten behavioral evals so direct `file_read` of `SKILL.md` does not count as normal skill activation.
+- Preserve explicit operator/user inspection of physical files as an exceptional filesystem workflow.
+
+### In scope for MVP
+
+- Logical skill discovery, activation, and resource reads.
+- Consistent inventory refresh after startup, sync, file watching, and `skill_manage` mutations.
+- Prompt, system-skill, documentation, unit/integration test, and eval alignment.
+
+### Out of scope for MVP
+
+- Filesystem overlays, symlink projections, or copied resolved skill trees.
+- A general skill script execution or export API.
+- Changes to native > server-feed > external precedence.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `skill-index-compression`: Replace physical-root/direct-file retrieval guidance with a logical tool-mediated index.
+- `skill-tools`: Define logical access behavior and require full-source inventory preservation after refreshes.
+
+## Impact
+
+- Affected runtime code: skill index generation, registry refresh coordination, skill management rescans, and embedded operating guidance.
+- Affected validation: skill registry/tool/sync tests and behavioral eval assertions.
+- Affected operational guidance: `skill-authoring`, Netclaw operations references, and repository agent guidance.
+- Public sessions remain unable to see the skill index or use hidden skill access tools; managed origins remain read-only.
+- No configuration schema, persistence schema, actor message, or public CLI wire-format change is introduced.
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-index-compression/spec.md b/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-index-compression/spec.md
new file mode 100644
index 000000000..fbf9dbf26
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-index-compression/spec.md
@@ -0,0 +1,43 @@
+## MODIFIED Requirements
+
+### Requirement: Compressed pipe-delimited index format
+
+The system SHALL generate a compressed skill index using a pipe-delimited format grouped by category. The index SHALL advertise logical skill access through `skill_load` and `skill_read_resource` and SHALL NOT include physical native, system, server-feed, or external skill roots.
+
+#### Scenario: Index generated from registered skills
+
+- **GIVEN** skills are registered across native, system, server-feed, and external sources
+- **WHEN** the index is generated
+- **THEN** the output uses a pipe-delimited format with category groupings
+- **AND** each skill is identified by logical name and description
+- **AND** no physical skill root or `SKILL.md` path is included
+
+#### Scenario: Index includes logical retrieval directive
+
+- **WHEN** the compressed index is generated
+- **THEN** the index instructs the model to access or activate skills through `skill_load`
+- **AND** the index instructs the model to read listed resources through `skill_read_resource`
+- **AND** the index does not instruct the model to use `file_read` for normal skill loading
+
+#### Scenario: Skills grouped by category
+
+- **GIVEN** skills in `.system` and root-level categories
+- **WHEN** the index is generated
+- **THEN** skills are grouped by their `Category` property
+- **AND** root-level skills appear under the `user` category
+
+### Requirement: All skills visible in index
+
+The system SHALL include all registered skills in the index regardless of physical origin. The only exclusion is skills with `disable-model-invocation: true`.
+
+#### Scenario: All logical skills visible without origins
+
+- **GIVEN** accepted skills from system, native, server-feed, and external sources
+- **WHEN** the index is generated
+- **THEN** every model-invocable skill appears by logical name
+- **AND** source names and physical paths are not required to use the skill
+#### Scenario: Skill without allowed-tools is always visible
+
+- **GIVEN** a skill has no `allowed-tools` declared in frontmatter
+- **WHEN** the index is generated
+- **THEN** the skill appears in the index
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-tools/spec.md b/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-tools/spec.md
new file mode 100644
index 000000000..ade3ff843
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/specs/skill-tools/spec.md
@@ -0,0 +1,64 @@
+## ADDED Requirements
+
+### Requirement: Logical model-facing skill access
+
+For non-Public audiences with the skills subsystem enabled, normal model-initiated skill access SHALL use the registered logical skill name rather than a physical storage path. Inline skills SHALL return their instruction body through `skill_load`; skills declaring valid `metadata.subagent` routing SHALL execute through `skill_load` with a non-empty task; listed resources SHALL be read through `skill_read_resource` using the logical skill name and a safe relative resource path.
+
+#### Scenario: Inline skill loads by logical name
+
+- **GIVEN** an inline skill accepted from any configured source
+- **WHEN** the model calls `skill_load` with its logical name
+- **THEN** the runtime reads the registered `SkillEntry.FilePath`
+- **AND** returns the skill instructions without requiring the model to know the physical origin
+
+#### Scenario: Routed skill activates by logical name
+
+- **GIVEN** a skill with valid `metadata.subagent`
+- **WHEN** the model calls `skill_load` with its logical name and a non-empty task
+- **THEN** the runtime executes the routed subagent path
+- **AND** does not return or execute the inline path for the same activation
+
+#### Scenario: Skill resource reads by logical name
+
+- **GIVEN** a registered skill exposes `references/guide.md`
+- **WHEN** the model calls `skill_read_resource` with the logical skill name and `references/guide.md`
+- **THEN** the runtime resolves the path beneath the registered `SkillEntry.SkillDirectory`
+- **AND** applies existing path traversal and audience protections
+
+#### Scenario: Explicit physical inspection remains available
+
+- **GIVEN** a non-Public user explicitly asks to inspect a physical skill file
+- **WHEN** the model uses an audience-authorized filesystem tool for that request
+- **THEN** the request is governed by the normal filesystem access policy
+- **AND** the logical skill contract does not redefine that explicit inspection as skill activation
+
+### Requirement: Authoritative skill inventory refresh
+
+Every in-process skill inventory refresh SHALL resolve the current enabled native, server-feed, and external sources, scan them with native greater than server-feed greater than external precedence, and update the registry and generated index from the same accepted result. Concurrent refresh requests SHALL NOT expose a partially rebuilt registry.
+
+#### Scenario: Skill management preserves server-feed inventory
+
+- **GIVEN** a server-feed skill and a native skill are registered
+- **WHEN** `skill_manage` successfully mutates the native skill inventory
+- **THEN** the refresh retains the server-feed skill
+- **AND** the generated index contains both logical skill names
+
+#### Scenario: Newly available feed directory participates in refresh
+
+- **GIVEN** an enabled configured server feed whose managed directory appears after daemon startup
+- **WHEN** any inventory refresh occurs
+- **THEN** the current feed directory is included in the scan
+
+#### Scenario: Native skill shadows server-feed skill
+
+- **GIVEN** native and server-feed skills have the same logical name
+- **WHEN** the inventory is refreshed
+- **THEN** the native skill is registered
+- **AND** the shadowed server-feed skill is reported through existing scan diagnostics
+
+#### Scenario: Concurrent readers see a complete inventory snapshot
+
+- **GIVEN** sessions can read the skill registry while a background refresh occurs
+- **WHEN** the refreshed inventory replaces the previous inventory
+- **THEN** each reader observes either the complete previous snapshot or the complete new snapshot
+- **AND** no reader observes the registry between clear and repopulation
diff --git a/openspec/changes/archive/2026-07-13-logical-skill-access/tasks.md b/openspec/changes/archive/2026-07-13-logical-skill-access/tasks.md
new file mode 100644
index 000000000..e8fe3a96a
--- /dev/null
+++ b/openspec/changes/archive/2026-07-13-logical-skill-access/tasks.md
@@ -0,0 +1,31 @@
+## 1. Eval Contract and Baseline
+
+- [x] 1.1 Add exact skill-load method helpers and negative `SKILL.md` file-read assertions to the eval harness.
+- [x] 1.2 Add server-feed logical loading and explicit physical-inspection eval cases.
+- [x] 1.3 Capture focused pre-change eval results with the configured provider.
+
+## 2. Logical Index Contract
+
+- [x] 2.1 Add failing registry tests for logical tool guidance and absence of physical roots.
+- [x] 2.2 Change index generation and callers to use the origin-free logical contract.
+- [x] 2.3 Add tool tests proving server-feed skills and resources resolve by logical name, including routed-skill guidance.
+
+## 3. Authoritative Inventory Refresh
+
+- [x] 3.1 Add an inventory refresher that resolves live enabled server-feed sources and serializes complete refreshes.
+- [x] 3.2 Publish registry replacements as complete snapshots safe for concurrent readers.
+- [x] 3.3 Route startup, system/feed sync, directory watching, and `skill_manage` mutations through the refresher.
+- [x] 3.4 Add tests for mutation preservation, late feed directory discovery, precedence, and complete snapshot visibility.
+
+## 4. Guidance and Documentation
+
+- [x] 4.1 Replace physical skill-loading examples in runtime identity guidance.
+- [x] 4.2 Update and version-bump `skill-authoring`; align operations references and repository guidance.
+- [x] 4.3 Update eval documentation for exact method assertions and retained results.
+
+## 5. Verification
+
+- [x] 5.1 Run targeted actor and daemon skill tests.
+- [x] 5.2 Run the solution test suite, Slopwatch, copyright verification, and whitespace validation.
+- [x] 5.3 Run focused and full behavioral evals with the same provider settings, or document the external credential blocker.
+- [x] 5.4 Validate implementation against the OpenSpec change and prepare it for sync/archive.
diff --git a/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/.openspec.yaml b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/.openspec.yaml
new file mode 100644
index 000000000..64105fc96
--- /dev/null
+++ b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-14
diff --git a/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/design.md b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/design.md
new file mode 100644
index 000000000..fa38d8fe7
--- /dev/null
+++ b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/design.md
@@ -0,0 +1,58 @@
+## Context
+
+`McpClientManager` already owns one client per configured MCP server in `_clients`. Playwright additionally enters a second path selected by command/name heuristics: the manager retains the startup client for discovery, creates a `ScopedClientHandle` per `ToolExecutionContext.SessionId`, and scans those handles for idle cleanup only during later scoped invocations. This makes process count proportional to recent Netclaw sessions and embeds Playwright-specific behavior in the generic MCP manager.
+
+MCP authorization is enforced before calls reach the invoker. Session actors and persistence do not own MCP processes and require no changes.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make configured MCP server identity the sole MCP client/process ownership key.
+- Reuse the existing shared client invocation and reconnect path for Playwright.
+- Delete the alternate scoped-client lifecycle and Playwright command rewriting.
+- Preserve clear invocation failures, diagnostics, and deterministic daemon shutdown.
+
+**Non-Goals:**
+
+- Lazy startup or idle process reclamation.
+- Per-session browser contexts or state isolation.
+- New lifecycle configuration, pools, queues, or background maintenance.
+- Changes to actor boundaries, persisted state, grants, or remote transports.
+
+## Decisions
+
+### One client per configured server
+
+`_clients[McpServerName]` remains the sole live-client collection. `InvokeAsync` always uses the existing shared invocation path. This matches the configured-resource model used by other MCP harnesses and bounds a local STDIO profile to one root child process per daemon.
+
+Alternative: retain session-scoped clients but cap them. Rejected because it preserves two lifecycle models, ownership state, cleanup scans, and Playwright-specific classification.
+
+### Share server-internal state across authorized sessions
+
+Netclaw session identity will not select or partition MCP clients. Authorization remains the access boundary; state held inside an MCP server is daemon-scoped. For Playwright, authorized callers may observe or affect the same browser context.
+
+Alternative: multiplex Playwright contexts through per-session HTTP connections. Rejected because the STDIO tool surface exposes no context-selection primitive and per-session connection management recreates the lifecycle machinery being removed.
+
+### Pass configured STDIO arguments unchanged
+
+The manager will not recognize Playwright or append `--isolated`. Operators and canonical browser configuration own server arguments. This removes hidden product-specific behavior and makes the launched process match persisted configuration.
+
+### Preserve startup and shutdown behavior
+
+This change does not add lazy creation or idle teardown. Enabled servers still connect and discover tools at daemon startup, reconnect through the existing failure path, and dispose on daemon shutdown. Those behaviors provide a smaller, independently reviewable baseline; on-demand residency can be considered separately if process evidence still justifies it.
+
+## Risks / Trade-offs
+
+- **Authorized sessions share browser state** → Document that MCP state is daemon-scoped and keep existing audience/server grants as the access boundary.
+- **Concurrent calls may contend inside a stateful server** → Preserve the existing shared invocation behavior; add synchronization only if a reproducible server/client failure proves it necessary.
+- **Removing implicit `--isolated` changes profile persistence** → Pass the canonical configured arguments exactly and test that contract; operators can explicitly configure `--isolated` when desired.
+- **Startup residency remains** → Accept for this focused correction; the immediate unbounded multiplier is removed without adding a maintenance loop.
+
+## Migration Plan
+
+No configuration or persisted-state migration is required. Deploying the change collapses Playwright from a retained discovery client plus per-session clients to the single configured client. Rollback restores the former process model without data migration.
+
+## Open Questions
+
+None for this change.
diff --git a/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/proposal.md b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/proposal.md
new file mode 100644
index 000000000..d99ad9beb
--- /dev/null
+++ b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/proposal.md
@@ -0,0 +1,37 @@
+## Why
+
+Local STDIO MCP servers are daemon-owned child processes, but Netclaw currently gives Playwright a second, session-scoped lifecycle that retains an unused discovery process and can launch one additional process per session. This multiplies heavyweight browser process trees and makes MCP process ownership depend on Slack thread identity instead of the configured server.
+
+Source PRD: PRD-006.
+
+## What Changes
+
+- Remove Playwright-specific session-scoped MCP clients and process fan-out.
+- Treat every configured MCP server as one daemon-owned client connection; a local STDIO profile therefore owns at most one child process per daemon.
+- Keep the process and its state shared by every session authorized to invoke that server.
+- Stop adding Playwright's `--isolated` argument implicitly; configured command arguments pass through unchanged.
+- Preserve existing startup discovery, reconnect, diagnostics, authorization, and daemon-shutdown behavior.
+- Document that MCP server state is daemon-scoped rather than a Netclaw session-isolation boundary.
+
+In scope: MCP client ownership and invocation behavior for configured local STDIO servers, focused regression tests, and operator/agent guidance.
+
+Out of scope: lazy startup, idle shutdown, per-session browser contexts, client pools, queues, new lifecycle configuration, remote transport changes, and changes to the Playwright MCP server.
+
+## Capabilities
+
+### New Capabilities
+
+None.
+
+### Modified Capabilities
+
+- `netclaw-mcp`: Define one daemon-owned client per configured server and make local STDIO state shared across authorized Netclaw sessions.
+
+## Impact
+
+- Code: `McpClientManager` becomes smaller by deleting Playwright detection, scoped-client storage, scoped cleanup, and the alternate invocation path.
+- Tests: focused MCP manager coverage proves calls from different session identities reuse one client/process path and configured arguments are not rewritten.
+- Security: authorization remains enforced before MCP invocation, but an authorized MCP server's internal state is shared daemon-wide; sessions are not an isolation boundary for that state.
+- Operations: one configured local STDIO server produces at most one root child process per daemon and is disposed during reconnect or daemon shutdown.
+- Configuration/schema: unchanged.
+- Dependencies and public APIs: unchanged.
diff --git a/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/specs/netclaw-mcp/spec.md b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/specs/netclaw-mcp/spec.md
new file mode 100644
index 000000000..f14fb7a75
--- /dev/null
+++ b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/specs/netclaw-mcp/spec.md
@@ -0,0 +1,41 @@
+## ADDED Requirements
+
+### Requirement: Configured MCP server has daemon-bound client ownership
+
+The system SHALL maintain at most one live MCP client connection for each enabled configured MCP server within a daemon process. For a local STDIO server, that connection SHALL own the server child process and SHALL be shared by every Netclaw session authorized to invoke the server.
+
+#### Scenario: Different sessions invoke one local STDIO server
+
+- **GIVEN** a local STDIO MCP server is enabled and available to two authorized sessions
+- **WHEN** both sessions invoke tools from that server
+- **THEN** both invocations use the same configured MCP client connection
+- **AND** Netclaw does not launch a child process for either session identity
+
+#### Scenario: Session identity does not partition MCP state
+
+- **GIVEN** an authorized session changes state held by an MCP server
+- **WHEN** another authorized session invokes that server
+- **THEN** the second invocation uses the same daemon-scoped server state
+
+#### Scenario: Daemon shutdown owns local child cleanup
+
+- **GIVEN** an enabled local STDIO MCP server is connected
+- **WHEN** the Netclaw daemon stops
+- **THEN** Netclaw disposes the configured MCP client
+- **AND** the client transport terminates its owned child process
+
+### Requirement: Configured STDIO command is launched without server-specific rewriting
+
+The system SHALL pass the configured command and arguments to a local STDIO MCP transport without adding arguments based on the server name, command text, or implementation identity.
+
+#### Scenario: Playwright arguments pass through unchanged
+
+- **GIVEN** a local STDIO profile invokes the Playwright MCP package without `--isolated`
+- **WHEN** Netclaw creates its transport
+- **THEN** the launched argument list does not contain an implicitly added `--isolated` argument
+
+#### Scenario: Explicit isolation argument is preserved
+
+- **GIVEN** a local STDIO profile explicitly configures `--isolated`
+- **WHEN** Netclaw creates its transport
+- **THEN** the launched argument list contains the configured argument exactly once
diff --git a/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/tasks.md b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/tasks.md
new file mode 100644
index 000000000..98567ecd4
--- /dev/null
+++ b/openspec/changes/archive/2026-07-14-make-stdio-mcp-process-bound/tasks.md
@@ -0,0 +1,16 @@
+## 1. Collapse MCP Client Ownership
+
+- [x] 1.1 Delete Playwright detection, implicit argument rewriting, scoped-client collections, cleanup, and disposal paths from `McpClientManager`.
+- [x] 1.2 Route every configured MCP server invocation through the existing daemon-owned shared client and reconnect path.
+
+## 2. Automated Proof
+
+- [x] 2.1 Add focused tests proving different `ToolExecutionContext` session identities use one configured client/process path.
+- [x] 2.2 Add focused tests proving STDIO arguments pass through unchanged, including explicit `--isolated` preservation.
+- [x] 2.3 Run targeted MCP tests and the full relevant test project.
+
+## 3. Guidance and Quality Gates
+
+- [x] 3.1 Update `netclaw-operations` guidance to state that configured MCP servers and their state are daemon-scoped; bump the skill version.
+- [x] 3.2 Confirm the eval suite is not applicable because the change does not alter production tool definitions, skill matching, prompts, or model behavior.
+- [x] 3.3 Run OpenSpec validation, Slopwatch, file-header verification, and `git diff --check`.
diff --git a/openspec/specs/audience-context-filtering/spec.md b/openspec/specs/audience-context-filtering/spec.md
index 8aed5cada..d7538b0bc 100644
--- a/openspec/specs/audience-context-filtering/spec.md
+++ b/openspec/specs/audience-context-filtering/spec.md
@@ -12,9 +12,7 @@ defines how the audience parameter flows through context-layer assembly, session
and working-context blocks, file-access denial messaging, implicit file roots,
and audience derivation, with secure-by-default behavior and no default-audience
fallback.
-
## Requirements
-
### Requirement: Context layer audience filtering
The context layer system SHALL accept a `TrustAudience` parameter on
@@ -78,18 +76,24 @@ audiences.
### Requirement: Working context suppression for Public
-The working context block (project directory, recent files) SHALL NOT be
-injected into Public-audience sessions.
+The working context block, including project directory, recent files, Git worktree paths, branch, HEAD, and dirty state, SHALL NOT be injected into Public-audience main sessions or subagents.
#### Scenario: Public session has no working context
-- **WHEN** a Public-audience session has a non-empty working context
-- **THEN** `WorkingContext.ToContextBlock()` is NOT injected into the volatile context block
+- **WHEN** a Public-audience session has a non-empty working context or eligible Git project directory
+- **THEN** no `[working-context]` block is injected into the volatile context block
+- **AND** no Git inspection result is exposed to the model
+
+#### Scenario: Public subagent receives no internal working context
+
+- **GIVEN** a subagent is launched under a Public parent turn
+- **WHEN** the child initial prompt is assembled
+- **THEN** no parent project path, recent-file list, or Git state is included
#### Scenario: Team session receives working context
- **WHEN** a Team-audience session has a non-empty working context
-- **THEN** `WorkingContext.ToContextBlock()` IS injected into the volatile context block
+- **THEN** `WorkingContext` and any successfully derived Git enrichment are injected into the volatile context block
### Requirement: File access error message sanitization
diff --git a/openspec/specs/netclaw-agent-memory/spec.md b/openspec/specs/netclaw-agent-memory/spec.md
index 3c820b833..70a79d10b 100644
--- a/openspec/specs/netclaw-agent-memory/spec.md
+++ b/openspec/specs/netclaw-agent-memory/spec.md
@@ -14,68 +14,78 @@ stateless chat endpoint.
## Requirements
### Requirement: Layered system prompt assembly
-The system SHALL assemble session context from ordered layers: `SOUL.md`,
-`AGENTS.md`, `TOOLING.md`, dynamic context layers (tool index, skill index,
-memory index), and session-specific context. Later layers SHALL augment earlier
-layers. Identity files SHALL be loaded at session start and cached for the
-session lifetime. Missing files SHALL be omitted without error.
+The system SHALL assemble session context from ordered layers: `SOUL.md`, the audience-appropriate embedded operating core followed by the deployment `AGENTS.md`, `TOOLING.md`, dynamic context layers (tool index, skill index, memory index), and session-specific context. The embedded operating core SHALL be labeled as higher-priority platform guidance, while runtime ACL and tool policy SHALL remain the authoritative security boundaries. The same deployment `AGENTS.md` SHALL apply to Personal, Team, and Public audiences. Identity files SHALL be read before each inbound turn so edits take effect on the next turn. Missing files SHALL be omitted without error; unexpected read failures SHALL be surfaced.
-#### Scenario: Full layer assembly on session start
+#### Scenario: Full layer assembly on an inbound turn
-- **GIVEN** identity files exist at `~/.netclaw/identity/SOUL.md`,
- `~/.netclaw/identity/AGENTS.md`, and `~/.netclaw/identity/TOOLING.md`
-- **WHEN** a new session starts
-- **THEN** the system prompt includes content from all three identity files in
- layer order (soul, agents, tooling)
-- **AND** dynamic context layers and session-specific context are appended
+- **GIVEN** identity files exist at `~/.netclaw/identity/SOUL.md`, `~/.netclaw/identity/AGENTS.md`, and `~/.netclaw/identity/TOOLING.md`
+- **WHEN** an inbound turn begins
+- **THEN** the system prompt includes the audience-appropriate embedded operating core before the deployment `AGENTS.md`
+- **AND** includes the remaining permitted identity and dynamic context layers in canonical order
-#### Scenario: Missing identity file does not prevent session start
+#### Scenario: Deployment playbook applies to Public audience
-- **GIVEN** one or more identity files do not exist on disk
-- **WHEN** a new session starts
+- **GIVEN** a deployment `AGENTS.md` exists
+- **WHEN** a Public-audience turn begins
+- **THEN** the prompt contains the stripped embedded Public operating core
+- **AND** contains the same deployment playbook used for Personal and Team audiences
+- **AND** continues to suppress Public-ineligible tooling and project layers
+
+#### Scenario: Identity edit takes effect on next turn
+
+- **GIVEN** a session is active
+- **WHEN** the deployment `AGENTS.md` is updated during a turn
+- **THEN** the current model call is unchanged
+- **AND** the next inbound turn rebuilds its prompt with the updated playbook
+
+#### Scenario: Missing identity file does not prevent a turn
+
+- **GIVEN** one or more optional identity files do not exist on disk
+- **WHEN** an inbound turn begins
- **THEN** the system assembles the prompt from available layers
- **AND** the missing layer is omitted without error
### Requirement: Personality bootstrap via onboarding wizard
-The system SHALL bootstrap agent personality through the `netclaw init`
-onboarding wizard. The wizard SHALL collect owner identity, write initial
-`SOUL.md`, and configure the standard identity directory. The agent MAY
-refine personality through conversation using `file_write` on identity files,
-guided by the `netclaw-identity` skill.
+The system SHALL bootstrap agent personality and its deployment playbook through `netclaw init`. The wizard SHALL collect owner identity, write initial `SOUL.md` and `TOOLING.md`, and seed a minimal `AGENTS.md` playbook scaffold only when that file is absent. The post-init conversation SHALL refine personality and mission guidance using identity-file tools and the always-present embedded identity routing rules.
-#### Scenario: Init wizard bootstraps personality
+#### Scenario: Fresh init seeds identity files
- **GIVEN** a fresh install with no identity files
-- **WHEN** the operator runs the `netclaw init` onboarding wizard and provides
- owner identity
-- **THEN** the wizard writes an initial `SOUL.md` under the standard identity
- directory
-- **AND** the agent can later refine that personality through conversation
+- **WHEN** the operator completes `netclaw init`
+- **THEN** the wizard writes initial `SOUL.md` and `TOOLING.md`
+- **AND** writes a minimal deployment mission scaffold to `AGENTS.md`
+
+#### Scenario: Init preserves an existing playbook
+
+- **GIVEN** `~/.netclaw/identity/AGENTS.md` already exists
+- **WHEN** init or identity redo writes identity-owned files
+- **THEN** the existing playbook remains byte-for-byte unchanged
### Requirement: Self-configuration through conversation
-The system SHALL allow the agent to modify identity files (`SOUL.md`,
-`AGENTS.md`, `TOOLING.md`) and skill files (`~/.netclaw/skills/*.md`) through
-conversation using `file_read` and `file_write`. The `netclaw-identity`
-built-in skill SHALL provide triage guidance for what information goes where.
-The agent SHALL NOT have tools that directly modify `netclaw.json`,
-`secrets.json`, ACL, or security policy.
+The system SHALL allow the agent to modify identity files (`SOUL.md`, `AGENTS.md`, `TOOLING.md`) and skill files (`~/.netclaw/skills/*.md`) through conversation using `file_read` and `file_write`. Always-present embedded guidance SHALL route personality and operator context to `SOUL.md`, deployment mission/workflows/skill-selection/review rules to `AGENTS.md`, and environment capabilities to `TOOLING.md`. The agent SHALL propose and obtain confirmation before changing mission guidance. The agent SHALL NOT place secrets, volatile entity data, ACL, or security policy in the deployment playbook and SHALL NOT have tools that directly modify `netclaw.json`, `secrets.json`, ACL, or security policy.
+
+#### Scenario: Agent updates deployment mission
-#### Scenario: Agent updates identity file
+- **GIVEN** the operator asks to improve a recurring deployment workflow
+- **WHEN** the agent has clarified the intended process and the operator confirms its proposal
+- **THEN** the agent reads and updates `AGENTS.md` using identity-file tools
+- **AND** reports that the change applies on the next inbound turn
-- **GIVEN** the user asks the agent to adjust its personality
-- **WHEN** the agent proposes and the user confirms the change
-- **THEN** the agent writes the updated file using `file_write`
-- **AND** reports that the change was saved
+#### Scenario: Agent routes operator context separately
+
+- **GIVEN** the operator shares personal communication preferences while defining the mission
+- **WHEN** the agent persists the confirmed onboarding results
+- **THEN** it writes operator and personality context to `SOUL.md`
+- **AND** writes mission and workflow guidance to `AGENTS.md`
#### Scenario: Agent attempts to modify ACL
- **GIVEN** the user asks the agent to update ACL rules through conversation
- **WHEN** the agent evaluates the request
- **THEN** the agent refuses the modification
-- **AND** explains that ACL changes require CLI or direct file edit by the
- operator
+- **AND** explains that ACL changes require CLI or direct operator configuration
### Requirement: Pre-compaction memory flush
@@ -489,4 +499,3 @@ Memory recall, memory curation, and memory checkpoint payloads created during an
- **WHEN** memory recall or checkpoint payloads are created during the resumed continuation
- **THEN** their audience and boundary come from the restored turn context
- **AND** they do not fall back to Public because live `MessageSource` is absent
-
diff --git a/openspec/specs/netclaw-mcp/spec.md b/openspec/specs/netclaw-mcp/spec.md
index d9b3cbdf5..af2bd7a97 100644
--- a/openspec/specs/netclaw-mcp/spec.md
+++ b/openspec/specs/netclaw-mcp/spec.md
@@ -49,6 +49,46 @@ The system SHALL validate MCP server connectivity and discovery.
- **WHEN** operator runs MCP validation
- **THEN** output indicates handshake status and discovered tool count
+### Requirement: Configured MCP server has daemon-bound client ownership
+
+The system SHALL maintain at most one live MCP client connection for each enabled configured MCP server within a daemon process. For a local STDIO server, that connection SHALL own the server child process and SHALL be shared by every Netclaw session authorized to invoke the server.
+
+#### Scenario: Different sessions invoke one local STDIO server
+
+- **GIVEN** a local STDIO MCP server is enabled and available to two authorized sessions
+- **WHEN** both sessions invoke tools from that server
+- **THEN** both invocations use the same configured MCP client connection
+- **AND** Netclaw does not launch a child process for either session identity
+
+#### Scenario: Session identity does not partition MCP state
+
+- **GIVEN** an authorized session changes state held by an MCP server
+- **WHEN** another authorized session invokes that server
+- **THEN** the second invocation uses the same daemon-scoped server state
+
+#### Scenario: Daemon shutdown owns local child cleanup
+
+- **GIVEN** an enabled local STDIO MCP server is connected
+- **WHEN** the Netclaw daemon stops
+- **THEN** Netclaw disposes the configured MCP client
+- **AND** the client transport terminates its owned child process
+
+### Requirement: Configured STDIO command is launched without server-specific rewriting
+
+The system SHALL pass the configured command and arguments to a local STDIO MCP transport without adding arguments based on the server name, command text, or implementation identity.
+
+#### Scenario: Playwright arguments pass through unchanged
+
+- **GIVEN** a local STDIO profile invokes the Playwright MCP package without `--isolated`
+- **WHEN** Netclaw creates its transport
+- **THEN** the launched argument list does not contain an implicitly added `--isolated` argument
+
+#### Scenario: Explicit isolation argument is preserved
+
+- **GIVEN** a local STDIO profile explicitly configures `--isolated`
+- **WHEN** Netclaw creates its transport
+- **THEN** the launched argument list contains the configured argument exactly once
+
### Requirement: Policy-gated MCP invocation
The system SHALL apply ACL and grants before invoking MCP tools.
diff --git a/openspec/specs/netclaw-onboarding/spec.md b/openspec/specs/netclaw-onboarding/spec.md
index 4c32e2ff7..37d53842f 100644
--- a/openspec/specs/netclaw-onboarding/spec.md
+++ b/openspec/specs/netclaw-onboarding/spec.md
@@ -86,42 +86,29 @@ the dynamic count.
### Requirement: Phase 2 conversational personality bootstrap
-The system SHALL trigger a conversational personality bootstrap on the first
-conversation if identity files (`SOUL.md`, `TOOLING.md`) do not already carry
-operator-enriched content. The bootstrap is delivered as an initial chat message
-injected by the init wizard's navigate callback when `LaunchChat()` fires. The
-bootstrap message SHALL ask the operator about communication preferences, tone,
-name preferences, and working style, then instruct the agent to update `SOUL.md`
-with what it learns. `AGENTS.md` is loaded from embedded resources at runtime
-and is NOT written to disk by the wizard.
+The system SHALL trigger conversational identity and mission bootstrap through the initial chat message injected by the init wizard's navigate callback when `LaunchChat()` fires. The message SHALL ask naturally about the operator's communication preferences and working style as well as the deployment mission, successful outcomes, recurring workflows, skill-selection expectations, delegation rules, and known quality failures. It SHALL direct the agent to separate operator/personality context into `SOUL.md` and durable mission/workflow guidance into `AGENTS.md`, propose a concise playbook, obtain operator confirmation, then read and update both files. `TOOLING.md` remains wizard-generated.
-#### Scenario: First conversation triggers bootstrap
+#### Scenario: First conversation triggers identity and mission discovery
- **GIVEN** the operator completed the init wizard successfully
-- **WHEN** the health check step auto-launches chat via `LaunchChat()`
-- **THEN** the agent receives a pre-filled onboarding trigger message
-- **AND** the message instructs it to introduce itself, ask the operator about
- their primary use case, ask about background and preferences, and then update
- `SOUL.md` with the learned details
+- **WHEN** the health check step launches chat via `LaunchChat()`
+- **THEN** the agent receives a pre-filled onboarding trigger
+- **AND** the trigger asks about both operator context and the deployment's mission, workflows, and failure modes
-#### Scenario: Bootstrap writes soul files
+#### Scenario: Bootstrap writes canonical identity files
-- **GIVEN** the personality bootstrap conversation is complete
-- **WHEN** the operator has answered the agent's preference questions
-- **THEN** the agent updates `SOUL.md` in the config directory with what it
- learned
-- **AND** `TOOLING.md` is already in place from the init wizard's
- `WriteIdentityFiles` call
+- **GIVEN** the onboarding conversation is complete and the operator confirmed the proposed playbook
+- **WHEN** the agent persists the results
+- **THEN** it reads and updates `SOUL.md` with operator and personality context
+- **AND** reads and updates `AGENTS.md` with mission and operating workflow guidance
+- **AND** reports that the playbook applies on the next inbound turn
-#### Scenario: Bootstrap skipped when files exist
+#### Scenario: Wizard preserves an existing mission playbook
-- **GIVEN** `SOUL.md` already exists in the config directory with enriched
- content
-- **WHEN** a new conversation starts
-- **THEN** no personality bootstrap trigger is injected
-- **AND** the existing `SOUL.md` is loaded normally
-
----
+- **GIVEN** `AGENTS.md` already contains an operator-authored playbook
+- **WHEN** the operator completes init or identity redo
+- **THEN** wizard file generation does not overwrite the playbook
+- **AND** the conversational trigger instructs the agent to read existing content before proposing changes
### Requirement: Environment discovery during onboarding
diff --git a/openspec/specs/netclaw-subagents/spec.md b/openspec/specs/netclaw-subagents/spec.md
index 9d42eafe0..729808021 100644
--- a/openspec/specs/netclaw-subagents/spec.md
+++ b/openspec/specs/netclaw-subagents/spec.md
@@ -395,3 +395,65 @@ Each sub-agent approval-gated tool call SHALL settle exactly once as approved, d
- **THEN** the sub-agent sends at most one `SubAgentResult` to the caller
- **AND** the first terminal path wins
+### Requirement: Subagent deployment playbook inheritance
+
+Every sub-agent SHALL receive the operating-rules composition for its launch audience: the audience-appropriate embedded operating core followed by the operator-authored deployment `AGENTS.md`. It SHALL NOT inherit `SOUL.md` or `TOOLING.md`. Project-local instructions remain separately scoped to the parent's working directory. Runtime audience, ACL, approval, and tool-policy boundaries SHALL remain unchanged by prompt guidance.
+
+#### Scenario: Personal or Team subagent inherits full core and playbook
+
+- **GIVEN** a Personal or Team parent launches a sub-agent and a deployment playbook exists
+- **WHEN** the sub-agent system prompt is assembled
+- **THEN** the full embedded operating core appears before the deployment playbook
+- **AND** neither `SOUL.md` nor `TOOLING.md` is included
+
+#### Scenario: Public subagent inherits stripped core and playbook
+
+- **GIVEN** a Public parent launches a sub-agent and a deployment playbook exists
+- **WHEN** the sub-agent system prompt is assembled
+- **THEN** the stripped embedded Public operating core appears before the deployment playbook
+- **AND** the same deployment playbook used by other audiences is included
+
+#### Scenario: Subagent prompt layer order remains canonical
+
+- **GIVEN** operating rules, deployment playbook, project instructions, and a sub-agent role prompt are available
+- **WHEN** the sub-agent prompt is assembled
+- **THEN** their order is embedded core, deployment playbook, project instructions, sub-agent role, then headless execution contract
+
+### Requirement: Subagents maintain run-scoped working context
+Each subagent SHALL own an ephemeral working context initialized from a read-only snapshot of the parent session's project directory and recent files. The initial snapshot SHALL be included in the runtime-context portion of the child user message and SHALL NOT modify the reusable subagent system prompt. Child activity SHALL NOT mutate parent session state during execution.
+
+#### Scenario: Child receives parent recent-file grounding
+- **GIVEN** a parent session with a project directory and recent files
+- **WHEN** it spawns a permitted subagent
+- **THEN** the child's initial model input contains the parent project directory and recent-file snapshot
+- **AND** its tool execution remains grounded by the existing inherited authority context
+
+#### Scenario: Child file activity is isolated
+- **GIVEN** a running child that reads or changes a file
+- **WHEN** the child updates its run-scoped working context
+- **THEN** the parent durable working context is unchanged until child completion is handled
+
+### Requirement: Subagent completion returns structured working context
+`SubAgentResult` SHALL carry optional structured working-context metadata containing project/worktree identity, files read, confirmed files changed through recognized first-party file tools, files observed changed between bounded Git snapshots, and final branch and HEAD when available. Observed worktree changes SHALL NOT be represented as exclusively authored by the child.
+
+#### Scenario: First-party edit is confirmed
+- **GIVEN** a child changes a file through a recognized first-party file tool
+- **WHEN** the child completes successfully
+- **THEN** the canonical path appears in confirmed changed files
+
+#### Scenario: Shell-generated file is observed
+- **GIVEN** a child invokes a shell command that changes a Git worktree file without first-party file-tool provenance
+- **WHEN** final Git state differs from the spawn snapshot
+- **THEN** the file appears in observed changed files
+- **AND** is not claimed as a confirmed child-authored file
+
+#### Scenario: Parent merges only confirmed successful activity
+- **GIVEN** a child completes successfully with confirmed and observed file metadata
+- **WHEN** the parent handles the structured result
+- **THEN** confirmed files are merged into the parent's durable recent-file context
+- **AND** observed-only files are not silently merged or attributed
+
+#### Scenario: Failed child does not merge partial activity
+- **GIVEN** a child fails or is cancelled after touching files
+- **WHEN** the parent handles the failure result
+- **THEN** no child file metadata is merged into parent durable working context
diff --git a/openspec/specs/netclaw-testing/spec.md b/openspec/specs/netclaw-testing/spec.md
index 98dc193d7..7ede8553e 100644
--- a/openspec/specs/netclaw-testing/spec.md
+++ b/openspec/specs/netclaw-testing/spec.md
@@ -52,3 +52,16 @@ The system SHALL support optional smoke tests against live endpoints.
- **WHEN** CI runs without Tailscale connectivity
- **THEN** CI-required test suites still pass because live smoke tests are not required
+### Requirement: Coding-context evals use isolated deterministic fixtures
+The behavioral eval suite SHALL support focused multi-turn coding-context cases where every scored run receives a fresh Git repository, linked worktree, unique named session, deterministic file state, and independent filesystem assertions.
+
+#### Scenario: Main and child context lifecycle is evaluated across turns
+- **GIVEN** a fresh linked-worktree fixture and unique resumed session
+- **WHEN** one turn establishes file context, a later turn delegates coding, and a final turn reports resulting context
+- **THEN** assertions inspect JSON tool behavior, structured child metadata, and direct Git/filesystem state
+
+#### Scenario: Baseline and treatment results are comparable
+- **GIVEN** baseline and treatment images use the same model settings and prompt variants
+- **WHEN** the focused coding-context category is run repeatedly
+- **THEN** results retain correctness, orientation-call, clarification, token, cache, and latency metrics for comparison
+
diff --git a/openspec/specs/session-cwd/spec.md b/openspec/specs/session-cwd/spec.md
index e2f40f219..3e363db91 100644
--- a/openspec/specs/session-cwd/spec.md
+++ b/openspec/specs/session-cwd/spec.md
@@ -4,9 +4,7 @@ Define how a session tracks its project directory and how the agent
declares it via `set_working_directory`. The project directory is the
load-bearing input to the approval gate's safe-space root set: declaring
it expands the trust boundary for shell invocations under that tree.
-
## Requirements
-
### Requirement: Session-scoped project directory
Each session SHALL maintain a mutable `ProjectDirectory` in `WorkingContext`
@@ -243,3 +241,35 @@ when the project directory is set.
- **WHEN** `ToContextBlock()` is called
- **THEN** the output includes `project_dir: /home/user/workspaces/akadonic`
alongside the recent files listing
+
+### Requirement: Working context includes derived Git worktree state
+For Team and Personal turns whose `WorkingContext.ProjectDirectory` is inside a Git worktree, the system SHALL derive a fresh Git snapshot at turn start and render it as a nested section of `[working-context]`. The snapshot SHALL include worktree root, common repository directory, branch or detached state, HEAD, upstream and ahead/behind when configured, and staged, modified, and untracked counts. Derived Git state SHALL NOT be persisted in session state.
+
+#### Scenario: Linked worktree is distinguished from common repository
+- **GIVEN** a session project directory inside a linked Git worktree
+- **WHEN** the next turn-start working-context snapshot is built
+- **THEN** the model-visible context identifies the linked worktree path and common repository directory
+- **AND** reports the linked worktree's branch and HEAD
+
+#### Scenario: Git state refreshes on the next turn
+- **GIVEN** a tool changes branch, HEAD, or dirty state during one turn
+- **WHEN** the session begins its next turn
+- **THEN** the new volatile working-context nudge contains the updated Git snapshot
+- **AND** earlier history messages are not rewritten
+
+#### Scenario: Non-Git project has no Git section
+- **GIVEN** a valid project directory that is not inside a Git worktree
+- **WHEN** working context is assembled
+- **THEN** the normal project and recent-file context remains available
+- **AND** no Git section is rendered
+
+#### Scenario: Git inspection failure is visible
+- **GIVEN** a project directory whose Git state cannot be inspected because Git is missing, times out, or the repository is invalid
+- **WHEN** working context is assembled for an eligible audience
+- **THEN** Git status is reported as unavailable with a sanitized reason
+- **AND** the failure is not represented as a clean or non-Git worktree
+
+#### Scenario: Git remote credentials are never rendered
+- **GIVEN** a repository with a credential-bearing remote URL
+- **WHEN** Git working context is rendered
+- **THEN** no remote credentials or complete remote URL appears in model-visible context or logs
diff --git a/openspec/specs/skill-index-compression/spec.md b/openspec/specs/skill-index-compression/spec.md
index 24728df97..5f13f1265 100644
--- a/openspec/specs/skill-index-compression/spec.md
+++ b/openspec/specs/skill-index-compression/spec.md
@@ -5,47 +5,43 @@
Define the compressed skill index format for the skill discovery system.
The index is injected into the LLM system prompt and points directly at
skill files on disk for retrieval-led reasoning (no tool invocation needed).
-
## Requirements
-
### Requirement: Compressed pipe-delimited index format
-The system SHALL generate a compressed skill index using pipe-delimited format
-grouped by category. The index SHALL include the skills root path so the agent
-can construct file paths for direct reads.
+The system SHALL generate a compressed skill index using a pipe-delimited format grouped by category. The index SHALL advertise logical skill access through `skill_load` and `skill_read_resource` and SHALL NOT include physical native, system, server-feed, or external skill roots.
#### Scenario: Index generated from registered skills
-- **GIVEN** skills are registered across categories `.system` and root
+- **GIVEN** skills are registered across native, system, server-feed, and external sources
- **WHEN** the index is generated
-- **THEN** the output uses pipe-delimited format with category groupings
-- **AND** each category line lists skill file paths (e.g., `name/SKILL.md`)
-- **AND** the header includes the skills root directory path
+- **THEN** the output uses a pipe-delimited format with category groupings
+- **AND** each skill is identified by logical name and description
+- **AND** no physical skill root or `SKILL.md` path is included
-#### Scenario: Index includes retrieval-led reasoning directive
+#### Scenario: Index includes logical retrieval directive
- **WHEN** the compressed index is generated
-- **THEN** the index includes a directive to prefer retrieval-led reasoning
- over pre-training-led reasoning
+- **THEN** the index instructs the model to access or activate skills through `skill_load`
+- **AND** the index instructs the model to read listed resources through `skill_read_resource`
+- **AND** the index does not instruct the model to use `file_read` for normal skill loading
#### Scenario: Skills grouped by category
-- **GIVEN** skills in `.system/` category and root-level skills
+- **GIVEN** skills in `.system` and root-level categories
- **WHEN** the index is generated
- **THEN** skills are grouped by their `Category` property
- **AND** root-level skills appear under the `user` category
-- **AND** each category line uses brace-delimited file lists
### Requirement: All skills visible in index
-The system SHALL include all registered skills in the index regardless of
-origin. The only exclusion is skills with `disable-model-invocation: true`.
+The system SHALL include all registered skills in the index regardless of physical origin. The only exclusion is skills with `disable-model-invocation: true`.
-#### Scenario: All skills visible
+#### Scenario: All logical skills visible without origins
-- **GIVEN** skills from `.system/` and root-level directories
+- **GIVEN** accepted skills from system, native, server-feed, and external sources
- **WHEN** the index is generated
-- **THEN** all skills appear in the index
+- **THEN** every model-invocable skill appears by logical name
+- **AND** source names and physical paths are not required to use the skill
#### Scenario: Skill without allowed-tools is always visible
diff --git a/openspec/specs/skill-tools/spec.md b/openspec/specs/skill-tools/spec.md
index 58ab29f66..65f74f0ba 100644
--- a/openspec/specs/skill-tools/spec.md
+++ b/openspec/specs/skill-tools/spec.md
@@ -4,9 +4,7 @@
Define the `skill_load`, `skill_read_resource`, and `skill_manage` tools for
structured skill access and management.
-
## Requirements
-
### Requirement: skill_load tool
The system SHALL provide `skill_load` only when the skills subsystem is enabled
@@ -153,3 +151,66 @@ refreshing the index from a partial set.
- **WHEN** the registry rebuild rejects one or more other skills
- **THEN** the tool response reports that the edit succeeded
- **AND** the response also reports that the rebuilt inventory is degraded
+
+### Requirement: Logical model-facing skill access
+
+For non-Public audiences with the skills subsystem enabled, normal model-initiated skill access SHALL use the registered logical skill name rather than a physical storage path. Inline skills SHALL return their instruction body through `skill_load`; skills declaring valid `metadata.subagent` routing SHALL execute through `skill_load` with a non-empty task; listed resources SHALL be read through `skill_read_resource` using the logical skill name and a safe relative resource path.
+
+#### Scenario: Inline skill loads by logical name
+
+- **GIVEN** an inline skill accepted from any configured source
+- **WHEN** the model calls `skill_load` with its logical name
+- **THEN** the runtime reads the registered `SkillEntry.FilePath`
+- **AND** returns the skill instructions without requiring the model to know the physical origin
+
+#### Scenario: Routed skill activates by logical name
+
+- **GIVEN** a skill with valid `metadata.subagent`
+- **WHEN** the model calls `skill_load` with its logical name and a non-empty task
+- **THEN** the runtime executes the routed subagent path
+- **AND** does not return or execute the inline path for the same activation
+
+#### Scenario: Skill resource reads by logical name
+
+- **GIVEN** a registered skill exposes `references/guide.md`
+- **WHEN** the model calls `skill_read_resource` with the logical skill name and `references/guide.md`
+- **THEN** the runtime resolves the path beneath the registered `SkillEntry.SkillDirectory`
+- **AND** applies existing path traversal and audience protections
+
+#### Scenario: Explicit physical inspection remains available
+
+- **GIVEN** a non-Public user explicitly asks to inspect a physical skill file
+- **WHEN** the model uses an audience-authorized filesystem tool for that request
+- **THEN** the request is governed by the normal filesystem access policy
+- **AND** the logical skill contract does not redefine that explicit inspection as skill activation
+
+### Requirement: Authoritative skill inventory refresh
+
+Every in-process skill inventory refresh SHALL resolve the current enabled native, server-feed, and external sources, scan them with native greater than server-feed greater than external precedence, and update the registry and generated index from the same accepted result. Concurrent refresh requests SHALL NOT expose a partially rebuilt registry.
+
+#### Scenario: Skill management preserves server-feed inventory
+
+- **GIVEN** a server-feed skill and a native skill are registered
+- **WHEN** `skill_manage` successfully mutates the native skill inventory
+- **THEN** the refresh retains the server-feed skill
+- **AND** the generated index contains both logical skill names
+
+#### Scenario: Newly available feed directory participates in refresh
+
+- **GIVEN** an enabled configured server feed whose managed directory appears after daemon startup
+- **WHEN** any inventory refresh occurs
+- **THEN** the current feed directory is included in the scan
+
+#### Scenario: Native skill shadows server-feed skill
+
+- **GIVEN** native and server-feed skills have the same logical name
+- **WHEN** the inventory is refreshed
+- **THEN** the native skill is registered
+- **AND** the shadowed server-feed skill is reported through existing scan diagnostics
+
+#### Scenario: Concurrent readers see a complete inventory snapshot
+
+- **GIVEN** sessions can read the skill registry while a background refresh occurs
+- **WHEN** the refreshed inventory replaces the previous inventory
+- **THEN** each reader observes either the complete previous snapshot or the complete new snapshot
+- **AND** no reader observes the registry between clear and repopulation
diff --git a/src/Netclaw.Actors.Tests/Channels/DiscordFileFlowIntegrationTests.cs b/src/Netclaw.Actors.Tests/Channels/DiscordFileFlowIntegrationTests.cs
index 2f513248c..6c1baf43c 100644
--- a/src/Netclaw.Actors.Tests/Channels/DiscordFileFlowIntegrationTests.cs
+++ b/src/Netclaw.Actors.Tests/Channels/DiscordFileFlowIntegrationTests.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2026 - 2026 Petabridge, LLC
//
@@ -15,7 +15,6 @@
using Netclaw.Actors.Channels;
using Netclaw.Actors.Hosting;
using Netclaw.Actors.Tests.Channels.TestHelpers;
-using Netclaw.Actors.Memory;
using Netclaw.Actors.Protocol;
using Netclaw.Actors.Sessions;
using Netclaw.Actors.Tests.Sessions;
@@ -74,18 +73,7 @@ protected override void ConfigureServices(HostBuilderContext context, IServiceCo
services.AddSingleton(new ImageCapabilityResolver());
services.AddSingleton();
- services.AddSingleton(sp => new SessionServices(
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- sp.GetService>() ?? Array.Empty(),
- sp.GetService() ?? TimeProvider.System,
- sp.GetRequiredService()));
- services.AddSingleton(sp => new SessionMemoryServices(
- sp.GetService() ?? NullMemoryExtractor.Instance,
- sp.GetService() ?? NullMemoryRecallCoordinator.Instance,
- sp.GetService() ?? NullMemoryCheckpointSink.Instance,
- sp.GetService()));
- services.AddSingleton(new SessionObservability(null, null));
+ services.AddLlmSessionCompositeRecords();
}
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
diff --git a/src/Netclaw.Actors.Tests/Channels/SlackAttachmentIngressTests.cs b/src/Netclaw.Actors.Tests/Channels/SlackAttachmentIngressTests.cs
index 819f5d8a9..15ff02f52 100644
--- a/src/Netclaw.Actors.Tests/Channels/SlackAttachmentIngressTests.cs
+++ b/src/Netclaw.Actors.Tests/Channels/SlackAttachmentIngressTests.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2026 - 2026 Petabridge, LLC
//
@@ -15,7 +15,6 @@
using Netclaw.Actors.Channels;
using Netclaw.Actors.Hosting;
using Netclaw.Actors.Tests.Channels.TestHelpers;
-using Netclaw.Actors.Memory;
using Netclaw.Actors.Protocol;
using Netclaw.Actors.Sessions;
using Netclaw.Actors.Tests.Sessions;
@@ -98,18 +97,7 @@ protected override void ConfigureServices(HostBuilderContext context, IServiceCo
services.AddSingleton(new FakeCapabilityResolver());
services.AddSingleton();
- services.AddSingleton(sp => new SessionServices(
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- sp.GetService>() ?? Array.Empty(),
- sp.GetService() ?? TimeProvider.System,
- sp.GetRequiredService()));
- services.AddSingleton(sp => new SessionMemoryServices(
- sp.GetService() ?? NullMemoryExtractor.Instance,
- sp.GetService() ?? NullMemoryRecallCoordinator.Instance,
- sp.GetService() ?? NullMemoryCheckpointSink.Instance,
- sp.GetService()));
- services.AddSingleton(new SessionObservability(null, null));
+ services.AddLlmSessionCompositeRecords();
}
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
diff --git a/src/Netclaw.Actors.Tests/Channels/SlackFileFlowIntegrationTests.cs b/src/Netclaw.Actors.Tests/Channels/SlackFileFlowIntegrationTests.cs
index 6d65f8961..f167951c5 100644
--- a/src/Netclaw.Actors.Tests/Channels/SlackFileFlowIntegrationTests.cs
+++ b/src/Netclaw.Actors.Tests/Channels/SlackFileFlowIntegrationTests.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2026 - 2026 Petabridge, LLC
//
@@ -17,7 +17,6 @@
using Microsoft.Extensions.Hosting;
using Netclaw.Actors.Channels;
using Netclaw.Actors.Hosting;
-using Netclaw.Actors.Memory;
using Netclaw.Actors.Sessions;
using Netclaw.Actors.Protocol;
using Netclaw.Actors.Tests.Channels.TestHelpers;
@@ -84,19 +83,7 @@ protected override void ConfigureServices(HostBuilderContext context, IServiceCo
services.AddSingleton(new ImageCapabilityResolver());
services.AddSingleton();
- // Composite records for LlmSessionActor constructor
- services.AddSingleton(sp => new SessionServices(
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- sp.GetService>() ?? Array.Empty(),
- sp.GetService() ?? TimeProvider.System,
- sp.GetRequiredService()));
- services.AddSingleton(sp => new SessionMemoryServices(
- sp.GetService() ?? NullMemoryExtractor.Instance,
- sp.GetService() ?? NullMemoryRecallCoordinator.Instance,
- sp.GetService() ?? NullMemoryCheckpointSink.Instance,
- sp.GetService()));
- services.AddSingleton(new SessionObservability(null, null));
+ services.AddLlmSessionCompositeRecords();
}
// serialize-messages = on causes the Akka.Streams channel output pipeline to stop
diff --git a/src/Netclaw.Actors.Tests/Channels/SlackThreadBackfillIntegrationTests.cs b/src/Netclaw.Actors.Tests/Channels/SlackThreadBackfillIntegrationTests.cs
index dbf125bdd..9a1d27527 100644
--- a/src/Netclaw.Actors.Tests/Channels/SlackThreadBackfillIntegrationTests.cs
+++ b/src/Netclaw.Actors.Tests/Channels/SlackThreadBackfillIntegrationTests.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2026 - 2026 Petabridge, LLC
//
@@ -18,7 +18,6 @@
using Netclaw.Actors.Channels;
using Netclaw.Actors.Hosting;
using Netclaw.Actors.Tests.Channels.TestHelpers;
-using Netclaw.Actors.Memory;
using Netclaw.Actors.Sessions;
using Netclaw.Actors.Protocol;
using Netclaw.Actors.Tests.Sessions;
@@ -77,18 +76,7 @@ protected override void ConfigureServices(HostBuilderContext context, IServiceCo
"You are a test assistant."));
services.AddSingleton(new ImageCapabilityResolver());
services.AddSingleton();
- services.AddSingleton(sp => new SessionServices(
- sp.GetRequiredService(),
- sp.GetRequiredService(),
- sp.GetService>() ?? Array.Empty(),
- sp.GetService() ?? TimeProvider.System,
- sp.GetRequiredService()));
- services.AddSingleton(sp => new SessionMemoryServices(
- sp.GetService() ?? NullMemoryExtractor.Instance,
- sp.GetService() ?? NullMemoryRecallCoordinator.Instance,
- sp.GetService() ?? NullMemoryCheckpointSink.Instance,
- sp.GetService()));
- services.AddSingleton(new SessionObservability(null, null));
+ services.AddLlmSessionCompositeRecords();
}
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
diff --git a/src/Netclaw.Actors.Tests/Memory/CurationPromptBuilderTests.cs b/src/Netclaw.Actors.Tests/Memory/CurationPromptBuilderTests.cs
index 53fc45d12..d59d63fc5 100644
--- a/src/Netclaw.Actors.Tests/Memory/CurationPromptBuilderTests.cs
+++ b/src/Netclaw.Actors.Tests/Memory/CurationPromptBuilderTests.cs
@@ -48,6 +48,8 @@ public void ParseResponse_parses_CONSOLIDATE_with_multiple_ids()
Assert.Equal(2, decision.ConsolidationTargetIds.Count);
Assert.Equal("doc-abc123", decision.ConsolidationTargetIds[0]);
Assert.Equal("doc-def456", decision.ConsolidationTargetIds[1]);
+ // First listed id doubles as the primary write target for the collapse write.
+ Assert.Equal("doc-abc123", decision.TargetDocumentId);
}
[Fact]
diff --git a/src/Netclaw.Actors.Tests/Memory/CurationRulesEvaluatorTests.cs b/src/Netclaw.Actors.Tests/Memory/CurationRulesEvaluatorTests.cs
index 1e0eac47e..212c82dac 100644
--- a/src/Netclaw.Actors.Tests/Memory/CurationRulesEvaluatorTests.cs
+++ b/src/Netclaw.Actors.Tests/Memory/CurationRulesEvaluatorTests.cs
@@ -164,6 +164,8 @@ public void Evaluate_returns_Consolidate_for_fuzzy_match_with_high_overlap()
Assert.Equal(CurationDecisionKind.Consolidate, decision.Kind);
Assert.NotNull(decision.ConsolidationTargetIds);
Assert.Contains("doc-456", decision.ConsolidationTargetIds);
+ // Best match doubles as the primary write target for the collapse write.
+ Assert.Equal("doc-456", decision.TargetDocumentId);
}
// ── Fuzzy match + ambiguous overlap -> Ambiguous ────────────────
diff --git a/src/Netclaw.Actors.Tests/Memory/MemoryCurationEvaluatorParityTests.cs b/src/Netclaw.Actors.Tests/Memory/MemoryCurationEvaluatorParityTests.cs
index 3e633769c..8513620e8 100644
--- a/src/Netclaw.Actors.Tests/Memory/MemoryCurationEvaluatorParityTests.cs
+++ b/src/Netclaw.Actors.Tests/Memory/MemoryCurationEvaluatorParityTests.cs
@@ -5,6 +5,7 @@
// -----------------------------------------------------------------------
using System.Runtime.CompilerServices;
using Akka.Event;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -125,6 +126,49 @@ await SeedDocumentAsync(
Assert.Equal(CurationDecisionKind.Consolidate, fromActor.Kind);
Assert.NotNull(fromActor.ConsolidationTargetIds);
Assert.Contains("doc-akka", fromActor.ConsolidationTargetIds!);
+ // The primary write target: ApplyDecisionAsync sets MemoryId from this so the
+ // store takes the explicit-target overwrite path (collapse), not dedup-append.
+ Assert.Equal("doc-akka", fromActor.TargetDocumentId);
+ }
+
+ // ── Consolidate end-to-end: writes a guarded append into the explicit target ──
+
+ [Fact]
+ public async Task ConsolidateDecision_appliedThroughStore_writesGuardedAppendIntoExplicitTarget()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+ await SeedDocumentAsync(
+ "akka-net-latest-release", "doc-akka", "Akka.NET latest release version is 1.5.62", freshnessAtMs: 1000, ct);
+
+ // One extra token ("now") keeps Jaccard overlap at 0.9 (> 0.8 threshold) while making
+ // the proposal body distinct from the seed, so the write is observable.
+ var operation = MakeOperation(
+ "akka-net-release", "Akka.NET latest release version is now 1.5.62", freshnessAtMs: 2000);
+
+ var evaluator = new MemoryCurationEvaluator(_store, (ILoggingAdapter)NoLogger.Instance, new MemoryCurationConfig());
+ var evaluation = await evaluator.EvaluateAsync(operation, TestSessionId, ct);
+ Assert.Equal(CurationDecisionKind.Consolidate, evaluation.Decision.Kind);
+
+ var writeOp = await evaluator.ApplyDecisionAsync(operation, evaluation.Decision, evaluation.Candidates, ct);
+ Assert.NotNull(writeOp);
+ // The deterministic rules tier never synthesizes a MergedBody, so
+ // ApplyGuardedMergeOrAppend always takes the lossless structural-append branch for
+ // this tier — but it still writes into the SAME explicit primary target document
+ // (doc-akka) that ApplyConsolidate resolved from the decision's ConsolidationTargetIds,
+ // rather than falling through to the store's separate anchor-based dedup lookup.
+ Assert.Equal("doc-akka", writeOp!.MemoryId);
+ Assert.Equal("append-document", writeOp.UpdateSemantics);
+
+ await _store.ApplyInlineCurationBatchAsync([writeOp], ct);
+
+ var (body, updateSemantics) = await ReadDocumentBodyAndSemanticsAsync("doc-akka", ct);
+ // Lossless append: original content survives verbatim, the proposal is appended
+ // under a dated separator — never a destructive overwrite.
+ Assert.Contains("Akka.NET latest release version is 1.5.62", body);
+ Assert.Contains("Akka.NET latest release version is now 1.5.62", body);
+ Assert.Contains("_[merged", body);
+ Assert.Equal("append-document", updateSemantics);
}
// ── gray zone (0.4-0.8), no LLM -> deterministic auto-resolve ───
@@ -555,6 +599,18 @@ private static SQLiteMemoryCurationOperation MakeOperation(
FreshnessAtMs: freshnessAtMs,
ExpiresAtMs: null);
+ private async Task<(string Body, string UpdateSemantics)> ReadDocumentBodyAndSemanticsAsync(string documentId, CancellationToken ct)
+ {
+ await using var conn = new SqliteConnection($"Data Source={_dbPath}");
+ await conn.OpenAsync(ct);
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT markdown_body, update_semantics FROM memory_documents WHERE document_id = $id";
+ cmd.Parameters.AddWithValue("$id", documentId);
+ await using var reader = await cmd.ExecuteReaderAsync(ct);
+ Assert.True(await reader.ReadAsync(ct), $"Expected document row '{documentId}' to exist.");
+ return (reader.GetString(0), reader.GetString(1));
+ }
+
private static void AssertSameDecision(CurationDecision expected, CurationDecision actual)
{
Assert.Equal(expected.Kind, actual.Kind);
diff --git a/src/Netclaw.Actors.Tests/Memory/SqliteMemoryStoreDedupCollisionTests.cs b/src/Netclaw.Actors.Tests/Memory/SqliteMemoryStoreDedupCollisionTests.cs
new file mode 100644
index 000000000..6ea367984
--- /dev/null
+++ b/src/Netclaw.Actors.Tests/Memory/SqliteMemoryStoreDedupCollisionTests.cs
@@ -0,0 +1,297 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (C) 2026 - 2026 Petabridge, LLC
+//
+// -----------------------------------------------------------------------
+using Microsoft.Data.Sqlite;
+using Microsoft.Extensions.Time.Testing;
+using Netclaw.Actors.Memory;
+using Netclaw.Configuration;
+using Xunit;
+
+namespace Netclaw.Actors.Tests.Memory;
+
+///
+/// Regression coverage for the anchor-based dedup collision bug: a curation Create
+/// decision (or any no-MemoryId, merge-document proposal) that lands on an anchor which
+/// already has a document used to be written via a blind INSERT ... ON CONFLICT DO
+/// UPDATE, silently overwriting the existing row's title/body/classification with the
+/// new proposal's raw values. The rules tier () can
+/// legitimately emit Create for "different topic, similar anchor name" (fuzzy anchor match,
+/// low content overlap), so this collision is a real, reachable production path — not a
+/// theoretical one (audit: 88 silent overwrites/14 days, including a destroyed
+/// LLM-merged memory). Both batch appliers (,
+/// used by the daemon checkpoint worker, and ,
+/// used by the inline per-session actor) share the identical dedup logic and must both
+/// preserve the existing document by appending instead of overwriting. Also covers the
+/// idempotency guard on that path: a collision whose content is already present verbatim
+/// is a logged no-op (curation_dedup_duplicate_skipped), not a repeated append.
+///
+public sealed class SqliteMemoryStoreDedupCollisionTests : IAsyncDisposable
+{
+ private readonly string _baseDir = Path.Combine(Path.GetTempPath(), "netclaw-dedup-collision-tests", Guid.NewGuid().ToString("N"));
+ private readonly string _dbPath;
+ private readonly FakeTimeProvider _timeProvider;
+ private readonly SQLiteMemoryStore _store;
+
+ public SqliteMemoryStoreDedupCollisionTests()
+ {
+ Directory.CreateDirectory(_baseDir);
+ _dbPath = Path.Combine(_baseDir, "netclaw.db");
+ _timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-06-01T09:00:00Z"));
+ _store = new SQLiteMemoryStore(_dbPath, _timeProvider);
+ }
+
+ public async ValueTask DisposeAsync() => await SqliteTempDirectoryCleanup.TryDeleteDirectoryAsync(_baseDir);
+
+ [Fact]
+ public async Task ApplyCurationBatchAsync_CreateCollidesWithExistingAnchorDocument_AppendsInsteadOfOverwriting()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+
+ var createdAt = await SeedExistingDocumentAsync("doc-existing", "project-quasar", ct);
+
+ // Time advances between the seed write and the colliding batch, so updated_at
+ // must move while created_at (excluded from the ON CONFLICT SET clause) does not.
+ _timeProvider.Advance(TimeSpan.FromHours(3));
+
+ await _store.ApplyCurationBatchAsync(
+ "cp-collision-1",
+ [MakeColliderOperation("project-quasar")],
+ ct);
+
+ var row = await ReadDocumentAsync("doc-existing", ct);
+
+ Assert.Contains("Quasar uses a Postgres 15 read replica for reporting.", row.MarkdownBody);
+ Assert.Contains("Quasar's on-call rotation moved to PagerDuty last sprint.", row.MarkdownBody);
+ Assert.Contains("---", row.MarkdownBody);
+ Assert.Matches(@"_\[merged \d{4}-\d{2}-\d{2}\]_", row.MarkdownBody);
+
+ // Identity/classification of the existing row must survive the collision verbatim.
+ Assert.Equal("Project Quasar datastore", row.Title);
+ Assert.Equal(MemorySensitivity.Secret.ToWireValue(), row.Sensitivity);
+ Assert.Equal(TrustAudience.Personal.ToWireValue(), row.Audience);
+ Assert.Equal(TrustBoundary.PersonalValue, row.Boundary);
+
+ Assert.Equal(MemoryUpdateSemantics.AppendDocument.ToWireValue(), row.UpdateSemantics);
+ Assert.Equal(createdAt, row.CreatedAtMs);
+ Assert.True(row.UpdatedAtMs > createdAt);
+ }
+
+ [Fact]
+ public async Task ApplyInlineCurationBatchAsync_CreateCollidesWithExistingAnchorDocument_AppendsInsteadOfOverwriting()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+
+ var createdAt = await SeedExistingDocumentAsync("doc-existing-inline", "project-nebula", ct);
+
+ _timeProvider.Advance(TimeSpan.FromHours(3));
+
+ await _store.ApplyInlineCurationBatchAsync(
+ [MakeColliderOperation("project-nebula")],
+ ct);
+
+ var row = await ReadDocumentAsync("doc-existing-inline", ct);
+
+ Assert.Contains("Quasar uses a Postgres 15 read replica for reporting.", row.MarkdownBody);
+ Assert.Contains("Quasar's on-call rotation moved to PagerDuty last sprint.", row.MarkdownBody);
+ Assert.Matches(@"_\[merged \d{4}-\d{2}-\d{2}\]_", row.MarkdownBody);
+
+ Assert.Equal("Project Quasar datastore", row.Title);
+ Assert.Equal(MemorySensitivity.Secret.ToWireValue(), row.Sensitivity);
+ Assert.Equal(TrustAudience.Personal.ToWireValue(), row.Audience);
+ Assert.Equal(TrustBoundary.PersonalValue, row.Boundary);
+
+ Assert.Equal(MemoryUpdateSemantics.AppendDocument.ToWireValue(), row.UpdateSemantics);
+ Assert.Equal(createdAt, row.CreatedAtMs);
+ Assert.True(row.UpdatedAtMs > createdAt);
+ }
+
+ [Fact]
+ public async Task ApplyCurationBatchAsync_CollisionWithVerbatimDuplicateContent_LeavesRowUnchanged()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+
+ var createdAt = await SeedExistingDocumentAsync("doc-dup", "project-vega", ct);
+ _timeProvider.Advance(TimeSpan.FromHours(3));
+
+ // Content is verbatim already present in the seeded body: appending it would be
+ // pure bloat, so the write must be a logged no-op — the row stays byte-identical.
+ await _store.ApplyCurationBatchAsync(
+ "cp-dup-1",
+ [MakeColliderOperation("project-vega", content: "Quasar uses a Postgres 15 read replica for reporting.")],
+ ct);
+
+ var row = await ReadDocumentAsync("doc-dup", ct);
+ Assert.Equal("Quasar uses a Postgres 15 read replica for reporting.", row.MarkdownBody);
+ Assert.Equal("Project Quasar datastore", row.Title);
+ Assert.Equal(MemoryUpdateSemantics.MergeDocument.ToWireValue(), row.UpdateSemantics);
+ Assert.Equal(createdAt, row.CreatedAtMs);
+ Assert.Equal(createdAt, row.UpdatedAtMs);
+ }
+
+ [Fact]
+ public async Task ApplyInlineCurationBatchAsync_CollisionWithVerbatimDuplicateContent_LeavesRowUnchanged()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+
+ var createdAt = await SeedExistingDocumentAsync("doc-dup-inline", "project-lyra", ct);
+ _timeProvider.Advance(TimeSpan.FromHours(3));
+
+ await _store.ApplyInlineCurationBatchAsync(
+ [MakeColliderOperation("project-lyra", content: "Quasar uses a Postgres 15 read replica for reporting.")],
+ ct);
+
+ var row = await ReadDocumentAsync("doc-dup-inline", ct);
+ Assert.Equal("Quasar uses a Postgres 15 read replica for reporting.", row.MarkdownBody);
+ Assert.Equal("Project Quasar datastore", row.Title);
+ Assert.Equal(MemoryUpdateSemantics.MergeDocument.ToWireValue(), row.UpdateSemantics);
+ Assert.Equal(createdAt, row.CreatedAtMs);
+ Assert.Equal(createdAt, row.UpdatedAtMs);
+ }
+
+ [Fact]
+ public async Task ApplyCurationBatchAsync_CreateWithNoCollision_InsertsFreshDocumentUnchanged()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await _store.InitializeAsync(ct);
+
+ // No document exists under "project-comet" yet, so this Create-shaped proposal
+ // (no MemoryId, merge-document semantics) must be a plain insert: the dedup lookup
+ // finds nothing, so none of the collision/append machinery should engage.
+ var operation = MakeColliderOperation("project-comet");
+ await _store.ApplyCurationBatchAsync("cp-no-collision", [operation], ct);
+
+ var anchorId = MemoryTypedId.AnchorId("project-comet");
+ var documentId = await ReadDocumentIdForAnchorAsync(anchorId, ct);
+ Assert.NotNull(documentId);
+
+ var row = await ReadDocumentAsync(documentId!, ct);
+ Assert.Equal(operation.Title, row.Title);
+ Assert.Equal(operation.Content, row.MarkdownBody);
+ Assert.DoesNotContain("merged", row.MarkdownBody);
+ Assert.Equal(MemoryUpdateSemantics.MergeDocument.ToWireValue(), row.UpdateSemantics);
+ Assert.Equal(operation.Sensitivity, row.Sensitivity);
+ }
+
+ // ── helpers ──────────────────────────────────────────────────────
+
+ ///
+ /// Seeds an existing document under with a
+ /// distinct title/body/classification so the collision test can prove every one of
+ /// those values survives the append (rather than being replaced by the colliding
+ /// proposal's own values). Returns the seeded row's created_at for the created_at-
+ /// unchanged assertion.
+ ///
+ private async Task SeedExistingDocumentAsync(string documentId, string anchorCanonicalName, CancellationToken ct)
+ {
+ var anchor = _store.CreateDefaultAnchor(anchorCanonicalName);
+ var createdAt = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
+
+ await _store.UpsertDocumentAsync(new SQLiteMemoryDocument(
+ DocumentId: documentId,
+ Anchor: anchor,
+ MemoryClass: "durable_fact",
+ Title: "Project Quasar datastore",
+ MarkdownBody: "Quasar uses a Postgres 15 read replica for reporting.",
+ AliasesJson: null,
+ FacetsJson: null,
+ SlotsJson: null,
+ UpdateSemantics: MemoryUpdateSemantics.MergeDocument.ToWireValue(),
+ Sensitivity: MemorySensitivity.Secret.ToWireValue(),
+ RecallMode: "auto",
+ Confidence: 0.9,
+ FreshnessAtMs: createdAt,
+ ExpiresAtMs: null,
+ CreatedAtMs: createdAt,
+ UpdatedAtMs: createdAt,
+ Boundary: TrustBoundary.PersonalValue,
+ Audience: TrustAudience.Personal.ToWireValue()), ct);
+
+ return createdAt;
+ }
+
+ ///
+ /// A no-MemoryId, merge-document-semantics proposal — the shape a Create decision
+ /// takes by the time it reaches the store (see MemoryCurationEvaluator.ApplyDecisionAsync,
+ /// case CurationDecisionKind.Create: returns the operation unchanged). Content deliberately
+ /// shares no words with the seeded body, characterizing "fuzzy anchor match but low content
+ /// overlap": a genuinely different topic that happens to land on the same anchor.
+ /// Sensitivity/audience/boundary are deliberately public/normal — the OPPOSITE of the
+ /// seeded row's secret/personal/personal-boundary — so a test that only passed by
+ /// coincidence (both sides equal) would not slip through.
+ ///
+ private static SQLiteMemoryCurationOperation MakeColliderOperation(
+ string anchorCanonicalName,
+ string content = "Quasar's on-call rotation moved to PagerDuty last sprint.") =>
+ new(
+ Kind: "document",
+ MemoryClass: "durable_fact",
+ MemoryId: null,
+ AnchorCanonicalName: anchorCanonicalName,
+ AnchorType: "concept",
+ Title: "On-call rotation",
+ Content: content,
+ AliasesJson: null,
+ FacetsJson: null,
+ SlotsJson: null,
+ Relations: null,
+ UpdateSemantics: MemoryUpdateSemantics.MergeDocument.ToWireValue(),
+ Boundary: TrustBoundary.PublicValue,
+ Audience: TrustAudience.Public,
+ Sensitivity: MemorySensitivity.Normal.ToWireValue(),
+ RecallMode: "auto",
+ Confidence: 0.9,
+ FreshnessAtMs: null,
+ ExpiresAtMs: null);
+
+ private sealed record DocumentRow(
+ string Title,
+ string MarkdownBody,
+ string UpdateSemantics,
+ string? Boundary,
+ string? Audience,
+ string Sensitivity,
+ long CreatedAtMs,
+ long UpdatedAtMs);
+
+ private async Task ReadDocumentAsync(string documentId, CancellationToken ct)
+ {
+ await using var conn = new SqliteConnection($"Data Source={_dbPath}");
+ await conn.OpenAsync(ct);
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = """
+ SELECT title, markdown_body, update_semantics, boundary, audience, sensitivity, created_at, updated_at
+ FROM memory_documents
+ WHERE document_id = $id;
+ """;
+ cmd.Parameters.AddWithValue("$id", documentId);
+ await using var reader = await cmd.ExecuteReaderAsync(ct);
+ var found = await reader.ReadAsync(ct);
+ Assert.True(found, $"Expected document row '{documentId}' to exist.");
+
+ return new DocumentRow(
+ reader.GetString(0),
+ reader.GetString(1),
+ reader.GetString(2),
+ reader.IsDBNull(3) ? null : reader.GetString(3),
+ reader.IsDBNull(4) ? null : reader.GetString(4),
+ reader.GetString(5),
+ reader.GetInt64(6),
+ reader.GetInt64(7));
+ }
+
+ private async Task ReadDocumentIdForAnchorAsync(string anchorId, CancellationToken ct)
+ {
+ await using var conn = new SqliteConnection($"Data Source={_dbPath}");
+ await conn.OpenAsync(ct);
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT document_id FROM memory_documents WHERE anchor_id = $anchorId";
+ cmd.Parameters.AddWithValue("$anchorId", anchorId);
+ return (string?)await cmd.ExecuteScalarAsync(ct);
+ }
+}
diff --git a/src/Netclaw.Actors.Tests/Sessions/LlmSessionTestExtensions.cs b/src/Netclaw.Actors.Tests/Sessions/LlmSessionTestExtensions.cs
index 1c8648d34..ff1a93492 100644
--- a/src/Netclaw.Actors.Tests/Sessions/LlmSessionTestExtensions.cs
+++ b/src/Netclaw.Actors.Tests/Sessions/LlmSessionTestExtensions.cs
@@ -21,10 +21,12 @@ internal static class LlmSessionTestExtensions
{
public static IServiceCollection AddLlmSessionCompositeRecords(this IServiceCollection services)
{
+ services.TryAddSingleton();
services.TryAddSingleton(sp => new SessionServices(
sp.GetRequiredService(),
sp.GetRequiredService(),
sp.GetService>() ?? Array.Empty(),
+ sp.GetRequiredService(),
sp.GetService() ?? TimeProvider.System,
sp.GetRequiredService()));
diff --git a/src/Netclaw.Actors.Tests/Sessions/SessionMessageAssemblerTests.cs b/src/Netclaw.Actors.Tests/Sessions/SessionMessageAssemblerTests.cs
index d956f421a..7dc950234 100644
--- a/src/Netclaw.Actors.Tests/Sessions/SessionMessageAssemblerTests.cs
+++ b/src/Netclaw.Actors.Tests/Sessions/SessionMessageAssemblerTests.cs
@@ -462,7 +462,11 @@ public void Personal_audience_includes_working_context_in_volatile_block()
WorkingContext = WorkingContext.Empty.AddRecentFile("src/Rect.cs")
};
var input = MakeInput(SeedHistory("hi"), FakeRecall("mem-1"), audience: TrustAudience.Personal);
- input = input with { State = stateWithWorkingContext };
+ input = input with
+ {
+ State = stateWithWorkingContext,
+ WorkingContextBlock = stateWithWorkingContext.WorkingContext.ToContextBlock()
+ };
var block = SessionMessageAssembler.BuildVolatileContextBlock(input);
Assert.Contains("[working-context]", block);
@@ -491,6 +495,7 @@ private static ContextAssemblyInput MakeInput(
SessionsBasePath: "/tmp/netclaw-test",
FileReadGranted: fileReadGranted,
ActiveRecall: activeRecall,
+ WorkingContextBlock: state.WorkingContext.ToContextBlock(),
Audience: audience);
}
diff --git a/src/Netclaw.Actors.Tests/Sessions/SessionStateTests.cs b/src/Netclaw.Actors.Tests/Sessions/SessionStateTests.cs
index 02f282b82..cd334e11a 100644
--- a/src/Netclaw.Actors.Tests/Sessions/SessionStateTests.cs
+++ b/src/Netclaw.Actors.Tests/Sessions/SessionStateTests.cs
@@ -7,6 +7,7 @@
using Netclaw.Actors.Protocol;
using Netclaw.Actors.Reminders;
using Netclaw.Actors.Sessions;
+using Netclaw.Tools;
using Xunit;
using static Netclaw.Actors.Sessions.SessionProtocol;
@@ -630,6 +631,36 @@ public void ProcessedReminderIds_is_not_persisted_in_snapshot()
Assert.Empty(restored.ProcessedReminderIds);
}
+ [Fact]
+ public void Successful_subagent_merge_adds_only_confirmed_changed_files()
+ {
+ var child = new SubAgentWorkingContextInfo
+ {
+ ReadFiles = ["src/ReadOnly.cs"],
+ ConfirmedChangedFiles = ["src/Changed.cs"],
+ ObservedChangedFiles = ["src/ObservedOnly.cs"]
+ };
+
+ var merged = LlmSessionActor.MergeSuccessfulSubAgentWorkingContext(
+ WorkingContext.Empty, true, child);
+
+ Assert.Equal(["src/Changed.cs"], merged.RecentFiles);
+ }
+
+ [Fact]
+ public void Failed_subagent_merge_does_not_change_parent_working_context()
+ {
+ var current = WorkingContext.Empty.AddRecentFile("src/Existing.cs");
+ var child = new SubAgentWorkingContextInfo
+ {
+ ConfirmedChangedFiles = ["src/Denied.cs"]
+ };
+
+ var merged = LlmSessionActor.MergeSuccessfulSubAgentWorkingContext(current, false, child);
+
+ Assert.Same(current, merged);
+ }
+
private static SessionState WithSystemPrompt(string content)
{
return SessionState.Empty with
diff --git a/src/Netclaw.Actors.Tests/Sessions/SubAgentSpawnIntegrationTests.cs b/src/Netclaw.Actors.Tests/Sessions/SubAgentSpawnIntegrationTests.cs
index 26e8aaab7..dd6021dcc 100644
--- a/src/Netclaw.Actors.Tests/Sessions/SubAgentSpawnIntegrationTests.cs
+++ b/src/Netclaw.Actors.Tests/Sessions/SubAgentSpawnIntegrationTests.cs
@@ -178,6 +178,7 @@ You specialize in daemon health checks.
toolAccessPolicy,
approvalService: null,
promptProvider,
+ new WorkingContextSnapshotProvider(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance),
Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
registry.Register(new SpawnAgentTool(subAgentRegistry, spawner, subAgentPaths));
diff --git a/src/Netclaw.Actors.Tests/Sessions/WorkingContextSnapshotTests.cs b/src/Netclaw.Actors.Tests/Sessions/WorkingContextSnapshotTests.cs
new file mode 100644
index 000000000..2666ef33f
--- /dev/null
+++ b/src/Netclaw.Actors.Tests/Sessions/WorkingContextSnapshotTests.cs
@@ -0,0 +1,108 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (C) 2026 - 2026 Petabridge, LLC
+//
+// -----------------------------------------------------------------------
+using Microsoft.Extensions.Logging.Abstractions;
+using Netclaw.Actors.Sessions;
+using Netclaw.Configuration;
+
+namespace Netclaw.Actors.Tests.Sessions;
+
+public class WorkingContextSnapshotTests
+{
+ [Fact]
+ public void ParseStatus_reads_branch_divergence_and_dirty_counts()
+ {
+ var snapshot = WorkingContextSnapshotProvider.ParseStatus(
+ "/worktrees/feature",
+ "/repos/app/.git",
+ """
+ # branch.oid 0123456789abcdef
+ # branch.head feature/context
+ # branch.upstream origin/dev
+ # branch.ab +2 -1
+ 1 M. N... 100644 100644 100644 aaaaaaa bbbbbbb src/Staged.cs
+ 1 .M N... 100644 100644 100644 aaaaaaa bbbbbbb src/Modified.cs
+ ? src/New.cs
+ """);
+
+ Assert.Equal("feature/context", snapshot.Branch);
+ Assert.Equal("0123456789abcdef", snapshot.Head);
+ Assert.Equal("origin/dev", snapshot.Upstream);
+ Assert.Equal(2, snapshot.Ahead);
+ Assert.Equal(1, snapshot.Behind);
+ Assert.Equal(1, snapshot.Staged);
+ Assert.Equal(1, snapshot.Modified);
+ Assert.Equal(1, snapshot.Untracked);
+ Assert.Equal(3, snapshot.ChangedFiles.Count);
+ }
+
+ [Fact]
+ public void ParseStatus_uses_rename_destination_as_changed_file()
+ {
+ var snapshot = WorkingContextSnapshotProvider.ParseStatus(
+ "/worktrees/feature",
+ "/repos/app/.git",
+ "2 R. N... 100644 100644 100644 aaaaaaa bbbbbbb R100 src/New Name.cs\tsrc/Old Name.cs");
+
+ Assert.Equal(["src/New Name.cs"], snapshot.ChangedFiles);
+ }
+
+ [Fact]
+ public void Render_nests_git_under_working_context_without_remote_url()
+ {
+ var snapshot = new WorkingContextSnapshot
+ {
+ WorkingContext = WorkingContext.Empty
+ .WithProjectDirectory("/worktrees/feature")
+ .AddRecentFile("src/App.cs"),
+ Git = new GitWorkingContextSnapshot
+ {
+ Worktree = "/worktrees/feature",
+ CommonDirectory = "/repos/app/.git",
+ Branch = "feature/context",
+ Head = "01234567",
+ Upstream = "origin/dev",
+ Staged = 1,
+ Modified = 2,
+ Untracked = 3
+ }
+ };
+
+ var block = snapshot.ToContextBlock();
+
+ Assert.Contains("[working-context]", block);
+ Assert.Contains("recent_files:\n - src/App.cs", block);
+ Assert.Contains("git:\n worktree: /worktrees/feature", block);
+ Assert.Contains("branch: feature/context", block);
+ Assert.DoesNotContain("https://", block, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Public_audience_does_not_inspect_or_render_git()
+ {
+ var provider = new WorkingContextSnapshotProvider(
+ NullLogger.Instance);
+ var context = WorkingContext.Empty.WithProjectDirectory("/path/that/does/not/exist");
+
+ var snapshot = provider.Create(context, TrustAudience.Public);
+
+ Assert.Null(snapshot.Git);
+ Assert.Null(snapshot.GitUnavailableReason);
+ Assert.Equal(string.Empty, snapshot.ToContextBlock());
+ }
+
+ [Fact]
+ public void Missing_project_directory_reports_unavailable_for_personal_audience()
+ {
+ var provider = new WorkingContextSnapshotProvider(
+ NullLogger.Instance);
+ var context = WorkingContext.Empty.WithProjectDirectory("/path/that/does/not/exist");
+
+ var snapshot = provider.Create(context, TrustAudience.Personal);
+
+ Assert.Equal("project directory does not exist", snapshot.GitUnavailableReason);
+ Assert.Contains("status: unavailable", snapshot.ToContextBlock());
+ }
+}
diff --git a/src/Netclaw.Actors.Tests/Skills/SkillInventoryRefresherTests.cs b/src/Netclaw.Actors.Tests/Skills/SkillInventoryRefresherTests.cs
new file mode 100644
index 000000000..41e826cd0
--- /dev/null
+++ b/src/Netclaw.Actors.Tests/Skills/SkillInventoryRefresherTests.cs
@@ -0,0 +1,124 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (C) 2026 - 2026 Petabridge, LLC
+//
+// -----------------------------------------------------------------------
+using System.Collections.Concurrent;
+using Netclaw.Actors.Skills;
+using Netclaw.Configuration;
+using Xunit;
+
+namespace Netclaw.Actors.Tests.Skills;
+
+public sealed class SkillInventoryRefresherTests : IDisposable
+{
+ private readonly string _home = Path.Join(
+ Path.GetTempPath(), $"netclaw-inventory-test-{Guid.NewGuid():N}");
+ private readonly NetclawPaths _paths;
+ private readonly SkillRegistry _registry = new();
+ private readonly SkillIndexContextLayer _index = new();
+
+ public SkillInventoryRefresherTests()
+ {
+ _paths = new NetclawPaths(_home);
+ _paths.EnsureDirectoriesExist();
+ }
+
+ [Fact]
+ public void Refresh_discovers_server_feed_directory_created_after_construction()
+ {
+ var feeds = new SkillFeedsConfig
+ {
+ Feeds = [new SkillFeedSource { Name = "managed" }]
+ };
+ var refresher = new SkillInventoryRefresher(_paths, feeds, [], _registry, _index);
+
+ Assert.Empty(refresher.Refresh().AcceptedSkills);
+
+ WriteSkill(_paths.ServerFeedDirectory("managed"), "feed-skill", "managed guidance");
+ var result = refresher.Refresh();
+
+ Assert.Contains(result.AcceptedSkills, skill => skill.Name == "feed-skill");
+ Assert.Contains("feed-skill: managed guidance", _index.GetContextLayer(TrustAudience.Personal));
+ }
+
+ [Fact]
+ public void Refresh_preserves_all_sources_and_applies_canonical_precedence()
+ {
+ var feedRoot = _paths.ServerFeedDirectory("managed");
+ var externalRoot = Path.Join(_home, "external");
+ WriteSkill(_paths.SkillsDirectory, "shared", "native wins");
+ WriteSkill(feedRoot, "shared", "feed loses");
+ WriteSkill(feedRoot, "feed-only", "managed");
+ WriteSkill(externalRoot, "external-only", "external");
+
+ var feeds = new SkillFeedsConfig
+ {
+ Feeds = [new SkillFeedSource { Name = "managed" }]
+ };
+ var external = new[]
+ {
+ new ResolvedExternalSource("external", [externalRoot], AllowSymlinks: false)
+ };
+ var refresher = new SkillInventoryRefresher(_paths, feeds, external, _registry, _index);
+
+ refresher.Refresh();
+ WriteSkill(_paths.SkillsDirectory, "new-native", "created by mutation");
+ var result = refresher.Refresh();
+
+ Assert.Equal("native wins", _registry.GetByName("shared")!.Description);
+ Assert.Contains(result.AcceptedSkills, skill => skill.Name == "feed-only");
+ Assert.Contains(result.AcceptedSkills, skill => skill.Name == "external-only");
+ Assert.Contains(result.AcceptedSkills, skill => skill.Name == "new-native");
+ }
+
+ [Fact]
+ public void ReplaceAll_never_exposes_a_partially_replaced_inventory()
+ {
+ var a = new[] { Entry("a-1"), Entry("a-2") };
+ var b = new[] { Entry("b-1"), Entry("b-2") };
+ _registry.ReplaceAll(a);
+ var failures = new ConcurrentQueue();
+
+ Parallel.For(0, 10_000, iteration =>
+ {
+ if ((iteration & 1) == 0)
+ _registry.ReplaceAll((iteration & 2) == 0 ? a : b);
+ else
+ {
+ var snapshot = _registry.GetAll();
+ if (snapshot.Count != 2 || snapshot.Any(skill => skill.Name[0] != snapshot[0].Name[0]))
+ failures.Enqueue(string.Join(',', snapshot.Select(skill => skill.Name)));
+ }
+ });
+
+ Assert.Empty(failures);
+ }
+
+ private static SkillEntry Entry(string name) => new(
+ name,
+ name,
+ "description",
+ $"/skills/{name}/SKILL.md",
+ $"/skills/{name}",
+ Category: null);
+
+ private static void WriteSkill(string root, string name, string description)
+ {
+ var directory = Path.Join(root, name);
+ Directory.CreateDirectory(directory);
+ File.WriteAllText(Path.Join(directory, "SKILL.md"), $$"""
+ ---
+ name: {{name}}
+ description: {{description}}
+ ---
+ # {{name}}
+ """);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_home))
+ Directory.Delete(_home, recursive: true);
+ }
+}
diff --git a/src/Netclaw.Actors.Tests/Skills/SkillRegistryTests.cs b/src/Netclaw.Actors.Tests/Skills/SkillRegistryTests.cs
index 60db78253..548839908 100644
--- a/src/Netclaw.Actors.Tests/Skills/SkillRegistryTests.cs
+++ b/src/Netclaw.Actors.Tests/Skills/SkillRegistryTests.cs
@@ -93,7 +93,7 @@ public void Search_respects_max_results()
public void GenerateIndex_returns_empty_when_no_skills()
{
var registry = new SkillRegistry();
- Assert.Equal(string.Empty, registry.GenerateIndex("/test/skills"));
+ Assert.Equal(string.Empty, registry.GenerateIndex());
}
[Fact]
@@ -102,20 +102,20 @@ public void GenerateIndex_includes_skill_with_description()
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "Short description"));
- var index = registry.GenerateIndex("/test/skills");
+ var index = registry.GenerateIndex();
Assert.Contains("my-skill: Short description", index);
}
[Fact]
- public void GenerateIndex_includes_root_path()
+ public void GenerateIndex_includes_logical_catalog_header()
{
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "desc"));
- var index = registry.GenerateIndex("/home/user/.netclaw/skills");
+ var index = registry.GenerateIndex();
- Assert.Contains("[skills]|root: /home/user/.netclaw/skills", index);
+ Assert.Contains("[skills]|invoke via /name", index);
}
[Fact]
@@ -125,7 +125,7 @@ public void GenerateIndex_groups_by_category()
registry.Register(MakeEntry("netclaw-memory", "Memory guidance", ".system"));
registry.Register(MakeEntry("my-workflow", "Workflow help"));
- var index = registry.GenerateIndex("/test/skills");
+ var index = registry.GenerateIndex();
Assert.Contains("|.system:", index);
Assert.Contains("netclaw-memory: Memory guidance", index);
@@ -141,7 +141,7 @@ public void DisableModelInvocation_skill_excluded_from_index()
disableModelInvocation: true));
registry.Register(MakeEntry("memory", "Memory guidance", ".system"));
- var index = registry.GenerateIndex("/test/skills");
+ var index = registry.GenerateIndex();
Assert.DoesNotContain("ops:", index);
Assert.Contains("memory: Memory guidance", index);
@@ -153,11 +153,11 @@ public void Clear_resets_index()
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "A user skill"));
- Assert.NotEqual(string.Empty, registry.GenerateIndex("/test/skills"));
+ Assert.NotEqual(string.Empty, registry.GenerateIndex());
registry.Clear();
- Assert.Equal(string.Empty, registry.GenerateIndex("/test/skills"));
+ Assert.Equal(string.Empty, registry.GenerateIndex());
}
// --- Slash-command dispatch tests ---
@@ -293,71 +293,42 @@ public void ActivationRouter_returns_deterministic_error_when_metadata_subagent_
Assert.Contains("/ops", decision.ErrorMessage!, StringComparison.Ordinal);
}
- // --- Multi-root index tests ---
+ // --- Logical index contract ---
[Fact]
- public void GenerateIndex_with_external_sources_uses_roots_header()
+ public void GenerateIndex_uses_logical_skill_tools_without_physical_roots()
{
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "desc"));
- var externalSources = new[]
- {
- new ResolvedExternalSource("claude-code", new[] { "/home/user/.claude/skills" }, true)
- };
-
- var index = registry.GenerateIndex("/home/user/.netclaw/skills", externalSources);
-
- Assert.Contains("roots: native=/home/user/.netclaw/skills,claude-code=/home/user/.claude/skills", index);
- Assert.DoesNotContain("[skills]|root:", index);
- }
-
- [Fact]
- public void GenerateIndex_with_multi_path_external_source_joins_paths_with_semicolon()
- {
- var registry = new SkillRegistry();
- registry.Register(MakeEntry("my-skill", "desc"));
-
- var externalSources = new[]
- {
- new ResolvedExternalSource(
- "claude-code",
- new[]
- {
- "/home/user/.claude/skills",
- "/home/user/.claude/commands",
- "/home/user/.claude/plugins/marketplaces/dotnet-skills/skills"
- },
- true)
- };
-
- var index = registry.GenerateIndex("/home/user/.netclaw/skills", externalSources);
+ var index = registry.GenerateIndex();
- Assert.Contains(
- "claude-code=/home/user/.claude/skills;/home/user/.claude/commands;/home/user/.claude/plugins/marketplaces/dotnet-skills/skills",
- index);
+ Assert.Contains("skill_load(name)", index);
+ Assert.Contains("skill_read_resource(skillName, resourcePath)", index);
+ Assert.DoesNotContain("file_read", index);
+ Assert.DoesNotContain("/home/", index);
}
[Fact]
- public void GenerateIndex_without_external_sources_uses_single_root_header()
+ public void GenerateIndex_explains_routed_skill_task_requirement()
{
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "desc"));
- var index = registry.GenerateIndex("/home/user/.netclaw/skills");
+ var index = registry.GenerateIndex();
- Assert.Contains("[skills]|root: /home/user/.netclaw/skills", index);
- Assert.DoesNotContain("roots:", index);
+ Assert.Contains("routed to a subagent require a concrete task", index);
}
[Fact]
- public void GenerateIndex_with_empty_external_sources_uses_single_root_header()
+ public void GenerateIndex_does_not_expose_skill_file_path()
{
var registry = new SkillRegistry();
registry.Register(MakeEntry("my-skill", "desc"));
- var index = registry.GenerateIndex("/home/user/.netclaw/skills", Array.Empty());
+ var index = registry.GenerateIndex();
- Assert.Contains("[skills]|root: /home/user/.netclaw/skills", index);
+ Assert.DoesNotContain("SKILL.md", index);
+ Assert.DoesNotContain("root", index, StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/src/Netclaw.Actors.Tests/SubAgents/SpawnAgentStreamingTests.cs b/src/Netclaw.Actors.Tests/SubAgents/SpawnAgentStreamingTests.cs
index 88e6fe65e..0595886d2 100644
--- a/src/Netclaw.Actors.Tests/SubAgents/SpawnAgentStreamingTests.cs
+++ b/src/Netclaw.Actors.Tests/SubAgents/SpawnAgentStreamingTests.cs
@@ -10,6 +10,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Netclaw.Actors.SubAgents;
+using Netclaw.Actors.Sessions;
using Netclaw.Actors.Tests.Memory;
using Netclaw.Actors.Tools;
using Netclaw.Configuration;
@@ -74,6 +75,7 @@ public async Task Spawn_agent_streams_activity_through_executor_dispatch_to_watc
toolAccessPolicy,
approvalService: null,
new StaticSystemPromptProvider("You are a summarizer."),
+ new WorkingContextSnapshotProvider(NullLogger.Instance),
NullLogger.Instance);
registry.Register(new SpawnAgentTool(subAgentRegistry, spawner, paths));
@@ -163,6 +165,7 @@ public async Task Spawn_agent_self_monitoring_survives_quiet_window_after_first_
toolAccessPolicy,
approvalService: null,
new StaticSystemPromptProvider("You are a summarizer."),
+ new WorkingContextSnapshotProvider(NullLogger.Instance),
NullLogger.Instance);
registry.Register(new SpawnAgentTool(subAgentRegistry, spawner, paths));
diff --git a/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs b/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs
index 91dc7a58b..f35474b64 100644
--- a/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs
+++ b/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs
@@ -187,6 +187,8 @@ public async Task System_prompt_includes_headless_subagent_contract()
Assert.NotNull(fakeClient.LastReceivedMessages);
Assert.Equal(ChatRole.System, fakeClient.LastReceivedMessages[0].Role);
Assert.Contains("headless, non-interactive worker", fakeClient.LastReceivedMessages[0].Text);
+ Assert.Contains("subagent role guidance and assigned task are more specific", fakeClient.LastReceivedMessages[0].Text);
+ Assert.Contains("safety, security, trust-boundary, approval, and tool-policy rules remain mandatory", fakeClient.LastReceivedMessages[0].Text);
Assert.Contains("Do not ask the user clarifying questions", fakeClient.LastReceivedMessages[0].Text);
Assert.Contains("Parent-mediated tool approval", fakeClient.LastReceivedMessages[0].Text);
}
@@ -197,7 +199,7 @@ public async Task System_prompt_layers_operating_rules_before_project_role_and_h
var fakeClient = new FakeChatClient();
var definition = CreateDefinition() with
{
- OperatingRules = "Operating rules: never invent runtime facts.",
+ OperatingRules = "Operating rules: never invent runtime facts.\n\nDeployment playbook: review customer email.",
ProjectInstructions = "Project rules: prefer C#.",
SystemPrompt = "You are a test agent.\n\n[Skill Overlay]\nUse focused analysis."
};
@@ -214,6 +216,7 @@ public async Task System_prompt_layers_operating_rules_before_project_role_and_h
AssertPromptOrder(
systemPrompt,
"Operating rules: never invent runtime facts.",
+ "Deployment playbook: review customer email.",
"Project rules: prefer C#.",
"You are a test agent.",
"[Skill Overlay]",
@@ -331,6 +334,7 @@ public async Task Tool_execution_inherits_parent_session_and_project_directories
Timeout = TimeSpan.FromSeconds(5),
ParentSessionDirectory = "/tmp/netclaw/sessions/abc",
ParentProjectDirectory = "/home/user/workspaces/netclaw",
+ ParentRecentFiles = ["src/Netclaw.Actors/SubAgents/SubAgentActor.cs"],
Audience = TrustAudience.Personal,
},
TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
@@ -339,6 +343,7 @@ public async Task Tool_execution_inherits_parent_session_and_project_directories
Assert.NotNull(fakeTool.LastContext);
Assert.Equal("/tmp/netclaw/sessions/abc", fakeTool.LastContext!.SessionDirectory);
Assert.Equal("/home/user/workspaces/netclaw", fakeTool.LastContext.ProjectDirectory);
+ Assert.Equal(["src/Netclaw.Actors/SubAgents/SubAgentActor.cs"], fakeTool.LastContext.RecentFiles);
}
[Fact]
@@ -1356,6 +1361,95 @@ public async Task Null_RuntimeContext_leaves_first_user_message_as_raw_task()
Assert.DoesNotContain("Context:", fakeClient.LastReceivedMessages[1].Text);
}
+ [Fact]
+ public async Task Parent_working_context_is_injected_into_child_user_message()
+ {
+ var fakeClient = new FakeChatClient();
+ var agent = Sys.ActorOf(SubAgentActor.CreateProps(CreateDefinition(), fakeClient));
+
+ var result = await agent.Ask(
+ new RunSubAgent
+ {
+ Task = "Continue the implementation.",
+ Timeout = TimeSpan.FromSeconds(5),
+ Audience = TrustAudience.Personal,
+ ParentProjectDirectory = MissingProjectDirectory,
+ ParentRecentFiles = ["src/Netclaw.Actors/Sessions/WorkingContext.cs"]
+ },
+ TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ var userMessage = fakeClient.LastReceivedMessages![1].Text;
+ Assert.Contains("[working-context]", userMessage);
+ Assert.Contains($"project_dir: {MissingProjectDirectory}", userMessage);
+ Assert.Contains("src/Netclaw.Actors/Sessions/WorkingContext.cs", userMessage);
+ Assert.DoesNotContain("[working-context]", fakeClient.LastReceivedMessages[0].Text);
+ }
+
+ [Fact]
+ public async Task Successful_first_party_edit_is_returned_as_confirmed_child_activity()
+ {
+ var editTool = new FakeNetclawTool("file_edit", "Successfully edited src/Calculator.cs: replaced 1 occurrence(s)");
+ var fakeClient = new FakeChatClient
+ {
+ ToolCallsOnFirstCall =
+ [
+ new FunctionCallContent("call-edit", "file_edit",
+ new Dictionary { ["Path"] = "src/Calculator.cs" })
+ ]
+ };
+ var agent = Sys.ActorOf(SubAgentActor.CreateProps(CreateDefinition([editTool]), fakeClient));
+
+ var result = await agent.Ask(
+ new RunSubAgent
+ {
+ Task = "Edit Calculator.",
+ Timeout = TimeSpan.FromSeconds(5),
+ Audience = TrustAudience.Personal,
+ ParentProjectDirectory = MissingProjectDirectory
+ },
+ TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.NotNull(result.WorkingContext);
+ Assert.Equal(
+ Path.GetFullPath(Path.Join(MissingProjectDirectory, "src", "Calculator.cs")),
+ Assert.Single(result.WorkingContext.ConfirmedChangedFiles));
+ Assert.Empty(result.WorkingContext.ObservedChangedFiles);
+ }
+
+ [Fact]
+ public async Task Denied_first_party_edit_is_not_returned_as_confirmed_child_activity()
+ {
+ var editTool = new FakeNetclawTool("file_edit", "Error: Permission denied: src/Calculator.cs");
+ var fakeClient = new FakeChatClient
+ {
+ ToolCallsOnFirstCall =
+ [
+ new FunctionCallContent("call-edit", "file_edit",
+ new Dictionary { ["Path"] = "src/Calculator.cs" })
+ ]
+ };
+ var agent = Sys.ActorOf(SubAgentActor.CreateProps(CreateDefinition([editTool]), fakeClient));
+
+ var result = await agent.Ask(
+ new RunSubAgent
+ {
+ Task = "Edit Calculator.",
+ Timeout = TimeSpan.FromSeconds(5),
+ Audience = TrustAudience.Personal,
+ ParentProjectDirectory = MissingProjectDirectory
+ },
+ TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.NotNull(result.WorkingContext);
+ Assert.Empty(result.WorkingContext.ConfirmedChangedFiles);
+ }
+
+ private static readonly string MissingProjectDirectory =
+ Path.Join(Path.GetTempPath(), "netclaw-missing-project");
+
// Real PNG: the egress normalizer decodes every model-input image, so a
// fake magic-byte stub would now be dropped. Small enough to pass through.
private static readonly byte[] FakePngBytes = TestImages.SmallPng();
diff --git a/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnObservabilityTests.cs b/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnObservabilityTests.cs
index afea66548..cd44c097e 100644
--- a/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnObservabilityTests.cs
+++ b/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnObservabilityTests.cs
@@ -4,6 +4,8 @@
//
// -----------------------------------------------------------------------
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Netclaw.Actors.Sessions;
using Netclaw.Actors.Protocol;
using Netclaw.Actors.SubAgents;
using Netclaw.Actors.Tools;
@@ -48,6 +50,7 @@ public async Task Spawner_missing_session_context_logs_lifecycle_under_session_s
toolAccessPolicy: null!,
approvalService: null,
promptProvider: null!,
+ workingContextSnapshots: new WorkingContextSnapshotProvider(NullLogger.Instance),
logger);
// A context with a session id but no SpawnChildActor factory — the
diff --git a/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnerTests.cs b/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnerTests.cs
index d04aca85a..8d52c9cfa 100644
--- a/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnerTests.cs
+++ b/src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnerTests.cs
@@ -10,6 +10,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Netclaw.Actors.Channels;
using Netclaw.Actors.SubAgents;
+using Netclaw.Actors.Sessions;
using Netclaw.Actors.Tests.Memory;
using Netclaw.Actors.Tools;
using Netclaw.Configuration;
@@ -49,6 +50,7 @@ public async Task Spawn_async_propagates_parent_resolved_cwd_on_run_message()
new ShellCommandPolicy()),
approvalService: null,
new StaticSystemPromptProvider("You are a summarizer."),
+ new WorkingContextSnapshotProvider(NullLogger.Instance),
NullLogger.Instance);
var childProbe = CreateTestProbe("subagent-child");
@@ -172,6 +174,7 @@ public async Task Spawn_async_ignores_definition_tool_metadata_for_runtime_tool_
new ShellCommandPolicy()),
approvalService: null,
new StaticSystemPromptProvider("You are a summarizer."),
+ new WorkingContextSnapshotProvider(NullLogger.Instance),
NullLogger.Instance);
var notifications = new List();
@@ -214,6 +217,57 @@ public async Task Spawn_async_ignores_definition_tool_metadata_for_runtime_tool_
Assert.Equal(1, started.ToolCount);
}
+ [Fact]
+ public async Task Spawn_async_returns_only_unconfirmed_git_changes_as_observed()
+ {
+ var projectDirectory = Path.GetFullPath(Path.Join(Path.GetTempPath(), "netclaw-spawner-context"));
+ var confirmedPath = Path.GetFullPath(Path.Join(projectDirectory, "src", "Confirmed.cs"));
+ var observedPath = Path.GetFullPath(Path.Join(projectDirectory, "src", "Observed.cs"));
+ var snapshots = new Queue(
+ [
+ new WorkingContextSnapshot
+ {
+ WorkingContext = WorkingContext.Empty.WithProjectDirectory(projectDirectory),
+ Git = GitSnapshot(projectDirectory)
+ },
+ new WorkingContextSnapshot
+ {
+ WorkingContext = WorkingContext.Empty.WithProjectDirectory(projectDirectory),
+ Git = GitSnapshot(projectDirectory, "src/Confirmed.cs", "src/Observed.cs")
+ }
+ ]);
+ var spawner = CreateSpawner(new SequenceWorkingContextSnapshotProvider(snapshots));
+ var childProbe = CreateTestProbe("working-context-child");
+ var context = new ToolExecutionContext("console/subagent-parent", "/tmp/netclaw/sessions/parent")
+ {
+ Audience = TrustAudience.Personal,
+ ProjectDirectory = projectDirectory,
+ SpawnChildActor = (_, _, _) => Task.FromResult