{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/missions/store.ts"],"names":[],"mappings":"AAMA,OAAO,EAEN,KAAK,uBAAuB,EAI5B,KAAK,kBAAkB,EAIvB,KAAK,iBAAiB,EAItB,KAAK,aAAa,EAIlB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAGzB,KAAK,kBAAkB,EACvB,MAAM,YAAY,CAAC;AAsFpB,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAc,GAAG,MAAM,CAM7E;AA+ED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,SAAmB,GAAG,aAAa,CAyC3F;AAOD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAoB,GAAG,kBAAkB,GAAG,SAAS,CAiCpH;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,oBAAoB,CAevB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3F;AAyDD,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,kBAAkB,EACzB,GAAG,OAAa,EAChB,cAAc,SAAgE,GAC5E,aAAa,CA2Bf;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAY,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAKhD;CACD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CAe5F;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,oBAAoB,GAAG,iBAAiB,CAmB9E;AAED,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,oBAAoB,EAC9B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,kBAAkB,EAC1B,GAAG,OAAa,EAChB,cAAc,SAAgE,GAC5E,aAAa,CA6Ff;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,uBAAuB,CA4ClF","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { writePrivateAtomicJson } from \"../shared/atomic-json.ts\";\nimport { getAgentDir } from \"../shared/utils.ts\";\nimport {\n\ttype GlobalMissionIndexRecord,\n\ttype GlobalMissionListResult,\n\tMISSION_STATUSES,\n\ttype MissionArtifact,\n\ttype MissionArtifactKind,\n\ttype MissionCreateInput,\n\ttype MissionDecision,\n\ttype MissionGoal,\n\ttype MissionIndexEntry,\n\ttype MissionListResult,\n\ttype MissionReceipt,\n\ttype MissionReceiptKind,\n\ttype MissionReceiptStatus,\n\ttype MissionRecord,\n\ttype MissionRunLink,\n\ttype MissionRunMode,\n\ttype MissionStatus,\n\ttype MissionStoreConfig,\n\ttype MissionStoreLocation,\n\ttype MissionTokenBudget,\n\ttype MissionTokenUsage,\n\ttype MissionUpdateInput,\n} from \"./types.ts\";\n\nconst MISSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;\nconst MISSION_RUN_MODES = new Set<MissionRunMode>([\"single\", \"parallel\", \"chain\", \"workflow\", \"scheduled\", \"external\"]);\nconst MISSION_ARTIFACT_KINDS = new Set<MissionArtifactKind>([\n\t\"status\",\n\t\"output\",\n\t\"patch\",\n\t\"manifest\",\n\t\"review\",\n\t\"note\",\n\t\"other\",\n]);\nconst MISSION_RECEIPT_KINDS = new Set<MissionReceiptKind>([\"pull_request\", \"ci\", \"deployment\", \"release\"]);\nconst MISSION_RECEIPT_STATUSES = new Set<MissionReceiptStatus>([\"pending\", \"ready\", \"succeeded\", \"failed\"]);\nconst MISSION_STATUS_SET = new Set<MissionStatus>(MISSION_STATUSES);\nconst TERMINAL_MISSION_STATUSES = new Set<MissionStatus>([\"completed\", \"failed\", \"cancelled\"]);\nconst DEFAULT_TERMINAL_MISSION_RETENTION = 200;\n\nfunction asObject(value: unknown, label: string): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`${label} must be a JSON object`);\n\treturn value as Record<string, unknown>;\n}\n\nfunction requiredString(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 optionalString(value: unknown, label: string): string | undefined {\n\tif (value === undefined) return undefined;\n\treturn requiredString(value, label);\n}\n\nfunction timestamp(value: unknown, label: string): string {\n\tconst result = requiredString(value, label);\n\tif (Number.isNaN(Date.parse(result))) throw new Error(`${label} must be an ISO timestamp`);\n\treturn result;\n}\n\nfunction missionStatus(value: unknown, label: string): MissionStatus {\n\tif (typeof value !== \"string\" || !MISSION_STATUS_SET.has(value as MissionStatus)) {\n\t\tthrow new Error(`${label} must be one of ${MISSION_STATUSES.join(\", \")}`);\n\t}\n\treturn value as MissionStatus;\n}\n\nfunction positiveTokenCount(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 1) throw new Error(`${label} must be a positive integer`);\n\treturn value as number;\n}\n\nfunction nonNegativeTokenCount(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0)\n\t\tthrow new Error(`${label} must be a non-negative integer`);\n\treturn value as number;\n}\n\nfunction parseStoredGoal(value: unknown, label: string): { goal?: MissionGoal; legacyObjective?: string } {\n\tif (typeof value === \"string\") return { legacyObjective: requiredString(value, label).trim() };\n\treturn { goal: parseGoal(value, label) };\n}\n\nfunction parseGoal(value: unknown, label: string): MissionGoal {\n\tconst input = asObject(value, label);\n\tif (input.status !== \"active\" && input.status !== \"paused\" && input.status !== \"budget-exhausted\")\n\t\tthrow new Error(`${label}.status is invalid`);\n\treturn { status: input.status };\n}\n\nfunction parseBudget(value: unknown, label: string): MissionTokenBudget {\n\tconst input = asObject(value, label);\n\treturn { tokens: positiveTokenCount(input.tokens, `${label}.tokens`) };\n}\n\nfunction parseUsage(value: unknown, label: string): MissionTokenUsage {\n\tconst input = asObject(value, label);\n\treturn { tokens: nonNegativeTokenCount(input.tokens, `${label}.tokens`) };\n}\n\nfunction stringArray(value: unknown, label: string): string[] {\n\tif (!Array.isArray(value)) throw new Error(`${label} must be an array of non-empty strings`);\n\tconst result = value.map((item, index) => requiredString(item, `${label}[${index}]`).trim());\n\treturn [...new Set(result)];\n}\n\nexport function validateMissionId(value: unknown, label = \"missionId\"): string {\n\tconst id = requiredString(value, label);\n\tif (!MISSION_ID_PATTERN.test(id) || id.includes(\"..\")) {\n\t\tthrow new Error(`${label} must contain only letters, numbers, '.', '_', or '-' and cannot contain '..'`);\n\t}\n\treturn id;\n}\n\nfunction parseRunLink(value: unknown, label: string): MissionRunLink {\n\tconst input = asObject(value, label);\n\tconst runId = requiredString(input.runId, `${label}.runId`);\n\tconst mode = requiredString(input.mode, `${label}.mode`) as MissionRunMode;\n\tif (!MISSION_RUN_MODES.has(mode)) throw new Error(`${label}.mode is invalid`);\n\tif (input.childIndex !== undefined && (!Number.isInteger(input.childIndex) || (input.childIndex as number) < 0)) {\n\t\tthrow new Error(`${label}.childIndex must be a non-negative integer`);\n\t}\n\treturn {\n\t\trunId,\n\t\tmode,\n\t\t...(optionalString(input.asyncDir, `${label}.asyncDir`) ? { asyncDir: input.asyncDir as string } : {}),\n\t\t...(input.childIndex !== undefined ? { childIndex: input.childIndex as number } : {}),\n\t\t...(optionalString(input.agent, `${label}.agent`) ? { agent: input.agent as string } : {}),\n\t\t...(optionalString(input.status, `${label}.status`) ? { status: input.status as string } : {}),\n\t\t...(input.startedAt !== undefined ? { startedAt: timestamp(input.startedAt, `${label}.startedAt`) } : {}),\n\t\t...(input.completedAt !== undefined ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {}),\n\t\t...(input.usage !== undefined ? { usage: parseUsage(input.usage, `${label}.usage`) } : {}),\n\t};\n}\n\nfunction parseDecision(value: unknown, label: string): MissionDecision {\n\tconst input = asObject(value, label);\n\tconst status = input.status;\n\tif (status !== \"open\" && status !== \"resolved\") throw new Error(`${label}.status must be \"open\" or \"resolved\"`);\n\treturn {\n\t\tid: validateMissionId(input.id, `${label}.id`),\n\t\tstatus,\n\t\ttitle: requiredString(input.title, `${label}.title`),\n\t\tcreatedAt: timestamp(input.createdAt, `${label}.createdAt`),\n\t\t...(optionalString(input.prompt, `${label}.prompt`) ? { prompt: input.prompt as string } : {}),\n\t\t...(input.options !== undefined ? { options: stringArray(input.options, `${label}.options`) } : {}),\n\t\t...(optionalString(input.recommendation, `${label}.recommendation`)\n\t\t\t? { recommendation: input.recommendation as string }\n\t\t\t: {}),\n\t\t...(input.resolvedAt !== undefined ? { resolvedAt: timestamp(input.resolvedAt, `${label}.resolvedAt`) } : {}),\n\t\t...(optionalString(input.resolution, `${label}.resolution`) ? { resolution: input.resolution as string } : {}),\n\t};\n}\n\nfunction parseArtifact(value: unknown, label: string): MissionArtifact {\n\tconst input = asObject(value, label);\n\tconst kind = requiredString(input.kind, `${label}.kind`) as MissionArtifactKind;\n\tif (!MISSION_ARTIFACT_KINDS.has(kind)) throw new Error(`${label}.kind is invalid`);\n\treturn {\n\t\tkind,\n\t\tpath: requiredString(input.path, `${label}.path`),\n\t\t...(optionalString(input.description, `${label}.description`)\n\t\t\t? { description: input.description as string }\n\t\t\t: {}),\n\t};\n}\n\nfunction parseReceipt(value: unknown, label: string): MissionReceipt {\n\tconst input = asObject(value, label);\n\tconst kind = requiredString(input.kind, `${label}.kind`) as MissionReceiptKind;\n\tconst status = requiredString(input.status, `${label}.status`) as MissionReceiptStatus;\n\tif (!MISSION_RECEIPT_KINDS.has(kind)) throw new Error(`${label}.kind is invalid`);\n\tif (!MISSION_RECEIPT_STATUSES.has(status)) throw new Error(`${label}.status is invalid`);\n\tconst url = requiredString(input.url, `${label}.url`);\n\ttry {\n\t\tnew URL(url);\n\t} catch {\n\t\tthrow new Error(`${label}.url must be an absolute URL`);\n\t}\n\treturn {\n\t\tkind,\n\t\tstatus,\n\t\ttitle: requiredString(input.title, `${label}.title`),\n\t\turl,\n\t\tcreatedAt: timestamp(input.createdAt, `${label}.createdAt`),\n\t\t...(optionalString(input.description, `${label}.description`)\n\t\t\t? { description: input.description as string }\n\t\t\t: {}),\n\t};\n}\n\nexport function parseMissionRecord(value: unknown, source = \"mission record\"): MissionRecord {\n\tconst input = asObject(value, source);\n\tif (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);\n\tif (!Array.isArray(input.runs)) throw new Error(`${source}.runs must be an array`);\n\tif (!Array.isArray(input.decisions)) throw new Error(`${source}.decisions must be an array`);\n\tif (!Array.isArray(input.artifacts)) throw new Error(`${source}.artifacts must be an array`);\n\tif (input.receipts !== undefined && !Array.isArray(input.receipts))\n\t\tthrow new Error(`${source}.receipts must be an array`);\n\tconst runs = input.runs as unknown[];\n\tconst decisions = input.decisions as unknown[];\n\tconst artifacts = input.artifacts as unknown[];\n\tconst receipts = (input.receipts ?? []) as unknown[];\n\tconst parsedGoal = input.goal !== undefined ? parseStoredGoal(input.goal, `${source}.goal`) : {};\n\tconst budget = input.budget !== undefined ? parseBudget(input.budget, `${source}.budget`) : undefined;\n\tconst usage = input.usage !== undefined ? parseUsage(input.usage, `${source}.usage`) : undefined;\n\tconst objective = optionalString(input.objective, `${source}.objective`)?.trim() ?? parsedGoal.legacyObjective;\n\tif (!objective) throw new Error(`${source}.objective must be a non-empty string`);\n\tif (parsedGoal.goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);\n\treturn {\n\t\tschemaVersion: 1,\n\t\tid: validateMissionId(input.id, `${source}.id`),\n\t\ttitle: requiredString(input.title, `${source}.title`),\n\t\tobjective,\n\t\t...(parsedGoal.goal ? { goal: parsedGoal.goal } : {}),\n\t\t...(budget ? { budget } : {}),\n\t\t...(usage ? { usage } : {}),\n\t\tstatus: missionStatus(input.status, `${source}.status`),\n\t\tcreatedAt: timestamp(input.createdAt, `${source}.createdAt`),\n\t\tupdatedAt: timestamp(input.updatedAt, `${source}.updatedAt`),\n\t\truns: runs.map((item, index) => parseRunLink(item, `${source}.runs[${index}]`)),\n\t\tdecisions: decisions.map((item, index) => parseDecision(item, `${source}.decisions[${index}]`)),\n\t\tartifacts: artifacts.map((item, index) => parseArtifact(item, `${source}.artifacts[${index}]`)),\n\t\treceipts: receipts.map((item, index) => parseReceipt(item, `${source}.receipts[${index}]`)),\n\t\t...(optionalString(input.cwd, `${source}.cwd`) ? { cwd: input.cwd as string } : {}),\n\t\t...(optionalString(input.ownerSessionId, `${source}.ownerSessionId`)\n\t\t\t? { ownerSessionId: input.ownerSessionId as string }\n\t\t\t: {}),\n\t\t...(optionalString(input.summary, `${source}.summary`) ? { summary: input.summary as string } : {}),\n\t\t...(input.acceptance !== undefined ? { acceptance: input.acceptance } : {}),\n\t\t...(input.labels !== undefined ? { labels: stringArray(input.labels, `${source}.labels`) } : {}),\n\t};\n}\n\nfunction expandConfiguredPath(value: string, projectRoot: string): string {\n\tconst expanded = value.startsWith(\"~/\") ? path.join(os.homedir(), value.slice(2)) : value;\n\treturn path.isAbsolute(expanded) ? path.normalize(expanded) : path.resolve(projectRoot, expanded);\n}\n\nexport function validateMissionStoreConfig(value: unknown, label = \"config.missions\"): MissionStoreConfig | undefined {\n\tif (value === undefined) return undefined;\n\tconst input = asObject(value, label);\n\tfor (const key of Object.keys(input)) {\n\t\tif (\n\t\t\tkey !== \"enabled\" &&\n\t\t\tkey !== \"directory\" &&\n\t\t\tkey !== \"globalIndex\" &&\n\t\t\tkey !== \"globalIndexDir\" &&\n\t\t\tkey !== \"retainTerminal\"\n\t\t) {\n\t\t\tthrow new Error(`${label}.${key} is unknown`);\n\t\t}\n\t}\n\tif (input.enabled !== undefined && typeof input.enabled !== \"boolean\")\n\t\tthrow new Error(`${label}.enabled must be boolean`);\n\tif (input.globalIndex !== undefined && typeof input.globalIndex !== \"boolean\")\n\t\tthrow new Error(`${label}.globalIndex must be boolean`);\n\tif (\n\t\tinput.retainTerminal !== undefined &&\n\t\t(!Number.isInteger(input.retainTerminal) || (input.retainTerminal as number) < 1)\n\t) {\n\t\tthrow new Error(`${label}.retainTerminal must be a positive integer`);\n\t}\n\tconst directory = optionalString(input.directory, `${label}.directory`);\n\tconst globalIndexDir = optionalString(input.globalIndexDir, `${label}.globalIndexDir`);\n\treturn {\n\t\t...(typeof input.enabled === \"boolean\" ? { enabled: input.enabled } : {}),\n\t\t...(directory ? { directory } : {}),\n\t\t...(typeof input.globalIndex === \"boolean\" ? { globalIndex: input.globalIndex } : {}),\n\t\t...(globalIndexDir ? { globalIndexDir } : {}),\n\t\t...(input.retainTerminal !== undefined ? { retainTerminal: input.retainTerminal as number } : {}),\n\t};\n}\n\nexport function resolveMissionStoreLocation(input: {\n\tprojectRoot: string;\n\tconfig?: MissionStoreConfig;\n\tagentDir?: string;\n}): MissionStoreLocation {\n\tconst projectRoot = path.resolve(input.projectRoot);\n\tconst missionDir = input.config?.directory\n\t\t? expandConfiguredPath(input.config.directory, projectRoot)\n\t\t: path.join(projectRoot, \".pi-subagents\", \"missions\");\n\tconst globalIndexDir = input.config?.globalIndexDir\n\t\t? expandConfiguredPath(input.config.globalIndexDir, projectRoot)\n\t\t: path.join(input.agentDir ?? getAgentDir(), \"missions\", \"index\");\n\treturn {\n\t\tprojectRoot,\n\t\tmissionDir,\n\t\tglobalIndexDir,\n\t\twriteGlobalIndex: input.config?.globalIndex !== false,\n\t\t...(input.config?.retainTerminal !== undefined ? { retainTerminal: input.config.retainTerminal } : {}),\n\t};\n}\n\nexport function missionRecordPath(location: MissionStoreLocation, missionId: string): string {\n\treturn path.join(location.missionDir, `${validateMissionId(missionId)}.json`);\n}\n\nfunction parseIndexEntry(value: unknown, source: string): MissionIndexEntry {\n\tconst input = asObject(value, source);\n\tif (input.schemaVersion !== 1) throw new Error(`${source}.schemaVersion must be 1`);\n\treturn {\n\t\tschemaVersion: 1,\n\t\tmissionId: validateMissionId(input.missionId, `${source}.missionId`),\n\t\tprojectRoot: requiredString(input.projectRoot, `${source}.projectRoot`),\n\t\trecordPath: requiredString(input.recordPath, `${source}.recordPath`),\n\t\ttitle: requiredString(input.title, `${source}.title`),\n\t\tstatus: missionStatus(input.status, `${source}.status`),\n\t\tupdatedAt: timestamp(input.updatedAt, `${source}.updatedAt`),\n\t\t...(optionalString(input.lastRunId, `${source}.lastRunId`) ? { lastRunId: input.lastRunId as string } : {}),\n\t};\n}\n\nfunction indexPath(location: MissionStoreLocation, record: MissionRecord): string {\n\tconst key = createHash(\"sha256\").update(`${location.projectRoot}\\0${record.id}`).digest(\"hex\");\n\treturn path.join(location.globalIndexDir, `${key}.json`);\n}\n\nfunction writeMission(location: MissionStoreLocation, record: MissionRecord): MissionRecord {\n\tconst validated = parseMissionRecord(record);\n\twritePrivateAtomicJson(missionRecordPath(location, validated.id), validated);\n\tif (location.writeGlobalIndex) {\n\t\tconst lastRunId = validated.runs.at(-1)?.runId;\n\t\tconst entry: MissionIndexEntry = {\n\t\t\tschemaVersion: 1,\n\t\t\tmissionId: validated.id,\n\t\t\tprojectRoot: location.projectRoot,\n\t\t\trecordPath: missionRecordPath(location, validated.id),\n\t\t\ttitle: validated.title,\n\t\t\tstatus: validated.status,\n\t\t\tupdatedAt: validated.updatedAt,\n\t\t\t...(lastRunId ? { lastRunId } : {}),\n\t\t};\n\t\twritePrivateAtomicJson(indexPath(location, validated), entry);\n\t}\n\treturn validated;\n}\n\nfunction pruneTerminalMissions(location: MissionStoreLocation, maxTerminal: number): void {\n\tconst terminal = listMissions(location)\n\t\t.records.filter((record) => TERMINAL_MISSION_STATUSES.has(record.status))\n\t\t.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));\n\tfor (const record of terminal.slice(maxTerminal)) {\n\t\ttry {\n\t\t\tfs.rmSync(missionRecordPath(location, record.id), { force: true });\n\t\t\tfs.rmSync(path.join(location.missionDir, record.id), { recursive: true, force: true });\n\t\t\tif (location.writeGlobalIndex) fs.rmSync(indexPath(location, record), { force: true });\n\t\t} catch {\n\t\t\t// Retention is best-effort and must never block a launch.\n\t\t}\n\t}\n}\n\nexport function createMission(\n\tlocation: MissionStoreLocation,\n\tinput: MissionCreateInput,\n\tnow = new Date(),\n\tretainTerminal = location.retainTerminal ?? DEFAULT_TERMINAL_MISSION_RETENTION,\n): MissionRecord {\n\tconst createdAt = now.toISOString();\n\tconst record: MissionRecord = {\n\t\tschemaVersion: 1,\n\t\tid: randomUUID(),\n\t\ttitle: requiredString(input.title, \"mission.title\").trim(),\n\t\tobjective: requiredString(input.objective, \"mission.objective\").trim(),\n\t\t...(input.goal === true ? { goal: { status: \"active\" as const } } : {}),\n\t\t...(input.budget ? { budget: parseBudget(input.budget, \"mission.budget\") } : {}),\n\t\t...(input.goal === true ? { usage: { tokens: 0 } } : {}),\n\t\tstatus: input.status ?? \"planned\",\n\t\tcreatedAt,\n\t\tupdatedAt: createdAt,\n\t\tcwd: location.projectRoot,\n\t\truns: [],\n\t\tdecisions: [],\n\t\tartifacts: [],\n\t\treceipts: [],\n\t\t...(input.ownerSessionId\n\t\t\t? { ownerSessionId: requiredString(input.ownerSessionId, \"mission.ownerSessionId\") }\n\t\t\t: {}),\n\t\t...(input.labels ? { labels: stringArray(input.labels, \"mission.labels\") } : {}),\n\t};\n\tif (input.goal === true && !input.budget) throw new Error(\"mission.budget is required when mission.goal is true\");\n\tconst created = writeMission(location, record);\n\tpruneTerminalMissions(location, retainTerminal);\n\treturn created;\n}\n\nexport class MissionNotFoundError extends Error {\n\treadonly code = \"MISSION_NOT_FOUND\";\n\treadonly missionId: string;\n\treadonly missionDir: string;\n\n\tconstructor(missionId: string, missionDir: string) {\n\t\tsuper(`Mission '${missionId}' was not found in ${missionDir}`);\n\t\tthis.name = \"MissionNotFoundError\";\n\t\tthis.missionId = missionId;\n\t\tthis.missionDir = missionDir;\n\t}\n}\n\nexport function readMission(location: MissionStoreLocation, missionId: string): MissionRecord {\n\tconst filePath = missionRecordPath(location, missionId);\n\tlet raw: string;\n\ttry {\n\t\traw = fs.readFileSync(filePath, \"utf-8\");\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\")\n\t\t\tthrow new MissionNotFoundError(missionId, location.missionDir);\n\t\tthrow error;\n\t}\n\ttry {\n\t\treturn parseMissionRecord(JSON.parse(raw), filePath);\n\t} catch (error) {\n\t\tthrow new Error(`Invalid mission file '${filePath}': ${error instanceof Error ? error.message : String(error)}`);\n\t}\n}\n\nexport function listMissions(location: MissionStoreLocation): MissionListResult {\n\tif (!fs.existsSync(location.missionDir)) return { records: [], warnings: [] };\n\tconst records: MissionRecord[] = [];\n\tconst warnings: string[] = [];\n\tfor (const name of fs\n\t\t.readdirSync(location.missionDir)\n\t\t.filter((item) => item.endsWith(\".json\"))\n\t\t.sort()) {\n\t\tconst filePath = path.join(location.missionDir, name);\n\t\ttry {\n\t\t\trecords.push(parseMissionRecord(JSON.parse(fs.readFileSync(filePath, \"utf-8\")), filePath));\n\t\t} catch (error) {\n\t\t\twarnings.push(\n\t\t\t\t`Skipped corrupt mission '${filePath}': ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t}\n\trecords.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));\n\treturn { records, warnings };\n}\n\nexport function updateMission(\n\tlocation: MissionStoreLocation,\n\tmissionId: string,\n\tupdate: MissionUpdateInput,\n\tnow = new Date(),\n\tretainTerminal = location.retainTerminal ?? DEFAULT_TERMINAL_MISSION_RETENTION,\n): MissionRecord {\n\tconst current = readMission(location, missionId);\n\tconst runs = [...current.runs];\n\tfor (const candidate of update.addRuns ?? []) {\n\t\tconst run = parseRunLink(candidate, \"mission.update.addRuns[]\");\n\t\tconst existingIndex = runs.findIndex((item) => item.runId === run.runId && item.childIndex === run.childIndex);\n\t\tif (existingIndex === -1) runs.push(run);\n\t\telse runs[existingIndex] = { ...runs[existingIndex]!, ...run };\n\t}\n\tconst artifacts = [...current.artifacts];\n\tfor (const candidate of update.addArtifacts ?? []) {\n\t\tconst artifact = parseArtifact(candidate, \"mission.update.addArtifacts[]\");\n\t\tconst existingIndex = artifacts.findIndex(\n\t\t\t(item) => item.kind === artifact.kind && path.resolve(item.path) === path.resolve(artifact.path),\n\t\t);\n\t\tif (existingIndex === -1) artifacts.push(artifact);\n\t\telse artifacts[existingIndex] = { ...artifacts[existingIndex]!, ...artifact };\n\t}\n\tconst createdAt = now.toISOString();\n\tconst receipts = [...current.receipts];\n\tfor (const candidate of update.addReceipts ?? []) {\n\t\tconst receipt = parseReceipt({ ...candidate, createdAt }, \"mission.update.addReceipts[]\");\n\t\tconst existingIndex = receipts.findIndex((item) => item.kind === receipt.kind && item.url === receipt.url);\n\t\tif (existingIndex === -1) receipts.push(receipt);\n\t\telse receipts[existingIndex] = { ...receipt, createdAt: receipts[existingIndex]!.createdAt };\n\t}\n\tconst decisions = [\n\t\t...current.decisions,\n\t\t...(update.addDecisions ?? []).map(\n\t\t\t(decision): MissionDecision => ({\n\t\t\t\tid: randomUUID(),\n\t\t\t\tstatus: \"open\",\n\t\t\t\ttitle: requiredString(decision.title, \"mission.update.addDecisions[].title\"),\n\t\t\t\tcreatedAt,\n\t\t\t\t...(decision.prompt\n\t\t\t\t\t? { prompt: requiredString(decision.prompt, \"mission.update.addDecisions[].prompt\") }\n\t\t\t\t\t: {}),\n\t\t\t\t...(decision.options\n\t\t\t\t\t? { options: stringArray(decision.options, \"mission.update.addDecisions[].options\") }\n\t\t\t\t\t: {}),\n\t\t\t\t...(decision.recommendation\n\t\t\t\t\t? {\n\t\t\t\t\t\t\trecommendation: requiredString(\n\t\t\t\t\t\t\t\tdecision.recommendation,\n\t\t\t\t\t\t\t\t\"mission.update.addDecisions[].recommendation\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t}),\n\t\t),\n\t];\n\tconst budget = update.budget !== undefined ? parseBudget(update.budget, \"mission.update.budget\") : current.budget;\n\tconst usage =\n\t\tupdate.usage !== undefined\n\t\t\t? parseUsage(update.usage, \"mission.update.usage\")\n\t\t\t: { tokens: runs.reduce((total, run) => total + (run.usage?.tokens ?? 0), 0) };\n\tlet goal =\n\t\tupdate.goal === false\n\t\t\t? undefined\n\t\t\t: update.goal !== undefined\n\t\t\t\t? parseGoal(update.goal, \"mission.update.goal\")\n\t\t\t\t: current.goal;\n\tif (goal && !budget) throw new Error(\"mission.update.budget is required when enabling a goal mission\");\n\tif (goal && budget) {\n\t\tgoal =\n\t\t\tusage.tokens >= budget.tokens\n\t\t\t\t? { status: \"budget-exhausted\" }\n\t\t\t\t: goal.status === \"budget-exhausted\"\n\t\t\t\t\t? { status: \"active\" }\n\t\t\t\t\t: goal;\n\t}\n\tconst next: MissionRecord = {\n\t\t...current,\n\t\tupdatedAt: createdAt,\n\t\truns,\n\t\tartifacts,\n\t\treceipts,\n\t\tdecisions,\n\t\t...(update.title !== undefined ? { title: requiredString(update.title, \"mission.update.title\").trim() } : {}),\n\t\t...(update.objective !== undefined\n\t\t\t? { objective: requiredString(update.objective, \"mission.update.objective\").trim() }\n\t\t\t: {}),\n\t\t...(budget ? { budget } : {}),\n\t\t...(goal ? { goal, usage } : {}),\n\t\t...(update.status !== undefined ? { status: missionStatus(update.status, \"mission.update.status\") } : {}),\n\t\t...(update.summary !== undefined ? { summary: requiredString(update.summary, \"mission.update.summary\") } : {}),\n\t\t...(update.labels !== undefined ? { labels: stringArray(update.labels, \"mission.update.labels\") } : {}),\n\t\t...(update.acceptance !== undefined ? { acceptance: update.acceptance } : {}),\n\t};\n\tif (!goal) delete next.goal;\n\tconst updated = writeMission(location, next);\n\tif (TERMINAL_MISSION_STATUSES.has(updated.status)) pruneTerminalMissions(location, retainTerminal);\n\treturn updated;\n}\n\nexport function listGlobalMissions(globalIndexDir: string): GlobalMissionListResult {\n\tif (!fs.existsSync(globalIndexDir)) return { entries: [], warnings: [] };\n\tconst entries: GlobalMissionIndexRecord[] = [];\n\tconst warnings: string[] = [];\n\tfor (const name of fs\n\t\t.readdirSync(globalIndexDir)\n\t\t.filter((item) => item.endsWith(\".json\"))\n\t\t.sort()) {\n\t\tconst filePath = path.join(globalIndexDir, name);\n\t\ttry {\n\t\t\tconst entry = parseIndexEntry(JSON.parse(fs.readFileSync(filePath, \"utf-8\")), filePath);\n\t\t\ttry {\n\t\t\t\tconst record = parseMissionRecord(JSON.parse(fs.readFileSync(entry.recordPath, \"utf-8\")), entry.recordPath);\n\t\t\t\tif (record.id !== entry.missionId)\n\t\t\t\t\tthrow new Error(`record id '${record.id}' does not match index id '${entry.missionId}'`);\n\t\t\t\tentries.push({ ...entry, stale: false });\n\t\t\t} catch (error) {\n\t\t\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfs.rmSync(filePath, { force: true });\n\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t`Removed stale global mission pointer '${filePath}' because '${entry.recordPath}' no longer exists.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (removeError) {\n\t\t\t\t\t\twarnings.push(\n\t\t\t\t\t\t\t`Failed to remove stale global mission pointer '${filePath}': ${removeError instanceof Error ? removeError.message : String(removeError)}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tentries.push({\n\t\t\t\t\t...entry,\n\t\t\t\t\tstale: true,\n\t\t\t\t\tstaleReason: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (error) {\n\t\t\twarnings.push(\n\t\t\t\t`Skipped corrupt global mission index entry '${filePath}': ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t);\n\t\t}\n\t}\n\tentries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));\n\treturn { entries, warnings };\n}\n"]}