{"version":3,"file":"research-plan.d.ts","sourceRoot":"","sources":["../../src/evolve/research-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAM7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAmB,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,KAAK,EAKX,qBAAqB,EACrB,YAAY,EACZ,MAAM,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA0N/D,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAgHrF;AAED,MAAM,WAAW,+BAA+B;IAC/C,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC3C,IAAI,EAAE,qBAAqB,CAAC;IAC5B,GAAG,EAAE,cAAc,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;CACpB;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE;IAC3D,KAAK,EAAE,QAAQ,CAAC;IAChB,GAAG,EAAE,YAAY,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC,CAehE;AAqBD,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,+BAA+B,GACtC,OAAO,CAAC,2BAA2B,CAAC,CA4FtC","sourcesContent":["import { join } from \"node:path\";\nimport type { ThinkingLevel } from \"@ch1nyzzz/pi-agent-core\";\nimport { StringEnum } from \"@ch1nyzzz/pi-ai\";\nimport { Type } from \"typebox\";\nimport { isTrialMetricName, parseTrialDurationDays, TRIAL_METRIC_DIRECTIONS } from \"../comparison.ts\";\nimport { createDefaultEvoAbiRegistry } from \"../components/registry.ts\";\nimport { readInboxEntry } from \"../inbox.ts\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport type { EvidenceCorpus } from \"../reflect/evidence.ts\";\nimport type { ModelRunner, ModelRunRequest, ModelRunResult } from \"../reflect/model-runner.ts\";\nimport { recordModelUsage } from \"../reflect/usage.ts\";\nimport { atomicWriteFile, atomicWriteJson, canonicalJson, sha256 } from \"../storage.ts\";\nimport type {\n\tEvoCheckProfile,\n\tEvoEvidenceStrategy,\n\tEvoExperimentSpec,\n\tEvolutionInboxDecision,\n\tEvolutionResearchPlan,\n\tEvolutionRun,\n} from \"../types.ts\";\nimport {\n\tdeclarableProfiles,\n\ttype EvolutionCandidateKind,\n\tHISTORICAL_PROFILE_CAPABILITIES,\n\tOFFLINE_PROFILE_CAPABILITIES,\n} from \"./check-profiles.ts\";\nimport { readEvolutionWorkflow } from \"./config.ts\";\nimport type { MaterializedCorpus } from \"./research-corpus.ts\";\nimport { createEvolutionResearchTools } from \"./research-tools.ts\";\nimport { evolutionRunDirectory, updateEvolutionRun } from \"./run.ts\";\n\nconst CHECK_PROFILES = new Set<EvoCheckProfile>([\n\t\"bundle-compile\",\n\t\"repo-check\",\n\t\"related-tests\",\n\t\"paired-replay\",\n\t\"session-comparison\",\n\t\"compaction-replay\",\n]);\n\nfunction asRecord(value: unknown, label: string): Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value))\n\t\tthrow new Error(`${label} must be an object`);\n\treturn value as Record<string, unknown>;\n}\n\nfunction string(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !value.trim()) throw new Error(`${label} must be a non-empty string`);\n\treturn value;\n}\n\nfunction stringArray(value: unknown, label: string): string[] {\n\tif (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || !entry.trim())) {\n\t\tthrow new Error(`${label} must be a non-empty string array`);\n\t}\n\treturn [...new Set(value as string[])];\n}\n\nconst PATCH_CLASSES = new Set([\"pure-transform\", \"component\", \"routing\", \"prompt\", \"tool\", \"infrastructure\"]);\n\n// Shape-level contract for the submission tool; cross-field semantics live in\n// parseEvolutionResearchPlanValue, whose errors flow back to the model as tool errors.\nconst RESEARCH_PLAN_PARAMETERS = Type.Object({\n\ttopic: Type.String({ minLength: 1 }),\n\treason: Type.String({ minLength: 1 }),\n\tplanMarkdown: Type.String({ minLength: 1 }),\n\texperiment: Type.Object({}, { additionalProperties: true }),\n\ttargetAbi: Type.Optional(Type.String({ minLength: 1 })),\n\trequiresNewAbi: Type.Boolean(),\n\tcandidateKind: StringEnum([\"none\", \"data\", \"component\", \"code\"] as const),\n\tbuilderInstructions: Type.String({ minLength: 1 }),\n\tinboxDecisions: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }))),\n});\n\nfunction profileArray(value: unknown, label: string, allowed: ReadonlySet<EvoCheckProfile>): EvoCheckProfile[] {\n\tconst profiles = stringArray(value, label);\n\tif (profiles.length === 0) throw new Error(`${label} must not be empty`);\n\tfor (const profile of profiles) {\n\t\tif (!allowed.has(profile as EvoCheckProfile)) {\n\t\t\tthrow new Error(`${label} contains profile ${profile}, which cannot execute for this candidate kind`);\n\t\t}\n\t}\n\treturn profiles as EvoCheckProfile[];\n}\n\nfunction nonEmptyStringArray(value: unknown, label: string): string[] {\n\tconst values = stringArray(value, label);\n\tif (values.length === 0) throw new Error(`${label} must not be empty`);\n\treturn values;\n}\n\nfunction positiveInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) <= 0) throw new Error(`${label} must be a positive integer`);\n\treturn value as number;\n}\n\nfunction trialDuration(value: unknown, label: string): string {\n\tconst duration = string(value, label);\n\tif (parseTrialDurationDays(duration) === undefined) {\n\t\tthrow new Error(`${label} must be a whole number of days such as \"14d\"`);\n\t}\n\treturn duration;\n}\n\nfunction parseEvidenceStrategy(\n\tvalue: unknown,\n\tcheckProfiles: readonly EvoCheckProfile[],\n\tcandidateKind: EvolutionCandidateKind,\n): EvoEvidenceStrategy {\n\tconst root = asRecord(value, \"ResearchPlanner output.experiment.evidenceStrategy\");\n\tif (typeof root.patchClass !== \"string\" || !PATCH_CLASSES.has(root.patchClass)) {\n\t\tthrow new Error(\"ResearchPlanner output.experiment.evidenceStrategy.patchClass is invalid\");\n\t}\n\tconst offline = asRecord(root.offline, \"ResearchPlanner output.experiment.evidenceStrategy.offline\");\n\tconst parsedOffline: EvoEvidenceStrategy[\"offline\"] | undefined =\n\t\toffline.mode === \"required\"\n\t\t\t? {\n\t\t\t\t\tmode: \"required\" as const,\n\t\t\t\t\tprofiles: profileArray(\n\t\t\t\t\t\toffline.profiles,\n\t\t\t\t\t\t\"evidenceStrategy.offline.profiles\",\n\t\t\t\t\t\tdeclarableProfiles(OFFLINE_PROFILE_CAPABILITIES, candidateKind),\n\t\t\t\t\t) as Array<Extract<EvoCheckProfile, \"bundle-compile\" | \"repo-check\" | \"related-tests\">>,\n\t\t\t\t}\n\t\t\t: offline.mode === \"not-applicable\"\n\t\t\t\t? { mode: \"not-applicable\" as const, reason: string(offline.reason, \"evidenceStrategy.offline.reason\") }\n\t\t\t\t: undefined;\n\tif (!parsedOffline) throw new Error(\"evidenceStrategy.offline.mode is invalid\");\n\tconst historical = asRecord(\n\t\troot.historicalReplay,\n\t\t\"ResearchPlanner output.experiment.evidenceStrategy.historicalReplay\",\n\t);\n\tconst parsedHistorical: EvoEvidenceStrategy[\"historicalReplay\"] | undefined =\n\t\thistorical.mode === \"required\"\n\t\t\t? {\n\t\t\t\t\tmode: \"required\" as const,\n\t\t\t\t\tprofiles: profileArray(\n\t\t\t\t\t\thistorical.profiles,\n\t\t\t\t\t\t\"evidenceStrategy.historicalReplay.profiles\",\n\t\t\t\t\t\tdeclarableProfiles(HISTORICAL_PROFILE_CAPABILITIES, candidateKind),\n\t\t\t\t\t) as Array<Extract<EvoCheckProfile, \"paired-replay\" | \"session-comparison\" | \"compaction-replay\">>,\n\t\t\t\t\tdatasets: nonEmptyStringArray(historical.datasets, \"evidenceStrategy.historicalReplay.datasets\"),\n\t\t\t\t\tminimumSamples: positiveInteger(\n\t\t\t\t\t\thistorical.minimumSamples,\n\t\t\t\t\t\t\"evidenceStrategy.historicalReplay.minimumSamples\",\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t: historical.mode === \"recommended\"\n\t\t\t\t? {\n\t\t\t\t\t\tmode: \"recommended\" as const,\n\t\t\t\t\t\tprofiles: profileArray(\n\t\t\t\t\t\t\thistorical.profiles,\n\t\t\t\t\t\t\t\"evidenceStrategy.historicalReplay.profiles\",\n\t\t\t\t\t\t\tdeclarableProfiles(HISTORICAL_PROFILE_CAPABILITIES, candidateKind),\n\t\t\t\t\t\t) as Array<Extract<EvoCheckProfile, \"paired-replay\" | \"session-comparison\" | \"compaction-replay\">>,\n\t\t\t\t\t\tdatasets: nonEmptyStringArray(historical.datasets, \"evidenceStrategy.historicalReplay.datasets\"),\n\t\t\t\t\t\tminimumSamples: positiveInteger(\n\t\t\t\t\t\t\thistorical.minimumSamples,\n\t\t\t\t\t\t\t\"evidenceStrategy.historicalReplay.minimumSamples\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\treason: string(historical.reason, \"evidenceStrategy.historicalReplay.reason\"),\n\t\t\t\t\t}\n\t\t\t\t: historical.mode === \"optional\" || historical.mode === \"not-applicable\"\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tmode: historical.mode,\n\t\t\t\t\t\t\treason: string(historical.reason, \"evidenceStrategy.historicalReplay.reason\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined;\n\tif (!parsedHistorical) throw new Error(\"evidenceStrategy.historicalReplay.mode is invalid\");\n\tconst online = asRecord(root.online, \"ResearchPlanner output.experiment.evidenceStrategy.online\");\n\tconst parsedOnline: EvoEvidenceStrategy[\"online\"] | undefined =\n\t\tonline.mode === \"none\"\n\t\t\t? { mode: \"none\" as const }\n\t\t\t: online.mode === \"shadow\" || online.mode === \"canary\"\n\t\t\t\t? {\n\t\t\t\t\t\tmode: online.mode,\n\t\t\t\t\t\tminimumSamples: positiveInteger(online.minimumSamples, \"evidenceStrategy.online.minimumSamples\"),\n\t\t\t\t\t\tmaximumDuration: trialDuration(online.maximumDuration, \"evidenceStrategy.online.maximumDuration\"),\n\t\t\t\t\t}\n\t\t\t\t: undefined;\n\tif (!parsedOnline) throw new Error(\"evidenceStrategy.online.mode is invalid\");\n\tif (root.rollout !== \"direct\" && root.rollout !== \"shadow-first\" && root.rollout !== \"canary-first\") {\n\t\tthrow new Error(\"evidenceStrategy.rollout is invalid\");\n\t}\n\tif ((root.rollout === \"direct\") !== (parsedOnline.mode === \"none\")) {\n\t\tthrow new Error(\"Direct rollout requires online.mode none and online evidence requires a staged rollout\");\n\t}\n\tif (root.rollout === \"shadow-first\" && parsedOnline.mode !== \"shadow\")\n\t\tthrow new Error(\"shadow-first requires shadow mode\");\n\tif (root.rollout === \"canary-first\" && parsedOnline.mode !== \"canary\")\n\t\tthrow new Error(\"canary-first requires canary mode\");\n\tconst selected = new Set(checkProfiles);\n\tfor (const profile of [\n\t\t...(parsedOffline.mode === \"required\" ? parsedOffline.profiles : []),\n\t\t...(parsedHistorical.mode === \"required\" || parsedHistorical.mode === \"recommended\"\n\t\t\t? parsedHistorical.profiles\n\t\t\t: []),\n\t]) {\n\t\tif (!selected.has(profile)) throw new Error(`Evidence strategy profile ${profile} is missing from checkProfiles`);\n\t}\n\treturn {\n\t\tpatchClass: root.patchClass as EvoEvidenceStrategy[\"patchClass\"],\n\t\toffline: parsedOffline,\n\t\thistoricalReplay: parsedHistorical,\n\t\tonline: parsedOnline,\n\t\trollout: root.rollout,\n\t};\n}\n\nfunction parseInboxDecisions(value: unknown): EvolutionInboxDecision[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value)) throw new Error(\"ResearchPlanner output.inboxDecisions must be an array\");\n\treturn value.map((entry, index) => {\n\t\tconst label = `ResearchPlanner output.inboxDecisions[${index}]`;\n\t\tconst decision = asRecord(entry, label);\n\t\tif (typeof decision.file !== \"string\" || !decision.file.endsWith(\".json\")) {\n\t\t\tthrow new Error(`${label}.file must be an inbox JSON file`);\n\t\t}\n\t\tif (\n\t\t\tdecision.kind !== \"preference\" &&\n\t\t\tdecision.kind !== \"request\" &&\n\t\t\tdecision.kind !== \"note\" &&\n\t\t\tdecision.kind !== \"task-local\"\n\t\t) {\n\t\t\tthrow new Error(`${label}.kind is invalid`);\n\t\t}\n\t\tif (typeof decision.reason !== \"string\" || !decision.reason.trim()) {\n\t\t\tthrow new Error(`${label}.reason must be a non-empty string`);\n\t\t}\n\t\tif (decision.kind === \"preference\") {\n\t\t\tif (typeof decision.instruction !== \"string\" || !decision.instruction.trim()) {\n\t\t\t\tthrow new Error(`${label}.instruction is required for a preference`);\n\t\t\t}\n\t\t} else if (decision.instruction !== undefined) {\n\t\t\tthrow new Error(`${label}.instruction is only valid for a preference`);\n\t\t}\n\t\treturn {\n\t\t\tfile: decision.file,\n\t\t\tkind: decision.kind,\n\t\t\treason: decision.reason,\n\t\t\t...(decision.instruction ? { instruction: decision.instruction } : {}),\n\t\t} as EvolutionInboxDecision;\n\t});\n}\n\nexport function parseEvolutionResearchPlanValue(value: unknown): EvolutionResearchPlan {\n\tconst root = asRecord(value, \"ResearchPlanner output\");\n\tconst experiment = asRecord(root.experiment, \"ResearchPlanner output.experiment\");\n\tif (!Array.isArray(experiment.checkProfiles)) {\n\t\tthrow new Error(\"ResearchPlanner output.experiment.checkProfiles must be an array\");\n\t}\n\tconst checkProfiles = experiment.checkProfiles.map((entry, index) => {\n\t\tif (typeof entry !== \"string\" || !CHECK_PROFILES.has(entry as EvoExperimentSpec[\"checkProfiles\"][number])) {\n\t\t\tthrow new Error(`ResearchPlanner output.experiment.checkProfiles[${index}] is unsupported`);\n\t\t}\n\t\treturn entry as EvoExperimentSpec[\"checkProfiles\"][number];\n\t});\n\tconst minimumEffectRecord = asRecord(experiment.minimumEffect, \"ResearchPlanner output.experiment.minimumEffect\");\n\tconst minimumEffect: Record<string, number> = {};\n\tfor (const [metric, value] of Object.entries(minimumEffectRecord)) {\n\t\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\t\tthrow new Error(`ResearchPlanner output.experiment.minimumEffect.${metric} must be non-negative`);\n\t\t}\n\t\tminimumEffect[metric] = value;\n\t}\n\tconst candidateKind = root.candidateKind;\n\tif (\n\t\tcandidateKind !== \"none\" &&\n\t\tcandidateKind !== \"data\" &&\n\t\tcandidateKind !== \"component\" &&\n\t\tcandidateKind !== \"code\"\n\t) {\n\t\tthrow new Error(\"ResearchPlanner output.candidateKind is invalid\");\n\t}\n\tif (typeof root.requiresNewAbi !== \"boolean\")\n\t\tthrow new Error(\"ResearchPlanner output.requiresNewAbi must be boolean\");\n\tif (root.targetAbi !== undefined && typeof root.targetAbi !== \"string\") {\n\t\tthrow new Error(\"ResearchPlanner output.targetAbi must be a string\");\n\t}\n\tif (candidateKind === \"component\" && !root.targetAbi) {\n\t\tthrow new Error(\"Component plans require targetAbi\");\n\t}\n\tif (root.requiresNewAbi === true && candidateKind !== \"code\") {\n\t\tthrow new Error(\"A plan requiring a new ABI must be an infrastructure code plan\");\n\t}\n\tif (root.requiresNewAbi === false && root.targetAbi) {\n\t\tcreateDefaultEvoAbiRegistry().require(root.targetAbi);\n\t}\n\tconst evidenceStrategy = parseEvidenceStrategy(\n\t\texperiment.evidenceStrategy,\n\t\tcheckProfiles,\n\t\tcandidateKind as EvolutionCandidateKind,\n\t);\n\tif (candidateKind === \"component\" && evidenceStrategy.patchClass !== \"component\") {\n\t\tthrow new Error(\"Component candidates require patchClass component\");\n\t}\n\tif (candidateKind === \"component\" && evidenceStrategy.rollout === \"direct\") {\n\t\tthrow new Error(\"Component candidates require shadow or Canary rollout\");\n\t}\n\t// The experiment is a contract: every metric must be one the trial comparison\n\t// actually measures, and a declared trial pre-registers one decision metric.\n\tconst metrics = stringArray(experiment.metrics, \"ResearchPlanner output.experiment.metrics\");\n\tfor (const metric of metrics) {\n\t\tif (!isTrialMetricName(metric)) {\n\t\t\tthrow new Error(\n\t\t\t\t`ResearchPlanner output.experiment.metrics contains unmeasurable metric ${metric}; measurable metrics: ${Object.keys(TRIAL_METRIC_DIRECTIONS).join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\tfor (const key of Object.keys(minimumEffect)) {\n\t\tif (!metrics.includes(key)) {\n\t\t\tthrow new Error(`ResearchPlanner output.experiment.minimumEffect.${key} is not one of the declared metrics`);\n\t\t}\n\t}\n\tconst trialDeclared = evidenceStrategy.online.mode !== \"none\";\n\tconst primaryMetric = experiment.primaryMetric;\n\tif (primaryMetric !== undefined && typeof primaryMetric !== \"string\") {\n\t\tthrow new Error(\"ResearchPlanner output.experiment.primaryMetric must be a string\");\n\t}\n\tif (trialDeclared) {\n\t\tif (!primaryMetric) {\n\t\t\tthrow new Error(\"A plan that declares shadow or canary evidence must pre-register experiment.primaryMetric\");\n\t\t}\n\t\tif (!metrics.includes(primaryMetric)) {\n\t\t\tthrow new Error(\"ResearchPlanner output.experiment.primaryMetric must be one of the declared metrics\");\n\t\t}\n\t\tconst threshold = minimumEffect[primaryMetric];\n\t\tif (typeof threshold !== \"number\" || threshold <= 0) {\n\t\t\tthrow new Error(\"ResearchPlanner output.experiment.minimumEffect must set a positive value for primaryMetric\");\n\t\t}\n\t} else if (primaryMetric && !metrics.includes(primaryMetric)) {\n\t\tthrow new Error(\"ResearchPlanner output.experiment.primaryMetric must be one of the declared metrics\");\n\t}\n\treturn {\n\t\ttopic: string(root.topic, \"ResearchPlanner output.topic\"),\n\t\treason: string(root.reason, \"ResearchPlanner output.reason\"),\n\t\tplanMarkdown: string(root.planMarkdown, \"ResearchPlanner output.planMarkdown\"),\n\t\texperiment: {\n\t\t\tbaseline: string(experiment.baseline, \"ResearchPlanner output.experiment.baseline\"),\n\t\t\thypothesis: string(experiment.hypothesis, \"ResearchPlanner output.experiment.hypothesis\"),\n\t\t\tcheckProfiles: [...new Set(checkProfiles)],\n\t\t\tevidenceStrategy,\n\t\t\tmetrics,\n\t\t\t...(primaryMetric ? { primaryMetric } : {}),\n\t\t\tminimumEffect,\n\t\t\ttrialPlan: string(experiment.trialPlan, \"ResearchPlanner output.experiment.trialPlan\"),\n\t\t\trollbackConditions: stringArray(\n\t\t\t\texperiment.rollbackConditions,\n\t\t\t\t\"ResearchPlanner output.experiment.rollbackConditions\",\n\t\t\t),\n\t\t},\n\t\t...(root.targetAbi ? { targetAbi: root.targetAbi } : {}),\n\t\trequiresNewAbi: root.requiresNewAbi,\n\t\tcandidateKind,\n\t\tbuilderInstructions: string(root.builderInstructions, \"ResearchPlanner output.builderInstructions\"),\n\t\tinboxDecisions: parseInboxDecisions(root.inboxDecisions),\n\t};\n}\n\nexport interface RunEvolutionResearchPlanOptions {\n\tpaths: EvoPaths;\n\trun: EvolutionRun;\n\tcorpus: EvidenceCorpus;\n\t/** On-disk corpus tree; when present the prompt carries only its index. */\n\tmaterializedCorpus?: MaterializedCorpus;\n\trunner: ModelRunner;\n\tcwd: string;\n\tagentDir?: string;\n\tmodel: string;\n\tthinkingLevel?: ThinkingLevel;\n\tactivePreferences?: string;\n\tsignal?: AbortSignal;\n}\n\nexport interface EvolutionResearchPlanResult {\n\tplan: EvolutionResearchPlan;\n\trun: ModelRunResult;\n\tstate: EvolutionRun;\n}\n\nexport async function persistEvolutionResearchPlan(options: {\n\tpaths: EvoPaths;\n\trun: EvolutionRun;\n\tplan: unknown;\n}): Promise<{ plan: EvolutionResearchPlan; state: EvolutionRun }> {\n\tconst plan = parseEvolutionResearchPlanValue(options.plan);\n\tconst directory = evolutionRunDirectory(options.paths, options.run.id);\n\tawait atomicWriteFile(join(directory, \"plan.md\"), `${plan.planMarkdown.trim()}\\n`);\n\tawait atomicWriteJson(join(directory, \"experiment.json\"), plan.experiment);\n\t// The full plan object enables evidence resumption and deterministic Builder\n\t// inputs without re-running research.\n\tawait atomicWriteJson(join(directory, \"plan.json\"), plan);\n\tconst state = await updateEvolutionRun(options.paths, options.run.id, {\n\t\tstatus: \"planned\",\n\t\tplanFile: \"plan.md\",\n\t\texperimentFile: \"experiment.json\",\n\t\texperimentDigest: sha256(canonicalJson(plan.experiment)),\n\t});\n\treturn { plan, state };\n}\n\nconst TRIAGE_NOTE_PREFIX = \"NOTE: triage hypothesis\";\nconst MAX_TRIAGE_HYPOTHESES_IN_PROMPT = 10;\n\nasync function collectTriageHypotheses(paths: EvoPaths, inboxFiles: readonly string[]): Promise<string[]> {\n\tconst notes: string[] = [];\n\tfor (const file of inboxFiles) {\n\t\tif (notes.length >= MAX_TRIAGE_HYPOTHESES_IN_PROMPT) break;\n\t\ttry {\n\t\t\tconst entry = await readInboxEntry(paths, file);\n\t\t\tif (entry.kind === \"note\" && entry.text.startsWith(TRIAGE_NOTE_PREFIX)) {\n\t\t\t\tnotes.push(`- [inbox ${file}] ${entry.text}`);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Unreadable inbox entries are already surfaced through the corpus itself.\n\t\t}\n\t}\n\treturn notes;\n}\n\nexport async function runEvolutionResearchPlan(\n\toptions: RunEvolutionResearchPlanOptions,\n): Promise<EvolutionResearchPlanResult> {\n\tconst workflow = await readEvolutionWorkflow(options.paths);\n\t// Request-channel runs stay narrow and fast; scheduled runs consume the\n\t// pre-triaged hypotheses the streaming triage filed into the inbox.\n\tconst triageHypotheses = options.run.request\n\t\t? []\n\t\t: await collectTriageHypotheses(options.paths, options.corpus.inboxFiles);\n\tconst prompt = [\n\t\t\"Research one worthwhile improvement and freeze its experiment before implementation.\",\n\t\toptions.run.request\n\t\t\t? `The user supplied this priority request: ${options.run.request}\\nThis is a request-channel run: research and plan exclusively for that direction, and do not survey the corpus for unrelated opportunities. When the evidence cannot support any candidate in the requested direction, return candidateKind none and explain why.`\n\t\t\t: \"No user request was supplied; select the best grounded opportunity yourself.\",\n\t\t...(triageHypotheses.length > 0\n\t\t\t? [\n\t\t\t\t\t\"The streaming session triage pre-filed the following hypotheses from recent session telemetry. Treat them as prioritized starting points: verify each against the corpus evidence, prefer a confirmed hypothesis over fresh mining, and discard any the evidence does not support.\",\n\t\t\t\t\t\"<triage_hypotheses>\",\n\t\t\t\t\t...triageHypotheses,\n\t\t\t\t\t\"</triage_hypotheses>\",\n\t\t\t\t]\n\t\t\t: []),\n\t\t\"Deliver exactly one plan by calling the submit_research_plan tool with: topic, reason, planMarkdown, experiment { baseline, hypothesis, checkProfiles, evidenceStrategy, metrics, primaryMetric, minimumEffect, trialPlan, rollbackConditions }, targetAbi (optional), requiresNewAbi, candidateKind, builderInstructions, and inboxDecisions.\",\n\t\t`experiment.metrics must come from the measured set: ${Object.keys(TRIAL_METRIC_DIRECTIONS).join(\", \")}. A plan declaring shadow or canary evidence must pre-register primaryMetric (one of its metrics) with a positive minimumEffect; keep and rollback are decided against that frozen threshold.`,\n\t\t\"Classify every supplied explicit inbox input in inboxDecisions as preference, request, note, or task-local. A preference instruction must be an exact user-authored substring. Feature requests are requests even when they contain words such as every or always. Do not classify the same file twice.\",\n\t\t\"Open durable preferences should be prioritized as a narrow data candidate that appends memory/preferences.json; cite the inbox file and exact source event. Requests remain open until a linked candidate is kept.\",\n\t\t\"A component candidate must target a pre-defined ABI. A missing ABI requires an infrastructure code plan and cannot be activated automatically.\",\n\t\t\"Allowed checkProfiles: bundle-compile, repo-check, related-tests, paired-replay, session-comparison, compaction-replay. evidenceStrategy must classify patchClass; mark offline required or explicitly not applicable with a concrete causal reason; mark historicalReplay required, recommended, optional, or not-applicable; choose online none, shadow, or canary with sample bounds; and choose the matching direct, shadow-first, or canary-first rollout. Offline infeasibility never implies direct rollout. Component replacement cannot use direct rollout. Do not emit shell commands.\",\n\t\t\"Reserve historicalReplay required for evidence without which the candidate must not release; the harness executes it inline and its absence blocks release. Use recommended (with profiles, datasets, minimumSamples, and a reason stating what the replay would add and what it cannot prove) when replay evidence is useful but not release-blocking: scheduled runs execute the recommendation automatically, while request runs pause after evaluation so the user decides execute, skip, or reject. For a reversible T0 data change, prefer recommended or optional over required.\",\n\t\t\"Use research as a hypothesis source and cite concrete sources in planMarkdown. State when external research is unavailable.\",\n\t\t\"\",\n\t\t...(options.activePreferences\n\t\t\t? [\"<active_preferences>\", options.activePreferences, \"</active_preferences>\", \"\"]\n\t\t\t: []),\n\t\t\"<workflow>\",\n\t\tworkflow,\n\t\t\"</workflow>\",\n\t\t\"\",\n\t\t...(options.materializedCorpus\n\t\t\t? [\n\t\t\t\t\t`<evidence_corpus_index truncated=\"${String(options.corpus.truncated)}\">`,\n\t\t\t\t\toptions.materializedCorpus.indexText,\n\t\t\t\t\t\"</evidence_corpus_index>\",\n\t\t\t\t]\n\t\t\t: [\n\t\t\t\t\t`<evidence_corpus truncated=\"${String(options.corpus.truncated)}\">`,\n\t\t\t\t\toptions.corpus.text,\n\t\t\t\t\t\"</evidence_corpus>\",\n\t\t\t\t]),\n\t].join(\"\\n\");\n\tlet failureUsage: Parameters<NonNullable<ModelRunRequest[\"onSessionStats\"]>> | undefined;\n\tlet modelRun: ModelRunResult;\n\ttry {\n\t\tmodelRun = await options.runner.run({\n\t\t\tcwd: options.cwd,\n\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\tsystemPrompt:\n\t\t\t\t\"You are Evo-Pi's ResearchPlanner. Research, reason, and write the frozen plan and experiment. You never implement or activate candidates.\",\n\t\t\tprompt,\n\t\t\tmodel: options.model,\n\t\t\t...(options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),\n\t\t\ttools: [\"read\", \"grep\", \"find\", \"ls\", \"evo_research_search\", \"evo_research_fetch\"],\n\t\t\tcustomTools: createEvolutionResearchTools(),\n\t\t\t// A stable identity lets recovery retries and later phases hit the provider prompt cache.\n\t\t\tsessionIdentity: `evo-research-${options.run.id}`,\n\t\t\trecoveryPrompt:\n\t\t\t\t\"You ran out of output space. Call the submit_research_plan tool now with the complete plan. Do not call other tools and do not repeat your analysis.\",\n\t\t\tsubmission: {\n\t\t\t\ttoolName: \"submit_research_plan\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Deliver the frozen research plan and experiment. Validation errors are returned for correction.\",\n\t\t\t\tparameters: RESEARCH_PLAN_PARAMETERS,\n\t\t\t\tvalidate: (params) => parseEvolutionResearchPlanValue(params),\n\t\t\t},\n\t\t\tonSessionStats: (...usage) => {\n\t\t\t\tfailureUsage = usage;\n\t\t\t},\n\t\t\t...(options.signal ? { signal: options.signal } : {}),\n\t\t});\n\t} catch (error) {\n\t\t// Preserve the usage of failed research runs so window pressure stays diagnosable.\n\t\tconst [stats, model] = failureUsage ?? [];\n\t\tif (stats && model) {\n\t\t\tawait recordModelUsage(options.paths, \"research-plan\", { text: \"\", stats, model }).catch(() => {});\n\t\t}\n\t\tthrow error;\n\t}\n\tawait recordModelUsage(options.paths, \"research-plan\", modelRun);\n\tconst persisted = await persistEvolutionResearchPlan({\n\t\tpaths: options.paths,\n\t\trun: options.run,\n\t\tplan: modelRun.submission,\n\t});\n\treturn { ...persisted, run: modelRun };\n}\n"]}