/** * `` — the attribution wrapper around a SUB-AGENT's INITIAL prompt. * * `ay send` already wraps every agent→agent message in `` * so the recipient knows who pinged it and exactly how to reply (see cmdSend in * ts/subcommands.ts). The initial prompt had no such wrapper: an agent spawned by * `ay claude -- ""` from inside another agent's Bash tool received a bare * task string with no idea that (a) it was spawned by an agent rather than a * human, (b) that agent is blocked/waiting on it, or (c) how to talk back. It * would finish, sit at an idle prompt, and nobody would ever learn. * * So the first message gets the same treatment as every later one — same * nonce-delimited XML-ish framing, same `reply:` route — plus the one thing a * later message doesn't need: an explicit REPORTING DUTY (ping the parent when * you finish, and when you're stuck). The runtime enforces that duty * independently in ts/parentPing.ts; this block is what makes the agent do it * deliberately, with content, instead of the wrapper's terse automatic ping. * * Pure + fs-free (the caller supplies the nonce) so it is trivially unit-testable, * mirroring `notifyRouter.ts` / `resultEnvelope.ts`. Mirrored in the Rust runtime * by `rs/src/init_msg.rs` — keep the two formats byte-identical. */ import { homedir } from "os"; /** The agent that spawned this one, as far as the wrapper could resolve it. */ export interface InitSpawner { cli: string; /** The spawner's agent pid (display only — pids die on restart). */ pid: number; /** * The spawner's stable agent_id, the ACTUAL reply route: it survives the * parent restarting (a pid does not). Falls back to the pid when a legacy * record carries no id. */ agentId?: string | null; cwd: string; } /** Replace a leading $HOME with `~`. Local copy of subcommands' shortenPath so * this module stays import-light (it runs on the wrapper's cold-start path). */ export function shortenHome(p: string, home = homedir()): string { if (!home || !p.startsWith(home)) return p; const rest = p.slice(home.length); if (rest !== "" && rest !== "/" && rest !== "\\") return "~" + rest; return "~"; } /** The stable id to address a reply to — agent_id when we have one, else pid. */ export function replyTargetOf(spawner: InitSpawner): string { const id = spawner.agentId?.trim(); return id ? id : String(spawner.pid); } /** * Wrap a sub-agent's initial `prompt` in ``. * * `nonce` is generated by the CALLER, after the prompt text already exists, so * text inside the body can't forge a matching open/close marker — the same * forgery guard `` relies on (nonce match, not tag syntax, is what makes * the boundary trustworthy; strict-XML validity is deliberately sacrificed). * * Returns the prompt unchanged when there is no spawner (a top-level agent * started from a human shell has nobody to report to, and the block would be * pure noise on the overwhelmingly common path). * * `identity` is the spawner's standardized identity * (`user@host:path:branch#pid`, see ts/identity.ts) — passed IN rather than * computed here so the golden fixture can pin a machine-independent value, and * so this stays a pure function of its arguments in both runtimes. */ export function buildInitMsg( prompt: string, spawner: InitSpawner | null | undefined, nonce: string, identity: string, ): string { if (!spawner) return prompt; const target = replyTargetOf(spawner); return [ ``, ``, prompt, ``, ``, `The task above was given to you by that agent — it spawned you and returned`, `immediately, so it is NOT watching your terminal and will not see anything you`, `print. Reach it by RUNNING THESE SHELL COMMANDS (in your bash/terminal tool):`, ` ay send ${target} "..." report progress, ask a question, deliver the result`, ` ay tail ${target} read what it has been doing`, ``, // Observed in the wild: a subagent under Claude Code read this block, reached // for its harness's OWN agent-messaging tool ("SendMessage"/"ListAgents"), // got "No agent named '' is reachable" — that tool only knows the // harness's own subagents, not agent-yes's registry — and gave up, reporting // nothing. Naming the collision explicitly is what stops it: the agent must // know these are shell commands and that its built-in tool cannot see us. `\`ay\` is a SHELL COMMAND — run it in your bash/terminal tool. It is NOT your`, `harness's built-in agent/message tool: that tool only knows agents your harness`, `spawned and will say "no agent named ${target}". Do not substitute it, and do`, `not conclude the parent is unreachable if it fails — use the shell.`, ``, `Reporting duty — you MUST run \`ay send ${target} "..."\` when either happens:`, ` 1. You finish the task. Send the outcome itself (what changed, what you found,`, ` files/PRs touched), not just "done" — it cannot read your transcript.`, ` 2. You are blocked or stuck: a decision only it can make, a missing`, ` credential/permission, a failing step you cannot get past. Say what you`, ` tried and what you need. Do NOT sit at an idle prompt waiting.`, `Until you do one of those, it is waiting on you.`, ``, ].join("\n"); }