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

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