Skip to content

fix(#55): generalize fleet context dual-injection fix across backends#56

Merged
suzuke merged 1 commit into
mainfrom
fix/55-fleet-context-dual-injection
Apr 27, 2026
Merged

fix(#55): generalize fleet context dual-injection fix across backends#56
suzuke merged 1 commit into
mainfrom
fix/55-fleet-context-dual-injection

Conversation

@suzuke

@suzuke suzuke commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Closes #55.

Summary

Before this change every backend received the same fleet context (identity, role, workflow, decisions, custom prompt) twice: once via the backend's native injection (--append-system-prompt-file for Claude/OpenCode, workspace project doc for Gemini/Codex/Kiro) and once via the MCP instructions capability emitted by mcp-server.ts. Wastes tokens, dilutes attention. Issue #55 originally scoped this to Gemini, triage extended it to all backends — same pattern everywhere.

Approach

A single declarative flag on each backend says how it natively delivers fleet instructions; the daemon uses it to decide whether the MCP instructions capability still fires.

  • CliBackend gains nativeInstructionsMechanism: 'append-flag' | 'project-doc' | 'none'.
    • claude-code, opencode'append-flag' (file outside workspace, loaded via flag/config).
    • gemini-cli, codex, kiro-cli'project-doc' (workspace markdown).
    • mock'none'.
  • Daemon.buildBackendConfig: when the flag is non-none, drop the fleet-context env vars (AGEND_DISPLAY_NAME, AGEND_DESCRIPTION, AGEND_WORKFLOW, AGEND_CUSTOM_PROMPT, AGEND_DECISIONS) from the MCP server's env, set AGEND_DISABLE_MCP_INSTRUCTIONS=1.
  • mcp-server.ts: when AGEND_DISABLE_MCP_INSTRUCTIONS=1, omit the instructions capability entirely. Fallback path (capability active) preserved for 'none' backends.
  • The backend's writeConfig() always receives the assembled instructions string — fleet context still reaches the model, through one channel instead of two.

Claude is included even though its native channel is a private instance-dir file (not a workspace doc): pre-fix it still received the same content twice (via --append-system-prompt-file AND via the MCP instructions capability), so the gate covers it uniformly. The "no workspace artefact" property only changed the symptom (no surprising CLAUDE.md on disk), not the duplication.

AGEND_DECISIONS was the second silent dual-injection source: passed both to the daemon-side buildFleetInstructions() (embedded into the project doc) and re-passed through mcpEnv for the MCP server to rebuild instructions. Now gated together.

Risks considered

  • Migration: all writeConfig() paths are idempotent (appendWithMarker for project-doc backends, plain overwrite for the others). MCP server is respawned on every CLI launch, so capability shape updates automatically. No migration code needed.
  • Resume invalidation: instructionsReloadedOnResume and prev-instructions force-new-session logic are unaffected — this changes the delivery channel, not the instructions text. Existing sessions continue with the same content.
  • CLI mode (agent_mode: 'cli'): does not spawn an MCP server, so the gate has no effect there. Verified the branch leaves isCliMode paths untouched.
  • Cross-backend MCP instructions surface behaviour: not relied upon — every real backend already injects natively. docs/fleet-instructions-injection.md documents how to verify a new backend before promoting it to 'none'.
  • Reviewer Contract §3.5.8 (cross-backend claim): the fix targets every CLI backend the daemon ships. Claim is backed by per-backend test evidence (see Test plan below).

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — 62 files, 517 tests pass (no regressions)
  • tests/backend/native-instructions-mechanism.test.ts — flag value asserted for all 6 backends
  • tests/daemon-build-backend-config.test.tsbuildBackendConfig exercised against all 6 backends, asserts:
    • Identity/operational env vars (AGEND_INSTANCE_NAME, AGEND_WORKING_DIR, AGEND_SOCKET_PATH) always present
    • cfg.instructions always populated (so native injection still works)
    • 'none' backend → all fleet context env vars present, no AGEND_DISABLE_MCP_INSTRUCTIONS
    • 'append-flag' / 'project-doc' backends → AGEND_DISABLE_MCP_INSTRUCTIONS=1, no fleet context env vars
  • Manual smoke (post-merge if dev-lead wants): start one Gemini and one Codex instance against this build, confirm the agent identifies itself once (not twice) when asked "what's your role?"

Files

  • src/backend/types.ts — interface field + JSDoc
  • src/backend/{claude-code,opencode,gemini-cli,codex,kiro,mock}.ts — flag value
  • src/daemon.tsbuildBackendConfig gate
  • src/channel/mcp-server.ts — capability gate
  • tests/backend/native-instructions-mechanism.test.ts — new
  • tests/daemon-build-backend-config.test.ts — new
  • docs/fleet-instructions-injection.md — new

[Edit 2026-04-27]: corrected wording in the "Approach" section per ts-reviewer's nit — pre-fix Claude was doubly-injected too (MCP instructions + --append-system-prompt-file); only the symptom (no workspace artefact) was different. No code change.

🤖 Generated with Claude Code

Before this change every non-Claude backend received the same fleet
context (identity, role, workflow, decisions, custom prompt) twice:
once via the workspace project doc the CLI auto-loads (GEMINI.md,
AGENTS.md, .kiro/steering/) and once via the MCP `instructions`
capability emitted by mcp-server.ts. This wasted tokens and diluted
attention. Issue #55 originally scoped this to Gemini, but the same
pattern affects Codex, Kiro, and (latently) OpenCode.

Approach: single declarative flag on each backend says how it natively
injects fleet instructions; the daemon uses it to decide whether the
MCP `instructions` capability still fires.

- Add `nativeInstructionsMechanism: 'append-flag' | 'project-doc' |
  'none'` to `CliBackend`. Tag claude-code/opencode as `append-flag`,
  gemini-cli/codex/kiro-cli as `project-doc`, mock as `none`.
- `Daemon.buildBackendConfig`: when the flag is non-`none`, drop the
  fleet-context env vars (`AGEND_DISPLAY_NAME`, `AGEND_DESCRIPTION`,
  `AGEND_WORKFLOW`, `AGEND_CUSTOM_PROMPT`, `AGEND_DECISIONS`) from the
  MCP server's env and set `AGEND_DISABLE_MCP_INSTRUCTIONS=1`.
- `mcp-server.ts`: omit the `instructions` capability when
  `AGEND_DISABLE_MCP_INSTRUCTIONS=1` is set. Fallback path
  (capability active) preserved for backends with no native injection.
- The backend's `writeConfig()` always receives the assembled
  instructions string for native injection, so identity/workflow/etc.
  still reach the model — just through one channel, not two.

Notes:
- Claude was never doubly-injected (it uses an instance-dir file via
  `--append-system-prompt-file`, not a workspace doc) but we still tag
  it explicitly so the rule is uniform.
- `AGEND_DECISIONS` was the second silent dual-injection source: it
  was both passed to the daemon-side `buildFleetInstructions()` (which
  embeds decisions in the project doc) and re-passed through mcpEnv to
  rebuild instructions on the MCP side. Now gated together with the
  rest.
- All existing `writeConfig()` paths are idempotent (`appendWithMarker`
  for project-doc backends, plain overwrite for the others), so
  upgrading existing instances does not require a migration. The MCP
  server is respawned on every CLI launch, so it picks up the new
  capability shape automatically.
- `instructionsReloadedOnResume` and the daemon's force-new-session
  detection are unaffected: this fix changes only the *delivery
  channel*, not the instructions text content.

Tests:
- New `tests/backend/native-instructions-mechanism.test.ts` asserts the
  flag value on every backend.
- New `tests/daemon-build-backend-config.test.ts` exercises
  `buildBackendConfig` against all 6 backends and verifies fleet context
  env vars / `AGEND_DISABLE_MCP_INSTRUCTIONS` follow the gate.
- `npx tsc --noEmit` clean. `npx vitest run` 62 files / 517 tests pass.

Docs:
- New `docs/fleet-instructions-injection.md` explains per-backend
  delivery channels and how to verify a new backend.

Closes #55.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@suzuke suzuke merged commit f58ad9d into main Apr 27, 2026
2 checks passed
@suzuke suzuke deleted the fix/55-fleet-context-dual-injection branch April 27, 2026 02:55
suzuke added a commit that referenced this pull request Apr 27, 2026
Sprint 0 outcome: 0 open issues, 0 open PRs. Three PRs shipped
(#56 #55 fix; #57 #24 fix; #58 #24 follow-up broadcast surface),
four issues closed-with-migration to agend-terminal (#52 #53 #54 #8).

Adds:
- docs/ts-team-sprint-0-2026-04-27-wrap.md — sprint outcome, fix
  details, process notes, next-sprint pointer
- docs/ts-team-backlog.md — non-blocking gaps surfaced during
  deprecated-mode maintenance. B-001 = session-routing edge case
  in cost-guard gate (deferred from PR #57 review)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate Fleet Context Injection in Gemini Backend (gemini.md vs MCP server instructions)

1 participant