Skip to content

[Feature]: Multi-agent isolation protocol — process-level feature context without shared-state races #4128

Description

@wittkung

Problem Statement

When multiple AI agents run Spec Kit pipelines concurrently within the same repository checkout (e.g., Antigravity subagents, Claude Code sub-agents, or parallel Cursor Composer tabs), they share a single .specify/feature.json file as their feature context pointer. This creates a write-write race condition:

Timeline:
  t0  Agent-A: SPECIFY_FEATURE_DIRECTORY="specs/003-auth" → setup-plan.sh
      → get_feature_paths() persists "specs/003-auth" to feature.json  ✓
  t1  Agent-B: SPECIFY_FEATURE_DIRECTORY="specs/004-perf" → setup-tasks.sh
      → get_feature_paths() persists "specs/004-perf" to feature.json  ← overwrites A's value
  t2  Agent-A: (new process, no env var) → check-prerequisites.sh
      → reads feature.json → resolves "specs/004-perf"  ← WRONG FEATURE

The root issue: get_feature_paths() in common.sh (L191-192) always persists SPECIFY_FEATURE_DIRECTORY back to feature.json unless the caller explicitly passes --no-persist. Since most scripts (setup-plan.sh, setup-tasks.sh) call get_feature_paths without --no-persist, every agent invocation silently overwrites the shared singleton.

