Skip to content

arch: directive capture + sub-agent decision-recording bypass the governed memory classifier #1246

Closed

Description

@tamirdresher

Summary

The coordinator's directive-capture flow and the sub-agent decision-recording flow are hard-coded in squad.agent.md to write directly to .squad/decisions/inbox/{name}-{slug}.md via squad_state_write. This bypasses the LocalMemoryStore classifier — the FORBIDDEN scan, classification, audit log, and loadGuidance machinery shipped in 0.10.x never run for the most common write paths.

The recently-opened PR to bridge memory.* tools through the squad_state MCP server makes those tools REACHABLE from agents. But the instructions in squad.agent.md still tell agents to use the drop-box pattern, so the governed pipeline stays unused even with the bridge in place.

This issue asks maintainers to decide whether the canonical write path for directives and decisions should route through memory.write instead.

Evidence — what the spawn prompt says today

Coordinator directive-capture (from .github/agents/squad.agent.md, line 260):

Prefer squad_state_write to write decisions/inbox/copilot-directive-{timestamp}.md

Sub-agent decision-recording (lines 406 and 531):

Agents record decisions with squad_decide or squad_state_write to decisions/inbox/{agent-name}-{brief-slug}.md.

Both instructions hard-code the drop-box pattern. Neither mentions memory.classify or memory.write.

The governed tools DO exist (packages/squad-sdk/src/tools/index.ts, lines 814/846/1189/1190): memory.classify and memory.write are registered alongside squad_state_write (line 667) — but the spawn prompt never routes to them.

Evidence — what actually happens (verified empirically)

I installed a workaround skill at .copilot/skills/governed-memory-cli-bridge/SKILL.md that teaches agents to invoke squad memory write --class POLICY via shell for durable team policies. Then I ran a fresh squad init repo through a real prompt that requires both directive capture and sub-agent work:

"First, cast a small 3-person team. Then have the Backend dev research the project and record their findings as a durable team policy that future agents will need."

20-minute end-to-end run. Result:

Signal Value
.squad/memory/audit.jsonl entries 0
.squad/memory/index.json []
.squad/memory/local/ files 0
.squad/memory/policy-inbox/ files 0
Output mentions of squad memory <op> 0 (in 8.5KB of stdout)
Output mentions of the skill by name 0
Where Tuvok's finding actually landed .squad/decisions/inbox/tuvok-project-stack-baseline.md (3.3KB), then merged into .squad/decisions.md by Scribe — pure drop-box flow

The skill was physically present in the right directory the whole time. The coordinator AND the spawned sub-agent both followed the explicit drop-box instruction in their spawn prompt — neither path ever asked the classifier.

Why this matters

Without a change here, the governance benefits of 0.10.x's memory work — classification, audit trail, FORBIDDEN scan, loadGuidance — apply only to:

  • Manual CLI invocations by the human operator (squad memory write ... at a terminal)
  • Direct SDK consumers
  • Custom skills that explicitly call the CLI AND are explicitly invoked by name

They do NOT apply to:

  • The default directive-capture path (coordinator → decisions/inbox/)
  • The default sub-agent decision-recording path (sub-agent → decisions/inbox/)
  • Anything that follows the conventions baked into squad.agent.md

In other words: a fresh squad with v0.10.x does not exercise the governance subsystem, even though it's shipped and tested.

Three concrete options for the maintainers

Option A — Update squad.agent.md to route through memory.write

Replace both instructions:

  • Directive capture: squad_state_write → decisions/inbox/copilot-directive-{ts}.md → memory.write --class POLICY --author coordinator
  • Sub-agent decision-recording: squad_state_write → decisions/inbox/{agent}-{slug}.md → memory.write --class DECISION --author {agent}

Pre-condition: the memory_write MCP alias must ship first so agents can actually call the tool. Until then, fall back to the drop-box pattern with a TODO comment.

Pros: every directive + decision gets classified, audited, and assigned a loadGuidance automatically. Forbidden scan catches secrets and CI-status patterns before they land in decisions.md. The 0.10.x governance work becomes operational, not aspirational.

Cons: requires the MCP bridge first. Behavior change for every existing squad on the next upgrade.

Option B — Keep the drop-box as the WRITE path, run classification ASYNCHRONOUSLY in Scribe's merge step

Sub-agents continue writing to decisions/inbox/. When Scribe processes the inbox, it runs each entry through LocalMemoryStore.classify() before merging into decisions.md. Rejected entries get audit-logged + moved to a quarantine folder.

Pros: no change to spawn templates. Defense-in-depth: even if an agent leaks something forbidden into the inbox, Scribe catches it before it lands in decisions.md.

Cons: classification happens after the write. Agents don't get the dry-run / forbidden-feedback signal. The audit log shows what was classified, not what was attempted.

Option C — Hybrid: keep the drop-box for the write, but require the inbox file to carry classification metadata

Sub-agents write to the inbox AND embed a classification result (from a preceding memory.classify call) as frontmatter. Scribe trusts the embedded classification when merging; agents that omit it get rejected at merge time.

Pros: agents have to engage with the classifier (incentive to use it). Backwards compatible during transition.

Cons: complicated. Two-step protocol. Easy for agents to forget.

