{"version":3,"file":"parallel-handoff.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/parallel-handoff.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEX,uBAAuB,EACvB,wBAAwB,EACxB,oBAAoB,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG/G,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,oBAAoB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAyDD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAC3B,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,wBAAwB,CA2E3B;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAC3B,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,aAAa,CAAC;CACrB,GAAG,wBAAwB,CAE3B;AAED,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,wBAAwB,GAAG,MAAM,CAE1F;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjE;AAED,wBAAgB,yBAAyB,CACxC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,OAAO,CAAC,qBAAqB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC,eAAe,CAAC,GACjF;IAAE,QAAQ,EAAE,uBAAuB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAsErD","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { writeAtomicJson } from \"../../shared/atomic-json.ts\";\nimport type {\n\tParallelHandoffGroup,\n\tParallelHandoffManifest,\n\tParallelHandoffReference,\n\tSubagentResultStatus,\n} from \"../../shared/types.ts\";\nimport type { WorktreeCleanupIntent, WorktreeCleanupReport, WorktreeDiff, WorktreeSetup } from \"./worktree.ts\";\nimport { cleanupWorktrees } from \"./worktree.ts\";\n\nexport interface ParallelHandoffResult {\n\tagent: string;\n\tstatus: SubagentResultStatus;\n\tsummary: string;\n\toutputPath?: string;\n\tstructuredOutput?: unknown;\n\tstructuredOutputPath?: string;\n\tsessionPath?: string;\n}\n\nfunction readManifest(manifestPath: string): ParallelHandoffManifest | undefined {\n\tif (!fs.existsSync(manifestPath)) return undefined;\n\tconst parsed = JSON.parse(fs.readFileSync(manifestPath, \"utf-8\")) as ParallelHandoffManifest;\n\tif (parsed.version !== 1 || !Array.isArray(parsed.groups)) {\n\t\tthrow new Error(`Invalid parallel handoff manifest: ${manifestPath}`);\n\t}\n\treturn parsed;\n}\n\nfunction referenceFor(manifestPath: string, manifest: ParallelHandoffManifest): ParallelHandoffReference {\n\tconst children = manifest.groups.flatMap((group) => group.children);\n\treturn {\n\t\tversion: 1,\n\t\tpath: manifestPath,\n\t\tgroupCount: manifest.groups.length,\n\t\tchildCount: children.length,\n\t\tchangedPatches: children.filter((child) => child.patch.changed).length,\n\t\tcleanupState: manifest.groups.every((group) => group.cleanup.state === \"complete\") ? \"complete\" : \"partial\",\n\t};\n}\n\nfunction safeHandoffAgentName(agent: string): string {\n\treturn agent.replace(/[^\\w.-]/g, \"_\");\n}\n\nfunction missingDiff(input: {\n\tmanifestPath: string;\n\tstepIndex: number;\n\ttaskIndex: number;\n\tagent: string;\n\tbranch?: string;\n}): WorktreeDiff {\n\tconst patchPath = path.join(\n\t\tpath.dirname(input.manifestPath),\n\t\t`missing-diff-step-${input.stepIndex}-task-${input.taskIndex}-${safeHandoffAgentName(input.agent)}.patch`,\n\t);\n\ttry {\n\t\tfs.mkdirSync(path.dirname(patchPath), { recursive: true });\n\t\tfs.writeFileSync(patchPath, \"\", \"utf-8\");\n\t} catch {\n\t\t// Handoff records the artifact failure below; patch creation remains best-effort.\n\t}\n\treturn {\n\t\tindex: input.taskIndex,\n\t\tagent: input.agent,\n\t\tbranch: input.branch ?? \"\",\n\t\tdiffStat: \"\",\n\t\tfilesChanged: 0,\n\t\tinsertions: 0,\n\t\tdeletions: 0,\n\t\tpatchPath,\n\t\terror: \"diff artifact unavailable; no patch was captured\",\n\t};\n}\n\nexport function writeParallelHandoffGroup(input: {\n\tmanifestPath: string;\n\trunId: string;\n\tmode: \"parallel\" | \"chain\";\n\tsource: \"foreground\" | \"async\";\n\tcwd: string;\n\tstepIndex: number;\n\tflatStartIndex: number;\n\tsetup: WorktreeSetup;\n\tdiffs: WorktreeDiff[];\n\tcleanup?: WorktreeCleanupReport;\n\tresults: ParallelHandoffResult[];\n\tnow?: number;\n}): ParallelHandoffReference {\n\tconst now = input.now ?? Date.now();\n\tconst existing = readManifest(input.manifestPath);\n\tif (\n\t\texisting &&\n\t\t(existing.runId !== input.runId || existing.mode !== input.mode || existing.source !== input.source)\n\t) {\n\t\tthrow new Error(`Parallel handoff manifest belongs to a different run: ${input.manifestPath}`);\n\t}\n\tconst group: ParallelHandoffGroup = {\n\t\tstepIndex: input.stepIndex,\n\t\tbaseCommit: input.setup.baseCommit,\n\t\trepoRoot: input.setup.cwd,\n\t\tchildren: input.results.map((result, taskIndex) => {\n\t\t\tconst diff =\n\t\t\t\tinput.diffs[taskIndex] ??\n\t\t\t\tmissingDiff({\n\t\t\t\t\tmanifestPath: input.manifestPath,\n\t\t\t\t\tstepIndex: input.stepIndex,\n\t\t\t\t\ttaskIndex,\n\t\t\t\t\tagent: result.agent,\n\t\t\t\t\tbranch: input.setup.worktrees[taskIndex]?.branch,\n\t\t\t\t});\n\t\t\treturn {\n\t\t\t\tindex: input.flatStartIndex + taskIndex,\n\t\t\t\ttaskIndex,\n\t\t\t\tagent: result.agent,\n\t\t\t\tstatus: result.status,\n\t\t\t\tsummary: result.summary,\n\t\t\t\t...(result.outputPath ? { outputPath: result.outputPath } : {}),\n\t\t\t\t...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),\n\t\t\t\t...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),\n\t\t\t\t...(result.sessionPath ? { sessionPath: result.sessionPath } : {}),\n\t\t\t\tpatch: {\n\t\t\t\t\tpath: diff.patchPath,\n\t\t\t\t\tbranch: diff.branch,\n\t\t\t\t\tchanged:\n\t\t\t\t\t\tdiff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0,\n\t\t\t\t\tdiffStat: diff.diffStat,\n\t\t\t\t\tfilesChanged: diff.filesChanged,\n\t\t\t\t\tinsertions: diff.insertions,\n\t\t\t\t\tdeletions: diff.deletions,\n\t\t\t\t\t...(diff.error ? { error: diff.error } : {}),\n\t\t\t\t},\n\t\t\t};\n\t\t}),\n\t\tcleanup: input.cleanup ?? {\n\t\t\tstate: \"partial\",\n\t\t\tpruned: false,\n\t\t\ttasks: input.setup.worktrees.map((worktree) => ({\n\t\t\t\tindex: worktree.index,\n\t\t\t\tpath: worktree.path,\n\t\t\t\tbranch: worktree.branch,\n\t\t\t\tworktreeRemoved: false,\n\t\t\t\tbranchRemoved: false,\n\t\t\t\tpreserved: true,\n\t\t\t\treason: \"cleanup pending durable handoff capture\",\n\t\t\t})),\n\t\t},\n\t};\n\tconst groups = existing?.groups.filter((candidate) => candidate.stepIndex !== input.stepIndex) ?? [];\n\tgroups.push(group);\n\tgroups.sort((left, right) => left.stepIndex - right.stepIndex);\n\tconst manifest: ParallelHandoffManifest = {\n\t\tversion: 1,\n\t\trunId: input.runId,\n\t\tmode: input.mode,\n\t\tsource: input.source,\n\t\tcwd: input.cwd,\n\t\tcreatedAt: existing?.createdAt ?? now,\n\t\tupdatedAt: now,\n\t\tgroups,\n\t};\n\twriteAtomicJson(input.manifestPath, manifest);\n\treturn referenceFor(input.manifestPath, manifest);\n}\n\nexport function parallelHandoffPath(baseDir: string, runId?: string): string {\n\treturn runId ? path.join(baseDir, \"handoffs\", `${runId}.json`) : path.join(baseDir, \"handoff.json\");\n}\n\nexport function writePendingParallelHandoff(input: {\n\tmanifestPath: string;\n\trunId: string;\n\tmode: \"parallel\" | \"chain\";\n\tsource: \"foreground\" | \"async\";\n\tcwd: string;\n\tstepIndex: number;\n\tflatStartIndex: number;\n\tsetup: WorktreeSetup;\n}): ParallelHandoffReference {\n\treturn writeParallelHandoffGroup({ ...input, diffs: [], results: [] });\n}\n\nexport function formatParallelHandoffReference(reference: ParallelHandoffReference): string {\n\treturn `Parallel handoff: ${reference.path} (${reference.childCount} children, ${reference.changedPatches} changed patches, cleanup ${reference.cleanupState})`;\n}\n\nexport function formatParallelHandoffError(error: unknown): string {\n\treturn `Parallel handoff unavailable: ${error instanceof Error ? error.message : String(error)}`;\n}\n\nexport function discardPreservedWorktrees(\n\tmanifestPath: string,\n\tauthorization: Extract<WorktreeCleanupIntent, { kind: \"discard\" }>[\"authorization\"],\n): { manifest: ParallelHandoffManifest; text: string } {\n\tconst resolvedPath = path.resolve(manifestPath);\n\tconst manifest = readManifest(resolvedPath);\n\tif (!manifest) throw new Error(`Parallel handoff manifest not found: ${resolvedPath}`);\n\tlet attempted = 0;\n\tfor (const group of manifest.groups) {\n\t\tconst pending = group.cleanup.tasks.filter(\n\t\t\t(task) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved),\n\t\t);\n\t\tif (pending.length === 0) continue;\n\t\tattempted += pending.length;\n\t\tconst report = cleanupWorktrees(\n\t\t\t{\n\t\t\t\tcwd: group.repoRoot,\n\t\t\t\tbaseCommit: group.baseCommit,\n\t\t\t\tworktrees: pending.map((task) => ({\n\t\t\t\t\tpath: task.path,\n\t\t\t\t\tagentCwd: task.path,\n\t\t\t\t\tbranch: task.branch,\n\t\t\t\t\tindex: task.index,\n\t\t\t\t\tnodeModulesLinked: false,\n\t\t\t\t\tsyntheticPaths: [],\n\t\t\t\t})),\n\t\t\t},\n\t\t\t{ kind: \"discard\", authorization },\n\t\t);\n\t\tconst updates = new Map(\n\t\t\treport.tasks.map((task) => [\n\t\t\t\ttask.index,\n\t\t\t\ttask.worktreeRemoved && task.branchRemoved\n\t\t\t\t\t? task\n\t\t\t\t\t: { ...task, preserved: true, reason: task.reason ?? \"discard cleanup remains incomplete\" },\n\t\t\t]),\n\t\t);\n\t\tgroup.cleanup = {\n\t\t\tstate:\n\t\t\t\tgroup.cleanup.tasks.every((task) => {\n\t\t\t\t\tconst next = updates.get(task.index) ?? task;\n\t\t\t\t\treturn next.worktreeRemoved && next.branchRemoved;\n\t\t\t\t}) && report.pruned\n\t\t\t\t\t? \"complete\"\n\t\t\t\t\t: \"partial\",\n\t\t\ttasks: group.cleanup.tasks.map((task) => updates.get(task.index) ?? task),\n\t\t\tpruned: report.pruned,\n\t\t\t...(report.errors ? { errors: report.errors } : {}),\n\t\t};\n\t}\n\tmanifest.updatedAt = Date.now();\n\twriteAtomicJson(resolvedPath, manifest);\n\tconst remaining = manifest.groups\n\t\t.flatMap((group) => group.cleanup.tasks.map((task) => ({ group, task })))\n\t\t.filter(({ task }) => task.preserved && (!task.worktreeRemoved || !task.branchRemoved));\n\tconst lines =\n\t\tattempted === 0\n\t\t\t? [`No preserved worktrees remain in ${resolvedPath}.`]\n\t\t\t: [\n\t\t\t\t\t`Discard processed ${attempted} preserved worktree${attempted === 1 ? \"\" : \"s\"}.`,\n\t\t\t\t\t`Manifest: ${resolvedPath}`,\n\t\t\t\t];\n\tif (remaining.length > 0) {\n\t\tlines.push(\"\", \"Some worktrees remain. Inspect and remove them manually if appropriate:\");\n\t\tfor (const { group, task } of remaining) {\n\t\t\tlines.push(\n\t\t\t\t`  git -C ${JSON.stringify(group.repoRoot)} status --short`,\n\t\t\t\t`  git -C ${JSON.stringify(group.repoRoot)} worktree remove --force ${JSON.stringify(task.path)}`,\n\t\t\t\t`  git -C ${JSON.stringify(group.repoRoot)} branch -D ${JSON.stringify(task.branch)}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn { manifest, text: lines.join(\"\\n\") };\n}\n"]}