diff --git a/scripts/audit-seams.mjs b/scripts/audit-seams.mjs index 70d4225fcf9..3e5bbaa17c6 100644 --- a/scripts/audit-seams.mjs +++ b/scripts/audit-seams.mjs @@ -25,7 +25,9 @@ Sections: seamTestInventory High-signal seam candidates with nearby-test gap signals, including cron orchestration seams for agent handoff, outbound/media delivery, heartbeat/followup handoff, - and scheduler state crossings + and scheduler state crossings, plus subagent seams + for spawn/session handoff, announce delivery, + lifecycle registry, cleanup, and parent streaming Notes: - Output is JSON only. @@ -560,6 +562,16 @@ function isCronProductionPath(relativePath) { return relativePath.startsWith("src/cron/") && isProductionLikeFile(relativePath); } +function isSubagentProductionPath(relativePath) { + return ( + (relativePath.startsWith("src/agents/") || relativePath.startsWith("src/cron/")) && + isProductionLikeFile(relativePath) && + (/subagent|sessions-spawn|acp-spawn/.test(relativePath) || + relativePath === "src/agents/tools/sessions-spawn-tool.ts" || + relativePath === "src/agents/tools/subagents-tool.ts") + ); +} + function describeCronSeamKinds(relativePath, source) { if (!isCronProductionPath(relativePath)) { return []; @@ -662,6 +674,102 @@ function describeCronSeamKinds(relativePath, source) { return seamKinds; } +function describeSubagentSeamKinds(relativePath, source) { + if (!isSubagentProductionPath(relativePath)) { + return []; + } + + const seamKinds = []; + const isAnnounceDispatchPath = + relativePath === "src/agents/subagent-announce.ts" || + relativePath === "src/agents/subagent-announce-dispatch.ts"; + const importsSpawnRuntime = hasAnyImportSource(source, [ + "./subagent-spawn.js", + "../subagent-spawn.js", + "./acp-spawn.js", + "../acp-spawn.js", + "./subagent-registry.js", + "../subagent-registry.js", + "../acp/control-plane/manager.js", + ]); + const importsLifecycleRegistry = hasAnyImportSource(source, [ + "./subagent-registry-completion.js", + "./subagent-registry-cleanup.js", + "./subagent-registry-state.js", + "./subagent-registry.js", + "./subagent-lifecycle-events.js", + "../context-engine/init.js", + "../context-engine/registry.js", + "../sessions/session-lifecycle-events.js", + ]); + const importsAnnounceDelivery = hasAnyImportSource(source, [ + "./subagent-announce.js", + "./subagent-announce-dispatch.js", + "./subagent-announce-queue.js", + "../infra/outbound/bound-delivery-router.js", + "../utils/delivery-context.js", + "../gateway/call.js", + ]); + const importsCleanup = hasAnyImportSource(source, [ + "../gateway/call.js", + "./subagent-registry-cleanup.js", + "../acp/control-plane/spawn.js", + ]); + const importsParentStream = hasAnyImportSource(source, [ + "./acp-spawn-parent-stream.js", + "../infra/heartbeat-wake.js", + "../infra/system-events.js", + "../infra/agent-events.js", + ]); + + if ( + importsSpawnRuntime && + /\bspawnSubagentDirect\b|\bspawnAcpDirect\b|\bregisterSubagentRun\b|\bgetAcpSessionManager\b|\bspawnSubagent\b|\bspawnAcp\b/.test( + source, + ) + ) { + seamKinds.push("subagent-session-spawn"); + } + + if ( + importsLifecycleRegistry && + /\bemitSubagentEndedHookOnce\b|\bresolveDeferredCleanupDecision\b|\bpersistSubagentRunsToDisk\b|\brestoreSubagentRunsFromDisk\b|\bresolveContextEngine\b|\bemitSessionLifecycleEvent\b|\bcaptureSubagentCompletionReply\b/.test( + source, + ) + ) { + seamKinds.push("subagent-lifecycle-registry"); + } + + if ( + (importsAnnounceDelivery || isAnnounceDispatchPath) && + /\brunSubagentAnnounceFlow\b|\brunSubagentAnnounceDispatch\b|\benqueueAnnounce\b|\bcreateBoundDeliveryRouter\b|\bqueueEmbeddedPiMessage\b|\bwaitForEmbeddedPiRunEnd\b|\bqueue-fallback\b|\bdirect-primary\b/.test( + source, + ) + ) { + seamKinds.push("subagent-announce-delivery"); + } + + if ( + importsCleanup && + /\bsessions\.delete\b|\bdeleteTranscript\b|\bcleanupFailedAcpSpawn\b|\bcleanupProvisionalSession\b|\bcleanupFailedSpawnBeforeAgentStart\b|\bresolveDeferredCleanupDecision\b/.test( + source, + ) + ) { + seamKinds.push("subagent-session-cleanup"); + } + + if ( + importsParentStream && + /\bstartAcpSpawnParentStreamRelay\b|\brequestHeartbeatNow\b|\benqueueSystemEvent\b|\bonAgentEvent\b|\bstreamTo\b/.test( + source, + ) + ) { + seamKinds.push("subagent-parent-stream"); + } + + return seamKinds; +} + export function describeSeamKinds(relativePath, source) { const seamKinds = []; const isReplyDeliveryPath = @@ -702,6 +810,7 @@ export function describeSeamKinds(relativePath, source) { seamKinds.push("streaming-media-handoff"); } seamKinds.push(...describeCronSeamKinds(relativePath, source)); + seamKinds.push(...describeSubagentSeamKinds(relativePath, source)); return [...new Set(seamKinds)].toSorted(compareStrings); } @@ -836,7 +945,12 @@ export function determineSeamTestStatus(seamKinds, relatedTestMatches) { seamKinds.includes("cron-heartbeat-handoff") || seamKinds.includes("cron-scheduler-state") || seamKinds.includes("cron-media-delivery") || - seamKinds.includes("cron-followup-handoff") + seamKinds.includes("cron-followup-handoff") || + seamKinds.includes("subagent-session-spawn") || + seamKinds.includes("subagent-lifecycle-registry") || + seamKinds.includes("subagent-announce-delivery") || + seamKinds.includes("subagent-session-cleanup") || + seamKinds.includes("subagent-parent-stream") ) { return { status: "partial", diff --git a/test/scripts/audit-seams.test.ts b/test/scripts/audit-seams.test.ts index f9c79b22712..5649ef8e39b 100644 --- a/test/scripts/audit-seams.test.ts +++ b/test/scripts/audit-seams.test.ts @@ -46,45 +46,74 @@ describe("audit-seams cron seam classification", () => { expect(describeSeamKinds("src/cron/service/ops.ts", source)).toContain("cron-scheduler-state"); }); +}); - it("detects heartbeat, media, and followup handoff seams", () => { +describe("audit-seams subagent seam classification", () => { + it("detects subagent spawn and cleanup handoff boundaries", () => { const source = ` - import { stripHeartbeatToken } from "../../auto-reply/heartbeat.js"; - import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js"; - import { callGateway } from "../../gateway/call.js"; - import { waitForDescendantSubagentSummary } from "./subagent-followup.js"; + import { callGateway } from "../gateway/call.js"; + import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js"; + import { registerSubagentRun } from "./subagent-registry.js"; - export async function dispatchCronDelivery(payloads) { - const heartbeat = stripHeartbeatToken(payloads[0]?.text ?? "", { mode: "heartbeat" }); - await waitForDescendantSubagentSummary({ sessionKey: "agent:main:cron:job-1", timeoutMs: 1 }); - await callGateway({ method: "agent.wait", params: { runId: "run-1" } }); - return { heartbeat, mediaUrl: payloads[0]?.mediaUrl, sent: deliverOutboundPayloads }; + export async function spawnSubagentDirect() { + const response = await callGateway({ method: "agent.run", params: { task: "do it" } }); + registerSubagentRun({ childSessionKey: "agent:main:subagent:child" }); + await callGateway({ method: "sessions.delete", params: { key: "agent:main:subagent:child" } }); + emitSessionLifecycleEvent({ sessionKey: "agent:main:subagent:child", type: "spawned" }); + return response; } `; - expect(describeSeamKinds("src/cron/isolated-agent/delivery-dispatch.ts", source)).toEqual([ - "cron-followup-handoff", - "cron-heartbeat-handoff", - "cron-media-delivery", - "cron-outbound-delivery", + expect(describeSeamKinds("src/agents/subagent-spawn.ts", source)).toEqual([ + "subagent-lifecycle-registry", + "subagent-session-cleanup", + "subagent-session-spawn", ]); }); - it("ignores pure cron helpers without subsystem crossings", () => { + it("detects subagent lifecycle registry and announce delivery seams", () => { const source = ` - import { truncateUtf16Safe } from "../../utils.js"; + import { resolveContextEngine } from "../context-engine/registry.js"; + import { captureSubagentCompletionReply, runSubagentAnnounceFlow } from "./subagent-announce.js"; + import { emitSubagentEndedHookOnce } from "./subagent-registry-completion.js"; + import { persistSubagentRunsToDisk } from "./subagent-registry-state.js"; - export function normalizeOptionalText(raw) { - if (typeof raw !== "string") return undefined; - return truncateUtf16Safe(raw.trim(), 40); + export async function completeRun(entry) { + await resolveContextEngine({}); + await captureSubagentCompletionReply(entry.childSessionKey); + await emitSubagentEndedHookOnce({ runId: entry.runId }); + persistSubagentRunsToDisk(new Map()); + return runSubagentAnnounceFlow({ childSessionKey: entry.childSessionKey }); } `; - expect(describeSeamKinds("src/cron/service/normalize.ts", source)).toEqual([]); + expect(describeSeamKinds("src/agents/subagent-registry.ts", source)).toEqual([ + "subagent-announce-delivery", + "subagent-lifecycle-registry", + ]); + }); + + it("detects parent-stream seams for ACP spawn relays", () => { + const source = ` + import { onAgentEvent } from "../infra/agent-events.js"; + import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; + import { enqueueSystemEvent } from "../infra/system-events.js"; + + export function startAcpSpawnParentStreamRelay() { + onAgentEvent("agent-output", () => {}); + requestHeartbeatNow({ sessionKey: "agent:main" }); + enqueueSystemEvent("progress", { sessionKey: "agent:main", contextKey: "stream" }); + return { streamTo: "parent" }; + } + `; + + expect(describeSeamKinds("src/agents/acp-spawn-parent-stream.ts", source)).toEqual([ + "subagent-parent-stream", + ]); }); }); -describe("audit-seams cron status/help", () => { +describe("audit-seams status/help", () => { it("keeps cron seam statuses conservative when nearby tests exist", () => { expect( determineSeamTestStatus( @@ -98,9 +127,23 @@ describe("audit-seams cron status/help", () => { }); }); - it("documents cron seam coverage in help text", () => { + it("keeps subagent seam statuses conservative when nearby tests exist", () => { + expect( + determineSeamTestStatus( + ["subagent-session-spawn"], + [{ file: "src/agents/subagent-spawn.workspace.test.ts", matchQuality: "direct-import" }], + ), + ).toEqual({ + status: "partial", + reason: + "Nearby tests exist (best match: direct-import), but this inventory does not prove cross-layer seam coverage end to end.", + }); + }); + + it("documents cron and subagent seam coverage in help text", () => { expect(HELP_TEXT).toContain("cron orchestration seams"); - expect(HELP_TEXT).toContain("agent handoff"); - expect(HELP_TEXT).toContain("heartbeat/followup handoff"); + expect(HELP_TEXT).toContain("subagent seams"); + expect(HELP_TEXT).toContain("announce delivery"); + expect(HELP_TEXT).toContain("parent streaming"); }); });