@oisincoveney/pipeline 1.29.3 → 2.0.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/README.md CHANGED
@@ -189,7 +189,7 @@ Package-owned defaults declare:
189
189
  - the singleton `pipeline-gateway` MCP connection
190
190
  - workflows, schedules, hooks, gates, artifacts, retries, and timeouts
191
191
  - generated OpenCode host resources
192
- - goal-loop contracts and continuation context
192
+ - internal goal-state artifacts and goal-loop continuation context
193
193
 
194
194
  Current default entrypoints:
195
195
 
@@ -3,7 +3,8 @@
3
3
  The v1 runtime pipeline is package-owned config. Package-owned defaults declare
4
4
  runner adapters, profiles, MCP gateway backends, the orchestrator profile,
5
5
  entrypoints, schedules, hooks, workflows, gates, artifacts, OpenCode host
6
- resources, and goal-loop contracts. OpenCode is the package runtime.
6
+ resources, and internal goal-loop continuation behavior. OpenCode is the package
7
+ runtime.
7
8
 
8
9
  Runtime code does not read `.pipeline/config.toml`, phase profiles, or hardcoded
9
10
  prompt constants.
package/package.json CHANGED
@@ -86,14 +86,6 @@
86
86
  "types": "./dist/runner-command-contract.d.ts",
87
87
  "import": "./dist/runner-command-contract.js"
88
88
  },
89
- "./runtime/goal-loop": {
90
- "types": "./dist/runtime/goal-loop.d.ts",
91
- "import": "./dist/runtime/goal-loop.js"
92
- },
93
- "./runtime/goal-state": {
94
- "types": "./dist/runtime/goal-state.d.ts",
95
- "import": "./dist/runtime/goal-state.js"
96
- },
97
89
  "./runtime": {
98
90
  "types": "./dist/pipeline-runtime.d.ts",
99
91
  "import": "./dist/pipeline-runtime.js"
@@ -123,7 +115,7 @@
123
115
  "prepack": "bun run build:cli"
124
116
  },
125
117
  "type": "module",
126
- "version": "1.29.3",
118
+ "version": "2.0.0",
127
119
  "description": "Config-driven multi-agent pipeline runner for repository work",
128
120
  "main": "./dist/index.js",
129
121
  "types": "./dist/index.d.ts",