What's NOT being proposed

  • Removing the drop-box pattern (it's important for the Scribe-merge model)
  • Changing the state backend (squad_state_write still works the same way at the storage level)
  • Forcing every memory tool through MCP (some operations are still terminal-only by design)

Related issues + PRs

  • The PR bridging memory.* tools through squad_state MCP server — precondition for Option A
  • This issue — directive capture + sub-agent decision recording still bypass the classifier

Acceptance criteria

  • Maintainer comment picking A, B, C, or D (an option I missed)
  • If A or C: design discussion on backwards compatibility before any code change
  • If B: design discussion on what classification rejection means for an already-written inbox file
  • Whatever direction is picked, squad.agent.md is updated to match so a fresh squad init produces a squad that actually exercises the governance subsystem in normal operation

Empirical reproduction

To reproduce the finding:

# Set up clean repo with v0.10.0
mkdir test-squad; cd test-squad
git init -q
git config user.name "t"; git config user.email "t@e.com"
"# x" > README.md; git add README.md; git commit -q -m "init"
squad init

# Install the workaround skill
mkdir -Force .copilot/skills/governed-memory-cli-bridge
# (add SKILL.md teaching agents to call: squad memory write --class POLICY)

# Run a real prompt non-interactively
copilot -p "Cast a 3-agent team. Have the Backend dev research the project and record findings as a durable team policy." `
  --agent squad --allow-all-tools --no-color > run.log

# Check governance signals (all will be zero / empty)
Get-Content .squad/memory/audit.jsonl 2>$null | Measure-Object | Select-Object Count
Get-Content .squad/memory/index.json   2>$null
Get-ChildItem .squad/memory/local 2>$null | Measure-Object | Select-Object Count
Get-ChildItem .squad/memory/policy-inbox 2>$null | Measure-Object | Select-Object Count

All four checks return zero / empty. The agents wrote to .squad/decisions/inbox/ and .squad/decisions.md instead — via squad_state_write, bypassing the classifier.

Activity

  1. tamirdresher commented on Jun 9, 2026

    @tamirdresher
    CollaboratorAuthor

    Empirical re-test of the combined fix (#1246 + #1245)

    I patched a local Squad install with both proposed fixes and re-ran a clean directive-capture flow. Findings:

    What the combined fix does NOT solve

    In the typical bootstrap state (workspace-level .mcp.json only, no user-level squad_state MCP):

    Scenario MCP calls Behavior
    .github/agents/squad.agent.md unchanged 0 Agent writes via Create/Edit, forges audit.jsonl entry
    .github/agents/squad.agent.md patched to require memory_write (this issue's proposal) still 0 Same fallback. Agent reports "memory_write tool unavailable" and proceeds to fabricate an audit entry

    In other words, telling the agent "REQUIRED: call memory_write with class POLICY" via squad.agent.md is not enough when copilot CLI hasn't loaded the MCP. The agent gracefully degrades and silently invents the data.

    When the combined fix DOES work

    Once squad_state is registered at user level (copilot mcp add squad_state -- node ... state-mcp), the same agent on the same repo with the same prompt:

    • Calls squad_state_health first ✅
    • Calls memory_classify ✅
    • Calls memory_write ✅ (real provider, persistent index entry, proper audit row)
    • Calls squad_state_write ✅ (drop-box for Scribe)
    • Does not touch .squad/decisions.md or .squad/decisions/inbox/ with platform tools

    So the squad.agent.md patches are necessary for guiding tool choice, but they don't override the agent's silent-fallback behavior when the MCP simply isn't there.

    Forged-audit problem

    Even worse — when the agent thinks MCP isn't available but it sees a directive to record a POLICY, it writes raw JSON into .squad/memory/audit.jsonl via PowerShell that looks like an SDK audit entry. Example actually captured:

    // FORGED by agent (workspace-only MCP, can't call real provider)
    {"class":"POLICY","ts":"2026-06-09T17:21:18Z","summary":"Never use force-push on shared branches.","destination":".squad/decisions/inbox/...","fallback":"local-file (memory_write tool unavailable)","actor":"copilot-coordinator","op":"write"}
    
    // REAL — written by the actual memory provider via memory_write MCP
    {"timestamp":"2026-06-09T17:12:12.725Z","action":"write","id":"e6bff63d-b6f4-443b-93ea-c5db21be955b","class":"POLICY","title":"...","path":".squad/memory/policy-inbox/...md","reason":"Content is allowed for governed local memory","actor":"...","provider":"local"}

    Different field names (ts/timestamp, op/action), no id/path/reason/provider. Downstream tooling that reads audit.jsonl would see both as "POLICY writes" without realizing one is real and one is theater.

    Suggested updates to this issue's scope

    1. The drop-box-only spawn template change is correct — please keep it.
    2. Add: squad.agent.md should forbid falling back to platform writes for .squad/decisions/* and .squad/memory/* when MCP isn't reachable. The right answer is to fail loudly, not to fabricate.
    3. The companion bridge (feat(mcp): bridge governed memory tools (memory.*) through squad_state MCP server #1245) needs to ship for these instructions to actually do anything.
    4. Squad bootstrap needs to register squad_state at user level (or at least warn) — workspace-level alone doesn't load reliably in non-interactive -p mode. (Tracking separately.)

    Verified locally on Windows / Node 24.16 / @bradygaster/squad-cli v0.10.0-build.7 with both fixes applied. Reproducible across 4 clean test runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions