{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAchF,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,qBAAa,sBAAuB,YAAW,kBAAkB;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,YAAY,OAAO,EAAE,6BAA6B,EAGjD;IACD,OAAO,CAAC,IAAI;IAIZ;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;YAOR,IAAI;YAOJ,KAAK;YAaL,MAAM;IAuBd,MAAM,CAAC,QAAQ,EAAE,yBAAyB;;;;;;;;;;;;OAU/C;IACK,IAAI,CAAC,eAAe,EAAE,MAAM;;;;;;;;;;;;OAyBjC;IACK,IAAI,CAAC,QAAQ,EAAE,yBAAyB,EAAE,OAAO,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAO;;;;;;;;;;;;;;;OAa1F;IACK,MAAM,CAAC,CAAC,EACb,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,CACT,QAAQ,EAAE,yBAAyB,KAC/B;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,yBAAyB,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,GAChG,OAAO,CAAC;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,SAAS,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAQzG;IACK,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAS9B;IACK,aAAa,CAAC,CAAC,EACpB,eAAe,EAAE,MAAM,EACvB,EAAE,EAAE,CACH,QAAQ,EAAE,yBAAyB,EACnC,IAAI,EAAE,CAAC,QAAQ,EAAE,yBAAyB,KAAK,OAAO,CAAC,IAAI,CAAC,KACxD,OAAO,CAAC,CAAC,CAAC,GACb,OAAO,CAAC,CAAC,CAAC,CAYZ;CACD;AAED,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,MAAmC,GAAG,sBAAsB,CAE9G","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../../config.js\";\nimport { loadOperatorRoster, OPERATOR_ROSTER_NAMES } from \"../operator-roster.js\";\nimport type { OrchestrationPlanDocument, OrchestrationStore } from \"./types.js\";\nimport { validateOrchestrationPlan } from \"./validation.js\";\n\nconst SUFFIX = \".orchestration.json\";\nconst TMP_SUFFIX = \".tmp\";\n\nfunction safeId(id: string): boolean {\n\treturn /^[A-Za-z0-9._-]+$/u.test(id) && id !== \".\" && id !== \"..\";\n}\n\nfunction equal(a: unknown, b: unknown): boolean {\n\treturn JSON.stringify(a) === JSON.stringify(b);\n}\n\nexport interface FileOrchestrationStoreOptions {\n\troot: string;\n\tstoreId?: string;\n}\n\nexport function defaultOrchestrationRoot(): string {\n\treturn process.env.JENSEN_ORCHESTRATION_DIR?.trim() || path.join(os.homedir(), \".jensen\", \"orchestrations\");\n}\n\nexport class FileOrchestrationStore implements OrchestrationStore {\n\treadonly storeId: string;\n\tprivate readonly root: string;\n\tconstructor(options: FileOrchestrationStoreOptions) {\n\t\tthis.root = path.resolve(options.root);\n\t\tthis.storeId = options.storeId ?? \"file\";\n\t}\n\tprivate file(id: string): string {\n\t\tif (!safeId(id)) throw new Error(`Unsafe orchestration id: ${id}`);\n\t\treturn path.join(this.root, `${id}${SUFFIX}`);\n\t}\n\t/**\n\t * Explicit operator set for this file-backed store's plan validation.\n\t * The canonical subagent registry remains the primary authority; this\n\t * only extends it with the operator roster names loaded from\n\t * `getAgentDir()/agents/*.md`, so a roster-based plan created through the\n\t * automatic path stays readable (status/join) after the fact. A roster\n\t * that cannot be read must not break store reads: in that case the\n\t * canonical operator roles — which the roster guarantees to exist —\n\t * remain the accepted fallback.\n\t */\n\tprivate operatorAgents(): readonly string[] {\n\t\ttry {\n\t\t\treturn loadOperatorRoster({ agentDir: getAgentDir() }).agents.map((agent) => agent.name);\n\t\t} catch {\n\t\t\treturn [...OPERATOR_ROSTER_NAMES];\n\t\t}\n\t}\n\tprivate async read(id: string): Promise<OrchestrationPlanDocument | undefined> {\n\t\ttry {\n\t\t\treturn JSON.parse(await fsp.readFile(this.file(id), \"utf8\")) as OrchestrationPlanDocument;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tprivate async write(id: string, document: OrchestrationPlanDocument): Promise<void> {\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst target = this.file(id);\n\t\tconst tmp = `${target}.${randomUUID()}${TMP_SUFFIX}`;\n\t\tconst fh = await fsp.open(tmp, \"w\", 0o600);\n\t\ttry {\n\t\t\tawait fh.writeFile(JSON.stringify(document, null, 2), \"utf8\");\n\t\t\tawait fh.sync();\n\t\t} finally {\n\t\t\tawait fh.close();\n\t\t}\n\t\tawait fsp.rename(tmp, target);\n\t}\n\tprivate async locked<T>(id: string, fn: () => Promise<T>): Promise<T> {\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst target = this.file(id);\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\ttry {\n\t\t\trelease = await lockfile.lock(target, {\n\t\t\t\trealpath: false,\n\t\t\t\tstale: 30_000,\n\t\t\t\tretries: { retries: 8, factor: 2, minTimeout: 20, maxTimeout: 250, randomize: true },\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthrow new Error(`ORCHESTRATION_LOCK_FAILED: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tawait release?.();\n\t\t\t} catch {\n\t\t\t\t/* best effort */\n\t\t\t}\n\t\t}\n\t}\n\tasync create(document: OrchestrationPlanDocument) {\n\t\treturn this.locked(document.plan.orchestrationId, async () => {\n\t\t\tconst existing = await this.read(document.plan.orchestrationId);\n\t\t\tif (existing)\n\t\t\t\treturn equal(existing.plan, document.plan)\n\t\t\t\t\t? { status: \"idempotent\" as const, document: existing }\n\t\t\t\t\t: { status: \"conflict\" as const, error: \"orchestration already exists with different plan\" };\n\t\t\tawait this.write(document.plan.orchestrationId, document);\n\t\t\treturn { status: \"created\" as const };\n\t\t});\n\t}\n\tasync load(orchestrationId: string) {\n\t\tconst document = await this.read(orchestrationId);\n\t\tif (!document) {\n\t\t\ttry {\n\t\t\t\tawait fsp.access(this.file(orchestrationId));\n\t\t\t\treturn { status: \"corrupt\" as const, diagnostic: \"invalid orchestration JSON\" };\n\t\t\t} catch {\n\t\t\t\treturn { status: \"missing\" as const };\n\t\t\t}\n\t\t}\n\t\tif (\n\t\t\tdocument.schemaVersion !== 1 ||\n\t\t\tdocument.plan.schemaVersion !== 1 ||\n\t\t\tdocument.plan.orchestrationId !== orchestrationId\n\t\t)\n\t\t\treturn { status: \"corrupt\" as const, diagnostic: \"invalid orchestration schema or identity\" };\n\t\tconst validation = validateOrchestrationPlan(document.plan, {\n\t\t\toperatorAgents: this.operatorAgents(),\n\t\t});\n\t\tif (!validation.valid)\n\t\t\treturn {\n\t\t\t\tstatus: \"corrupt\" as const,\n\t\t\t\tdiagnostic: validation.issues.map((issue) => `${issue.code}: ${issue.message}`).join(\"; \"),\n\t\t\t};\n\t\treturn { status: \"ok\" as const, document };\n\t}\n\tasync save(document: OrchestrationPlanDocument, options: { expectedRevision?: number } = {}) {\n\t\treturn this.locked(document.plan.orchestrationId, async () => {\n\t\t\tconst current = await this.read(document.plan.orchestrationId);\n\t\t\tif (!current) return { status: \"stale\" as const, expectedRevision: options.expectedRevision ?? 0 };\n\t\t\tif (options.expectedRevision !== undefined && current.plan.revision !== options.expectedRevision)\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"stale\" as const,\n\t\t\t\t\texpectedRevision: options.expectedRevision,\n\t\t\t\t\tactualRevision: current.plan.revision,\n\t\t\t\t};\n\t\t\tawait this.write(document.plan.orchestrationId, document);\n\t\t\treturn { status: \"saved\" as const, document };\n\t\t});\n\t}\n\tasync mutate<T>(\n\t\torchestrationId: string,\n\t\tmutation: (\n\t\t\tdocument: OrchestrationPlanDocument,\n\t\t) => { kind: \"write\"; document: OrchestrationPlanDocument; value: T } | { kind: \"noop\"; value: T },\n\t): Promise<{ status: \"ok\"; value: T } | { status: \"missing\" } | { status: \"corrupt\"; diagnostic: string }> {\n\t\treturn this.locked(orchestrationId, async () => {\n\t\t\tconst document = await this.read(orchestrationId);\n\t\t\tif (!document) return { status: \"missing\" as const };\n\t\t\tconst result = mutation(document);\n\t\t\tif (result.kind === \"write\") await this.write(orchestrationId, result.document);\n\t\t\treturn { status: \"ok\" as const, value: result.value };\n\t\t});\n\t}\n\tasync list(): Promise<string[]> {\n\t\ttry {\n\t\t\treturn (await fsp.readdir(this.root))\n\t\t\t\t.filter((entry) => entry.endsWith(SUFFIX))\n\t\t\t\t.map((entry) => entry.slice(0, -SUFFIX.length))\n\t\t\t\t.sort();\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\tasync withExclusive<T>(\n\t\torchestrationId: string,\n\t\tfn: (\n\t\t\tdocument: OrchestrationPlanDocument,\n\t\t\tsave: (document: OrchestrationPlanDocument) => Promise<void>,\n\t\t) => Promise<T>,\n\t): Promise<T> {\n\t\treturn this.locked(orchestrationId, async () => {\n\t\t\tconst current = await this.read(orchestrationId);\n\t\t\tif (!current) throw new Error(`ORCHESTRATION_NOT_FOUND: ${orchestrationId}`);\n\t\t\tlet next = current;\n\t\t\tconst save = async (document: OrchestrationPlanDocument): Promise<void> => {\n\t\t\t\tnext = document;\n\t\t\t};\n\t\t\tconst value = await fn(current, save);\n\t\t\tif (next !== current) await this.write(orchestrationId, next);\n\t\t\treturn value;\n\t\t});\n\t}\n}\n\nexport function createFileOrchestrationStore(root: string = defaultOrchestrationRoot()): FileOrchestrationStore {\n\treturn new FileOrchestrationStore({ root });\n}\n"]}