Impact

  • Silent cross-contamination: Agent B's plan/tasks get written into Agent A's feature directory (or vice versa) without any error signal.
  • Non-reproducible failures: The behavior depends on timing — sometimes it works, sometimes it doesn't, making debugging extremely difficult.
  • Blocks multi-agent orchestration: Features like /speckit-implement-waves ([Feature]: /speckit-implement-waves => run each phase in a subagent to prevent context rot #3507), which propose running phases in parallel subagents, cannot work safely without solving this shared-state problem first.

Real-world reproduction

We encountered this in TTZip (a macOS archive utility, 525+ tests, 28 design patterns) while running Antigravity subagents to parallelize a sorting-bugfix TDD suite alongside a 7z compression optimization. Both agents used SPECIFY_FEATURE_DIRECTORY correctly in their own processes, but the persist-on-read side effect in get_feature_paths caused each agent to clobber the other's feature.json entry on every script call.


Root Cause Analysis

The feature resolution chain in common.sh get_feature_paths() (L163-231) has a correct read priority:

1. SPECIFY_FEATURE_DIRECTORY env var  (explicit override)
2. .specify/feature.json              (persisted fallback)
3. Error                              (no context)

But it has an unconditional write side effect on the env-var branch (L191-192):

if [[ "$no_persist" != true ]]; then
    _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
fi

The --no-persist guard (added in #3025) is a function-level parameter, not an environment-level control. Scripts that are "just resolving paths" but don't know they should pass --no-persist (like setup-plan.sh, setup-tasks.sh) trigger the persist unconditionally.

What already works

Credit to the maintainers — the infrastructure for multi-agent isolation is already in place:

Mechanism Status Issue
SPECIFY_FEATURE_DIRECTORY env var priority ✅ Working —
--no-persist read-only resolution ✅ Working #3025
CURRENT_BRANCH fallback from feature dir basename ✅ Working #3026
SPECIFY_INIT_DIR for monorepo project scoping ✅ Working —
Parser fallback chain (jq → python3 → grep/sed) ✅ Working #3304

What's missing is the guidance layer: documentation, agent skill instructions, and an environment-level no-persist toggle.


Proposed Solution

1. Official multi-agent documentation (docs/multi-agent.md)

A new document covering:

  • The race condition scenario (as above)
  • The Multi-Agent Isolation Protocol: always inject SPECIFY_FEATURE_DIRECTORY per-process, never rely on feature.json for read
  • Integration-specific examples (Antigravity subagents, Claude Code sub-agents, Cursor multi-tab, CI matrix)
  • FAQ: "Do I need git worktrees?" → No, env-var isolation is sufficient for same-checkout concurrency

2. Update agent skill templates to stop instructing direct feature.json writes

Currently, the specify command template (the upstream equivalent of speckit-specify/SKILL.md) instructs agents to:

Persist the resolved path to .specify/feature.json: {"feature_directory": "<resolved feature dir>"}

This instruction should be replaced with:

Pass the resolved feature directory to downstream commands via SPECIFY_FEATURE_DIRECTORY environment variable prefix. Example: SPECIFY_FEATURE_DIRECTORY="specs/003-auth" .specify/scripts/bash/setup-plan.sh --json

The feature.json persistence should remain as an automatic side effect of get_feature_paths() for single-agent backward compatibility, but agents should not be told to write it directly (which bypasses the script's own idempotency guards in _persist_feature_json).

3. (Optional) SPECIFY_NO_PERSIST environment variable

Add an environment-level equivalent of the --no-persist function parameter:

# In get_feature_paths(), after the --no-persist argument check (L167-171):
if [[ "${SPECIFY_NO_PERSIST:-}" == "1" || "${SPECIFY_NO_PERSIST:-}" == "true" ]]; then
    no_persist=true
fi

This allows CI pipelines and agent orchestrators to set SPECIFY_NO_PERSIST=1 globally, ensuring that no script invocation can accidentally write feature.json — even scripts that don't pass --no-persist internally.


Backward Compatibility

This proposal is fully backward compatible:

Scenario Before After
Single agent, no env var Reads feature.json Identical behavior
Single agent, with env var Reads env var, persists to feature.json Identical behavior
Multi-agent, each sets env var Race on feature.json (bug) Each agent's reads are short-circuited by env var; persistence is harmless
Multi-agent + SPECIFY_NO_PERSIST=1 N/A No feature.json writes at all
specify integration upgrade Overwrites managed files docs/multi-agent.md is not in manifest; protocol rules live in user-space

No existing scripts, templates, or workflows change behavior. The persist side effect is still there (it's a "last writer wins" overwrite that's harmless when every reader uses env vars). SPECIFY_NO_PERSIST is strictly additive.


Reference Implementation

We've been running this protocol in production at TTZip with Antigravity (Google DeepMind's agentic coding tool) subagents. Our implementation consists of:

  1. Project-level rule file (.agents/rules/speckit-multiagent.md): Instructs all agents to inject SPECIFY_FEATURE_DIRECTORY per-process and never read/write feature.json directly.
  2. Global user rule: Gates (hard state machine gating) that prevent any agent from writing production code before spec/plan/tasks artifacts exist under the declared feature directory.
  3. Concurrent verification: Validated that two agents operating on specs/003-sorting-fix/ and specs/006-7z-conquest/ simultaneously produce zero cross-contamination.

The protocol adds zero overhead to single-agent workflows and requires no upstream code changes to function — it's purely a documentation and guidance contribution. The optional SPECIFY_NO_PERSIST env var is a small, additive improvement to common.sh.


Related Issues

Component

Core scripts (common.sh), Documentation, Agent skill templates

Activity

  1. wittkung commented on Aug 14, 2026

    @wittkung
    Author

    Addendum: Why this matters beyond any single platform — universal multi-agent safety through the process environment

    I want to highlight a design property of this proposal that I think deserves explicit attention: the isolation boundary is the UNIX/Win32 process environment — the most universal, zero-cost primitive available, and this makes the protocol work across all 30+ Spec Kit integrations without any platform-specific support.

    The landscape of multi-agent isolation approaches

    Approach Isolation boundary Requires platform support? Cost Portability
    Git worktrees (#1476) Filesystem (separate directory trees) Yes — agent must know how to create/manage worktrees High — full directory duplication, node_modules/venv bloat, disk I/O Git-specific
    Remote VMs / Codespaces OS-level (separate machines) Yes — platform must provision and manage VMs Very high — network latency, cold start, infrastructure cost Platform-locked (Cursor Cloud, Codespaces)
    Platform-native sub-agents Thread/session (platform-managed context) Yes — requires platform's sub-agent API Medium — depends on platform Platform-locked (Claude Code sub-agents, Copilot agents, Antigravity subagents)
    SPECIFY_FEATURE_DIRECTORY env var (this proposal) Process environment No — works on any shell, any OS, any agent Zero — one env var prefix per command Universal — bash, zsh, PowerShell, cmd, CI runners

    The key insight: every AI coding agent, regardless of platform, eventually executes a shell command. Whether it's Claude Code running bash, Cursor invoking a terminal, Copilot Workspace executing a script, or Antigravity calling run_command — they all spawn a process. And every process has an environment.

    What this means in practice

    A project maintainer can write one rule file (like our .agents/rules/speckit-multiagent.md) that says:

    "Prefix all Spec Kit script calls with SPECIFY_FEATURE_DIRECTORY=<your-feature-dir>"

    And this single instruction works identically across:

    • Antigravity (Google) — subagents with invoke_subagent
    • Claude Code — sub-agents with /speckit.implement in parallel terminals
    • Cursor — multiple Composer tabs or background agents
    • Copilot Workspace — parallel task execution
    • Windsurf / Cline / Roo Code — concurrent sessions
    • CI/CD (GitHub Actions matrix) — parallel jobs in the same checkout (with SPECIFY_NO_PERSIST=1)

    No platform needs to add any multi-agent primitives. No worktree management. No VM provisioning. The protocol is non-invasive — it rides on infrastructure that already exists everywhere.

    Contrast with branch-based approaches

    Many multi-agent systems default to "one branch per agent" for isolation. This introduces significant complexity:

    1. Branch conflicts: Two agents touching the same file on different branches create merge conflicts that require human resolution
    2. Branch management overhead: Creating, switching, merging, and cleaning up branches adds ceremony that dwarfs the actual work
    3. Platform coupling: Branch-based isolation requires the platform to implement branch lifecycle management — not all do
    4. Spec Kit's design already decoupled from branches: The SPECIFY_FEATURE_DIRECTORY → feature.json → error resolution chain ([Bug]: get_feature_paths persists .specify/feature.json during read-only path resolution (check-prerequisites.sh --paths-only) #3025, [Bug]: get_feature_paths emits empty CURRENT_BRANCH when SPECIFY_FEATURE unset, despite feature context via SPECIFY_FEATURE_DIRECTORY/feature.json #3026) was explicitly designed to work without git branch context. Our proposal simply documents this capability and makes agents aware of it.

    The elegance of the env-var approach is that multiple agents can work on different features within the same branch (e.g., main), because the isolation is at the feature-directory level, not the branch level. Each agent writes to its own specs/<NNN-feature>/ directory tree. There's no merge conflict because there's no branch divergence.

    Summary

    This proposal turns an implicit capability of Spec Kit's existing architecture into an explicit, documented, cross-platform protocol. The upstream cost is near-zero (docs + template wording change + optional 3-line env var). The downstream value is that every Spec Kit user, on any platform, gets multi-agent safety without waiting for their specific platform to ship isolation features.

  2. mnriem commented on Aug 17, 2026

    @mnriem
    Collaborator

    I can support a SPECIFY_FEATURE_NO_PERSIST for this use case

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