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
}
| Field | Type | Written by | Consumed by | Purpose |
|---|---|---|---|---|
schema_version | number | Orchestrating skill | All agents | Future-proofs retries across schema evolution |
task_id | string | Orchestrating skill | All agents | Unique identifier — avoids collisions between parallel tasks |
task_summary | string | cf-explorer/cf-planner | cf-implementer | Short description, cross-referenced against the prompt |
relevant_files | string[] | cf-explorer/cf-planner | cf-implementer | Starting points — saves the implementer from re-discovering files |
key_findings | string[] | cf-explorer/cf-planner | cf-implementer | Concrete observations not derivable from the diff alone |
constraints | string[] | cf-explorer/cf-planner | cf-implementer | Boundary conditions the implementation must respect |
suggested_approach | string | cf-planner | cf-implementer | High-level direction when the path is already clear |
previous_failure | object | null | Orchestrating skill | cf-implementer | Only 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:
| Role | Agents/Skills | Responsibility |
|---|---|---|
| Producers | cf-explorer, cf-planner | Write their findings to the context file before returning |
| Consumer | cf-implementer | Reads the file and treats it as primary context (ahead of the prompt) |
| Lifecycle owner | Orchestrating 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:
- Prints a visible retry notice to the user.
- Reads the existing context file.
- Appends a
previous_failureobject with the reason, a brief error summary, andattempt: 1. - Re-dispatches
cf-implementerwith the updated file and an amended prompt. - 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:
| Option | Verdict |
|---|---|
| JSON file on disk ✅ | Chosen — simple, debuggable, persistent across retries, no API dependency |
| Text message via main conversation | Rejected — this is the lossy path the mechanism is meant to replace |
| In-process shared memory | Rejected — Claude Code agents do not share process state |
| MCP memory server | Rejected — heavy for short-lived task state, harder to inspect |
| JSON embedded in the prompt | Rejected — 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.
Related
cf-explorer— producercf-planner— producercf-implementer— consumercf-tdd,cf-fix,cf-plan— lifecycle owners