Agent Context Handoff

How Coding Friend agents pass structured context to each other through JSON files.

Why a handoff mechanism exists

Coding Friend workflows routinely chain multiple agents. A typical /cf-fix run looks like this:

user request → main conversation
             → cf-explorer (find relevant files, understand the bug)
             → cf-implementer (write the failing test, then the fix)
             → back to main conversation (report + auto-review)

Each arrow crosses an agent boundary. Without a structured handoff, every crossing serializes context to text and passes it through the main conversation — a lossy process. Nuanced findings ("this function has a 2-year-old TODO at line 38 related to the bug") get flattened into prose, then re-summarized into the next agent's prompt, then paraphrased again. By the time the implementer starts work, the explorer's precise knowledge has been decimated.

The context handoff mechanism solves this by giving agents a shared on-disk slot to exchange structured data directly.

The context file

A context file is a JSON document at a deterministic path:

<docsDir>/context/<task-id>.json

Where:

  • <docsDir> resolves from .coding-friend/config.json (default: docs)
  • <task-id> is generated by the orchestrating skill — format: <timestamp>-<short-descriptor>, e.g. 1717500000-fix-auth-race

Example file path: docs/context/1717500000-fix-auth-race.json.

Schema

{
  "schema_version": 1,
  "task_id": "1717500000-fix-auth-race",
  "task_summary": "API returns 500 for emails containing a '+' character",
  "relevant_files": ["src/auth/email.ts", "tests/auth/email.test.ts"],
  "key_findings": [
    "validateEmail() at src/auth/email.ts:42 has regex rejecting '+'",
    "Legacy constraint from 2022 — see TODO at line 38",
    "Existing tests cover basic cases but not '+' character"
  ],
  "constraints": [
    "Must not break existing passing tests",
    "RFC 5322 allows '+' in the local part"
  ],
  "suggested_approach": "Update regex to allow '+' per RFC 5322, add test case for user+tag@example.com",
  "previous_failure": null
}
FieldTypeWritten byConsumed byPurpose
schema_versionnumberOrchestrating skillAll agentsFuture-proofs retries across schema evolution
task_idstringOrchestrating skillAll agentsUnique identifier — avoids collisions between parallel tasks
task_summarystringcf-explorer/cf-plannercf-implementerShort description, cross-referenced against the prompt
relevant_filesstring[]cf-explorer/cf-plannercf-implementerStarting points — saves the implementer from re-discovering files
key_findingsstring[]cf-explorer/cf-plannercf-implementerConcrete observations not derivable from the diff alone
constraintsstring[]cf-explorer/cf-plannercf-implementerBoundary conditions the implementation must respect
suggested_approachstringcf-plannercf-implementerHigh-level direction when the path is already clear
previous_failureobject | nullOrchestrating skillcf-implementerOnly present on retry — carries the prior attempt's error for course-correction

Who writes, who reads, who owns lifecycle

The division of responsibility is intentional:

RoleAgents/SkillsResponsibility
Producerscf-explorer, cf-plannerWrite their findings to the context file before returning
Consumercf-implementerReads the file and treats it as primary context (ahead of the prompt)
Lifecycle ownerOrchestrating skills (cf-tdd, cf-fix, cf-plan)Generate task_id, create the file, append previous_failure on retry, delete on completion

The lifecycle owner is explicitly not the implementer agent. This is a deliberate deviation from the original design: if the implementer deleted the file after consuming it, a retry pass could not add the previous_failure metadata and re-dispatch with the same context. Skills see the whole workflow; agents only see one invocation. Ownership belongs to the party with full visibility.

How the handoff works end-to-end

Happy path

1. Skill (cf-fix) generates task-id and context-file path
2. Skill dispatches cf-explorer, passing the path
3. cf-explorer explores, writes structured findings to the file
4. cf-explorer returns a short text report mentioning the file
5. Skill dispatches cf-implementer, passing the same path
6. cf-implementer reads the file, uses it as primary context
7. cf-implementer runs RED → GREEN → REFACTOR
8. cf-implementer appends [CF-RESULT: success] to its output
9. Skill parses the sentinel, confirms success
10. Skill deletes the context file

Retry path

When cf-implementer reports [CF-RESULT: failure] <reason>, the orchestrating skill:

  1. Prints a visible retry notice to the user.
  2. Reads the existing context file.
  3. Appends a previous_failure object with the reason, a brief error summary, and attempt: 1.
  4. Re-dispatches cf-implementer with the updated file and an amended prompt.
  5. If the second attempt also fails, escalates to the user and keeps the file on disk for inspection.

Example retry metadata:

"previous_failure": {
  "reason": "tests-failed",
  "error_summary": "tests/auth/email.test.ts - 'rejects overly long emails' expected length < 254",
  "attempt": 1
}

The second-attempt implementer reads this first and is explicitly instructed to avoid repeating the same mistake.

The result signal

Implementer output ends with a result signal on its last non-empty line:

[CF-RESULT: success]

or

[CF-RESULT: failure] tests-failed
[CF-RESULT: failure] compile-error
[CF-RESULT: failure] empty-output

Orchestrating skills parse this sentinel with a strict regex match (^\[CF-RESULT: (success|failure)( .*)?\]$ on the last non-empty line). If the sentinel is missing, the skill treats the run as a failure by default — silent success is never assumed. This closes a vector where an agent truncated by a context overflow could otherwise masquerade as a successful run.

Why JSON files, not messages

The design space was evaluated against several alternatives:

OptionVerdict
JSON file on diskChosen — simple, debuggable, persistent across retries, no API dependency
Text message via main conversationRejected — this is the lossy path the mechanism is meant to replace
In-process shared memoryRejected — Claude Code agents do not share process state
MCP memory serverRejected — heavy for short-lived task state, harder to inspect
JSON embedded in the promptRejected — expensive tokens, hard to mutate on retry

Files win because they are debuggable (you can cat them mid-run), persistent (they survive process boundaries and retries), and ownership-clear (filesystem semantics force an explicit lifecycle).

Orphaned files and cleanup

Because the orchestrating skill owns file deletion, a crash or user-aborted workflow can leave stale files in <docsDir>/context/. Coding Friend mitigates this with a SessionStart hook that vacuums context files older than 7 days on session startup. Manual cleanup:

rm -rf docs/context/*.json   # if you want to wipe them all

Nothing in the project reads stale context files, so a leftover file is cosmetic debt, not a correctness issue.

Schema evolution

Context files carry a schema_version field starting at 1. When the schema changes:

  • Minor additions (new optional fields) → bump is optional; readers ignore unknown fields.
  • Breaking changes (renamed/removed fields) → bump required; readers must either migrate or reject old versions.

A retry that reads an incompatible schema_version should fail fast rather than corrupt the retry prompt. Skills treat version mismatch as a reason to delete the file and re-run exploration from scratch.