(prompt: string)
| 287 | * a deliberate, recent act naming a kild that existed at the time. When the two disagree, the |
| 288 | * deliberate act is the truer one. |
| 289 | * |
| 290 | * It is also the only ordering that survives the trap this was found in. A harness's settings |
| 291 | * file can carry a kild id, that file is re-read on every start including resumes, and it |
| 292 | * outranks anything a shell exports — so a stale entry pointing at an ARCHIVED kild is |
| 293 | * invisible, unkillable from the shell, and drains nothing forever while reporting success. |
| 294 | * Ranking it below an actual attach means a real attach always wins, and the stale value can |
| 295 | * only ever be a fallback for a session that never attached at all. |
| 296 | * |
| 297 | * Pinning up front still works, which was the point of keeping the environment at all. Fields |
| 298 | * resolve independently, so a hook that passes `--as` and lets the id resolve works too. |
| 299 | */ |
| 300 | async function resolveAttachment( |
| 301 | id: string | undefined, |
| 302 | handle: string | undefined, |
| 303 | ): Promise<{ kildId?: string; handle?: string }> { |
| 304 | if (id && handle) return { kildId: id, handle }; |
| 305 | const session = harnessSession(); |
| 306 | // Deliberately NOT guarded. An unreadable record means "there may be an attachment and I |
| 307 | // cannot read it", and a `kild send` that swallowed that would post the message with no |
| 308 | // credential — silently unattributed, which is the exact bug this file exists to fix. The |
| 309 | // one caller allowed to degrade to silence is the turn-end hook, and it does so at its own |
| 310 | // call site where it knows it is a hook. |
| 311 | const record = session ? await findAttachment(session) : null; |
| 312 | return { |
| 313 | kildId: id ?? record?.kildId ?? process.env.KILD_KILD_ID ?? undefined, |
| 314 | handle: handle ?? record?.handle ?? process.env.KILD_HANDLE ?? undefined, |
| 315 | }; |
| 316 | } |
| 317 | |
| 318 | /** `kild attach <id> --as <handle>` — register an ATTACHED agent: a harness kild |
| 319 | * does not own (the Claude Code session you are driving) claiming a `@handle` in the |
| 320 | * kild. Nothing is spawned; the handle is addressable immediately. Idempotent. |
| 321 | * |
| 322 | * The attachment is also recorded against this session's own id, which is what lets the |
| 323 | * session find its handle again later without carrying it in the environment. That is the |
| 324 | * whole of what makes attaching mid-session work: a process's environment is fixed at exec |
| 325 | * time, so a session opened before the kild existed can never be told about it any other |
| 326 | * way. The token is deliberately NOT recorded — it dies with the engine, so a copy on disk |
| 327 | * would go stale and be rejected; `send` mints a fresh one from the idempotent attach. */ |
| 328 | async function kildAttach(id: string | undefined): Promise<void> { |
| 329 | const handle = values.as; |
| 330 | if (!id || !handle) throw new Error('usage: kild attach <id> --as <handle>'); |
| 331 | const result = await attachAgent(id, handle); |
| 332 | const session = harnessSession(); |
| 333 | if (session) await recordAttachment(session, id, handle); |
| 334 | console.log(json ? JSON.stringify(result, null, 2) : result.message); |
| 335 | } |
| 336 | |
| 337 | /** |
| 338 | * `kild inbox <id> --as <handle> [--format claude-stop]` — destructively read that |
| 339 | * agent's inbox. An empty drain is also its idle signal; there is no status verb. |
| 340 | * |
| 341 | * With `--format claude-stop` this IS a Claude Code Stop hook's entire body, so it obeys |
| 342 | * the hook contract absolutely: print the block JSON when mail is waiting, print **nothing |
| 343 | * at all** when it is not, and **exit 0 either way** — including when the engine is down, |
| 344 | * the kild is gone, or the handle was never attached. A hook that cannot reach kild must |
| 345 | * never stop the operator from finishing a turn. Without the flag it is an ordinary CLI |
| 346 | * verb and failures are ordinary loud errors. |
no test coverage detected