{"version":3,"file":"auto-improve.d.ts","sourceRoot":"","sources":["../src/auto-improve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAYlE,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,YAAY,CAAC;AACxD,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAoB,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAExF,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1C,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACzB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACrD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACjB;AAMD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,8BAAmC,GAAG,gBAAgB,CAmM5G","sourcesContent":["import type { ExtensionFactory } from \"@ch1nyzzz/pi-coding-agent\";\nimport { refreshEvoStatusIndicator } from \"./cli.ts\";\nimport {\n\tbuildTrialComparison,\n\tcontractMetricRegressions,\n\tevaluateTrialContractGate,\n\tparseTrialDurationDays,\n} from \"./comparison.ts\";\nimport { readEvoControlConfig } from \"./evolve/config.ts\";\nimport { runEvolutionCycle } from \"./evolve/cycle.ts\";\nimport { readFrozenExperimentForProposal } from \"./evolve/run.ts\";\nimport { runSessionTriage } from \"./evolve/triage.ts\";\nimport { type EvoPaths, getEvoPaths } from \"./paths.ts\";\nimport { createPiModelRunner, type ModelRunner } from \"./reflect/model-runner.ts\";\nimport { runRetrospective, type TrialRecommendation } from \"./reflect/retrospective.ts\";\nimport { readScheduleConfig, runConfiguredImprove } from \"./scheduler.ts\";\nimport { EvoService } from \"./service.ts\";\n\nconst DEFAULT_INITIAL_DELAY_MS = 45_000;\nconst DEFAULT_CHECK_INTERVAL_MS = 30 * 60_000;\n\nexport interface AutoImproveOutcome {\n\tproposals: ReadonlyArray<{ id: string }>;\n}\n\nexport interface AutoRetrospectiveOutcome {\n\tproposal: { id: string };\n\trecommendation?: TrialRecommendation;\n\treused?: boolean;\n}\n\nexport interface EvoAutoImproveExtensionOptions {\n\troot?: string;\n\tpaths?: EvoPaths;\n\tservice?: EvoService;\n\trunner?: ModelRunner;\n\tcwd?: string;\n\tagentDir?: string;\n\tmodel?: string;\n\tinitialDelayMs?: number;\n\tcheckIntervalMs?: number;\n\timprove?: (signal: AbortSignal) => Promise<AutoImproveOutcome>;\n\tretrospect?: () => Promise<AutoRetrospectiveOutcome>;\n\tnow?: () => Date;\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Runs the guarded scheduled-improve loop inside the live Pi session, so users\n * get background reflection at their configured cadence without any cron or\n * systemd setup. Every attempt still goes through runConfiguredImprove, which\n * enforces pause, cadence, quiet hours, activity, daily-limit, and lock gates.\n */\nexport function createEvoAutoImproveExtension(options: EvoAutoImproveExtensionOptions = {}): ExtensionFactory {\n\tconst paths = options.service?.paths ?? options.paths ?? getEvoPaths(options.root);\n\tconst service = options.service ?? new EvoService(paths);\n\tconst runner = options.runner ?? createPiModelRunner();\n\tconst initialDelayMs = options.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n\tconst checkIntervalMs = options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;\n\tconst improve =\n\t\toptions.improve ??\n\t\t((signal: AbortSignal) =>\n\t\t\trunEvolutionCycle({\n\t\t\t\tpaths,\n\t\t\t\trunner,\n\t\t\t\tservice,\n\t\t\t\t...(options.cwd ? { cwd: options.cwd } : {}),\n\t\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\t\tsignal,\n\t\t\t}));\n\tconst retrospect =\n\t\toptions.retrospect ??\n\t\t(async () => {\n\t\t\tconst config = await readEvoControlConfig(paths);\n\t\t\treturn runRetrospective({\n\t\t\t\tpaths,\n\t\t\t\trunner,\n\t\t\t\t...(options.cwd ? { cwd: options.cwd } : {}),\n\t\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\t\tmodel: options.model ?? config.models.evaluator.model,\n\t\t\t\t...(config.models.evaluator.thinkingLevel ? { thinkingLevel: config.models.evaluator.thinkingLevel } : {}),\n\t\t\t});\n\t\t});\n\tconst now = options.now ?? (() => new Date());\n\n\treturn (pi) => {\n\t\tlet timer: NodeJS.Timeout | undefined;\n\t\tlet stopped = true;\n\t\tlet activeTick: Promise<void> = Promise.resolve();\n\t\tlet notifiedFailure = false;\n\n\t\tconst clearTimer = (): void => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t};\n\n\t\tpi.on(\"session_start\", (_event, ctx) => {\n\t\t\tstopped = false;\n\t\t\tnotifiedFailure = false;\n\t\t\tclearTimer();\n\n\t\t\tconst tick = async (): Promise<void> => {\n\t\t\t\ttry {\n\t\t\t\t\tif (stopped || !ctx.isIdle()) return;\n\t\t\t\t\tconst status = await service.status();\n\t\t\t\t\tif (!status.initialized) return;\n\t\t\t\t\t// Streaming triage is cheap and cursor-gated: it no-ops until\n\t\t\t\t\t// enough new sessions accumulate, and never blocks the tick.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst config = await readEvoControlConfig(paths);\n\t\t\t\t\t\tconst triage = await runSessionTriage({\n\t\t\t\t\t\t\tpaths,\n\t\t\t\t\t\t\trunner,\n\t\t\t\t\t\t\tmodel: options.model ?? config.models.triage.model,\n\t\t\t\t\t\t\t...(config.models.triage.thinkingLevel\n\t\t\t\t\t\t\t\t? { thinkingLevel: config.models.triage.thinkingLevel }\n\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\teveryNSessions: config.triage.everyNSessions,\n\t\t\t\t\t\t\t...(options.cwd ? { cwd: options.cwd } : {}),\n\t\t\t\t\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (triage.ran && triage.hypotheses.length > 0) {\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`Evo-Pi triage filed ${triage.hypotheses.length} improvement hypothesis(es) from ${triage.newSessions} new session(s)`,\n\t\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Triage is best-effort; a missing triage model must not stall trials or improves.\n\t\t\t\t\t}\n\t\t\t\t\tif (status.trial) {\n\t\t\t\t\t\tconst proposal = await service.getProposal(status.trial.proposalId);\n\t\t\t\t\t\tconst schedule = await readScheduleConfig(paths);\n\t\t\t\t\t\tconst comparison = await buildTrialComparison(paths, proposal, status.trial);\n\t\t\t\t\t\t// The frozen experiment contract bounds the trial: its maximumDuration\n\t\t\t\t\t\t// caps the schedule window and its minimumSamples raises the session bar.\n\t\t\t\t\t\tconst experiment = await readFrozenExperimentForProposal(paths, status.trial.proposalId);\n\t\t\t\t\t\tconst online = experiment?.evidenceStrategy.online;\n\t\t\t\t\t\tconst contractDays =\n\t\t\t\t\t\t\tonline && online.mode !== \"none\" ? parseTrialDurationDays(online.maximumDuration) : undefined;\n\t\t\t\t\t\tconst contractSamples = online && online.mode !== \"none\" ? online.minimumSamples : 0;\n\t\t\t\t\t\tconst dueAfterDays =\n\t\t\t\t\t\t\tstatus.trial.canary?.maximumDurationDays ??\n\t\t\t\t\t\t\t(contractDays === undefined\n\t\t\t\t\t\t\t\t? schedule.trialDueAfterDays\n\t\t\t\t\t\t\t\t: Math.min(schedule.trialDueAfterDays, contractDays));\n\t\t\t\t\t\tconst dueAfterSessions =\n\t\t\t\t\t\t\tstatus.trial.canary?.minimumSamples ?? Math.max(schedule.trialDueAfterSessions, contractSamples);\n\t\t\t\t\t\tconst dueAt = Date.parse(status.trial.startedAt) + dueAfterDays * 24 * 60 * 60 * 1_000;\n\t\t\t\t\t\tconst due = now().getTime() >= dueAt || comparison.after.totals.sessions >= dueAfterSessions;\n\t\t\t\t\t\tif (due) {\n\t\t\t\t\t\t\tconst result = await retrospect();\n\t\t\t\t\t\t\tnotifiedFailure = false;\n\t\t\t\t\t\t\tconst config = await readEvoControlConfig(paths);\n\t\t\t\t\t\t\t// Machine rollback trigger: a measured regression of the frozen\n\t\t\t\t\t\t\t// primary metric or any pre-registered guardrail metric outranks\n\t\t\t\t\t\t\t// any model recommendation.\n\t\t\t\t\t\t\tconst regressions = contractMetricRegressions(experiment, comparison);\n\t\t\t\t\t\t\tif (regressions.length > 0) {\n\t\t\t\t\t\t\t\tconst regression = regressions.map((entry) => entry.reason).join(\"; \");\n\t\t\t\t\t\t\t\tawait service.rollback(undefined, `Automatic rollback: ${regression}`);\n\t\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t\t`Evo-Pi automatically rolled back trial ${result.proposal.id}: ${regression}`,\n\t\t\t\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (config.release.autoKeepSuccessfulTrial && result.recommendation === \"keep\") {\n\t\t\t\t\t\t\t\t// The model's keep is a veto-capable opinion; the deterministic\n\t\t\t\t\t\t\t\t// contract gate decides whether an automatic keep is allowed.\n\t\t\t\t\t\t\t\tconst gate = evaluateTrialContractGate(experiment, comparison);\n\t\t\t\t\t\t\t\tif (gate.allowed) {\n\t\t\t\t\t\t\t\t\tawait service.keep(\n\t\t\t\t\t\t\t\t\t\t\"Automatic keep: retrospective recommendation and frozen contract gate both passed\",\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tctx.ui.notify(`Evo-Pi automatically kept trial ${result.proposal.id}`, \"info\");\n\t\t\t\t\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t\t`Evo-Pi left trial ${result.proposal.id} open for review: ${gate.reasons.join(\"; \")}`,\n\t\t\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (result.recommendation === \"rollback\") {\n\t\t\t\t\t\t\t\tawait service.rollback(\n\t\t\t\t\t\t\t\t\tundefined,\n\t\t\t\t\t\t\t\t\t\"Automatic rollback after evidence-bound retrospective recommendation\",\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tctx.ui.notify(`Evo-Pi automatically rolled back trial ${result.proposal.id}`, \"warning\");\n\t\t\t\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!result.reused) {\n\t\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t\t`Evo-Pi automatically compared the trial with its baseline; review ${result.proposal.id} with /evo show ${result.proposal.id}`,\n\t\t\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst result = await runConfiguredImprove({\n\t\t\t\t\t\tpaths,\n\t\t\t\t\t\texcludeSessionIds: [ctx.sessionManager.getSessionId()],\n\t\t\t\t\t\timprove,\n\t\t\t\t\t});\n\t\t\t\t\tif (result.status !== \"completed\") return;\n\t\t\t\t\tnotifiedFailure = false;\n\t\t\t\t\tconst staged = result.value.proposals.length;\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\tstaged > 0\n\t\t\t\t\t\t\t? `Evo-Pi evolved in the background and produced ${staged} proposal${staged === 1 ? \"\" : \"s\"}; review with /evo list`\n\t\t\t\t\t\t\t: \"Evo-Pi completed a background research cycle without a candidate\",\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t);\n\t\t\t\t\tawait refreshEvoStatusIndicator({ service, paths }, ctx, now);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!notifiedFailure) {\n\t\t\t\t\t\tnotifiedFailure = true;\n\t\t\t\t\t\tctx.ui.notify(`Evo-Pi background reflection failed: ${errorMessage(error)}`, \"warning\");\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tschedule(checkIntervalMs);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tconst schedule = (delayMs: number): void => {\n\t\t\t\tif (stopped) return;\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tactiveTick = tick();\n\t\t\t\t}, delayMs);\n\t\t\t\ttimer.unref?.();\n\t\t\t};\n\n\t\t\tschedule(initialDelayMs);\n\t\t});\n\n\t\tpi.on(\"session_shutdown\", async () => {\n\t\t\tstopped = true;\n\t\t\tclearTimer();\n\t\t\tawait activeTick;\n\t\t});\n\t};\n}\n"]}