@@ -1,125 +0,0 @@
1
- import { goalStateNextRequirement } from "../goal-state/goal-requirement.js";
2
- //#region src/runtime/goal-loop/continuation-prompt.ts
3
- function renderContinuationPrompt(input) {
4
- const currentNode = currentScheduleNode(input.state, input.currentNodeId);
5
- const failedGates = input.state.gateFailures.filter((gate) => !gate.passed);
6
- return compactLines([
7
- "# Pipeline Continuation",
8
- "",
9
- "## Original Task",
10
- input.state.task.original,
11
- "",
12
- "## Task Refs",
13
- ...taskRefLines(input.state.task.context),
14
- "",
15
- "## Schedule",
16
- ...scheduleLines(input.state),
17
- "",
18
- "## Current Schedule Node Context",
19
- ...nodeContextLines(currentNode),
20
- "",
21
- "## Failed Gates",
22
- ...failedGateLines(failedGates),
23
- "",
24
- "## Verifier Evidence",
25
- ...verifierLines(input.state),
26
- "",
27
- "## Acceptance Evidence",
28
- ...acceptanceLines(input.state),
29
- "",
30
- "## Changed Files Summary",
31
- ...changedFileLines(input.state),
32
- "",
33
- "## Prior Attempts",
34
- ...priorAttemptLines(input.state),
35
- "",
36
- "## Exact Next Requirement",
37
- input.exactNextRequirement ?? exactNextRequirement(input.state),
38
- "",
39
- "## Discipline",
40
- "- Continue from the persisted goal state; do not restart the task.",
41
- "- Address the failed evidence directly and preserve completed work.",
42
- "- Run the real verification path needed to prove the fix.",
43
- "- Stop and report blocked if the same failure repeats without new files or evidence."
44
- ]);
45
- }
46
- function exactNextRequirement(state) {
47
- return goalStateNextRequirement(state);
48
- }
49
- function currentScheduleNode(state, requestedNodeId) {
50
- if (requestedNodeId && state.nodes[requestedNodeId]) return state.nodes[requestedNodeId];
51
- return [...Object.values(state.nodes)].reverse().find((node) => node.status === "failed") ?? Object.values(state.nodes).at(-1);
52
- }
53
- function taskRefLines(context) {
54
- if (!isRecord(context)) return ["- No task context recorded."];
55
- return [
56
- lineFor("id", context.id),
57
- lineFor("title", context.title),
58
- lineFor("description", context.description),
59
- ...acceptanceCriteriaRefLines(context.acceptanceCriteria)
60
- ].filter(Boolean);
61
- }
62
- function acceptanceCriteriaRefLines(value) {
63
- if (!Array.isArray(value) || value.length === 0) return [];
64
- return ["- acceptance_criteria:", ...value.flatMap((item) => {
65
- if (!isRecord(item)) return [];
66
- return [` - ${String(item.id ?? "?")}: ${String(item.text ?? "")}`];
67
- })];
68
- }
69
- function scheduleLines(state) {
70
- return [
71
- `- workflow_id: ${state.workflowId}`,
72
- state.runId ? `- run_id: ${state.runId}` : "",
73
- state.schedule?.id ? `- schedule_id: ${state.schedule.id}` : "",
74
- state.schedule?.path ? `- schedule_path: ${state.schedule.path}` : ""
75
- ].filter(Boolean);
76
- }
77
- function nodeContextLines(node) {
78
- if (!node) return ["- No schedule node has run yet."];
79
- return [
80
- `- node_id: ${node.nodeId}`,
81
- `- status: ${node.status}`,
82
- `- attempts: ${node.attempts}`,
83
- node.profile ? `- profile: ${node.profile}` : "",
84
- node.runnerId ? `- runner: ${node.runnerId}` : "",
85
- node.changedFiles.length ? `- node_changed_files: ${node.changedFiles.join(", ")}` : "",
86
- ...node.gates.map((gate) => `- gate ${gate.gateId}: ${gate.passed ? "PASS" : "FAIL"}${gate.reason ? ` (${gate.reason})` : ""}`)
87
- ].filter(Boolean);
88
- }
89
- function failedGateLines(gates) {
90
- if (gates.length === 0) return ["- No failed gates recorded."];
91
- return gates.flatMap((gate) => [`- ${gate.nodeId}/${gate.gateId}: ${gate.reason ?? "failed"}`, ...gate.evidence.map((item) => ` evidence: ${item}`)]);
92
- }
93
- function verifierLines(state) {
94
- const verifier = state.verifier;
95
- if (!verifier.verdict && verifier.evidence.length === 0) return ["- No verifier evidence recorded."];
96
- return [
97
- verifier.nodeId ? `- node_id: ${verifier.nodeId}` : "",
98
- verifier.verdict ? `- verdict: ${verifier.verdict}` : "",
99
- verifier.reason ? `- reason: ${verifier.reason}` : "",
100
- ...verifier.evidence.map((item) => `- evidence: ${item}`)
101
- ].filter(Boolean);
102
- }
103
- function acceptanceLines(state) {
104
- if (state.acceptance.length === 0) return ["- No acceptance evidence recorded."];
105
- return state.acceptance.flatMap((item) => [`- ${item.id}: ${item.verdict}`, ...item.evidence.map((evidence) => ` evidence: ${evidence}`)]);
106
- }
107
- function changedFileLines(state) {
108
- if (state.changedFiles.length === 0) return ["- No changed files recorded."];
109
- return state.changedFiles.map((file) => `- ${file}`);
110
- }
111
- function priorAttemptLines(state) {
112
- if (state.continuationAttempts.length === 0) return ["- No prior continuation attempts."];
113
- return state.continuationAttempts.map((attempt) => `- #${attempt.attempt}: ${attempt.reason}${attempt.promptPath ? ` (${attempt.promptPath})` : ""}`);
114
- }
115
- function lineFor(label, value) {
116
- return typeof value === "string" && value.length > 0 ? `- ${label}: ${value}` : "";
117
- }
118
- function compactLines(lines) {
119
- return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`;
120
- }
121
- function isRecord(value) {
122
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
123
- }
124
- //#endregion
125
- export { exactNextRequirement, renderContinuationPrompt };
@@ -1,37 +0,0 @@
1
- import { PipelineConfig } from "../config/schemas.js";
2
- import { RunnerLaunchPlan } from "../runner.js";
3
- import { PipelineGoalState } from "./goal-state.js";
4
-
5
- //#region src/runtime/goal-loop/goal-loop.d.ts
6
- type GoalLoopTerminalState = "blocked" | "cancelled" | "max_continuations_reached" | "no_progress_detected" | "passed";
7
- interface GoalLoopContinuationInput {
8
- attempt: number;
9
- prompt: string;
10
- promptPath?: string;
11
- state: PipelineGoalState;
12
- }
13
- interface GoalLoopOptions {
14
- initialState: PipelineGoalState;
15
- maxContinuations: number;
16
- now?: () => Date;
17
- runContinuation: (input: GoalLoopContinuationInput) => PipelineGoalState | Promise<PipelineGoalState>;
18
- shouldCancel?: () => boolean;
19
- writePrompt?: (attempt: number, prompt: string, state: PipelineGoalState) => string | Promise<string>;
20
- }
21
- interface GoalLoopResult {
22
- attempts: number;
23
- prompts: string[];
24
- reason: string;
25
- state: PipelineGoalState;
26
- terminalState: GoalLoopTerminalState;
27
- }
28
- declare function runBoundedGoalLoop(options: GoalLoopOptions): Promise<GoalLoopResult>;
29
- declare function createGoalContinuationLaunchPlan(input: {
30
- config: PipelineConfig;
31
- nodeId?: string;
32
- profileId?: string;
33
- prompt: string;
34
- worktreePath: string;
35
- }): RunnerLaunchPlan;
36
- //#endregion
37
- export { GoalLoopContinuationInput, GoalLoopOptions, GoalLoopResult, GoalLoopTerminalState, createGoalContinuationLaunchPlan, runBoundedGoalLoop };
@@ -1,138 +0,0 @@
1
- import { createRunnerLaunchPlan } from "../runner.js";
2
- import { goalStateCompletionEvidence, goalStateFailureSignature, markGoalStateBlocked, recordGoalStateContinuationAttempt } from "./goal-state.js";
3
- import { exactNextRequirement, renderContinuationPrompt } from "./goal-loop/continuation-prompt.js";
4
- //#region src/runtime/goal-loop/goal-loop.ts
5
- async function runBoundedGoalLoop(options) {
6
- if (!Number.isInteger(options.maxContinuations) || options.maxContinuations < 0) throw new Error("maxContinuations must be a non-negative integer");
7
- let state = options.initialState;
8
- const prompts = [];
9
- for (;;) {
10
- const terminal = terminalResult(state, prompts, options.shouldCancel);
11
- if (terminal) return terminal;
12
- const maxContinuations = maxContinuationsResult(state, prompts, options);
13
- if (maxContinuations) return maxContinuations;
14
- const beforeProgress = progressSignature(state);
15
- const nextState = await runGoalContinuation(options, state, prompts);
16
- const postContinuation = postContinuationResult(nextState, beforeProgress, prompts, options.shouldCancel);
17
- if (postContinuation) return postContinuation;
18
- state = nextState;
19
- }
20
- }
21
- function maxContinuationsResult(state, prompts, options) {
22
- return state.continuationAttempts.length >= options.maxContinuations ? {
23
- attempts: state.continuationAttempts.length,
24
- prompts,
25
- reason: `maximum continuations reached: ${options.maxContinuations}`,
26
- state,
27
- terminalState: "max_continuations_reached"
28
- } : null;
29
- }
30
- async function runGoalContinuation(options, state, prompts) {
31
- const attempt = state.continuationAttempts.length + 1;
32
- const prompt = renderContinuationPrompt({
33
- exactNextRequirement: exactNextRequirement(state),
34
- state
35
- });
36
- const promptPath = await options.writePrompt?.(attempt, prompt, state);
37
- prompts.push(prompt);
38
- return options.runContinuation({
39
- attempt,
40
- prompt,
41
- ...promptPath ? { promptPath } : {},
42
- state: stateWithContinuationAttempt(state, promptPath)
43
- });
44
- }
45
- function stateWithContinuationAttempt(state, promptPath) {
46
- return recordGoalStateContinuationAttempt(state, {
47
- ...promptPath ? { promptPath } : {},
48
- reason: continuationReason(state),
49
- ...state.verifier.nodeId ? { verifierNodeId: state.verifier.nodeId } : {}
50
- });
51
- }
52
- function postContinuationResult(nextState, beforeProgress, prompts, shouldCancel) {
53
- const terminal = terminalResult(nextState, prompts, shouldCancel);
54
- if (terminal) return terminal;
55
- return isNoProgress(beforeProgress, progressSignature(nextState)) ? noProgressResult(nextState, prompts) : null;
56
- }
57
- function noProgressResult(state, prompts) {
58
- const reason = "same failure repeated without new changed files or evidence";
59
- return {
60
- attempts: state.continuationAttempts.length,
61
- prompts,
62
- reason,
63
- state: markGoalStateBlocked(state, reason),
64
- terminalState: "no_progress_detected"
65
- };
66
- }
67
- function createGoalContinuationLaunchPlan(input) {
68
- return createRunnerLaunchPlan(input.config, {
69
- nodeId: input.nodeId ?? "goal-continuation",
70
- profileId: input.profileId ?? "moka-code-writer",
71
- prompt: input.prompt,
72
- worktreePath: input.worktreePath
73
- });
74
- }
75
- function terminalResult(state, prompts, shouldCancel) {
76
- if (shouldCancel?.()) return {
77
- attempts: state.continuationAttempts.length,
78
- prompts,
79
- reason: "goal loop cancelled",
80
- state,
81
- terminalState: "cancelled"
82
- };
83
- switch (state.terminalOutcome) {
84
- case "PASS":
85
- if (!goalStateCompletionEvidence(state).passed) return {
86
- attempts: state.continuationAttempts.length,
87
- prompts,
88
- reason: "missing deterministic verifier or acceptance evidence",
89
- state: markGoalStateBlocked(state, "missing deterministic verifier or acceptance evidence"),
90
- terminalState: "blocked"
91
- };
92
- return {
93
- attempts: state.continuationAttempts.length,
94
- prompts,
95
- reason: "goal passed",
96
- state,
97
- terminalState: "passed"
98
- };
99
- case "BLOCKED": return {
100
- attempts: state.continuationAttempts.length,
101
- prompts,
102
- reason: state.blockedReasons.at(-1) ?? "goal blocked",
103
- state,
104
- terminalState: "blocked"
105
- };
106
- case "CANCELLED": return {
107
- attempts: state.continuationAttempts.length,
108
- prompts,
109
- reason: "goal cancelled",
110
- state,
111
- terminalState: "cancelled"
112
- };
113
- default: return null;
114
- }
115
- }
116
- function continuationReason(state) {
117
- const latestGate = state.gateFailures.at(-1);
118
- if (latestGate) return latestGate.reason ?? `failed gate ${latestGate.gateId}`;
119
- if (state.verifier.verdict === "FAIL") return state.verifier.reason ?? "verifier requested remediation";
120
- return "goal incomplete";
121
- }
122
- function progressSignature(state) {
123
- const failedAcceptance = state.acceptance.filter((item) => item.verdict === "FAIL");
124
- return {
125
- changedFiles: state.changedFiles.join("\0"),
126
- evidence: [
127
- ...state.gateFailures.flatMap((gate) => gate.evidence),
128
- ...state.verifier.evidence,
129
- ...failedAcceptance.flatMap((item) => item.evidence)
130
- ].join("\0"),
131
- failure: goalStateFailureSignature(state)
132
- };
133
- }
134
- function isNoProgress(before, after) {
135
- return before.failure.length > 0 && before.failure === after.failure && before.changedFiles === after.changedFiles && before.evidence === after.evidence;
136
- }
137
- //#endregion
138
- export { createGoalContinuationLaunchPlan, runBoundedGoalLoop };
@@ -1,11 +0,0 @@
1
- //#region src/runtime/goal-state/goal-requirement.ts
2
- function goalStateNextRequirement(state) {
3
- const failedAcceptance = state.acceptance.filter((item) => item.verdict === "FAIL");
4
- if (failedAcceptance.length > 0) return `Satisfy failed acceptance criteria: ${failedAcceptance.map((item) => item.id).join(", ")}.`;
5
- if (state.verifier.verdict === "FAIL") return `Satisfy verifier node '${state.verifier.nodeId ?? "verify"}' and return passing evidence.`;
6
- const latestGate = state.gateFailures.at(-1);
7
- if (latestGate) return `Resolve failed gate '${latestGate.gateId}' on node '${latestGate.nodeId}' and rerun the required verification.`;
8
- return "Complete the remaining schedule work and provide passing evidence.";
9
- }
10
- //#endregion
11
- export { goalStateNextRequirement };
@@ -1,146 +0,0 @@
1
- import { PipelineRuntimeEvent, PipelineTaskContext } from "./contracts/contracts.js";
2
- import { z } from "zod";
3
-
4
- //#region src/runtime/goal-state/goal-state.d.ts
5
- declare const pipelineGoalStateSchema: z.ZodObject<{
6
- acceptance: z.ZodDefault<z.ZodArray<z.ZodObject<{
7
- evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
8
- id: z.ZodString;
9
- violations: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
- verdict: z.ZodEnum<{
11
- FAIL: "FAIL";
12
- PASS: "PASS";
13
- }>;
14
- }, z.core.$strict>>>;
15
- blockedReasons: z.ZodDefault<z.ZodArray<z.ZodString>>;
16
- changedFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
17
- continuationAttempts: z.ZodDefault<z.ZodArray<z.ZodObject<{
18
- attempt: z.ZodNumber;
19
- promptPath: z.ZodOptional<z.ZodString>;
20
- reason: z.ZodString;
21
- verifierNodeId: z.ZodOptional<z.ZodString>;
22
- }, z.core.$strict>>>;
23
- gateFailures: z.ZodDefault<z.ZodArray<z.ZodObject<{
24
- evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
25
- gateId: z.ZodString;
26
- kind: z.ZodString;
27
- nodeId: z.ZodString;
28
- passed: z.ZodBoolean;
29
- reason: z.ZodOptional<z.ZodString>;
30
- }, z.core.$strict>>>;
31
- nodes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
32
- attempts: z.ZodNumber;
33
- changedFiles: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
- exitCode: z.ZodOptional<z.ZodNumber>;
35
- gates: z.ZodDefault<z.ZodArray<z.ZodObject<{
36
- evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
- gateId: z.ZodString;
38
- kind: z.ZodString;
39
- nodeId: z.ZodString;
40
- passed: z.ZodBoolean;
41
- reason: z.ZodOptional<z.ZodString>;
42
- }, z.core.$strict>>>;
43
- nodeId: z.ZodString;
44
- profile: z.ZodOptional<z.ZodString>;
45
- runnerId: z.ZodOptional<z.ZodString>;
46
- status: z.ZodEnum<{
47
- passed: "passed";
48
- failed: "failed";
49
- pending: "pending";
50
- running: "running";
51
- }>;
52
- }, z.core.$strict>>>;
53
- runId: z.ZodOptional<z.ZodString>;
54
- schedule: z.ZodOptional<z.ZodObject<{
55
- id: z.ZodOptional<z.ZodString>;
56
- path: z.ZodOptional<z.ZodString>;
57
- }, z.core.$strict>>;
58
- task: z.ZodObject<{
59
- context: z.ZodOptional<z.ZodUnknown>;
60
- original: z.ZodString;
61
- }, z.core.$strict>;
62
- terminalOutcome: z.ZodOptional<z.ZodEnum<{
63
- FAIL: "FAIL";
64
- PASS: "PASS";
65
- BLOCKED: "BLOCKED";
66
- CANCELLED: "CANCELLED";
67
- }>>;
68
- verifier: z.ZodDefault<z.ZodObject<{
69
- evidence: z.ZodDefault<z.ZodArray<z.ZodString>>;
70
- nodeId: z.ZodOptional<z.ZodString>;
71
- reason: z.ZodOptional<z.ZodString>;
72
- violations: z.ZodOptional<z.ZodArray<z.ZodString>>;
73
- verdict: z.ZodOptional<z.ZodEnum<{
74
- FAIL: "FAIL";
75
- PASS: "PASS";
76
- }>>;
77
- }, z.core.$strict>>;
78
- version: z.ZodLiteral<1>;
79
- workflowId: z.ZodString;
80
- }, z.core.$strict>;
81
- type PipelineGoalState = z.infer<typeof pipelineGoalStateSchema>;
82
- interface CreateGoalStateOptions {
83
- runId?: string;
84
- scheduleId?: string;
85
- schedulePath?: string;
86
- task: string;
87
- taskContext?: PipelineTaskContext;
88
- workflowId: string;
89
- }
90
- declare function createGoalState(options: CreateGoalStateOptions): PipelineGoalState;
91
- declare function parseGoalState(value: unknown): PipelineGoalState;
92
- declare function applyGoalStateEvent(state: PipelineGoalState, event: PipelineRuntimeEvent): PipelineGoalState;
93
- declare function reconstructGoalStateFromEvents(options: CreateGoalStateOptions, events: PipelineRuntimeEvent[]): PipelineGoalState;
94
- declare function recordGoalStateChangedFiles(state: PipelineGoalState, nodeId: string, files: string[]): PipelineGoalState;
95
- declare function recordGoalStateContinuationAttempt(state: PipelineGoalState, attempt: {
96
- promptPath?: string;
97
- reason: string;
98
- verifierNodeId?: string;
99
- }): PipelineGoalState;
100
- declare function markGoalStateBlocked(state: PipelineGoalState, reason: string): PipelineGoalState;
101
- declare function goalStateCompletionEvidence(state: PipelineGoalState): {
102
- evidence: string[];
103
- passed: boolean;
104
- };
105
- declare function goalStateContinuationInput(state: PipelineGoalState): {
106
- acceptance: {
107
- evidence: string[];
108
- id: string;
109
- verdict: "FAIL" | "PASS";
110
- violations?: string[] | undefined;
111
- }[];
112
- changedFiles: string[];
113
- currentNodeId: string | undefined;
114
- exactNextRequirement: string;
115
- failedGates: {
116
- evidence: string[];
117
- gateId: string;
118
- kind: string;
119
- nodeId: string;
120
- passed: boolean;
121
- reason?: string | undefined;
122
- }[];
123
- failureSignature: string;
124
- originalTask: string;
125
- priorAttempts: {
126
- attempt: number;
127
- reason: string;
128
- promptPath?: string | undefined;
129
- verifierNodeId?: string | undefined;
130
- }[];
131
- taskContext: unknown;
132
- verifier: {
133
- evidence: string[];
134
- nodeId?: string | undefined;
135
- reason?: string | undefined;
136
- violations?: string[] | undefined;
137
- verdict?: "FAIL" | "PASS" | undefined;
138
- };
139
- };
140
- declare function goalStateFailureSignature(state: PipelineGoalState): string;
141
- declare function goalStateArtifactPath(runDirectory: string): string;
142
- declare function saveGoalState(state: PipelineGoalState, runDirectory: string): void;
143
- declare function loadGoalState(path: string): PipelineGoalState;
144
- declare function loadGoalStateFromRunDirectory(runDirectory: string): PipelineGoalState;
145
- //#endregion
146
- export { CreateGoalStateOptions, PipelineGoalState, applyGoalStateEvent, createGoalState, goalStateArtifactPath, goalStateCompletionEvidence, goalStateContinuationInput, goalStateFailureSignature, loadGoalState, loadGoalStateFromRunDirectory, markGoalStateBlocked, parseGoalState, pipelineGoalStateSchema, reconstructGoalStateFromEvents, recordGoalStateChangedFiles, recordGoalStateContinuationAttempt, saveGoalState };
@@ -1,339 +0,0 @@
1
- import { uniqueStrings } from "../strings.js";
2
- import { goalStateNextRequirement } from "./goal-state/goal-requirement.js";
3
- import { z } from "zod";
4
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- //#region src/runtime/goal-state/goal-state.ts
7
- const OUTCOME_VALUES = [
8
- "BLOCKED",
9
- "CANCELLED",
10
- "FAIL",
11
- "PASS"
12
- ];
13
- const NODE_STATUS_VALUES = [
14
- "failed",
15
- "passed",
16
- "pending",
17
- "running"
18
- ];
19
- const VERDICT_VALUES = ["FAIL", "PASS"];
20
- const MAX_EVIDENCE_ITEMS = 8;
21
- const MAX_EVIDENCE_LENGTH = 500;
22
- const evidenceSchema = z.array(z.string().max(MAX_EVIDENCE_LENGTH)).max(32);
23
- const acceptanceCriterionVerdictSchema = z.object({
24
- evidence: evidenceSchema.default([]),
25
- id: z.string().min(1),
26
- violations: evidenceSchema.optional(),
27
- verdict: z.enum(VERDICT_VALUES)
28
- }).strict();
29
- const goalGateAttemptSchema = z.object({
30
- evidence: evidenceSchema.default([]),
31
- gateId: z.string().min(1),
32
- kind: z.string().min(1),
33
- nodeId: z.string().min(1),
34
- passed: z.boolean(),
35
- reason: z.string().min(1).optional()
36
- }).strict();
37
- const goalNodeStateSchema = z.object({
38
- attempts: z.number().int().nonnegative(),
39
- changedFiles: z.array(z.string().min(1)).default([]),
40
- exitCode: z.number().int().optional(),
41
- gates: z.array(goalGateAttemptSchema).default([]),
42
- nodeId: z.string().min(1),
43
- profile: z.string().min(1).optional(),
44
- runnerId: z.string().min(1).optional(),
45
- status: z.enum(NODE_STATUS_VALUES)
46
- }).strict();
47
- const continuationAttemptSchema = z.object({
48
- attempt: z.number().int().positive(),
49
- promptPath: z.string().min(1).optional(),
50
- reason: z.string().min(1),
51
- verifierNodeId: z.string().min(1).optional()
52
- }).strict();
53
- const pipelineGoalStateSchema = z.object({
54
- acceptance: z.array(acceptanceCriterionVerdictSchema).default([]),
55
- blockedReasons: evidenceSchema.default([]),
56
- changedFiles: z.array(z.string().min(1)).default([]),
57
- continuationAttempts: z.array(continuationAttemptSchema).default([]),
58
- gateFailures: z.array(goalGateAttemptSchema).default([]),
59
- nodes: z.record(z.string().min(1), goalNodeStateSchema).default({}),
60
- runId: z.string().min(1).optional(),
61
- schedule: z.object({
62
- id: z.string().min(1).optional(),
63
- path: z.string().min(1).optional()
64
- }).strict().optional(),
65
- task: z.object({
66
- context: z.unknown().optional(),
67
- original: z.string()
68
- }).strict(),
69
- terminalOutcome: z.enum(OUTCOME_VALUES).optional(),
70
- verifier: z.object({
71
- evidence: evidenceSchema.default([]),
72
- nodeId: z.string().min(1).optional(),
73
- reason: z.string().min(1).optional(),
74
- violations: evidenceSchema.optional(),
75
- verdict: z.enum(VERDICT_VALUES).optional()
76
- }).strict().default({ evidence: [] }),
77
- version: z.literal(1),
78
- workflowId: z.string().min(1)
79
- }).strict();
80
- function createGoalState(options) {
81
- return parseGoalState({
82
- acceptance: [],
83
- blockedReasons: [],
84
- changedFiles: [],
85
- continuationAttempts: [],
86
- gateFailures: [],
87
- nodes: {},
88
- ...options.runId ? { runId: options.runId } : {},
89
- ...options.scheduleId || options.schedulePath ? { schedule: {
90
- ...options.scheduleId ? { id: options.scheduleId } : {},
91
- ...options.schedulePath ? { path: options.schedulePath } : {}
92
- } } : {},
93
- task: {
94
- ...options.taskContext ? { context: options.taskContext } : {},
95
- original: options.task
96
- },
97
- verifier: {},
98
- version: 1,
99
- workflowId: options.workflowId
100
- });
101
- }
102
- function parseGoalState(value) {
103
- return pipelineGoalStateSchema.parse(value);
104
- }
105
- function applyGoalStateEvent(state, event) {
106
- const next = cloneGoalState(state);
107
- switch (event.type) {
108
- case "workflow.planned":
109
- for (const node of event.nodes) upsertNode(next, node.id, {
110
- profile: node.profile,
111
- runnerId: node.runnerId,
112
- status: "pending"
113
- });
114
- break;
115
- case "node.start":
116
- case "agent.start":
117
- upsertNode(next, event.nodeId, {
118
- attempts: event.attempt,
119
- profile: event.profile,
120
- runnerId: event.runnerId,
121
- status: "running"
122
- });
123
- break;
124
- case "node.finish":
125
- upsertNode(next, event.nodeId, {
126
- attempts: event.attempt,
127
- exitCode: event.exitCode,
128
- profile: event.profile,
129
- runnerId: event.runnerId,
130
- status: event.status === "passed" ? "passed" : "failed"
131
- });
132
- break;
133
- case "gate.finish":
134
- recordGateAttempt(next, {
135
- evidence: safeEvidence(event.evidence),
136
- gateId: event.gateId,
137
- kind: event.kind,
138
- nodeId: event.nodeId,
139
- passed: event.passed,
140
- ...event.reason ? { reason: event.reason } : {}
141
- });
142
- break;
143
- case "node.output.recorded":
144
- recordStructuredVerdicts(next, event);
145
- break;
146
- case "workflow.finish":
147
- next.terminalOutcome = event.outcome;
148
- break;
149
- default: break;
150
- }
151
- return parseGoalState(next);
152
- }
153
- function reconstructGoalStateFromEvents(options, events) {
154
- return events.reduce(applyGoalStateEvent, createGoalState(options));
155
- }
156
- function recordGoalStateChangedFiles(state, nodeId, files) {
157
- const next = cloneGoalState(state);
158
- const uniqueFiles = uniqueChangedFiles(files);
159
- upsertNode(next, nodeId, { changedFiles: uniqueFiles });
160
- next.changedFiles = uniqueChangedFiles([...next.changedFiles, ...uniqueFiles]);
161
- return parseGoalState(next);
162
- }
163
- function recordGoalStateContinuationAttempt(state, attempt) {
164
- const next = cloneGoalState(state);
165
- next.continuationAttempts.push({
166
- attempt: next.continuationAttempts.length + 1,
167
- ...attempt.promptPath ? { promptPath: attempt.promptPath } : {},
168
- reason: attempt.reason,
169
- ...attempt.verifierNodeId ? { verifierNodeId: attempt.verifierNodeId } : {}
170
- });
171
- return parseGoalState(next);
172
- }
173
- function markGoalStateBlocked(state, reason) {
174
- const next = cloneGoalState(state);
175
- next.blockedReasons = safeEvidence([...next.blockedReasons, reason]);
176
- next.terminalOutcome = "BLOCKED";
177
- return parseGoalState(next);
178
- }
179
- function goalStateCompletionEvidence(state) {
180
- const evidence = [];
181
- const verifierPassed = state.verifier.verdict === "PASS" && state.verifier.evidence.length > 0 && (state.verifier.violations?.length ?? 0) === 0;
182
- evidence.push(verifierPassed ? `verifier '${state.verifier.nodeId ?? "verify"}' passed with evidence` : "missing passing verifier evidence");
183
- for (const item of acceptanceCompletionEvidence(state)) evidence.push(item);
184
- return {
185
- evidence,
186
- passed: verifierPassed && evidence.every((item) => !item.startsWith("missing ")) && evidence.every((item) => !item.includes(" failed")) && evidence.every((item) => !item.includes(" violations"))
187
- };
188
- }
189
- function goalStateContinuationInput(state) {
190
- return {
191
- acceptance: state.acceptance,
192
- changedFiles: state.changedFiles,
193
- currentNodeId: currentFailedNodeId(state),
194
- exactNextRequirement: goalStateNextRequirement(state),
195
- failedGates: state.gateFailures.filter((gate) => !gate.passed),
196
- failureSignature: goalStateFailureSignature(state),
197
- originalTask: state.task.original,
198
- priorAttempts: state.continuationAttempts,
199
- taskContext: state.task.context,
200
- verifier: state.verifier
201
- };
202
- }
203
- function goalStateFailureSignature(state) {
204
- return [
205
- ...state.gateFailures.map((gate) => [
206
- gate.nodeId,
207
- gate.gateId,
208
- gate.reason ?? "",
209
- ...gate.evidence
210
- ].join("/")),
211
- state.verifier.verdict === "FAIL" ? [
212
- state.verifier.nodeId ?? "verify",
213
- state.verifier.reason ?? "",
214
- ...state.verifier.evidence,
215
- ...state.verifier.violations ?? []
216
- ].join("/") : "",
217
- ...state.acceptance.filter((item) => item.verdict === "FAIL").map((item) => [
218
- "acceptance",
219
- item.id,
220
- ...item.evidence,
221
- ...item.violations ?? []
222
- ].filter(Boolean).join("/"))
223
- ].filter(Boolean).join("\0");
224
- }
225
- function goalStateArtifactPath(runDirectory) {
226
- return join(runDirectory, "goal-state.json");
227
- }
228
- function saveGoalState(state, runDirectory) {
229
- writeFileSync(goalStateArtifactPath(runDirectory), `${JSON.stringify(parseGoalState(state), null, 2)}\n`);
230
- }
231
- function loadGoalState(path) {
232
- return parseGoalState(JSON.parse(readFileSync(path, "utf8")));
233
- }
234
- function loadGoalStateFromRunDirectory(runDirectory) {
235
- const path = goalStateArtifactPath(runDirectory);
236
- if (!existsSync(path)) throw new Error(`goal state artifact not found: ${path}`);
237
- return loadGoalState(path);
238
- }
239
- function recordGateAttempt(state, gate) {
240
- upsertNode(state, gate.nodeId, {});
241
- state.nodes[gate.nodeId].gates.push(gate);
242
- if (!gate.passed) {
243
- state.gateFailures.push(gate);
244
- if (isVerifierGate(gate)) state.verifier = {
245
- evidence: safeEvidence([...state.verifier.evidence, ...gate.evidence]),
246
- nodeId: gate.nodeId,
247
- ...gate.reason ? { reason: gate.reason } : {},
248
- ...state.verifier.violations ? { violations: state.verifier.violations } : {},
249
- verdict: "FAIL"
250
- };
251
- }
252
- }
253
- function recordStructuredVerdicts(state, event) {
254
- const output = event.output;
255
- if (!isRecord(output)) return;
256
- const acceptance = output.acceptance;
257
- if (Array.isArray(acceptance)) state.acceptance = acceptance.flatMap((item) => {
258
- if (!(isRecord(item) && isVerdict(item.verdict)) || typeof item.id !== "string") return [];
259
- return [{
260
- evidence: safeEvidence(Array.isArray(item.evidence) ? item.evidence : []),
261
- id: item.id,
262
- ...optionalEvidence("violations", item.violations),
263
- verdict: item.verdict
264
- }];
265
- });
266
- if (isVerifierNode(event) && isVerdict(output.verdict)) state.verifier = {
267
- evidence: safeEvidence(Array.isArray(output.evidence) ? output.evidence : []),
268
- nodeId: event.nodeId,
269
- ...optionalEvidence("violations", output.violations),
270
- verdict: output.verdict
271
- };
272
- }
273
- function upsertNode(state, nodeId, patch) {
274
- const current = state.nodes[nodeId] ?? {
275
- attempts: 0,
276
- changedFiles: [],
277
- gates: [],
278
- nodeId,
279
- status: "pending"
280
- };
281
- state.nodes[nodeId] = {
282
- ...current,
283
- ...patch,
284
- attempts: Math.max(current.attempts, patch.attempts ?? 0),
285
- changedFiles: uniqueChangedFiles([...current.changedFiles, ...patch.changedFiles ?? []]),
286
- gates: patch.gates ?? current.gates,
287
- nodeId
288
- };
289
- }
290
- function isVerifierGate(gate) {
291
- return gate.kind === "verdict" || gate.nodeId.includes("verif");
292
- }
293
- function isVerifierNode(event) {
294
- return event.nodeId.includes("verif") || Boolean(event.profile?.includes("verif")) || Boolean(event.schemaPath?.includes("verify"));
295
- }
296
- function isVerdict(value) {
297
- return value === "PASS" || value === "FAIL";
298
- }
299
- function safeEvidence(value) {
300
- return (value ?? []).flatMap((item) => typeof item === "string" ? [item] : []).slice(0, MAX_EVIDENCE_ITEMS).map((item) => item.slice(0, MAX_EVIDENCE_LENGTH));
301
- }
302
- function optionalEvidence(key, value) {
303
- const evidence = Array.isArray(value) ? safeEvidence(value) : [];
304
- return evidence.length > 0 ? { [key]: evidence } : {};
305
- }
306
- function acceptanceCompletionEvidence(state) {
307
- const expected = expectedAcceptanceIds(state.task.context);
308
- if (expected.length === 0) return ["acceptance criteria not required by task context"];
309
- const byId = new Map(state.acceptance.map((item) => [item.id, item]));
310
- return expected.map((id) => {
311
- const item = byId.get(id);
312
- if (!item) return `missing acceptance criterion '${id}' evidence`;
313
- if (item.verdict !== "PASS") return `acceptance criterion '${id}' failed`;
314
- if (item.evidence.length === 0) return `missing acceptance criterion '${id}' evidence`;
315
- if ((item.violations?.length ?? 0) > 0) return `acceptance criterion '${id}' has violations`;
316
- return `acceptance criterion '${id}' passed with evidence`;
317
- });
318
- }
319
- function expectedAcceptanceIds(context) {
320
- if (!(isRecord(context) && Array.isArray(context.acceptanceCriteria))) return [];
321
- return context.acceptanceCriteria.flatMap((item) => isRecord(item) && typeof item.id === "string" ? [item.id] : []);
322
- }
323
- function currentFailedNodeId(state) {
324
- return Object.values(state.nodes).filter((node) => node.status === "failed").at(-1)?.nodeId;
325
- }
326
- function uniqueChangedFiles(values) {
327
- return uniqueStrings(values, {
328
- filterEmpty: true,
329
- sort: true
330
- });
331
- }
332
- function cloneGoalState(state) {
333
- return JSON.parse(JSON.stringify(state));
334
- }
335
- function isRecord(value) {
336
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
337
- }
338
- //#endregion
339
- export { applyGoalStateEvent, createGoalState, goalStateArtifactPath, goalStateCompletionEvidence, goalStateContinuationInput, goalStateFailureSignature, loadGoalState, loadGoalStateFromRunDirectory, markGoalStateBlocked, parseGoalState, pipelineGoalStateSchema, reconstructGoalStateFromEvents, recordGoalStateChangedFiles, recordGoalStateContinuationAttempt, saveGoalState };