{"version":3,"file":"permit.d.ts","sourceRoot":"","sources":["../../src/reflect/permit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAS5C,OAAO,EAAE,KAAK,YAAY,EAAyC,MAAM,0BAA0B,CAAC;AACpG,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAAyB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAC;AACnG,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMrE,UAAU,kBAAkB;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA2B,SAAQ,kBAAkB;IACrE,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,YAAY,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,GAAG,EAAE,cAAc,CAAC;CACpB;AAED,MAAM,WAAW,oCAAqC,SAAQ,kBAAkB;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,aAAa,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,aAAa,CAAC;CACpC;AAED,MAAM,WAAW,2BAA2B;IAC3C,KAAK,EAAE,QAAQ,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AA2ID,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAqDjH;AAED,wBAAsB,6BAA6B,CAAC,OAAO,EAAE,oCAAoC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmFpH;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,YAAY,CAAC,CAW7F;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,YAAY,CAAC,CAU9F","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { ThinkingLevel } from \"@ch1nyzzz/pi-agent-core\";\nimport { loadCompiledBundle } from \"../bundle/compile.ts\";\nimport type { CodeValidationExecutor } from \"../code/worktree.ts\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport {\n\tassertProposalApproval,\n\ttype DraftProposal,\n\tloadProposal,\n\tparseReflectorOutput,\n\tproposalApproval,\n\treviseProposal,\n} from \"../proposal.ts\";\nimport { type ApprovalTurn, appendApprovalTurn, readApprovalTurns } from \"../proposal-artifacts.ts\";\nimport type { EvidenceReference, Proposal, ProposalApproval } from \"../types.ts\";\nimport { runCritic } from \"./critic.ts\";\nimport { collectEvidenceCorpus, type EvidenceCorpus, validateDraftGrounding } from \"./evidence.ts\";\nimport type { ModelRunner, ModelRunResult } from \"./model-runner.ts\";\nimport { type CounterfactualReplayResult, runCounterfactualReplay } from \"./replay.ts\";\nimport { recordModelUsage } from \"./usage.ts\";\n\nconst PERMIT_PROMPT_URL = new URL(\"../prompts/permit.md\", import.meta.url);\n\ninterface PermitModelOptions {\n\tpaths: EvoPaths;\n\trunner: ModelRunner;\n\tproposalId: string;\n\texpected: ProposalApproval;\n\tcwd?: string;\n\tagentDir?: string;\n\tmodel?: string;\n\tthinkingLevel?: ThinkingLevel;\n\tmaxCorpusBytes?: number;\n}\n\nexport interface AskProposalQuestionOptions extends PermitModelOptions {\n\tquestion: string;\n}\n\nexport interface AskProposalQuestionResult {\n\tproposal: Proposal;\n\tquestionTurn: ApprovalTurn;\n\tanswerTurn: ApprovalTurn;\n\tanswerMarkdown: string;\n\tevidence: EvidenceReference[];\n\tcorpus: EvidenceCorpus;\n\trun: ModelRunResult;\n}\n\nexport interface ReviseProposalFromInstructionOptions extends PermitModelOptions {\n\tinstruction: string;\n\trepositoryCwd?: string;\n\tcodeValidationExecutor?: CodeValidationExecutor;\n\tcriticModel?: string;\n\tcriticThinkingLevel?: ThinkingLevel;\n\treplayModel?: string;\n\treplayThinkingLevel?: ThinkingLevel;\n}\n\nexport interface RecordPermitDecisionOptions {\n\tpaths: EvoPaths;\n\tproposalId: string;\n\trevision: number;\n\tdiffDigest: string;\n\treason: string;\n\tuntil?: string;\n}\n\ninterface PermitAnswer {\n\tanswerMarkdown: string;\n\tevidence: EvidenceReference[];\n}\n\nfunction assertExpectedProposal(\n\tproposal: Proposal,\n\texpected: ProposalApproval,\n\tallowedStatuses: ReadonlySet<Proposal[\"status\"]>,\n): void {\n\tif (!allowedStatuses.has(proposal.status)) throw new Error(`Proposal ${proposal.id} is ${proposal.status}`);\n\tif (proposal.tier === \"T0\") throw new Error(`Proposal ${proposal.id} does not require conversational approval`);\n\tassertProposalApproval(proposal, expected);\n}\n\nfunction parseExactJson(text: string, label: string): Record<string, unknown> {\n\tconst trimmed = text\n\t\t.trim()\n\t\t.replace(/^```(?:json)?\\s*/i, \"\")\n\t\t.replace(/\\s*```$/, \"\");\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(trimmed);\n\t} catch {\n\t\tthrow new Error(`${label} must be exactly one JSON object`);\n\t}\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be exactly one JSON object`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction parsePermitAnswer(text: string): PermitAnswer {\n\tconst record = parseExactJson(text, \"Permit answer\");\n\tif (typeof record.answerMarkdown !== \"string\" || !record.answerMarkdown.trim()) {\n\t\tthrow new Error(\"Permit answer.answerMarkdown must be a non-empty string\");\n\t}\n\tif (!Array.isArray(record.evidence)) throw new Error(\"Permit answer.evidence must be an array\");\n\tconst evidence = record.evidence.map((value, index) => {\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\t\tthrow new Error(`Permit answer.evidence[${index}] must be an object`);\n\t\t}\n\t\tconst reference = value as Record<string, unknown>;\n\t\tif (\n\t\t\ttypeof reference.sessionId !== \"string\" ||\n\t\t\t!reference.sessionId ||\n\t\t\t!Number.isSafeInteger(reference.sequence) ||\n\t\t\t(reference.sequence as number) <= 0\n\t\t) {\n\t\t\tthrow new Error(`Permit answer.evidence[${index}] must contain sessionId and sequence`);\n\t\t}\n\t\tif (reference.quote !== undefined && typeof reference.quote !== \"string\") {\n\t\t\tthrow new Error(`Permit answer.evidence[${index}].quote must be a string`);\n\t\t}\n\t\treturn {\n\t\t\tsessionId: reference.sessionId,\n\t\t\tsequence: reference.sequence as number,\n\t\t\t...(reference.quote !== undefined ? { quote: reference.quote } : {}),\n\t\t} satisfies EvidenceReference;\n\t});\n\treturn { answerMarkdown: record.answerMarkdown, evidence };\n}\n\nfunction groundingDraft(proposal: Proposal, evidence: EvidenceReference[]): DraftProposal {\n\treturn {\n\t\tmotivation: proposal.motivation,\n\t\texpectedEffect: proposal.expectedEffect,\n\t\trisk: proposal.risk,\n\t\tverifyPlan: proposal.verifyPlan,\n\t\ttrialPlan: proposal.trialPlan,\n\t\tsource: proposal.source,\n\t\tevidence,\n\t\tinboxReferences: proposal.inboxReferences,\n\t\treplayScenarios: [],\n\t\tchanges: [],\n\t};\n}\n\nasync function validatePermitAnswer(\n\tpaths: EvoPaths,\n\tproposal: Proposal,\n\tanswer: PermitAnswer,\n): Promise<EvidenceReference[]> {\n\tif (answer.evidence.length === 0 && proposal.inboxReferences.length === 0) {\n\t\tthrow new Error(\"Permit answer must cite concrete evidence\");\n\t}\n\tconst evidence = await validateDraftGrounding(paths, groundingDraft(proposal, answer.evidence));\n\tfor (const reference of evidence) {\n\t\tif (!answer.answerMarkdown.includes(`${reference.sessionId}:${reference.sequence}`)) {\n\t\t\tthrow new Error(`Permit answer does not cite ${reference.sessionId}:${reference.sequence} in its text`);\n\t\t}\n\t}\n\tif (\n\t\tevidence.length === 0 &&\n\t\t!proposal.inboxReferences.some((fileName) => answer.answerMarkdown.includes(fileName))\n\t) {\n\t\tthrow new Error(\"Permit answer does not cite a concrete inbox reference\");\n\t}\n\treturn evidence;\n}\n\nfunction renderApprovalTranscript(turns: ApprovalTurn[]): string {\n\treturn turns.length === 0 ? \"No prior approval turns.\" : turns.map((turn) => JSON.stringify(turn)).join(\"\\n\");\n}\n\nfunction permitRunRequest(options: {\n\tproposal: Proposal;\n\tcorpus: EvidenceCorpus;\n\tturns: ApprovalTurn[];\n\tinstruction: string;\n\tmode: \"QUESTION\" | \"REVISION\";\n}): string {\n\treturn [\n\t\t`${options.mode} MODE`,\n\t\t\"\",\n\t\t`Human ${options.mode === \"QUESTION\" ? \"question\" : \"revision instruction\"}:`,\n\t\toptions.instruction,\n\t\t\"\",\n\t\t\"<proposal>\",\n\t\tJSON.stringify(options.proposal, undefined, \"\\t\"),\n\t\t\"</proposal>\",\n\t\t\"\",\n\t\t\"<approval_transcript>\",\n\t\trenderApprovalTranscript(options.turns),\n\t\t\"</approval_transcript>\",\n\t\t\"\",\n\t\t`<evidence_corpus truncated=\"${String(options.corpus.truncated)}\">`,\n\t\toptions.corpus.text,\n\t\t\"</evidence_corpus>\",\n\t].join(\"\\n\");\n}\n\nasync function proposalModels(paths: EvoPaths, proposal: Proposal): Promise<{ reflector?: string; critic?: string }> {\n\tconst bundle = await loadCompiledBundle(paths, proposal.parentBundleDigest);\n\treturn bundle.policy.modelRouting ?? {};\n}\n\nexport async function askProposalQuestion(options: AskProposalQuestionOptions): Promise<AskProposalQuestionResult> {\n\tconst question = options.question.trim();\n\tif (!question) throw new Error(\"Permit question must not be empty\");\n\tconst proposal = await loadProposal(options.paths, options.proposalId);\n\tassertExpectedProposal(proposal, options.expected, new Set([\"pending\", \"deferred\"]));\n\tconst questionTurn = await appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: proposal.id,\n\t\trevision: proposal.revision,\n\t\tdiffDigest: proposal.diffDigest,\n\t\trole: \"human\",\n\t\tkind: \"question\",\n\t\ttext: question,\n\t});\n\tconst [corpus, models, systemPrompt, turns] = await Promise.all([\n\t\tcollectEvidenceCorpus(options.paths, { maxBytes: options.maxCorpusBytes, mode: \"full\" }),\n\t\tproposalModels(options.paths, proposal),\n\t\treadFile(PERMIT_PROMPT_URL, \"utf8\"),\n\t\treadApprovalTurns(options.paths, proposal.id),\n\t]);\n\tconst run = await options.runner.run({\n\t\tcwd: options.cwd ?? process.cwd(),\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\tsystemPrompt,\n\t\tprompt: permitRunRequest({ proposal, corpus, turns, instruction: question, mode: \"QUESTION\" }),\n\t\t...((options.model ?? models.reflector) ? { model: options.model ?? models.reflector } : {}),\n\t\t...(options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),\n\t\tsessionIdentity: `evo-permit-${proposal.id}-r${proposal.revision}`,\n\t});\n\tawait recordModelUsage(options.paths, \"permit\", run);\n\tconst parsed = parsePermitAnswer(run.text);\n\tconst evidence = await validatePermitAnswer(options.paths, proposal, parsed);\n\tconst current = await loadProposal(options.paths, proposal.id);\n\tassertExpectedProposal(current, options.expected, new Set([\"pending\", \"deferred\"]));\n\tconst answerTurn = await appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: proposal.id,\n\t\trevision: proposal.revision,\n\t\tdiffDigest: proposal.diffDigest,\n\t\trole: \"meta\",\n\t\tkind: \"answer\",\n\t\ttext: parsed.answerMarkdown,\n\t\t...(evidence.length > 0 ? { evidence } : {}),\n\t});\n\treturn {\n\t\tproposal: current,\n\t\tquestionTurn,\n\t\tanswerTurn,\n\t\tanswerMarkdown: parsed.answerMarkdown,\n\t\tevidence,\n\t\tcorpus,\n\t\trun,\n\t};\n}\n\nexport async function reviseProposalFromInstruction(options: ReviseProposalFromInstructionOptions): Promise<Proposal> {\n\tconst instruction = options.instruction.trim();\n\tif (!instruction) throw new Error(\"Permit revision instruction must not be empty\");\n\tconst current = await loadProposal(options.paths, options.proposalId);\n\tassertExpectedProposal(current, options.expected, new Set([\"pending\"]));\n\tawait appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: current.id,\n\t\trevision: current.revision,\n\t\tdiffDigest: current.diffDigest,\n\t\trole: \"human\",\n\t\tkind: \"revision-request\",\n\t\ttext: instruction,\n\t});\n\tconst [corpus, models, systemPrompt, turns] = await Promise.all([\n\t\tcollectEvidenceCorpus(options.paths, { maxBytes: options.maxCorpusBytes, mode: \"full\" }),\n\t\tproposalModels(options.paths, current),\n\t\treadFile(PERMIT_PROMPT_URL, \"utf8\"),\n\t\treadApprovalTurns(options.paths, current.id),\n\t]);\n\tconst run = await options.runner.run({\n\t\tcwd: options.cwd ?? options.repositoryCwd ?? process.cwd(),\n\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\tsystemPrompt,\n\t\tprompt: permitRunRequest({ proposal: current, corpus, turns, instruction, mode: \"REVISION\" }),\n\t\t...((options.model ?? models.reflector) ? { model: options.model ?? models.reflector } : {}),\n\t\t...(options.thinkingLevel ? { thinkingLevel: options.thinkingLevel } : {}),\n\t\tsessionIdentity: `evo-permit-${current.id}-r${current.revision}`,\n\t});\n\tawait recordModelUsage(options.paths, \"permit\", run);\n\tconst output = parseReflectorOutput(run.text);\n\tif (output.proposals.length !== 1) throw new Error(\"Permit revision must return exactly one proposal draft\");\n\tconst draft = output.proposals[0];\n\tdraft.evidence = await validateDraftGrounding(options.paths, draft);\n\tlet revised = await reviseProposal({\n\t\tpaths: options.paths,\n\t\tid: current.id,\n\t\texpected: options.expected,\n\t\tdraft,\n\t\tobservationsMarkdown: output.observationsMarkdown,\n\t\t...(options.repositoryCwd ? { repositoryCwd: options.repositoryCwd } : {}),\n\t\t...(options.codeValidationExecutor ? { codeValidationExecutor: options.codeValidationExecutor } : {}),\n\t});\n\tlet replay: CounterfactualReplayResult | undefined;\n\tif (revised.tier === \"T2\") {\n\t\treplay = await runCounterfactualReplay({\n\t\t\tpaths: options.paths,\n\t\t\trunner: options.runner,\n\t\t\tproposal: revised,\n\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\t...(options.replayModel ? { model: options.replayModel } : {}),\n\t\t\t...(options.replayThinkingLevel ? { thinkingLevel: options.replayThinkingLevel } : {}),\n\t\t});\n\t\trevised = replay.proposal;\n\t}\n\tif (revised.tier === \"T1\" || revised.tier === \"T2\") {\n\t\trevised = (\n\t\t\tawait runCritic({\n\t\t\t\tpaths: options.paths,\n\t\t\t\trunner: options.runner,\n\t\t\t\tproposal: revised,\n\t\t\t\tcorpus,\n\t\t\t\t...(replay ? { replay } : {}),\n\t\t\t\tcwd: options.cwd ?? options.repositoryCwd ?? process.cwd(),\n\t\t\t\t...(options.agentDir ? { agentDir: options.agentDir } : {}),\n\t\t\t\t...((options.criticModel ?? models.critic) ? { model: options.criticModel ?? models.critic } : {}),\n\t\t\t\t...(options.criticThinkingLevel ? { thinkingLevel: options.criticThinkingLevel } : {}),\n\t\t\t})\n\t\t).proposal;\n\t}\n\tconst stored = await loadProposal(options.paths, revised.id);\n\tassertExpectedProposal(stored, proposalApproval(revised), new Set([\"pending\"]));\n\tawait appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: stored.id,\n\t\trevision: stored.revision,\n\t\tdiffDigest: stored.diffDigest,\n\t\trole: \"meta\",\n\t\tkind: \"revision-result\",\n\t\ttext: `Revision ${stored.revision} is ready as ${stored.tier}/${stored.kind}; L1 ${stored.l1.passed ? \"passed\" : \"failed\"}; diff digest ${stored.diffDigest}.`,\n\t\t...(stored.evidence.length > 0 ? { evidence: stored.evidence } : {}),\n\t});\n\treturn stored;\n}\n\nexport function recordPermitDefer(options: RecordPermitDecisionOptions): Promise<ApprovalTurn> {\n\tconst detail = options.until ? `${options.reason} (until ${options.until})` : options.reason;\n\treturn appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: options.proposalId,\n\t\trevision: options.revision,\n\t\tdiffDigest: options.diffDigest,\n\t\trole: \"human\",\n\t\tkind: \"defer\",\n\t\ttext: detail,\n\t});\n}\n\nexport function recordPermitReopen(options: RecordPermitDecisionOptions): Promise<ApprovalTurn> {\n\treturn appendApprovalTurn({\n\t\tpaths: options.paths,\n\t\tproposalId: options.proposalId,\n\t\trevision: options.revision,\n\t\tdiffDigest: options.diffDigest,\n\t\trole: \"human\",\n\t\tkind: \"reopen\",\n\t\ttext: options.reason,\n\t});\n}\n"]}