@kontextmind/kxm 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { vnextCanonicalJson, vnextPortablePath, type JsonObject, type JsonValue } from "./vnext-config.ts";
|
|
3
|
+
import {
|
|
4
|
+
VNEXT_COMPILED_WORKFLOW_SCHEMA,
|
|
5
|
+
type VnextCompiledAssignments,
|
|
6
|
+
type VnextCompiledEvidence,
|
|
7
|
+
type VnextCompiledJoin,
|
|
8
|
+
type VnextCompiledPlan,
|
|
9
|
+
type VnextCompiledStep,
|
|
10
|
+
type VnextCompiledTransition,
|
|
11
|
+
type VnextRepositoryAccess,
|
|
12
|
+
} from "./vnext-engine-compile.ts";
|
|
13
|
+
import {
|
|
14
|
+
VNEXT_ABSENT_MEMORY_REVISION,
|
|
15
|
+
runtimeError,
|
|
16
|
+
type VnextRunEventStore,
|
|
17
|
+
type VnextRunRecord,
|
|
18
|
+
} from "./vnext-runtime-store.ts";
|
|
19
|
+
|
|
20
|
+
export const VNEXT_RUN_PLAN_SCHEMA = "kxm.run-plan.v2";
|
|
21
|
+
const SUPPORTED_KINDS = new Set(["agent", "moa", "gate", "approval", "wait"]);
|
|
22
|
+
const TERMINAL = new Set(["completed", "failed", "cancelled"]);
|
|
23
|
+
const JOIN_STRATEGIES = new Set(["all", "all-settled", "quorum", "first-success"]);
|
|
24
|
+
const EVIDENCE_KINDS = new Set(["assignment-result", "gate", "receipt", "approval", "artifact"]);
|
|
25
|
+
const REPOSITORY_ACCESS = new Set(["none", "read", "write"]);
|
|
26
|
+
const DISTINCT_BY = new Set(["provider", "model", "profile"]);
|
|
27
|
+
const ENVELOPE_REQUIRED = ["schema", "runId", "projectId", "homeRuntimeId", "workflowId", "revisions", "projectLimits", "plan", "gates"] as const;
|
|
28
|
+
const GATES_REQUIRED = ["registry", "definitions", "controlRoot"] as const;
|
|
29
|
+
const REGISTRY_PIN_REQUIRED = ["schema", "hash"] as const;
|
|
30
|
+
const CONTROL_ROOT_REQUIRED = ["repositoryId", "projectKey"] as const;
|
|
31
|
+
const GATE_REGISTRY_SCHEMA = "kxm.gate-registry.v1";
|
|
32
|
+
const REVISION_REQUIRED = ["config", "executorPolicy", "toolPolicy", "memory"] as const;
|
|
33
|
+
const PROJECT_LIMIT_REQUIRED = ["maxConcurrentRuns"] as const;
|
|
34
|
+
const PROJECT_LIMIT_OPTIONAL = ["maxRunDurationMs", "maxAgentTimeMs"] as const;
|
|
35
|
+
const PLAN_REQUIRED = ["schema", "workflowId", "coordinator", "limits", "transitionBudget", "hasBackEdges", "entryStepId", "order", "steps", "requirePlanHash"] as const;
|
|
36
|
+
const PLAN_OPTIONAL = ["sourcePath", "reproOracle", "planHash"] as const;
|
|
37
|
+
const LIMIT_OPTIONAL = ["maxTransitions", "maxRunDurationMs", "maxAgentTimeMs", "maxModelCost", "currency"] as const;
|
|
38
|
+
const STEP_REQUIRED = [
|
|
39
|
+
"id",
|
|
40
|
+
"index",
|
|
41
|
+
"kind",
|
|
42
|
+
"maxAttempts",
|
|
43
|
+
"safeSpeculation",
|
|
44
|
+
"repositories",
|
|
45
|
+
"secrets",
|
|
46
|
+
"requiredEvidence",
|
|
47
|
+
"outcomes",
|
|
48
|
+
"transitions",
|
|
49
|
+
"requiresPlanHash",
|
|
50
|
+
"assignments",
|
|
51
|
+
"join",
|
|
52
|
+
] as const;
|
|
53
|
+
const STEP_OPTIONAL = ["description", "instructions", "timeoutMs", "tools", "model"] as const;
|
|
54
|
+
const ASSIGNMENT_REQUIRED = ["allowedAgents", "minimum", "target", "maximum", "maxParallel", "maxAttemptsPerAssignment", "distinctBy"] as const;
|
|
55
|
+
const ASSIGNMENT_OPTIONAL = ["maxWriteRepositories"] as const;
|
|
56
|
+
const JOIN_REQUIRED = ["strategy"] as const;
|
|
57
|
+
const JOIN_OPTIONAL = ["minimumPassed", "cancelRemaining"] as const;
|
|
58
|
+
const EVIDENCE_REQUIRED = ["key", "kind", "minimum", "reusableAcrossAttempts"] as const;
|
|
59
|
+
const EVIDENCE_OPTIONAL = ["producerPolicy"] as const;
|
|
60
|
+
|
|
61
|
+
export type VnextGateDefinition =
|
|
62
|
+
| { readonly kind: "command"; readonly argv: readonly string[]; readonly timeoutMs: number; readonly cwd?: "control" }
|
|
63
|
+
| { readonly kind: "artifacts-exist"; readonly paths: readonly string[] }
|
|
64
|
+
| { readonly kind: "reserved" };
|
|
65
|
+
|
|
66
|
+
export interface VnextPinnedGates {
|
|
67
|
+
readonly registry: { readonly schema: typeof GATE_REGISTRY_SCHEMA; readonly hash: string } | null;
|
|
68
|
+
readonly definitions: Readonly<Record<string, VnextGateDefinition>>;
|
|
69
|
+
readonly controlRoot: { readonly repositoryId: "control"; readonly projectKey: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface VnextRunPlanEnvelope {
|
|
73
|
+
readonly schema: typeof VNEXT_RUN_PLAN_SCHEMA;
|
|
74
|
+
readonly runId: string;
|
|
75
|
+
readonly projectId: string;
|
|
76
|
+
readonly homeRuntimeId: string;
|
|
77
|
+
readonly workflowId: string;
|
|
78
|
+
readonly revisions: {
|
|
79
|
+
readonly config: string;
|
|
80
|
+
readonly executorPolicy: string;
|
|
81
|
+
readonly toolPolicy: string;
|
|
82
|
+
readonly memory: string;
|
|
83
|
+
};
|
|
84
|
+
readonly projectLimits: {
|
|
85
|
+
readonly maxConcurrentRuns: number;
|
|
86
|
+
readonly maxRunDurationMs?: number;
|
|
87
|
+
readonly maxAgentTimeMs?: number;
|
|
88
|
+
};
|
|
89
|
+
readonly plan: VnextCompiledPlan;
|
|
90
|
+
readonly gates: VnextPinnedGates;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function vnextSha256(input: string): string {
|
|
94
|
+
return `sha256:${createHash("sha256").update(input, "utf8").digest("hex")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function hashVnextRunPlanEnvelope(envelope: VnextRunPlanEnvelope): string {
|
|
98
|
+
return vnextSha256(vnextCanonicalJson(envelope as unknown as JsonValue));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function freezeVnextCompiledPlan(plan: VnextCompiledPlan): VnextCompiledPlan {
|
|
102
|
+
const steps = Object.create(null) as Record<string, VnextCompiledStep>;
|
|
103
|
+
for (const id of plan.order) {
|
|
104
|
+
const step = plan.steps[id];
|
|
105
|
+
if (!step) throw runtimeError("run_plan_corrupt", plan.workflowId, `compiled plan is missing step ${id}`);
|
|
106
|
+
steps[id] = freezeStep(step);
|
|
107
|
+
}
|
|
108
|
+
return deepFreeze({
|
|
109
|
+
...plan,
|
|
110
|
+
steps,
|
|
111
|
+
order: [...plan.order],
|
|
112
|
+
requirePlanHash: [...plan.requirePlanHash],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function freezeStep(step: VnextCompiledStep): VnextCompiledStep {
|
|
117
|
+
const transitions = Object.create(null) as Record<string, VnextCompiledTransition>;
|
|
118
|
+
for (const outcome of step.outcomes) {
|
|
119
|
+
const transition = step.transitions[outcome];
|
|
120
|
+
if (!transition) throw runtimeError("run_plan_corrupt", step.id, `compiled plan is missing transition ${outcome}`);
|
|
121
|
+
transitions[outcome] = { ...transition };
|
|
122
|
+
}
|
|
123
|
+
const repositories = Object.create(null) as Record<string, VnextRepositoryAccess>;
|
|
124
|
+
for (const [id, access] of Object.entries(step.repositories)) repositories[id] = access;
|
|
125
|
+
return deepFreeze({
|
|
126
|
+
...step,
|
|
127
|
+
outcomes: [...step.outcomes],
|
|
128
|
+
transitions,
|
|
129
|
+
repositories,
|
|
130
|
+
secrets: step.secrets.map((entry) => ({ ...entry })),
|
|
131
|
+
requiredEvidence: step.requiredEvidence.map((entry) => ({ ...entry })),
|
|
132
|
+
assignments: {
|
|
133
|
+
...step.assignments,
|
|
134
|
+
allowedAgents: [...step.assignments.allowedAgents],
|
|
135
|
+
distinctBy: [...step.assignments.distinctBy],
|
|
136
|
+
},
|
|
137
|
+
join: { ...step.join },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseVnextRunPlanEnvelope(raw: string, run: VnextRunRecord, expectedHash: string): VnextRunPlanEnvelope {
|
|
142
|
+
let parsed: unknown;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(raw) as unknown;
|
|
145
|
+
} catch {
|
|
146
|
+
throw runtimeError("run_plan_corrupt", run.runId, "run plan envelope is not JSON");
|
|
147
|
+
}
|
|
148
|
+
const envelope = parseEnvelope(parsed, run);
|
|
149
|
+
const hash = hashVnextRunPlanEnvelope(envelope);
|
|
150
|
+
if (hash !== expectedHash) {
|
|
151
|
+
throw runtimeError("run_plan_corrupt", run.runId, "run plan envelope hash does not match");
|
|
152
|
+
}
|
|
153
|
+
return envelope;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function rehydrateVnextCompiledPlanFromStore(store: VnextRunEventStore, run: VnextRunRecord): VnextCompiledPlan {
|
|
157
|
+
return loadPinnedEnvelope(store, run).plan;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function loadVnextRunPlanEnvelope(store: VnextRunEventStore, run: VnextRunRecord): VnextRunPlanEnvelope {
|
|
161
|
+
return loadPinnedEnvelope(store, run);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loadPinnedEnvelope(store: VnextRunEventStore, run: VnextRunRecord): VnextRunPlanEnvelope {
|
|
165
|
+
const row = store.runPlan(run.runId);
|
|
166
|
+
if (!row) throw runtimeError("run_plan_missing", run.runId, "run has no pinned plan");
|
|
167
|
+
const events = store.events(run.runId, 0, row.pinnedSequence);
|
|
168
|
+
const pinEvent = events.find((event) => event.sequence === row.pinnedSequence);
|
|
169
|
+
const eventHash = pinEvent?.payload.runPlanHash;
|
|
170
|
+
if (typeof eventHash !== "string" || eventHash !== row.runPlanHash) {
|
|
171
|
+
throw runtimeError("run_plan_corrupt", run.runId, "pinned runPlanHash does not match the plan row");
|
|
172
|
+
}
|
|
173
|
+
return parseVnextRunPlanEnvelope(row.envelope, run, row.runPlanHash);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function absentMemoryRevision(): string {
|
|
177
|
+
return VNEXT_ABSENT_MEMORY_REVISION;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseEnvelope(parsed: unknown, run: VnextRunRecord): VnextRunPlanEnvelope {
|
|
181
|
+
const value = asObject(parsed, run.runId, "run plan envelope");
|
|
182
|
+
assertExactKeys(value, ENVELOPE_REQUIRED, [], run.runId, "run plan envelope");
|
|
183
|
+
if (value.schema !== VNEXT_RUN_PLAN_SCHEMA) {
|
|
184
|
+
throw runtimeError("run_plan_corrupt", run.runId, "schema is not kxm.run-plan.v2");
|
|
185
|
+
}
|
|
186
|
+
const runId = asString(value.runId, run.runId, "runId");
|
|
187
|
+
const projectId = asString(value.projectId, run.runId, "projectId");
|
|
188
|
+
const homeRuntimeId = asString(value.homeRuntimeId, run.runId, "homeRuntimeId");
|
|
189
|
+
const workflowId = asString(value.workflowId, run.runId, "workflowId");
|
|
190
|
+
if (runId !== run.runId || projectId !== run.projectId || homeRuntimeId !== run.homeRuntimeId || workflowId !== run.workflowId) {
|
|
191
|
+
throw runtimeError("run_plan_corrupt", run.runId, "run plan envelope identity does not match the run");
|
|
192
|
+
}
|
|
193
|
+
const revisionsValue = asObject(value.revisions, run.runId, "revisions");
|
|
194
|
+
assertExactKeys(revisionsValue, REVISION_REQUIRED, [], run.runId, "revisions");
|
|
195
|
+
const revisions = {
|
|
196
|
+
config: asString(revisionsValue.config, run.runId, "revisions.config"),
|
|
197
|
+
executorPolicy: asString(revisionsValue.executorPolicy, run.runId, "revisions.executorPolicy"),
|
|
198
|
+
toolPolicy: asString(revisionsValue.toolPolicy, run.runId, "revisions.toolPolicy"),
|
|
199
|
+
memory: asString(revisionsValue.memory, run.runId, "revisions.memory"),
|
|
200
|
+
};
|
|
201
|
+
if (
|
|
202
|
+
revisions.config !== run.configRevision
|
|
203
|
+
|| revisions.executorPolicy !== run.executorPolicyRevision
|
|
204
|
+
|| revisions.toolPolicy !== run.toolPolicyRevision
|
|
205
|
+
|| revisions.memory !== run.memoryRevision
|
|
206
|
+
) {
|
|
207
|
+
throw runtimeError("run_plan_corrupt", run.runId, "run plan envelope revisions do not match the run");
|
|
208
|
+
}
|
|
209
|
+
const limitsValue = asObject(value.projectLimits, run.runId, "projectLimits");
|
|
210
|
+
assertExactKeys(limitsValue, PROJECT_LIMIT_REQUIRED, PROJECT_LIMIT_OPTIONAL, run.runId, "projectLimits");
|
|
211
|
+
const projectLimits: VnextRunPlanEnvelope["projectLimits"] = {
|
|
212
|
+
maxConcurrentRuns: asCount(limitsValue.maxConcurrentRuns, run.runId, "projectLimits.maxConcurrentRuns"),
|
|
213
|
+
...(limitsValue.maxRunDurationMs !== undefined ? { maxRunDurationMs: asDuration(limitsValue.maxRunDurationMs, run.runId, "projectLimits.maxRunDurationMs") } : {}),
|
|
214
|
+
...(limitsValue.maxAgentTimeMs !== undefined ? { maxAgentTimeMs: asDuration(limitsValue.maxAgentTimeMs, run.runId, "projectLimits.maxAgentTimeMs") } : {}),
|
|
215
|
+
};
|
|
216
|
+
const plan = freezeVnextCompiledPlan(parseCompiledPlan(value.plan, run.runId));
|
|
217
|
+
return {
|
|
218
|
+
schema: VNEXT_RUN_PLAN_SCHEMA,
|
|
219
|
+
runId,
|
|
220
|
+
projectId,
|
|
221
|
+
homeRuntimeId,
|
|
222
|
+
workflowId,
|
|
223
|
+
revisions,
|
|
224
|
+
projectLimits,
|
|
225
|
+
plan,
|
|
226
|
+
gates: freezeGates(parseGates(value.gates, plan, run.runId)),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseGates(raw: unknown, plan: VnextCompiledPlan, runId: string): VnextPinnedGates {
|
|
231
|
+
const value = asObject(raw, runId, "gates");
|
|
232
|
+
assertExactKeys(value, GATES_REQUIRED, [], runId, "gates");
|
|
233
|
+
const referenced = new Set<string>();
|
|
234
|
+
for (const stepId of plan.order) {
|
|
235
|
+
const step = plan.steps[stepId];
|
|
236
|
+
if (step?.kind === "gate") referenced.add(step.gate);
|
|
237
|
+
}
|
|
238
|
+
const registry = parseRegistryPin(value.registry, runId, referenced.size > 0);
|
|
239
|
+
const controlRoot = parseControlRoot(value.controlRoot, runId);
|
|
240
|
+
const definitionsValue = asObject(value.definitions, runId, "gates.definitions");
|
|
241
|
+
const defined = Object.keys(definitionsValue);
|
|
242
|
+
for (const stepId of plan.order) {
|
|
243
|
+
const step = plan.steps[stepId];
|
|
244
|
+
if (step?.kind !== "gate") continue;
|
|
245
|
+
if (!(step.gate in definitionsValue)) {
|
|
246
|
+
throw runtimeError("run_plan_corrupt", runId, `gate step ${stepId} has no pinned definition`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const gateId of defined) {
|
|
250
|
+
if (!referenced.has(gateId)) {
|
|
251
|
+
throw runtimeError("run_plan_corrupt", runId, `definition ${gateId} is not referenced`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const definitions = Object.create(null) as Record<string, VnextGateDefinition>;
|
|
255
|
+
for (const gateId of defined) {
|
|
256
|
+
definitions[gateId] = parseGateDefinition(definitionsValue[gateId], gateId, runId);
|
|
257
|
+
}
|
|
258
|
+
return { registry, definitions, controlRoot };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseRegistryPin(raw: unknown, runId: string, required: boolean): VnextPinnedGates["registry"] {
|
|
262
|
+
if (raw === null) {
|
|
263
|
+
if (required) throw runtimeError("run_plan_corrupt", runId, "gate steps require a non-null registry pin");
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const value = asObject(raw, runId, "gates.registry");
|
|
267
|
+
assertExactKeys(value, REGISTRY_PIN_REQUIRED, [], runId, "gates.registry");
|
|
268
|
+
if (value.schema !== GATE_REGISTRY_SCHEMA) {
|
|
269
|
+
throw runtimeError("run_plan_corrupt", runId, "gates.registry.schema is not kxm.gate-registry.v1");
|
|
270
|
+
}
|
|
271
|
+
const hash = asString(value.hash, runId, "gates.registry.hash");
|
|
272
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(hash)) {
|
|
273
|
+
throw runtimeError("run_plan_corrupt", runId, "gates.registry.hash is not a sha256 digest");
|
|
274
|
+
}
|
|
275
|
+
return { schema: GATE_REGISTRY_SCHEMA, hash };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseControlRoot(raw: unknown, runId: string): VnextPinnedGates["controlRoot"] {
|
|
279
|
+
const value = asObject(raw, runId, "gates.controlRoot");
|
|
280
|
+
assertExactKeys(value, CONTROL_ROOT_REQUIRED, [], runId, "gates.controlRoot");
|
|
281
|
+
const repositoryId = asString(value.repositoryId, runId, "gates.controlRoot.repositoryId");
|
|
282
|
+
if (repositoryId !== "control") {
|
|
283
|
+
throw runtimeError("run_plan_corrupt", runId, "gates.controlRoot.repositoryId must be control");
|
|
284
|
+
}
|
|
285
|
+
return { repositoryId: "control", projectKey: asString(value.projectKey, runId, "gates.controlRoot.projectKey") };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function parseGateDefinition(raw: unknown, gateId: string, runId: string): VnextGateDefinition {
|
|
289
|
+
const value = asObject(raw, runId, `gates.definitions.${gateId}`);
|
|
290
|
+
const kind = asString(value.kind, runId, `gates.definitions.${gateId}.kind`);
|
|
291
|
+
if (kind === "command") {
|
|
292
|
+
assertExactKeys(value, ["kind", "argv", "timeoutMs"], ["cwd"], runId, `gates.definitions.${gateId}`);
|
|
293
|
+
const argv = asStringArray(value.argv, runId, `gates.definitions.${gateId}.argv`);
|
|
294
|
+
if (argv.length < 1 || argv.length > 64) {
|
|
295
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.argv length is invalid`);
|
|
296
|
+
}
|
|
297
|
+
for (const [index, entry] of argv.entries()) {
|
|
298
|
+
if (entry.length === 0 || entry.length > 4096 || entry.includes("\u0000")) {
|
|
299
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.argv[${index}] is invalid`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const executable = argv[0]!;
|
|
303
|
+
if (executable === "." || executable === ".." || executable.includes("\\") || (!executable.startsWith("/") && executable.includes("/"))) {
|
|
304
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId} argv[0] must be a bare executable or absolute POSIX path`);
|
|
305
|
+
}
|
|
306
|
+
const timeoutMs = value.timeoutMs;
|
|
307
|
+
if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647) {
|
|
308
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.timeoutMs is invalid`);
|
|
309
|
+
}
|
|
310
|
+
if (value.cwd !== undefined && value.cwd !== "control") {
|
|
311
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.cwd is invalid`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
kind: "command",
|
|
315
|
+
argv,
|
|
316
|
+
timeoutMs,
|
|
317
|
+
...(value.cwd === "control" ? { cwd: "control" as const } : {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (kind === "artifacts-exist") {
|
|
321
|
+
assertExactKeys(value, ["kind", "paths"], [], runId, `gates.definitions.${gateId}`);
|
|
322
|
+
const paths = asStringArray(value.paths, runId, `gates.definitions.${gateId}.paths`);
|
|
323
|
+
if (paths.length < 1 || paths.length > 64) {
|
|
324
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.paths length is invalid`);
|
|
325
|
+
}
|
|
326
|
+
if (new Set(paths).size !== paths.length) {
|
|
327
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId}.paths are not unique`);
|
|
328
|
+
}
|
|
329
|
+
for (const path of paths) {
|
|
330
|
+
if (path === "." || !vnextPortablePath(path) || path.startsWith("/") || path.includes("\\")) {
|
|
331
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId} path is not a portable relative path`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { kind: "artifacts-exist", paths };
|
|
335
|
+
}
|
|
336
|
+
if (kind === "reserved") {
|
|
337
|
+
assertExactKeys(value, ["kind"], [], runId, `gates.definitions.${gateId}`);
|
|
338
|
+
return { kind: "reserved" };
|
|
339
|
+
}
|
|
340
|
+
throw runtimeError("run_plan_corrupt", runId, `gates.definitions.${gateId} kind is invalid`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function freezeGates(gates: VnextPinnedGates): VnextPinnedGates {
|
|
344
|
+
const definitions = Object.create(null) as Record<string, VnextGateDefinition>;
|
|
345
|
+
for (const [id, definition] of Object.entries(gates.definitions)) {
|
|
346
|
+
definitions[id] = deepFreeze({ ...definition, ...(definition.kind === "command" ? { argv: [...definition.argv] } : {}), ...(definition.kind === "artifacts-exist" ? { paths: [...definition.paths] } : {}) } as VnextGateDefinition);
|
|
347
|
+
}
|
|
348
|
+
return deepFreeze({
|
|
349
|
+
registry: gates.registry ? { ...gates.registry } : null,
|
|
350
|
+
definitions,
|
|
351
|
+
controlRoot: { ...gates.controlRoot },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseCompiledPlan(raw: unknown, runId: string): VnextCompiledPlan {
|
|
356
|
+
const value = asObject(raw, runId, "compiled plan");
|
|
357
|
+
assertExactKeys(value, PLAN_REQUIRED, PLAN_OPTIONAL, runId, "compiled plan");
|
|
358
|
+
if (value.schema !== VNEXT_COMPILED_WORKFLOW_SCHEMA) {
|
|
359
|
+
throw runtimeError("run_plan_corrupt", runId, "compiled plan schema is not kxm.compiled-workflow.v1");
|
|
360
|
+
}
|
|
361
|
+
const order = asStringArray(value.order, runId, "order");
|
|
362
|
+
const stepsValue = asObject(value.steps, runId, "steps");
|
|
363
|
+
const stepIds = Object.keys(stepsValue);
|
|
364
|
+
if (!sameSet(stepIds, order)) {
|
|
365
|
+
throw runtimeError("run_plan_corrupt", runId, "compiled plan step keys do not equal order");
|
|
366
|
+
}
|
|
367
|
+
const steps = Object.create(null) as Record<string, VnextCompiledStep>;
|
|
368
|
+
for (const stepId of order) {
|
|
369
|
+
steps[stepId] = parseStep(stepsValue[stepId], stepId, runId);
|
|
370
|
+
}
|
|
371
|
+
const entryStepId = asString(value.entryStepId, runId, "entryStepId");
|
|
372
|
+
if (!steps[entryStepId] || !order.includes(entryStepId)) {
|
|
373
|
+
throw runtimeError("run_plan_corrupt", runId, "entryStepId is not in compiled steps");
|
|
374
|
+
}
|
|
375
|
+
const limitsObject = asObject(value.limits, runId, "limits");
|
|
376
|
+
assertExactKeys(limitsObject, [], LIMIT_OPTIONAL, runId, "limits");
|
|
377
|
+
const limits: VnextCompiledPlan["limits"] = {
|
|
378
|
+
...(limitsObject.maxTransitions !== undefined ? { maxTransitions: asCount(limitsObject.maxTransitions, runId, "limits.maxTransitions") } : {}),
|
|
379
|
+
...(limitsObject.maxRunDurationMs !== undefined ? { maxRunDurationMs: asDuration(limitsObject.maxRunDurationMs, runId, "limits.maxRunDurationMs") } : {}),
|
|
380
|
+
...(limitsObject.maxAgentTimeMs !== undefined ? { maxAgentTimeMs: asDuration(limitsObject.maxAgentTimeMs, runId, "limits.maxAgentTimeMs") } : {}),
|
|
381
|
+
...(limitsObject.maxModelCost !== undefined ? { maxModelCost: asCost(limitsObject.maxModelCost, runId, "limits.maxModelCost") } : {}),
|
|
382
|
+
...(limitsObject.currency !== undefined ? { currency: asString(limitsObject.currency, runId, "limits.currency") } : {}),
|
|
383
|
+
};
|
|
384
|
+
const requirePlanHash = asStringArray(value.requirePlanHash, runId, "requirePlanHash");
|
|
385
|
+
for (const stageId of requirePlanHash) {
|
|
386
|
+
if (!steps[stageId]) throw runtimeError("run_plan_corrupt", runId, `requirePlanHash references unknown step ${stageId}`);
|
|
387
|
+
}
|
|
388
|
+
const plan: VnextCompiledPlan = {
|
|
389
|
+
schema: VNEXT_COMPILED_WORKFLOW_SCHEMA,
|
|
390
|
+
workflowId: asString(value.workflowId, runId, "workflowId"),
|
|
391
|
+
...(value.sourcePath !== undefined ? { sourcePath: asStringAllowEmpty(value.sourcePath, runId, "sourcePath") } : {}),
|
|
392
|
+
coordinator: asString(value.coordinator, runId, "coordinator"),
|
|
393
|
+
limits,
|
|
394
|
+
transitionBudget: asCount(value.transitionBudget, runId, "transitionBudget"),
|
|
395
|
+
hasBackEdges: asBoolean(value.hasBackEdges, runId, "hasBackEdges"),
|
|
396
|
+
entryStepId,
|
|
397
|
+
order,
|
|
398
|
+
steps,
|
|
399
|
+
...(value.reproOracle !== undefined ? { reproOracle: parseOracle(value.reproOracle, steps, runId, "reproOracle") } : {}),
|
|
400
|
+
...(value.planHash !== undefined ? { planHash: parseOracle(value.planHash, steps, runId, "planHash") } : {}),
|
|
401
|
+
requirePlanHash,
|
|
402
|
+
};
|
|
403
|
+
return plan;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function parseStep(raw: unknown, stepId: string, runId: string): VnextCompiledStep {
|
|
407
|
+
const value = asObject(raw, runId, `step ${stepId}`);
|
|
408
|
+
const kind = asString(value.kind, runId, `${stepId}.kind`);
|
|
409
|
+
if (!SUPPORTED_KINDS.has(kind)) {
|
|
410
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} has unsupported kind ${kind}`);
|
|
411
|
+
}
|
|
412
|
+
const extraRequired = kind === "agent" || kind === "moa"
|
|
413
|
+
? ["agent"]
|
|
414
|
+
: kind === "gate"
|
|
415
|
+
? ["gate", "expect"]
|
|
416
|
+
: kind === "wait"
|
|
417
|
+
? ["signal"]
|
|
418
|
+
: [];
|
|
419
|
+
assertExactKeys(value, [...STEP_REQUIRED, ...extraRequired], STEP_OPTIONAL, runId, `step ${stepId}`);
|
|
420
|
+
const id = asString(value.id, runId, `${stepId}.id`);
|
|
421
|
+
if (id !== stepId) throw runtimeError("run_plan_corrupt", runId, `step ${stepId} id does not match its key`);
|
|
422
|
+
const outcomes = asStringArray(value.outcomes, runId, `${stepId}.outcomes`);
|
|
423
|
+
if (new Set(outcomes).size !== outcomes.length) {
|
|
424
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} outcomes are not unique`);
|
|
425
|
+
}
|
|
426
|
+
const sorted = [...outcomes].sort();
|
|
427
|
+
if (outcomes.join("\0") !== sorted.join("\0")) {
|
|
428
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} outcomes are not sorted`);
|
|
429
|
+
}
|
|
430
|
+
const transitionsValue = asObject(value.transitions, runId, `${stepId}.transitions`);
|
|
431
|
+
if (!sameSet(Object.keys(transitionsValue), outcomes)) {
|
|
432
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} transition keys do not equal outcomes`);
|
|
433
|
+
}
|
|
434
|
+
const transitions = Object.create(null) as Record<string, VnextCompiledTransition>;
|
|
435
|
+
for (const outcome of outcomes) {
|
|
436
|
+
transitions[outcome] = parseTransition(transitionsValue[outcome], stepId, outcome, runId);
|
|
437
|
+
}
|
|
438
|
+
const repositoriesValue = asObject(value.repositories, runId, `${stepId}.repositories`);
|
|
439
|
+
const repositories = Object.create(null) as Record<string, VnextRepositoryAccess>;
|
|
440
|
+
for (const [repositoryId, access] of Object.entries(repositoriesValue)) {
|
|
441
|
+
if (typeof access !== "string" || !REPOSITORY_ACCESS.has(access)) {
|
|
442
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} repository ${repositoryId} has invalid access`);
|
|
443
|
+
}
|
|
444
|
+
repositories[repositoryId] = access as VnextRepositoryAccess;
|
|
445
|
+
}
|
|
446
|
+
const base = {
|
|
447
|
+
id,
|
|
448
|
+
index: asIndex(value.index, runId, `${stepId}.index`),
|
|
449
|
+
...(value.description !== undefined ? { description: asStringAllowEmpty(value.description, runId, `${stepId}.description`) } : {}),
|
|
450
|
+
...(value.instructions !== undefined ? { instructions: asStringAllowEmpty(value.instructions, runId, `${stepId}.instructions`) } : {}),
|
|
451
|
+
maxAttempts: asCount(value.maxAttempts, runId, `${stepId}.maxAttempts`),
|
|
452
|
+
...(value.timeoutMs !== undefined ? { timeoutMs: asDuration(value.timeoutMs, runId, `${stepId}.timeoutMs`) } : {}),
|
|
453
|
+
safeSpeculation: asBoolean(value.safeSpeculation, runId, `${stepId}.safeSpeculation`),
|
|
454
|
+
repositories,
|
|
455
|
+
...(value.tools !== undefined ? { tools: asJsonObject(value.tools, runId, `${stepId}.tools`) } : {}),
|
|
456
|
+
secrets: asObjectArray(value.secrets, runId, `${stepId}.secrets`).map((entry) => ({ ...entry })),
|
|
457
|
+
...(value.model !== undefined ? { model: cloneJson(value.model) } : {}),
|
|
458
|
+
requiredEvidence: asUnknownArray(value.requiredEvidence, runId, `${stepId}.requiredEvidence`).map((entry, index) => (
|
|
459
|
+
parseEvidence(entry, stepId, index, runId)
|
|
460
|
+
)),
|
|
461
|
+
outcomes,
|
|
462
|
+
transitions,
|
|
463
|
+
requiresPlanHash: asBoolean(value.requiresPlanHash, runId, `${stepId}.requiresPlanHash`),
|
|
464
|
+
assignments: parseAssignments(value.assignments, stepId, runId),
|
|
465
|
+
join: parseJoin(value.join, stepId, runId),
|
|
466
|
+
};
|
|
467
|
+
if (kind === "agent" || kind === "moa") {
|
|
468
|
+
return { ...base, kind, agent: asString(value.agent, runId, `${stepId}.agent`) };
|
|
469
|
+
}
|
|
470
|
+
if (kind === "gate") {
|
|
471
|
+
const expect = asString(value.expect, runId, `${stepId}.expect`);
|
|
472
|
+
if (expect !== "pass" && expect !== "fail") {
|
|
473
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} expect is invalid`);
|
|
474
|
+
}
|
|
475
|
+
return { ...base, kind, gate: asString(value.gate, runId, `${stepId}.gate`), expect };
|
|
476
|
+
}
|
|
477
|
+
if (kind === "wait") {
|
|
478
|
+
return { ...base, kind, signal: asString(value.signal, runId, `${stepId}.signal`) };
|
|
479
|
+
}
|
|
480
|
+
return { ...base, kind: "approval" };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseTransition(raw: unknown, stepId: string, outcome: string, runId: string): VnextCompiledTransition {
|
|
484
|
+
const value = asObject(raw, runId, `${stepId}.${outcome}`);
|
|
485
|
+
const to = asString(value.to, runId, `${stepId}.${outcome}.to`);
|
|
486
|
+
const maxTransitions = value.maxTransitions !== undefined
|
|
487
|
+
? asCount(value.maxTransitions, runId, `${stepId}.${outcome}.maxTransitions`)
|
|
488
|
+
: undefined;
|
|
489
|
+
if (to === "step") {
|
|
490
|
+
assertExactKeys(value, ["to", "target", "edge"], ["maxTransitions"], runId, `${stepId}.${outcome}`);
|
|
491
|
+
const edge = asString(value.edge, runId, `${stepId}.${outcome}.edge`);
|
|
492
|
+
if (edge !== "forward" && edge !== "back") {
|
|
493
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} transition ${outcome} edge is invalid`);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
to: "step",
|
|
497
|
+
target: asString(value.target, runId, `${stepId}.${outcome}.target`),
|
|
498
|
+
edge,
|
|
499
|
+
...(maxTransitions !== undefined ? { maxTransitions } : {}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (to === "terminal") {
|
|
503
|
+
assertExactKeys(value, ["to", "terminalStatus"], ["maxTransitions"], runId, `${stepId}.${outcome}`);
|
|
504
|
+
const terminalStatus = asString(value.terminalStatus, runId, `${stepId}.${outcome}.terminalStatus`);
|
|
505
|
+
if (!TERMINAL.has(terminalStatus)) {
|
|
506
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} has an illegal terminal status`);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
to: "terminal",
|
|
510
|
+
terminalStatus: terminalStatus as "completed" | "failed" | "cancelled",
|
|
511
|
+
...(maxTransitions !== undefined ? { maxTransitions } : {}),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} transition ${outcome} is not a discriminated transition`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function parseAssignments(raw: unknown, stepId: string, runId: string): VnextCompiledAssignments {
|
|
518
|
+
const value = asObject(raw, runId, `${stepId}.assignments`);
|
|
519
|
+
assertExactKeys(value, ASSIGNMENT_REQUIRED, ASSIGNMENT_OPTIONAL, runId, `${stepId}.assignments`);
|
|
520
|
+
const distinctBy = asStringArray(value.distinctBy, runId, `${stepId}.assignments.distinctBy`);
|
|
521
|
+
if (distinctBy.some((entry) => !DISTINCT_BY.has(entry))) {
|
|
522
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} distinctBy is invalid`);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
allowedAgents: asStringArray(value.allowedAgents, runId, `${stepId}.assignments.allowedAgents`),
|
|
526
|
+
minimum: asCount(value.minimum, runId, `${stepId}.assignments.minimum`),
|
|
527
|
+
target: asCount(value.target, runId, `${stepId}.assignments.target`),
|
|
528
|
+
maximum: asCount(value.maximum, runId, `${stepId}.assignments.maximum`),
|
|
529
|
+
maxParallel: asCount(value.maxParallel, runId, `${stepId}.assignments.maxParallel`),
|
|
530
|
+
maxAttemptsPerAssignment: asCount(value.maxAttemptsPerAssignment, runId, `${stepId}.assignments.maxAttemptsPerAssignment`),
|
|
531
|
+
...(value.maxWriteRepositories !== undefined
|
|
532
|
+
? { maxWriteRepositories: asCount(value.maxWriteRepositories, runId, `${stepId}.assignments.maxWriteRepositories`) }
|
|
533
|
+
: {}),
|
|
534
|
+
distinctBy: distinctBy as Array<"provider" | "model" | "profile">,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function parseJoin(raw: unknown, stepId: string, runId: string): VnextCompiledJoin {
|
|
539
|
+
const value = asObject(raw, runId, `${stepId}.join`);
|
|
540
|
+
assertExactKeys(value, JOIN_REQUIRED, JOIN_OPTIONAL, runId, `${stepId}.join`);
|
|
541
|
+
const strategy = asString(value.strategy, runId, `${stepId}.join.strategy`);
|
|
542
|
+
if (!JOIN_STRATEGIES.has(strategy)) {
|
|
543
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} join strategy is invalid`);
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
strategy: strategy as VnextCompiledJoin["strategy"],
|
|
547
|
+
...(value.minimumPassed !== undefined ? { minimumPassed: asCount(value.minimumPassed, runId, `${stepId}.join.minimumPassed`) } : {}),
|
|
548
|
+
...(value.cancelRemaining !== undefined ? { cancelRemaining: asBoolean(value.cancelRemaining, runId, `${stepId}.join.cancelRemaining`) } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function parseEvidence(raw: unknown, stepId: string, index: number, runId: string): VnextCompiledEvidence {
|
|
553
|
+
const value = asObject(raw, runId, `${stepId}.requiredEvidence[${index}]`);
|
|
554
|
+
assertExactKeys(value, EVIDENCE_REQUIRED, EVIDENCE_OPTIONAL, runId, `${stepId}.requiredEvidence[${index}]`);
|
|
555
|
+
const kind = asString(value.kind, runId, `${stepId}.requiredEvidence[${index}].kind`);
|
|
556
|
+
if (!EVIDENCE_KINDS.has(kind)) {
|
|
557
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} evidence kind is invalid`);
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
key: asString(value.key, runId, `${stepId}.requiredEvidence[${index}].key`),
|
|
561
|
+
kind: kind as VnextCompiledEvidence["kind"],
|
|
562
|
+
minimum: asCount(value.minimum, runId, `${stepId}.requiredEvidence[${index}].minimum`),
|
|
563
|
+
reusableAcrossAttempts: asBoolean(value.reusableAcrossAttempts, runId, `${stepId}.requiredEvidence[${index}].reusableAcrossAttempts`),
|
|
564
|
+
...(value.producerPolicy !== undefined ? { producerPolicy: parseProducerPolicy(value.producerPolicy, stepId, runId) } : {}),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function parseProducerPolicy(raw: unknown, stepId: string, runId: string): NonNullable<VnextCompiledEvidence["producerPolicy"]> {
|
|
569
|
+
const value = asObject(raw, runId, `${stepId}.producerPolicy`);
|
|
570
|
+
assertExactKeys(value, ["minimumProducers", "eligibleAgents", "acceptedStatuses"], ["degradation"], runId, `${stepId}.producerPolicy`);
|
|
571
|
+
const accepted = value.acceptedStatuses;
|
|
572
|
+
if (!Array.isArray(accepted) || accepted.length !== 1 || accepted[0] !== "passed") {
|
|
573
|
+
throw runtimeError("run_plan_corrupt", runId, `step ${stepId} producerPolicy.acceptedStatuses is invalid`);
|
|
574
|
+
}
|
|
575
|
+
let degradation: { minimumProducers: number } | undefined;
|
|
576
|
+
if (value.degradation !== undefined) {
|
|
577
|
+
const nested = asObject(value.degradation, runId, `${stepId}.producerPolicy.degradation`);
|
|
578
|
+
assertExactKeys(nested, ["minimumProducers"], [], runId, `${stepId}.producerPolicy.degradation`);
|
|
579
|
+
degradation = { minimumProducers: asCount(nested.minimumProducers, runId, `${stepId}.producerPolicy.degradation.minimumProducers`) };
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
minimumProducers: asCount(value.minimumProducers, runId, `${stepId}.producerPolicy.minimumProducers`),
|
|
583
|
+
eligibleAgents: asStringArray(value.eligibleAgents, runId, `${stepId}.producerPolicy.eligibleAgents`),
|
|
584
|
+
acceptedStatuses: ["passed"],
|
|
585
|
+
...(degradation ? { degradation } : {}),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseOracle(
|
|
590
|
+
raw: unknown,
|
|
591
|
+
steps: Readonly<Record<string, VnextCompiledStep>>,
|
|
592
|
+
runId: string,
|
|
593
|
+
label: string,
|
|
594
|
+
): { stageId: string; evidenceKey: string } {
|
|
595
|
+
const value = asObject(raw, runId, label);
|
|
596
|
+
assertExactKeys(value, ["stageId", "evidenceKey"], [], runId, label);
|
|
597
|
+
const stageId = asString(value.stageId, runId, `${label}.stageId`);
|
|
598
|
+
const evidenceKey = asString(value.evidenceKey, runId, `${label}.evidenceKey`);
|
|
599
|
+
const step = steps[stageId];
|
|
600
|
+
if (!step) throw runtimeError("run_plan_corrupt", runId, `${label} references unknown stage ${stageId}`);
|
|
601
|
+
if (!step.requiredEvidence.some((item) => item.key === evidenceKey)) {
|
|
602
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} references undeclared evidence ${evidenceKey}`);
|
|
603
|
+
}
|
|
604
|
+
return { stageId, evidenceKey };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function asObject(value: unknown, runId: string, label: string): Record<string, unknown> {
|
|
608
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
609
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not an object`);
|
|
610
|
+
}
|
|
611
|
+
return value as Record<string, unknown>;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function assertExactKeys(
|
|
615
|
+
value: Record<string, unknown>,
|
|
616
|
+
required: readonly string[],
|
|
617
|
+
optional: readonly string[],
|
|
618
|
+
runId: string,
|
|
619
|
+
label: string,
|
|
620
|
+
): void {
|
|
621
|
+
const allowed = new Set([...required, ...optional]);
|
|
622
|
+
for (const key of required) {
|
|
623
|
+
if (!(key in value)) throw runtimeError("run_plan_corrupt", runId, `${label} is missing ${key}`);
|
|
624
|
+
}
|
|
625
|
+
for (const key of Object.keys(value)) {
|
|
626
|
+
if (!allowed.has(key)) throw runtimeError("run_plan_corrupt", runId, `${label} has unexpected key ${key}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function asString(value: unknown, runId: string, label: string): string {
|
|
631
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
632
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a non-empty string`);
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function asStringAllowEmpty(value: unknown, runId: string, label: string): string {
|
|
638
|
+
if (typeof value !== "string") throw runtimeError("run_plan_corrupt", runId, `${label} is not a string`);
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function asBoolean(value: unknown, runId: string, label: string): boolean {
|
|
643
|
+
if (typeof value !== "boolean") throw runtimeError("run_plan_corrupt", runId, `${label} is not a boolean`);
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function asCount(value: unknown, runId: string, label: string): number {
|
|
648
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
649
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a positive integer`);
|
|
650
|
+
}
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function asDuration(value: unknown, runId: string, label: string): number {
|
|
655
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
656
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a duration integer`);
|
|
657
|
+
}
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function asCost(value: unknown, runId: string, label: string): number {
|
|
662
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
663
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a positive cost`);
|
|
664
|
+
}
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function asIndex(value: unknown, runId: string, label: string): number {
|
|
669
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
670
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a non-negative integer`);
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function asStringArray(value: unknown, runId: string, label: string): string[] {
|
|
676
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
677
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not a string array`);
|
|
678
|
+
}
|
|
679
|
+
return [...value];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function asUnknownArray(value: unknown, runId: string, label: string): unknown[] {
|
|
683
|
+
if (!Array.isArray(value)) throw runtimeError("run_plan_corrupt", runId, `${label} is not an array`);
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function asObjectArray(value: unknown, runId: string, label: string): JsonObject[] {
|
|
688
|
+
if (!Array.isArray(value)) throw runtimeError("run_plan_corrupt", runId, `${label} is not an array`);
|
|
689
|
+
return value.map((entry, index) => asJsonObject(entry, runId, `${label}[${index}]`));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function asJsonObject(value: unknown, runId: string, label: string): JsonObject {
|
|
693
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
694
|
+
throw runtimeError("run_plan_corrupt", runId, `${label} is not an object`);
|
|
695
|
+
}
|
|
696
|
+
return cloneJson(value) as JsonObject;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function cloneJson(value: unknown): JsonValue {
|
|
700
|
+
return JSON.parse(JSON.stringify(value)) as JsonValue;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function sameSet(left: readonly string[], right: readonly string[]): boolean {
|
|
704
|
+
if (left.length !== right.length) return false;
|
|
705
|
+
const set = new Set(right);
|
|
706
|
+
return left.every((entry) => set.has(entry));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function deepFreeze<T>(value: T): T {
|
|
710
|
+
if (value === null || typeof value !== "object") return value;
|
|
711
|
+
if (Array.isArray(value)) {
|
|
712
|
+
for (const entry of value) deepFreeze(entry);
|
|
713
|
+
} else {
|
|
714
|
+
for (const entry of Object.values(value as Record<string, unknown>)) deepFreeze(entry);
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze(value);
|
|
717
|
+
}
|