UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

757 lines (734 loc) • 30.5 kB
import { randomUUID } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { stringify } from "yaml"; import { openReadOnlyDatabase } from "../sqlite.ts"; import { buildRetrospective, writeRetrospective } from "../retrospective.ts"; import { redactSecrets } from "../redact.ts"; import { postWorkflowSignal, watchGithubChecks } from "../github-watch.ts"; import { listWorkflowDefinitions, addWorkflowDefinition, removeWorkflowDefinition, modifyWorkflowDefinition, scaffoldWorkflowDefinition, WORKFLOW_TEMPLATES, } from "../workflow-manager.ts"; import { canonicalWorkflowEvidenceKey, parseWorkflowDefinitions, workflowWebhookHeaders, type WorkflowEvidenceInput, type WorkflowJournalEntry, type WorkflowRun, } from "../workflow.ts"; import { discoverKxmProjectRoot, kxmWorkflowWriteIssues, parseRestrictedYaml } from "../project-config.ts"; import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts"; import { projectRuntimeOwnsRun } from "../runtime-store.ts"; import type { WorkerOutcome } from "../envelope.ts"; import { print, printPlan, printWorker, gateOf, parseEvidencePairs, maskEnvName, type CliIo, type Runtime, } from "./types.ts"; import { resolvePickItem, type PickCandidate } from "./roles.ts"; import { CloudTokenError } from "../hub-binding.ts"; import { resolveControlPlaneAuthToken } from "../hub-env.ts"; const CLI_NAME = "kxm"; export function localWorkflowSnapshot(dataPath: string, runId?: string): { runs: WorkflowRun[]; journal: WorkflowJournalEntry[] } { if (!existsSync(dataPath)) throw new Error("state_database_not_found"); const database = openReadOnlyDatabase(dataPath); try { const rows = runId ? database.prepare("SELECT record FROM workflow_runs WHERE id = ?").all(runId) : database.prepare("SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 200").all(); const runs = (rows as Array<{ record: string }>).map((row) => JSON.parse(row.record) as WorkflowRun); const journal = runId ? (database.prepare("SELECT record FROM workflow_journal WHERE run_id = ? ORDER BY rowid").all(runId) as Array<{ record: string }>).map((row) => JSON.parse(row.record) as WorkflowJournalEntry) : []; return { runs, journal }; } finally { database.close(); } } export async function postWorkflowStart(input: { serverUrl: string; definitionId: string; secret: string; deliveryId: string; event?: string | undefined; payload: Record<string, unknown>; fetchImpl: typeof fetch; }): Promise<{ status: number; runId?: string | undefined; duplicate: boolean }> { const payload = input.event && input.payload["event"] === undefined ? { ...input.payload, event: input.event } : input.payload; const body = JSON.stringify(payload); const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}/v1/webhooks/${encodeURIComponent(input.definitionId)}`, { method: "POST", headers: { "content-type": "application/json", ...workflowWebhookHeaders({ secret: input.secret, scope: { definitionId: input.definitionId }, deliveryId: input.deliveryId, body, }), ...(input.event ? { "x-github-event": input.event } : {}), }, body, }); const responseText = (await response.text()).slice(0, 8_000); let parsed: { runId?: string; duplicate?: boolean } = {}; try { parsed = JSON.parse(responseText); } catch { // fallback } if (!response.ok) throw new Error(`workflow_start_http_${response.status}`); return { status: response.status, ...(parsed.runId ? { runId: parsed.runId } : {}), duplicate: parsed.duplicate === true, }; } export async function postWorkflowDegradation(input: { serverUrl: string; authToken: string; runId: string; stageId: string; requirementKey: string; reason: string; fetchImpl: typeof fetch; }): Promise<{ status: number; duplicate: boolean; approvalId?: string | undefined }> { const response = await input.fetchImpl( `${input.serverUrl.replace(/\/$/, "")}/v1/workflows/${encodeURIComponent(input.runId)}/degradations`, { method: "POST", headers: { authorization: `Bearer ${input.authToken}`, "content-type": "application/json", }, body: JSON.stringify({ stageId: input.stageId, requirementKey: input.requirementKey, reason: input.reason, }), }, ); const responseText = (await response.text()).slice(0, 8_000); let parsed: { duplicate?: boolean; approval?: { id?: string } } = {}; try { parsed = JSON.parse(responseText); } catch { // fallback } if (!response.ok) throw new Error(`workflow_degradation_http_${response.status}`); return { status: response.status, duplicate: parsed.duplicate === true, ...(parsed.approval?.id ? { approvalId: parsed.approval.id } : {}), }; } export function activeWorkflowDefinition(runtime: Runtime, definitionId: string) { const inline = runtime.env.KXM_WEBHOOK_WORKFLOWS?.trim(); const file = runtime.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim(); if (inline && file) { throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE"); } if (!inline && !file) return undefined; let raw: string; if (file) { try { raw = readFileSync(resolve(runtime.cwd, file), "utf8"); } catch { throw new Error("workflow definition file is unavailable"); } } else { raw = inline!; } const definition = parseWorkflowDefinitions(raw, runtime.env).find((candidate) => candidate.id === definitionId); if (!definition) throw new Error(`workflow definition not found: ${definitionId}`); return definition; } export function workflowCredential(runtime: Runtime, definitionId: string, kind: "start" | "signal"): string | undefined { const definition = activeWorkflowDefinition(runtime, definitionId); if (definition) return kind === "start" ? definition.secret : definition.signalSecret ?? definition.secret; return kind === "start" ? runtime.env.KXM_WORKFLOW_SECRET?.trim() : runtime.env.KXM_WORKFLOW_SIGNAL_SECRET?.trim(); } export function reportWorkflowConfigError(runtime: Runtime, error: unknown): number { const message = error instanceof Error ? redactSecrets(error.message) : "invalid workflow configuration"; runtime.io.stderr(`${message}\n`); return 2; } export async function cmdWorkflowDefinitions( runtime: Runtime, options: { scope?: "all" | "global" | "local" | undefined }, ): Promise<number> { const workflows = listWorkflowDefinitions({ scope: options.scope, repoRoot: runtime.cwd, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, }); if (runtime.json) { print(runtime.io, true, { ok: true, command: "workflow definitions", workflows }, ""); return 0; } if (workflows.length === 0) { runtime.io.stdout("No workflow definitions found.\n"); return 0; } const lines: string[] = ["WORKFLOW DEFINITIONS:"]; for (const w of workflows) { const scopeTag = w.scope === "overridden" ? "[local override]" : `[${w.scope}]`; const rolesTag = w.roles.length > 0 ? `(roles: ${w.roles.join(", ")})` : ""; lines.push(` ${w.id.padEnd(24)} ${scopeTag.padEnd(16)} ${w.stepCount} steps ${rolesTag} ${w.description}`); } print(runtime.io, false, {}, `${lines.join("\n")}\n`); return 0; } export async function cmdWorkflowAdd( runtime: Runtime, workflowId: string | undefined, options: { file?: string | undefined; description?: string | undefined; scope?: "global" | "local" | undefined; overwrite?: boolean | undefined; pick?: string | boolean | undefined; template?: string | undefined; }, ): Promise<number> { const scope = options.scope ?? "local"; const refuse = (error: string, message: string, code = 2): number => { print(runtime.io, runtime.json, { ok: false, command: "workflow add", error, message }, `workflow add failed: ${message}`); return code; }; try { let content: Record<string, unknown> | string | undefined; if (options.template !== undefined) { if (options.file !== undefined || options.pick !== undefined) { return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick"); } if (!workflowId) return refuse("workflow_id_required", "usage: kxm workflow add <workflowId> --template <name>"); const template = Object.hasOwn(WORKFLOW_TEMPLATES, options.template) ? WORKFLOW_TEMPLATES[options.template] : undefined; if (!template) { return refuse("workflow_template_unknown", `unknown template ${options.template}; choose ${Object.keys(WORKFLOW_TEMPLATES).join(", ")}`); } content = { ...template, ...(options.description !== undefined ? { description: options.description } : {}) }; } else if (!workflowId || options.pick) { const candidates: PickCandidate[] = Object.entries(WORKFLOW_TEMPLATES).map(([id, tmpl]) => ({ id, description: String(tmpl.description ?? id), label: "template", payload: tmpl, })); if (scope === "local") { const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR }); for (const gd of globalDefs) { // Read the listed path when selected: globals may use `.yml`, not only `.yaml`. if (!candidates.some((c) => c.id === gd.id)) { candidates.push({ id: gd.id, description: gd.description, label: "global", payload: gd.filePath }); } } } const picked = await resolvePickItem(runtime.io, `Select a workflow template to add (${scope})`, candidates, options.pick, runtime.env); if (!picked) { return refuse("workflow_id_required", "provide a workflowId or select an available workflow with --pick <id>", 1); } workflowId ??= picked.id; if (options.file === undefined) { const pickedContent = typeof picked.payload === "string" ? readFileSync(picked.payload, "utf8") : picked.payload; content = options.description !== undefined ? { ...(typeof pickedContent === "string" ? parseRestrictedYaml(pickedContent, String(picked.payload)) : pickedContent), description: options.description, } : pickedContent; } } if (options.file !== undefined) { const filePath = resolve(runtime.cwd, options.file); content = readFileSync(filePath, "utf8"); } else if (!content) { content = scaffoldWorkflowDefinition(options.description || `Workflow ${workflowId}`); } const document = typeof content === "string" ? content : stringify(content); // A local workflow is read by the project loader, which refuses the whole // project over one bad file, so it is checked by that loader before it lands. let repoRoot = runtime.cwd; if (scope === "local") { const projectRoot = discoverKxmProjectRoot(runtime.cwd); if (!projectRoot) { return refuse("project_not_found", "local workflows belong to a KXM project; run kxm init at the repository root, or pass --scope global"); } const issues = kxmWorkflowWriteIssues(projectRoot, workflowId!, document); if (issues.length > 0) { print( runtime.io, runtime.json, { ok: false, command: "workflow add", error: "workflow_invalid", message: "with this workflow the project would not load, so nothing was written", issues }, `workflow add failed: with this workflow the project would not load, so nothing was written\n${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`, ); return 2; } repoRoot = projectRoot; } const res = addWorkflowDefinition(workflowId!, document, { scope, repoRoot, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, overwrite: options.overwrite, dryRun: runtime.dryRun, }); if (runtime.dryRun) { printPlan(runtime, { command: "workflow add", workflowId, ...res }, [{ action: "write", target: res.filePath }], `add workflow '${workflowId}' to ${res.scope}`); return 0; } print( runtime.io, runtime.json, { ok: true, command: "workflow add", workflowId, ...res }, `Added workflow '${workflowId}' to ${res.scope} (${res.filePath})\n`, ); return 0; } catch (err: unknown) { return refuse("workflow_add_failed", err instanceof Error ? err.message : String(err), 1); } } export async function cmdWorkflowRemove( runtime: Runtime, workflowId: string | undefined, options: { scope?: "global" | "local" | undefined; pick?: string | boolean | undefined }, ): Promise<number> { const scope = options.scope ?? "local"; if (!workflowId || options.pick) { const workflows = listWorkflowDefinitions({ scope, repoRoot: runtime.cwd, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, }); if (workflows.length === 0) { runtime.io.stdout(`No workflow definitions found in ${scope} scope to remove.\n`); return 0; } const candidates: PickCandidate[] = workflows.map((w) => ({ id: w.id, description: w.description, label: w.scope, })); const picked = await resolvePickItem(runtime.io, `Select a workflow to remove (${scope})`, candidates, options.pick, runtime.env); if (!picked) { if (!workflowId) { runtime.io.stderr("workflow remove failed: missing workflowId or pick selection\n"); return 1; } } else { workflowId = picked.id; } } try { const res = removeWorkflowDefinition(workflowId!, { scope, repoRoot: runtime.cwd, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, dryRun: runtime.dryRun, }); if (runtime.dryRun) { printPlan(runtime, { command: "workflow remove", workflowId, ...res }, [{ action: "delete", target: res.filePath }], `remove workflow '${workflowId}' from ${res.scope}`); return 0; } print( runtime.io, runtime.json, { ok: true, command: "workflow remove", workflowId, ...res }, `Removed workflow '${workflowId}' from ${res.scope}\n`, ); return 0; } catch (err: unknown) { runtime.io.stderr(`workflow remove failed: ${(err as Error).message}\n`); return 1; } } export async function cmdWorkflowModify( runtime: Runtime, workflowId: string | undefined, options: { description?: string | undefined; scope?: "global" | "local" | undefined; pick?: string | boolean | undefined; }, ): Promise<number> { if (!workflowId || options.pick) { const workflows = listWorkflowDefinitions({ scope: options.scope, repoRoot: runtime.cwd, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, }); if (workflows.length === 0) { runtime.io.stdout("No workflow definitions found to modify.\n"); return 0; } const candidates: PickCandidate[] = workflows.map((w) => ({ id: w.id, description: w.description, label: w.scope, })); const picked = await resolvePickItem(runtime.io, "Select a workflow to modify", candidates, options.pick, runtime.env); if (!picked) { if (!workflowId) { runtime.io.stderr("workflow modify failed: missing workflowId or pick selection\n"); return 1; } } else { workflowId = picked.id; } } try { const res = modifyWorkflowDefinition( workflowId!, { ...(options.description ? { description: options.description } : {}) }, { scope: options.scope, repoRoot: runtime.cwd, userConfigDir: runtime.env.KXM_USER_CONFIG_DIR, dryRun: runtime.dryRun, }, ); if (runtime.dryRun) { printPlan(runtime, { command: "workflow modify", workflowId, ...res }, [{ action: "write", target: res.filePath }], `modify workflow '${workflowId}' in ${res.scope}`); return 0; } print( runtime.io, runtime.json, { ok: true, command: "workflow modify", workflowId, ...res }, `Modified workflow '${workflowId}' in ${res.scope}\n`, ); return 0; } catch (err: unknown) { runtime.io.stderr(`workflow modify failed: ${(err as Error).message}\n`); return 1; } } export async function cmdWorkflowStart(runtime: Runtime, definitionIdArg: string | undefined, options: { payload?: string | undefined; deliveryId?: string | undefined; event?: string | undefined }): Promise<number> { const definitionId = definitionIdArg || runtime.env.KXM_WORKFLOW_ID?.trim(); const deliveryId = String(options.deliveryId || `cli-${randomUUID()}`); const event = options.event; const payloadFlag = options.payload ?? "{}"; if (!definitionId) { runtime.io.stderr("workflow start requires <definitionId> (or KXM_WORKFLOW_ID)\n"); return 2; } let secret: string | undefined; try { secret = workflowCredential(runtime, definitionId, "start"); } catch (error) { return reportWorkflowConfigError(runtime, error); } if (!secret) { runtime.io.stderr("workflow start requires KXM_WORKFLOW_SECRET when no active definition source is configured\n"); return 2; } let payload: Record<string, unknown>; try { const raw = payloadFlag.startsWith("@") ? readFileSync(resolve(runtime.cwd, payloadFlag.slice(1)), "utf8") : payloadFlag; const value = JSON.parse(raw) as unknown; if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("object required"); payload = value as Record<string, unknown>; } catch { print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "invalid_payload" }, "workflow payload must be a JSON object or @file"); return 2; } if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: "workflow start", dryRun: true, definitionId, deliveryId, event }, "would POST a signed workflow webhook"); return 0; } try { const response = await postWorkflowStart({ serverUrl: runtime.serverUrl, definitionId, secret, deliveryId, ...(event ? { event } : {}), payload, fetchImpl: runtime.fetchImpl }); print(runtime.io, runtime.json, { ok: true, command: "workflow start", definitionId, deliveryId, ...response }, `started workflow ${response.runId ?? "accepted"}`); return 0; } catch { print(runtime.io, runtime.json, { ok: false, command: "workflow start", error: "workflow_start_failed" }, "signed workflow start failed"); return 1; } } export async function cmdWorkflowDegrade(runtime: Runtime, runId: string, stageId: string, options: { requirement?: string | undefined; reason?: string | undefined }): Promise<number> { const requirementKey = options.requirement?.trim() ?? ""; const reason = options.reason?.trim() ?? ""; let adminToken: string | undefined; try { adminToken = resolveControlPlaneAuthToken(runtime.env); } catch (error) { if (error instanceof CloudTokenError) { runtime.io.stderr(`${error.message}\n`); return 2; } throw error; } if (!runId || !stageId || !requirementKey || !reason || !adminToken) { runtime.io.stderr("workflow degrade requires <runId> <stageId>, --requirement, --reason, and KXM_AUTH_TOKEN\n"); return 2; } const worker = gateOf(runtime, "degrade"); if (runtime.dryRun) { printWorker( runtime, worker, { ok: true, command: "workflow degrade", dryRun: true, runId, stageId, requirementKey }, `would approve configured degraded quorum for ${runId}/${stageId}/${requirementKey}`, ); return 0; } try { const result = await postWorkflowDegradation({ serverUrl: runtime.serverUrl, authToken: adminToken, runId, stageId, requirementKey, reason, fetchImpl: runtime.fetchImpl, }); printWorker( runtime, worker, { ok: true, command: "workflow degrade", runId, stageId, requirementKey, ...result }, result.duplicate ? "degraded quorum was already approved" : "approved configured degraded quorum", ); return 0; } catch { printWorker( runtime, worker, { ok: false, command: "workflow degrade", error: "workflow_degradation_failed", runId, stageId, requirementKey }, "workflow degradation approval failed", ); return 1; } } export async function cmdWorkflowInspect(runtime: Runtime, action: "list" | "get", runId?: string | undefined): Promise<number> { const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db")); if (action === "get" && !runId) { runtime.io.stderr(`Usage: ${CLI_NAME} workflow get <runId>\n`); return 2; } try { const snapshot = localWorkflowSnapshot(dataPath, runId); if (runId && snapshot.runs.length === 0) { print(runtime.io, runtime.json, { ok: false, command: "workflow get", error: "workflow_not_found" }, "workflow not found"); return 1; } const value = action === "get" ? { ok: true, command: "workflow get", run: snapshot.runs[0], journal: snapshot.journal } : { ok: true, command: "workflow list", runs: snapshot.runs }; print(runtime.io, runtime.json, value, action === "get" ? `workflow ${runId}` : `${snapshot.runs.length} workflow(s)`); return 0; } catch { print(runtime.io, runtime.json, { ok: false, command: `workflow ${action}`, error: "state_unavailable" }, "local workflow state is unavailable"); return 1; } } export async function cmdSignal(runtime: Runtime, runId: string, signalKey: string, status: string, summary: string, evidenceArgs: string[], deliveryIdFlag?: string | undefined, recoveryAction?: string | undefined): Promise<number> { if (!runId || !signalKey || !status || !summary) { runtime.io.stderr(`Usage: ${CLI_NAME} gate signal <runId> <signalKey> <passed|warning|failed> <summary> [<required-key>=<evidence> ...]\n`); return 2; } if (status !== "passed" && status !== "warning" && status !== "failed") { runtime.io.stderr("status must be passed, warning, or failed\n"); return 2; } let evidence: WorkflowEvidenceInput; try { evidence = parseEvidencePairs(evidenceArgs); } catch (error) { runtime.io.stderr(`${error instanceof Error ? error.message : "invalid evidence"}\n`); return 2; } const worker = gateOf(runtime, "signal"); const projectRoot = discoverKxmProjectRoot(runtime.cwd); if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) { if (runtime.dryRun) { printWorker(runtime, worker, { ok: true, command: "signal", dryRun: true, runId, signalKey, status, summary, evidence }, "would post signal to KXM run"); return 0; } const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`); try { const supervisor = await ensureKxmSupervisor({ env: runtime.env }); const posted = await kxmRuntimeRequest( supervisor, "POST", `/v1/runs/${encodeURIComponent(runId)}/signal?projectRoot=${encodeURIComponent(projectRoot)}`, { signalKey, status, summary, evidence, deliveryId, ...(recoveryAction ? { action: recoveryAction } : {}) }, ); printWorker(runtime, worker, { ok: true, command: "signal", runId, signalKey, status, unblocked: posted.unblocked === true, deliveryId }, "posted signal to KXM run"); return 0; } catch (error) { const msg = error instanceof Error ? error.message : "signal_failed"; printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", detail: msg, deliveryId }, "signal to KXM run failed"); return 1; } } const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim(); if (!definitionId) { runtime.io.stderr("signal requires KXM_WORKFLOW_ID\n"); return 2; } let signalSecret: string | undefined; try { signalSecret = workflowCredential(runtime, definitionId, "signal"); } catch (error) { return reportWorkflowConfigError(runtime, error); } if (!signalSecret) { runtime.io.stderr("signal requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n"); return 2; } if (runtime.dryRun) { printWorker(runtime, worker, { ok: true, command: "signal", dryRun: true, runId, signalKey, status, summary, evidence }, "would post signed signal"); return 0; } const deliveryId = String(deliveryIdFlag || `cli-signal:${randomUUID()}`); try { const posted = await postWorkflowSignal({ serverUrl: runtime.serverUrl, definitionId, signalSecret, runId, signalKey, status, summary, evidence, deliveryId, fetchImpl: runtime.fetchImpl, }); printWorker(runtime, worker, { ok: true, command: "signal", duplicate: posted.duplicate, deliveryId }, "posted signed signal"); return 0; } catch { printWorker(runtime, worker, { ok: false, command: "signal", error: "signal_failed", deliveryId }, "signed signal failed"); return 1; } } export async function cmdGithubWatch(runtime: Runtime, options: { runId?: string | undefined; stageId?: string | undefined; signalKey?: string | undefined; repo?: string | undefined; pr?: string | undefined; required?: string | undefined; timeoutMs?: string | undefined; intervalMs?: string | undefined; deliveryId?: string | undefined; }): Promise<number> { const token = runtime.env.GITHUB_TOKEN?.trim() || runtime.env.GH_TOKEN?.trim(); const definitionId = runtime.env.KXM_WORKFLOW_ID?.trim(); const runId = String(options.runId || ""); const stageId = String(options.stageId || ""); const signalKey = String(options.signalKey || ""); const repo = String(options.repo || ""); const pr = Number(options.pr); if (!definitionId || !runId || !stageId || !signalKey || !repo || !Number.isInteger(pr)) { runtime.io.stderr("github watch requires KXM_WORKFLOW_ID, --run-id, --stage-id, --signal-key, --repo, --pr\n"); return 2; } let signalSecret: string | undefined; try { signalSecret = workflowCredential(runtime, definitionId, "signal"); } catch (error) { return reportWorkflowConfigError(runtime, error); } if (!signalSecret) { runtime.io.stderr("github watch requires KXM_WORKFLOW_SIGNAL_SECRET when no active definition source is configured\n"); return 2; } const result = await watchGithubChecks({ serverUrl: runtime.serverUrl, definitionId, signalSecret, runId, stageId, signalKey, repo, pr, required: options.required ? [...new Set(String(options.required).split(",").map((name) => name.trim()).filter(Boolean))] : [], timeoutMs: Number(options.timeoutMs || 1_800_000), intervalMs: Number(options.intervalMs || 15_000), ...(options.deliveryId ? { deliveryId: options.deliveryId } : {}), ...(token ? { token } : {}), dryRun: runtime.dryRun, fetchImpl: runtime.fetchImpl, ...(runtime.io.now ? { now: runtime.io.now } : {}), ...(runtime.io.sleep ? { sleep: runtime.io.sleep } : {}), }); const worker = gateOf(runtime, "github-watch"); const payload = { ok: result.exitCode === 0, command: "github watch", posted: result.posted, status: result.status, summary: result.summary, evidence: result.evidence, deliveryId: result.deliveryId, skipped: result.skipped, }; const outcome: WorkerOutcome = result.exitCode === 0 ? (result.status === "warning" ? "warning" : "passed") : "failed"; if (JSON.stringify(payload).includes(token ?? "___never___") || Object.keys(runtime.env).some((key) => maskEnvName(key) && JSON.stringify(payload).includes(String(runtime.env[key])))) { printWorker(runtime, worker, { ok: false, command: "github watch", error: "redaction_failure" }, "refusing to print a payload that contains a secret"); return 1; } printWorker(runtime, worker, payload, result.summary, outcome); return result.exitCode; } export async function cmdRetrospectiveExport(runtime: Runtime, runId: string, options: { input?: string | undefined; outDir?: string | undefined }): Promise<number> { if (!runId) { runtime.io.stderr(`Usage: ${CLI_NAME} workflow export <runId>\n`); return 2; } const snapshotFlag = String(options.input || ""); const snapshotPath = snapshotFlag ? resolve(runtime.cwd, snapshotFlag) : ""; let snapshot: { run: WorkflowRun; journal: WorkflowJournalEntry[] }; try { if (snapshotPath) { if (!existsSync(snapshotPath)) throw new Error("snapshot_missing"); snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as typeof snapshot; } else { const dataPath = resolve(runtime.dirs.workdir, runtime.env.KXM_DATA_PATH?.trim() || join(runtime.dirs.state, "kxm.db")); const local = localWorkflowSnapshot(dataPath, runId); if (!local.runs[0]) throw new Error("workflow_not_found"); snapshot = { run: local.runs[0], journal: local.journal }; } if (snapshot.run.id !== runId) throw new Error("run_id_mismatch"); } catch (error) { const reason = error instanceof Error ? error.message : "snapshot_invalid"; print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: reason }, "retrospective source is invalid or unavailable"); return 1; } const doc = buildRetrospective(snapshot.run, snapshot.journal); const outDir = resolve(runtime.cwd, String(options.outDir || join(runtime.dirs.assets, "retrospectives"))); const assetsRoot = resolve(runtime.dirs.assets); const assetsPrefix = `${assetsRoot}${process.platform === "win32" ? "\\" : "/"}`; if (outDir !== assetsRoot && !outDir.startsWith(assetsPrefix)) { print(runtime.io, runtime.json, { ok: false, command: "retrospective export", error: "output_outside_workspace_assets" }, "retrospectives must stay under the workspace assets directory"); return 2; } if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: "retrospective export", dryRun: true, runId: doc.runId }, `would export ${doc.runId}`); return 0; } const written = writeRetrospective(outDir, doc); print(runtime.io, runtime.json, { ok: true, command: "retrospective export", ...written, reviewDecision: doc.reviewDecision }, `exported ${written.jsonPath}`); return 0; }