@oisincoveney/pipeline 1.11.3 → 1.13.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 +14 -0
- package/dist/config.d.ts +2 -2
- package/dist/kubernetes-runner.js +27 -9
- package/dist/pipeline-init.js +1 -0
- package/dist/runner-event-sink.js +21 -1
- package/dist/runner-job-contract.d.ts +281 -0
- package/dist/runner-job-contract.js +144 -12
- package/dist/schedule-planner.js +51 -0
- package/docs/operator-guide.md +15 -2
- package/docs/pipeline-console-runner-contract.md +27 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -128,6 +128,9 @@ flushing.
|
|
|
128
128
|
The console starts the image with `OISIN_PIPELINE_RUNNER_PAYLOAD_JSON` and the
|
|
129
129
|
runner-side event token. The payload contract is documented in
|
|
130
130
|
[`docs/pipeline-console-runner-contract.md`](docs/pipeline-console-runner-contract.md).
|
|
131
|
+
The executable contract is exported from
|
|
132
|
+
`@oisincoveney/pipeline/runner-job-contract` for payload construction,
|
|
133
|
+
validation, contract-version checks, and JSON Schema generation.
|
|
131
134
|
Use `PIPELINE_TARGET_PATH=/path/to/worktree` when the checked-out target repo is
|
|
132
135
|
mounted somewhere other than the process working directory.
|
|
133
136
|
|
|
@@ -374,6 +377,17 @@ import {
|
|
|
374
377
|
} from "@oisincoveney/pipeline/runtime";
|
|
375
378
|
```
|
|
376
379
|
|
|
380
|
+
Runner Job producers can import the shared payload contract:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import {
|
|
384
|
+
RUNNER_JOB_CONTRACT_VERSION,
|
|
385
|
+
buildRunnerJobPayload,
|
|
386
|
+
parseRunnerJobPayload,
|
|
387
|
+
runnerJobPayloadJsonSchema,
|
|
388
|
+
} from "@oisincoveney/pipeline/runner-job-contract";
|
|
389
|
+
```
|
|
390
|
+
|
|
377
391
|
## Verification
|
|
378
392
|
|
|
379
393
|
Use these commands before committing changes in this repository:
|
package/dist/config.d.ts
CHANGED
|
@@ -231,10 +231,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
231
231
|
}, z.core.$strict>>;
|
|
232
232
|
output: z.ZodOptional<z.ZodObject<{
|
|
233
233
|
format: z.ZodEnum<{
|
|
234
|
-
json_schema: "json_schema";
|
|
235
234
|
text: "text";
|
|
236
235
|
json: "json";
|
|
237
236
|
jsonl: "jsonl";
|
|
237
|
+
json_schema: "json_schema";
|
|
238
238
|
}>;
|
|
239
239
|
repair: z.ZodOptional<z.ZodObject<{
|
|
240
240
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -274,10 +274,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
274
274
|
disabled: "disabled";
|
|
275
275
|
}>>>;
|
|
276
276
|
output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
277
|
-
json_schema: "json_schema";
|
|
278
277
|
text: "text";
|
|
279
278
|
json: "json";
|
|
280
279
|
jsonl: "jsonl";
|
|
280
|
+
json_schema: "json_schema";
|
|
281
281
|
}>>>;
|
|
282
282
|
rules: z.ZodOptional<z.ZodBoolean>;
|
|
283
283
|
skills: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PipelineConfigError } from "./config.js";
|
|
2
2
|
import { runPipelineFromConfig } from "./pipeline-runtime.js";
|
|
3
|
-
import { RUNNER_PAYLOAD_ENV,
|
|
3
|
+
import { RUNNER_PAYLOAD_ENV, parseRunnerJobPayloadWithIssues, resolveRunnerEventSinkAuthToken } from "./runner-job-contract.js";
|
|
4
4
|
import { createRunnerEventSink } from "./runner-event-sink.js";
|
|
5
5
|
//#region src/kubernetes-runner.ts
|
|
6
6
|
const EXIT_PASS = 0;
|
|
@@ -11,6 +11,7 @@ const EXIT_STARTUP = 70;
|
|
|
11
11
|
async function runKubernetesRunnerJob(options = {}) {
|
|
12
12
|
const prepared = prepareRunnerJob(options);
|
|
13
13
|
if (prepared.exitCode !== void 0) return prepared.exitCode;
|
|
14
|
+
if ("validationFailure" in prepared) return reportPreparedValidationFailure(prepared.validationFailure, options);
|
|
14
15
|
const { authToken, env, payload, stderr } = prepared.job;
|
|
15
16
|
const controller = new AbortController();
|
|
16
17
|
const signalEmitter = options.signalEmitter ?? process;
|
|
@@ -51,6 +52,7 @@ async function runKubernetesRunnerJob(options = {}) {
|
|
|
51
52
|
runId: payload.run.runId,
|
|
52
53
|
signal: controller.signal,
|
|
53
54
|
task: payload.task.prompt,
|
|
55
|
+
hookPolicy: { allowCommandHooks: payload.selector.allowCommandHooks },
|
|
54
56
|
workflowId: payload.selector.workflowId,
|
|
55
57
|
worktreePath: env.PIPELINE_TARGET_PATH ?? options.cwd ?? process.cwd()
|
|
56
58
|
});
|
|
@@ -77,24 +79,40 @@ function prepareRunnerJob(options) {
|
|
|
77
79
|
return { exitCode: EXIT_VALIDATION };
|
|
78
80
|
}
|
|
79
81
|
const payload = parsePayload(payloadRaw, stderr);
|
|
80
|
-
if (!payload) return {
|
|
82
|
+
if (!payload.ok) return { validationFailure: {
|
|
83
|
+
authToken: resolveAuthToken(env, stderr) ?? void 0,
|
|
84
|
+
env,
|
|
85
|
+
error: payload.error,
|
|
86
|
+
recoverable: payload.recoverable,
|
|
87
|
+
stderr
|
|
88
|
+
} };
|
|
81
89
|
const authToken = resolveAuthToken(env, stderr);
|
|
82
90
|
if (!authToken) return { exitCode: EXIT_VALIDATION };
|
|
83
91
|
return { job: {
|
|
84
92
|
authToken,
|
|
85
93
|
env,
|
|
86
|
-
payload,
|
|
94
|
+
payload: payload.payload,
|
|
87
95
|
stderr
|
|
88
96
|
} };
|
|
89
97
|
}
|
|
90
98
|
function parsePayload(payloadRaw, stderr) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
const result = parseRunnerJobPayloadWithIssues(payloadRaw);
|
|
100
|
+
if (!result.ok) stderr.write(`${result.error.message}\n`);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
async function reportPreparedValidationFailure(failure, options) {
|
|
104
|
+
if (failure.recoverable && failure.authToken) {
|
|
105
|
+
const sink = createRunnerEventSink({
|
|
106
|
+
authHeader: failure.recoverable.eventSink.authHeader,
|
|
107
|
+
authToken: failure.authToken,
|
|
108
|
+
fetch: options.fetch,
|
|
109
|
+
runId: failure.recoverable.run.runId,
|
|
110
|
+
url: failure.recoverable.eventSink.url
|
|
111
|
+
});
|
|
112
|
+
sink.recordSchemaValidationFailure(failure.error.message, failure.error.issues, failure.recoverable.workflowId);
|
|
113
|
+
await flushAndReport(sink.flush, failure.stderr);
|
|
97
114
|
}
|
|
115
|
+
return EXIT_VALIDATION;
|
|
98
116
|
}
|
|
99
117
|
function resolveAuthToken(env, stderr) {
|
|
100
118
|
try {
|
package/dist/pipeline-init.js
CHANGED
|
@@ -734,6 +734,7 @@ const SCAFFOLD_FILES = {
|
|
|
734
734
|
"",
|
|
735
735
|
"Use the provided backlog work units as the source of truth when present. Assign each work unit to explicit generated agent nodes with only its `task_context.id`, use only allowed configured profiles, and ensure implementation work has downstream acceptance, verification, or review coverage. Do not invent profiles or node-level skill overrides.",
|
|
736
736
|
"Do not copy backlog descriptions or acceptance criteria into output; the scheduler hydrates them from the assigned `task_context.id` after parsing.",
|
|
737
|
+
"Preserve Backlog dependency ids as schedule needs edges. A node assigned a dependent work unit must depend on the nodes assigned its prerequisite work units, directly or through an explicit path.",
|
|
737
738
|
"",
|
|
738
739
|
"Shape the graph by intent, not by ticket count. Do not create a full RED/GREEN/ACCEPTANCE/VERIFY chain for each backlog ticket unless each step needs ticket-specific evidence. Use one RED node for a group of tickets when they share a test strategy, fan out to parallel GREEN implementation nodes where the work can be implemented independently, and fan back in to shared acceptance or verifier nodes when the same acceptance checklist or real repository commands prove the group. Only serialize ticket nodes when the backlog, a shared migration/schema/API dependency, or implementation risk requires it.",
|
|
739
740
|
"",
|
|
@@ -87,7 +87,27 @@ function createRunnerEventSink(options) {
|
|
|
87
87
|
workflowId
|
|
88
88
|
});
|
|
89
89
|
},
|
|
90
|
-
recordRuntimeEvent
|
|
90
|
+
recordRuntimeEvent,
|
|
91
|
+
recordSchemaValidationFailure(message, issues, workflowId) {
|
|
92
|
+
queue.push({
|
|
93
|
+
...nextEnvelope(),
|
|
94
|
+
log: {
|
|
95
|
+
level: "warn",
|
|
96
|
+
message: `Runner payload schema validation failed: ${message}`,
|
|
97
|
+
output: { issues },
|
|
98
|
+
workflowId
|
|
99
|
+
},
|
|
100
|
+
type: "runner.schema.validation"
|
|
101
|
+
});
|
|
102
|
+
queue.push({
|
|
103
|
+
...nextEnvelope(),
|
|
104
|
+
finalResult: {
|
|
105
|
+
outcome: "FAIL",
|
|
106
|
+
workflowId
|
|
107
|
+
},
|
|
108
|
+
type: "workflow.finish"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
91
111
|
};
|
|
92
112
|
}
|
|
93
113
|
async function postBatch(options, fetchImpl, events) {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { PipelineRuntimeEvent } from "./pipeline-runtime.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/runner-job-contract.d.ts
|
|
5
|
+
declare const RUNNER_PAYLOAD_ENV = "OISIN_PIPELINE_RUNNER_PAYLOAD_JSON";
|
|
6
|
+
declare const RUNNER_JOB_CONTRACT_VERSION = "1";
|
|
7
|
+
declare const runnerEventSinkConfigSchema: z.ZodObject<{
|
|
8
|
+
authHeader: z.ZodString;
|
|
9
|
+
url: z.ZodString;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
declare const runnerRunIdentitySchema: z.ZodObject<{
|
|
12
|
+
projectId: z.ZodString;
|
|
13
|
+
requestedBy: z.ZodOptional<z.ZodString>;
|
|
14
|
+
runId: z.ZodString;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
declare const runnerWorkflowSelectorSchema: z.ZodObject<{
|
|
17
|
+
allowCommandHooks: z.ZodDefault<z.ZodBoolean>;
|
|
18
|
+
workflowId: z.ZodString;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
declare const runnerTaskPromptSchema: z.ZodObject<{
|
|
21
|
+
filePath: z.ZodOptional<z.ZodString>;
|
|
22
|
+
githubUrl: z.ZodOptional<z.ZodString>;
|
|
23
|
+
prompt: z.ZodString;
|
|
24
|
+
taskId: z.ZodString;
|
|
25
|
+
title: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
declare const runnerRepositoryContextSchema: z.ZodObject<{
|
|
28
|
+
branch: z.ZodString;
|
|
29
|
+
cloneUrl: z.ZodString;
|
|
30
|
+
fullName: z.ZodString;
|
|
31
|
+
owner: z.ZodString;
|
|
32
|
+
repo: z.ZodString;
|
|
33
|
+
sha: z.ZodString;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
declare const runnerMomokayaContextSchema: z.ZodObject<{
|
|
36
|
+
automationNamespace: z.ZodOptional<z.ZodString>;
|
|
37
|
+
previewEnabled: z.ZodBoolean;
|
|
38
|
+
repoKey: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
declare const runnerJobPayloadSchema: z.ZodObject<{
|
|
41
|
+
contractVersion: z.ZodDefault<z.ZodLiteral<"1">>;
|
|
42
|
+
eventSink: z.ZodObject<{
|
|
43
|
+
authHeader: z.ZodString;
|
|
44
|
+
url: z.ZodString;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
momokaya: z.ZodOptional<z.ZodObject<{
|
|
47
|
+
automationNamespace: z.ZodOptional<z.ZodString>;
|
|
48
|
+
previewEnabled: z.ZodBoolean;
|
|
49
|
+
repoKey: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strict>>;
|
|
51
|
+
repository: z.ZodOptional<z.ZodObject<{
|
|
52
|
+
branch: z.ZodString;
|
|
53
|
+
cloneUrl: z.ZodString;
|
|
54
|
+
fullName: z.ZodString;
|
|
55
|
+
owner: z.ZodString;
|
|
56
|
+
repo: z.ZodString;
|
|
57
|
+
sha: z.ZodString;
|
|
58
|
+
}, z.core.$strict>>;
|
|
59
|
+
run: z.ZodObject<{
|
|
60
|
+
projectId: z.ZodString;
|
|
61
|
+
requestedBy: z.ZodOptional<z.ZodString>;
|
|
62
|
+
runId: z.ZodString;
|
|
63
|
+
}, z.core.$strict>;
|
|
64
|
+
selector: z.ZodObject<{
|
|
65
|
+
allowCommandHooks: z.ZodDefault<z.ZodBoolean>;
|
|
66
|
+
workflowId: z.ZodString;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
task: z.ZodObject<{
|
|
69
|
+
filePath: z.ZodOptional<z.ZodString>;
|
|
70
|
+
githubUrl: z.ZodOptional<z.ZodString>;
|
|
71
|
+
prompt: z.ZodString;
|
|
72
|
+
taskId: z.ZodString;
|
|
73
|
+
title: z.ZodOptional<z.ZodString>;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
}, z.core.$strict>;
|
|
76
|
+
type RunnerEventSinkConfig = z.infer<typeof runnerEventSinkConfigSchema>;
|
|
77
|
+
type RunnerJobPayload = z.infer<typeof runnerJobPayloadSchema>;
|
|
78
|
+
type RunnerMomokayaContext = z.infer<typeof runnerMomokayaContextSchema>;
|
|
79
|
+
type RunnerRepositoryContext = z.infer<typeof runnerRepositoryContextSchema>;
|
|
80
|
+
type RunnerRunIdentity = z.infer<typeof runnerRunIdentitySchema>;
|
|
81
|
+
type RunnerTaskPrompt = z.infer<typeof runnerTaskPromptSchema>;
|
|
82
|
+
type RunnerWorkflowSelector = z.infer<typeof runnerWorkflowSelectorSchema>;
|
|
83
|
+
declare const runnerJobPayloadJsonSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
|
|
84
|
+
contractVersion: z.ZodDefault<z.ZodLiteral<"1">>;
|
|
85
|
+
eventSink: z.ZodObject<{
|
|
86
|
+
authHeader: z.ZodString;
|
|
87
|
+
url: z.ZodString;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
momokaya: z.ZodOptional<z.ZodObject<{
|
|
90
|
+
automationNamespace: z.ZodOptional<z.ZodString>;
|
|
91
|
+
previewEnabled: z.ZodBoolean;
|
|
92
|
+
repoKey: z.ZodOptional<z.ZodString>;
|
|
93
|
+
}, z.core.$strict>>;
|
|
94
|
+
repository: z.ZodOptional<z.ZodObject<{
|
|
95
|
+
branch: z.ZodString;
|
|
96
|
+
cloneUrl: z.ZodString;
|
|
97
|
+
fullName: z.ZodString;
|
|
98
|
+
owner: z.ZodString;
|
|
99
|
+
repo: z.ZodString;
|
|
100
|
+
sha: z.ZodString;
|
|
101
|
+
}, z.core.$strict>>;
|
|
102
|
+
run: z.ZodObject<{
|
|
103
|
+
projectId: z.ZodString;
|
|
104
|
+
requestedBy: z.ZodOptional<z.ZodString>;
|
|
105
|
+
runId: z.ZodString;
|
|
106
|
+
}, z.core.$strict>;
|
|
107
|
+
selector: z.ZodObject<{
|
|
108
|
+
allowCommandHooks: z.ZodDefault<z.ZodBoolean>;
|
|
109
|
+
workflowId: z.ZodString;
|
|
110
|
+
}, z.core.$strict>;
|
|
111
|
+
task: z.ZodObject<{
|
|
112
|
+
filePath: z.ZodOptional<z.ZodString>;
|
|
113
|
+
githubUrl: z.ZodOptional<z.ZodString>;
|
|
114
|
+
prompt: z.ZodString;
|
|
115
|
+
taskId: z.ZodString;
|
|
116
|
+
title: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strict>;
|
|
118
|
+
}, z.core.$strict>>;
|
|
119
|
+
interface RunnerJobPayloadValidationIssue {
|
|
120
|
+
code: string;
|
|
121
|
+
message: string;
|
|
122
|
+
path: string;
|
|
123
|
+
}
|
|
124
|
+
interface RecoverableRunnerJobPayloadEnvelope {
|
|
125
|
+
eventSink: RunnerEventSinkConfig;
|
|
126
|
+
run: RunnerRunIdentity;
|
|
127
|
+
workflowId: string;
|
|
128
|
+
}
|
|
129
|
+
declare class RunnerJobPayloadValidationError extends Error {
|
|
130
|
+
readonly issues: RunnerJobPayloadValidationIssue[];
|
|
131
|
+
constructor(message: string, issues: RunnerJobPayloadValidationIssue[]);
|
|
132
|
+
}
|
|
133
|
+
type RunnerJobPayloadParseResult = {
|
|
134
|
+
ok: true;
|
|
135
|
+
payload: RunnerJobPayload;
|
|
136
|
+
} | {
|
|
137
|
+
error: RunnerJobPayloadValidationError;
|
|
138
|
+
ok: false;
|
|
139
|
+
recoverable?: RecoverableRunnerJobPayloadEnvelope;
|
|
140
|
+
};
|
|
141
|
+
interface BuildRunnerJobPayloadOptions {
|
|
142
|
+
allowCommandHooks?: boolean;
|
|
143
|
+
eventSink: RunnerEventSinkConfig;
|
|
144
|
+
momokaya?: RunnerMomokayaContext;
|
|
145
|
+
repository?: RunnerRepositoryContext;
|
|
146
|
+
run: RunnerRunIdentity;
|
|
147
|
+
task: RunnerTaskPrompt;
|
|
148
|
+
workflowId: string;
|
|
149
|
+
}
|
|
150
|
+
interface CreateRunnerJobPayloadEnvOptions {
|
|
151
|
+
allowCommandHooks?: boolean;
|
|
152
|
+
eventSinkUrl: string;
|
|
153
|
+
projectId: string;
|
|
154
|
+
requestedBy?: string;
|
|
155
|
+
runId: string;
|
|
156
|
+
taskId: string;
|
|
157
|
+
taskPrompt: string;
|
|
158
|
+
workflowId: string;
|
|
159
|
+
}
|
|
160
|
+
interface ResolveRunnerEventSinkAuthTokenOptions {
|
|
161
|
+
env?: Record<string, string | undefined>;
|
|
162
|
+
serviceAccountTokenPath?: string;
|
|
163
|
+
}
|
|
164
|
+
interface RunnerEventMappingContext {
|
|
165
|
+
runId: string;
|
|
166
|
+
sequence?: number;
|
|
167
|
+
timestamp: string;
|
|
168
|
+
}
|
|
169
|
+
interface RunnerWorkflowNodeDetails {
|
|
170
|
+
id: string;
|
|
171
|
+
kind: string;
|
|
172
|
+
needs: string[];
|
|
173
|
+
profile?: string;
|
|
174
|
+
runnerId?: string;
|
|
175
|
+
}
|
|
176
|
+
interface RunnerWorkflowEdgeDetails {
|
|
177
|
+
source: string;
|
|
178
|
+
target: string;
|
|
179
|
+
}
|
|
180
|
+
interface RunnerWorkflowPlanDetails {
|
|
181
|
+
edges?: RunnerWorkflowEdgeDetails[];
|
|
182
|
+
nodeIds?: string[];
|
|
183
|
+
nodes?: RunnerWorkflowNodeDetails[];
|
|
184
|
+
workflowId: string;
|
|
185
|
+
}
|
|
186
|
+
interface RunnerWorkflowEdgeRecordDetails {
|
|
187
|
+
id: string;
|
|
188
|
+
source: string;
|
|
189
|
+
target: string;
|
|
190
|
+
}
|
|
191
|
+
type RunnerNodeStatus = "agent-finished" | "agent-running" | "failed" | "passed" | "running";
|
|
192
|
+
interface RunnerNodeDetails {
|
|
193
|
+
attempt: number;
|
|
194
|
+
exitCode?: number;
|
|
195
|
+
nodeId: string;
|
|
196
|
+
profile?: string;
|
|
197
|
+
runnerId?: string;
|
|
198
|
+
status: RunnerNodeStatus;
|
|
199
|
+
}
|
|
200
|
+
type RunnerGateStatus = "failed" | "passed" | "running";
|
|
201
|
+
interface RunnerGateDetails {
|
|
202
|
+
event?: string;
|
|
203
|
+
evidence?: string[];
|
|
204
|
+
gateId?: string;
|
|
205
|
+
hookId?: string;
|
|
206
|
+
kind?: string;
|
|
207
|
+
label?: string;
|
|
208
|
+
nodeId?: string;
|
|
209
|
+
passed?: boolean;
|
|
210
|
+
reason?: string;
|
|
211
|
+
required?: boolean;
|
|
212
|
+
status: RunnerGateStatus;
|
|
213
|
+
workflowId?: string;
|
|
214
|
+
}
|
|
215
|
+
type RunnerArtifactStatus = "failed" | "passed" | "running";
|
|
216
|
+
interface RunnerArtifactDetails {
|
|
217
|
+
kind: "artifact";
|
|
218
|
+
label: string;
|
|
219
|
+
nodeId: string;
|
|
220
|
+
passed?: boolean;
|
|
221
|
+
path: string;
|
|
222
|
+
reason?: string;
|
|
223
|
+
required: boolean;
|
|
224
|
+
status: RunnerArtifactStatus;
|
|
225
|
+
uri: string;
|
|
226
|
+
}
|
|
227
|
+
type RunnerLogLevel = "info" | "warn";
|
|
228
|
+
interface RunnerLogDetails {
|
|
229
|
+
attempt?: number;
|
|
230
|
+
format?: string;
|
|
231
|
+
level: RunnerLogLevel;
|
|
232
|
+
message: string;
|
|
233
|
+
nodeId?: string;
|
|
234
|
+
output?: unknown;
|
|
235
|
+
passed?: boolean;
|
|
236
|
+
reason?: string;
|
|
237
|
+
workflowId?: string;
|
|
238
|
+
}
|
|
239
|
+
interface RunnerFinalResultDetails {
|
|
240
|
+
outcome: "CANCELLED" | "FAIL" | "PASS";
|
|
241
|
+
workflowId: string;
|
|
242
|
+
}
|
|
243
|
+
interface RunnerEventEnvelope {
|
|
244
|
+
at?: string;
|
|
245
|
+
sequence: number;
|
|
246
|
+
type: string;
|
|
247
|
+
}
|
|
248
|
+
type RunnerEventRecord = (RunnerEventEnvelope & {
|
|
249
|
+
type: "workflow.planned" | "workflow.start";
|
|
250
|
+
workflowPlan: RunnerWorkflowPlanDetails;
|
|
251
|
+
}) | (RunnerEventEnvelope & {
|
|
252
|
+
edge: RunnerWorkflowEdgeRecordDetails;
|
|
253
|
+
type: "workflow.edge";
|
|
254
|
+
}) | (RunnerEventEnvelope & {
|
|
255
|
+
node: RunnerNodeDetails;
|
|
256
|
+
type: "agent.finish" | "agent.start" | "node.finish" | "node.start";
|
|
257
|
+
}) | (RunnerEventEnvelope & {
|
|
258
|
+
gate: RunnerGateDetails;
|
|
259
|
+
type: "gate.finish" | "gate.start" | "hook.finish" | "hook.start";
|
|
260
|
+
}) | (RunnerEventEnvelope & {
|
|
261
|
+
artifact: RunnerArtifactDetails;
|
|
262
|
+
type: "artifact.check.finish" | "artifact.check.start";
|
|
263
|
+
}) | (RunnerEventEnvelope & {
|
|
264
|
+
log: RunnerLogDetails;
|
|
265
|
+
type: "node.output.recorded" | "output.repair" | "run.cancelled" | "runner.schema.validation" | "runtime.observability";
|
|
266
|
+
}) | (RunnerEventEnvelope & {
|
|
267
|
+
finalResult: RunnerFinalResultDetails;
|
|
268
|
+
type: "workflow.finish";
|
|
269
|
+
});
|
|
270
|
+
declare function resolveRunnerEventSinkAuthToken(options?: ResolveRunnerEventSinkAuthTokenOptions): string;
|
|
271
|
+
declare function resolveRunnerEventSinkAuthHeader(options?: ResolveRunnerEventSinkAuthTokenOptions): string;
|
|
272
|
+
declare function createRunnerJobPayloadEnv(options: CreateRunnerJobPayloadEnvOptions): {
|
|
273
|
+
name: typeof RUNNER_PAYLOAD_ENV;
|
|
274
|
+
value: string;
|
|
275
|
+
};
|
|
276
|
+
declare function buildRunnerJobPayload(options: BuildRunnerJobPayloadOptions): RunnerJobPayload;
|
|
277
|
+
declare function parseRunnerJobPayload(rawPayload: string): RunnerJobPayload;
|
|
278
|
+
declare function parseRunnerJobPayloadWithIssues(rawPayload: string): RunnerJobPayloadParseResult;
|
|
279
|
+
declare function mapRuntimeEventToRunnerEventRecords(event: PipelineRuntimeEvent, context: RunnerEventMappingContext): RunnerEventRecord[];
|
|
280
|
+
//#endregion
|
|
281
|
+
export { BuildRunnerJobPayloadOptions, CreateRunnerJobPayloadEnvOptions, RUNNER_JOB_CONTRACT_VERSION, RUNNER_PAYLOAD_ENV, RecoverableRunnerJobPayloadEnvelope, ResolveRunnerEventSinkAuthTokenOptions, RunnerArtifactDetails, RunnerArtifactStatus, RunnerEventMappingContext, RunnerEventRecord, RunnerEventSinkConfig, RunnerFinalResultDetails, RunnerGateDetails, RunnerGateStatus, RunnerJobPayload, RunnerJobPayloadParseResult, RunnerJobPayloadValidationError, RunnerJobPayloadValidationIssue, RunnerLogDetails, RunnerLogLevel, RunnerMomokayaContext, RunnerNodeDetails, RunnerNodeStatus, RunnerRepositoryContext, RunnerRunIdentity, RunnerTaskPrompt, RunnerWorkflowEdgeDetails, RunnerWorkflowEdgeRecordDetails, RunnerWorkflowNodeDetails, RunnerWorkflowPlanDetails, RunnerWorkflowSelector, buildRunnerJobPayload, createRunnerJobPayloadEnv, mapRuntimeEventToRunnerEventRecords, parseRunnerJobPayload, parseRunnerJobPayloadWithIssues, resolveRunnerEventSinkAuthHeader, resolveRunnerEventSinkAuthToken, runnerEventSinkConfigSchema, runnerJobPayloadJsonSchema, runnerJobPayloadSchema, runnerMomokayaContextSchema, runnerRepositoryContextSchema, runnerRunIdentitySchema, runnerTaskPromptSchema, runnerWorkflowSelectorSchema };
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region src/runner-job-contract.ts
|
|
5
5
|
const RUNNER_PAYLOAD_ENV = "OISIN_PIPELINE_RUNNER_PAYLOAD_JSON";
|
|
6
|
+
const RUNNER_JOB_CONTRACT_VERSION = "1";
|
|
6
7
|
const runnerEventSinkConfigSchema = z.object({
|
|
7
8
|
authHeader: z.string().min(1),
|
|
8
9
|
url: z.string().url()
|
|
@@ -12,17 +13,48 @@ const runnerRunIdentitySchema = z.object({
|
|
|
12
13
|
requestedBy: z.string().min(1).optional(),
|
|
13
14
|
runId: z.string().min(1)
|
|
14
15
|
}).strict();
|
|
15
|
-
const runnerWorkflowSelectorSchema = z.object({
|
|
16
|
+
const runnerWorkflowSelectorSchema = z.object({
|
|
17
|
+
allowCommandHooks: z.boolean().default(true),
|
|
18
|
+
workflowId: z.string().min(1)
|
|
19
|
+
}).strict();
|
|
16
20
|
const runnerTaskPromptSchema = z.object({
|
|
21
|
+
filePath: z.string().min(1).optional(),
|
|
22
|
+
githubUrl: z.string().url().optional(),
|
|
17
23
|
prompt: z.string().min(1),
|
|
18
|
-
taskId: z.string().min(1)
|
|
24
|
+
taskId: z.string().min(1),
|
|
25
|
+
title: z.string().min(1).optional()
|
|
26
|
+
}).strict();
|
|
27
|
+
const runnerRepositoryContextSchema = z.object({
|
|
28
|
+
branch: z.string().min(1),
|
|
29
|
+
cloneUrl: z.string().url(),
|
|
30
|
+
fullName: z.string().min(1),
|
|
31
|
+
owner: z.string().min(1),
|
|
32
|
+
repo: z.string().min(1),
|
|
33
|
+
sha: z.string().min(1)
|
|
34
|
+
}).strict();
|
|
35
|
+
const runnerMomokayaContextSchema = z.object({
|
|
36
|
+
automationNamespace: z.string().min(1).optional(),
|
|
37
|
+
previewEnabled: z.boolean(),
|
|
38
|
+
repoKey: z.string().min(1).optional()
|
|
19
39
|
}).strict();
|
|
20
40
|
const runnerJobPayloadSchema = z.object({
|
|
41
|
+
contractVersion: z.literal("1", { error: "runner job payload contract version must be 1" }).default("1"),
|
|
21
42
|
eventSink: runnerEventSinkConfigSchema,
|
|
43
|
+
momokaya: runnerMomokayaContextSchema.optional(),
|
|
44
|
+
repository: runnerRepositoryContextSchema.optional(),
|
|
22
45
|
run: runnerRunIdentitySchema,
|
|
23
46
|
selector: runnerWorkflowSelectorSchema,
|
|
24
47
|
task: runnerTaskPromptSchema
|
|
25
48
|
}).strict();
|
|
49
|
+
const runnerJobPayloadJsonSchema = z.toJSONSchema(runnerJobPayloadSchema);
|
|
50
|
+
var RunnerJobPayloadValidationError = class extends Error {
|
|
51
|
+
issues;
|
|
52
|
+
constructor(message, issues) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "RunnerJobPayloadValidationError";
|
|
55
|
+
this.issues = issues;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
26
58
|
const EVENT_AUTH_TOKEN_ENV_KEYS = ["OISIN_PIPELINE_EVENT_AUTH_TOKEN", "PIPELINE_EVENT_API_TOKEN"];
|
|
27
59
|
const DEFAULT_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
28
60
|
function resolveRunnerEventSinkAuthToken(options = {}) {
|
|
@@ -38,17 +70,79 @@ function resolveRunnerEventSinkAuthToken(options = {}) {
|
|
|
38
70
|
}
|
|
39
71
|
throw new Error(`Runner event auth token is required. Set ${EVENT_AUTH_TOKEN_ENV_KEYS.join(" or ")}, or mount a readable Kubernetes service account token at ${tokenPath}.`);
|
|
40
72
|
}
|
|
73
|
+
function resolveRunnerEventSinkAuthHeader(options = {}) {
|
|
74
|
+
return `Bearer ${resolveRunnerEventSinkAuthToken(options)}`;
|
|
75
|
+
}
|
|
76
|
+
function createRunnerJobPayloadEnv(options) {
|
|
77
|
+
const payload = buildRunnerJobPayload({
|
|
78
|
+
eventSink: {
|
|
79
|
+
authHeader: "Authorization",
|
|
80
|
+
url: options.eventSinkUrl
|
|
81
|
+
},
|
|
82
|
+
run: {
|
|
83
|
+
projectId: options.projectId,
|
|
84
|
+
requestedBy: options.requestedBy,
|
|
85
|
+
runId: options.runId
|
|
86
|
+
},
|
|
87
|
+
allowCommandHooks: options.allowCommandHooks,
|
|
88
|
+
task: {
|
|
89
|
+
prompt: options.taskPrompt,
|
|
90
|
+
taskId: options.taskId
|
|
91
|
+
},
|
|
92
|
+
workflowId: options.workflowId
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
name: RUNNER_PAYLOAD_ENV,
|
|
96
|
+
value: JSON.stringify(payload)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function buildRunnerJobPayload(options) {
|
|
100
|
+
return runnerJobPayloadSchema.parse({
|
|
101
|
+
contractVersion: "1",
|
|
102
|
+
eventSink: options.eventSink,
|
|
103
|
+
momokaya: options.momokaya,
|
|
104
|
+
repository: options.repository,
|
|
105
|
+
run: options.run,
|
|
106
|
+
selector: {
|
|
107
|
+
allowCommandHooks: options.allowCommandHooks ?? true,
|
|
108
|
+
workflowId: options.workflowId
|
|
109
|
+
},
|
|
110
|
+
task: options.task
|
|
111
|
+
});
|
|
112
|
+
}
|
|
41
113
|
function parseRunnerJobPayload(rawPayload) {
|
|
114
|
+
const result = parseRunnerJobPayloadWithIssues(rawPayload);
|
|
115
|
+
if (!result.ok) throw result.error;
|
|
116
|
+
return result.payload;
|
|
117
|
+
}
|
|
118
|
+
function parseRunnerJobPayloadWithIssues(rawPayload) {
|
|
42
119
|
let parsed;
|
|
43
120
|
try {
|
|
44
121
|
parsed = parseJson(rawPayload, "runner payload JSON");
|
|
45
122
|
} catch (err) {
|
|
46
123
|
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
-
|
|
124
|
+
return {
|
|
125
|
+
error: new RunnerJobPayloadValidationError(`Malformed runner payload JSON: ${message}`, [{
|
|
126
|
+
code: "invalid_json",
|
|
127
|
+
message,
|
|
128
|
+
path: "payload"
|
|
129
|
+
}]),
|
|
130
|
+
ok: false
|
|
131
|
+
};
|
|
48
132
|
}
|
|
49
133
|
const result = runnerJobPayloadSchema.safeParse(parsed);
|
|
50
|
-
if (!result.success)
|
|
51
|
-
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
const issues = runnerJobPayloadIssues(result.error);
|
|
136
|
+
return {
|
|
137
|
+
error: new RunnerJobPayloadValidationError(formatRunnerJobPayloadIssues(issues, parsed), issues),
|
|
138
|
+
ok: false,
|
|
139
|
+
...recoverablePayloadEnvelope(parsed)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
payload: result.data
|
|
145
|
+
};
|
|
52
146
|
}
|
|
53
147
|
function mapRuntimeEventToRunnerEventRecords(event, context) {
|
|
54
148
|
const record = {
|
|
@@ -261,15 +355,53 @@ function formatLogMessage(output) {
|
|
|
261
355
|
return String(output);
|
|
262
356
|
}
|
|
263
357
|
}
|
|
264
|
-
function formatRunnerJobPayloadIssues(
|
|
358
|
+
function formatRunnerJobPayloadIssues(issues, payload) {
|
|
265
359
|
const selector = readRecord(readRecord(payload)?.selector);
|
|
266
360
|
if (selector && !("workflowId" in selector) && Object.keys(selector).length > 0) return `Unsupported selector fields: ${Object.keys(selector).join(", ")}; selector.workflowId is required`;
|
|
267
|
-
return
|
|
361
|
+
return issues.map((issue) => `${issue.path || "payload"}: ${issue.message}`).join("; ");
|
|
362
|
+
}
|
|
363
|
+
function runnerJobPayloadIssues(error) {
|
|
364
|
+
return error.issues.flatMap((issue) => {
|
|
268
365
|
const path = issue.path.join(".");
|
|
269
|
-
if (issue.code === "
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
366
|
+
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) return issue.keys.map((key) => ({
|
|
367
|
+
code: issue.code,
|
|
368
|
+
message: "Unrecognized key",
|
|
369
|
+
path: [path, key].filter(Boolean).join(".")
|
|
370
|
+
}));
|
|
371
|
+
if (issue.code === "invalid_type" && issue.input === void 0) return [{
|
|
372
|
+
code: issue.code,
|
|
373
|
+
message: "is required",
|
|
374
|
+
path: path || "payload"
|
|
375
|
+
}];
|
|
376
|
+
if (path === "eventSink.url") return [{
|
|
377
|
+
code: issue.code,
|
|
378
|
+
message: "must be a valid URL",
|
|
379
|
+
path
|
|
380
|
+
}];
|
|
381
|
+
if (path === "contractVersion") return [{
|
|
382
|
+
code: issue.code,
|
|
383
|
+
message: `runner job payload contract version must be 1`,
|
|
384
|
+
path
|
|
385
|
+
}];
|
|
386
|
+
return [{
|
|
387
|
+
code: issue.code,
|
|
388
|
+
message: issue.message,
|
|
389
|
+
path: path || "payload"
|
|
390
|
+
}];
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function recoverablePayloadEnvelope(payload) {
|
|
394
|
+
const envelope = readRecord(payload);
|
|
395
|
+
if (!envelope) return {};
|
|
396
|
+
const eventSink = runnerEventSinkConfigSchema.safeParse(envelope.eventSink);
|
|
397
|
+
const run = runnerRunIdentitySchema.safeParse(envelope.run);
|
|
398
|
+
const workflowId = readRecord(envelope.selector)?.workflowId;
|
|
399
|
+
if (!(eventSink.success && run.success && typeof workflowId === "string")) return {};
|
|
400
|
+
return { recoverable: {
|
|
401
|
+
eventSink: eventSink.data,
|
|
402
|
+
run: run.data,
|
|
403
|
+
workflowId
|
|
404
|
+
} };
|
|
273
405
|
}
|
|
274
406
|
function omitUndefined(value) {
|
|
275
407
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
|
|
@@ -281,4 +413,4 @@ function assertNever(value) {
|
|
|
281
413
|
throw new Error(`Unhandled runtime event: ${String(value)}`);
|
|
282
414
|
}
|
|
283
415
|
//#endregion
|
|
284
|
-
export { RUNNER_PAYLOAD_ENV, mapRuntimeEventToRunnerEventRecords, parseRunnerJobPayload, resolveRunnerEventSinkAuthToken };
|
|
416
|
+
export { RUNNER_JOB_CONTRACT_VERSION, RUNNER_PAYLOAD_ENV, RunnerJobPayloadValidationError, buildRunnerJobPayload, createRunnerJobPayloadEnv, mapRuntimeEventToRunnerEventRecords, parseRunnerJobPayload, parseRunnerJobPayloadWithIssues, resolveRunnerEventSinkAuthHeader, resolveRunnerEventSinkAuthToken, runnerEventSinkConfigSchema, runnerJobPayloadJsonSchema, runnerJobPayloadSchema, runnerMomokayaContextSchema, runnerRepositoryContextSchema, runnerRunIdentitySchema, runnerTaskPromptSchema, runnerWorkflowSelectorSchema };
|
package/dist/schedule-planner.js
CHANGED
|
@@ -372,6 +372,7 @@ function plannerPrompt(entrypointId, task, baseline, config, planningContext) {
|
|
|
372
372
|
"Assign each backlog work unit to explicit generated agent nodes with task_context.id. The scheduler hydrates title, description, and acceptance_criteria after parsing.",
|
|
373
373
|
"Do not copy backlog descriptions or acceptance criteria into task_context output.",
|
|
374
374
|
"Implementation work must have downstream acceptance, verification, or review coverage in the generated DAG.",
|
|
375
|
+
"Preserve Backlog dependency ids as schedule needs edges. A node assigned a dependent work unit must depend on the nodes assigned its prerequisite work units, directly or through an explicit path.",
|
|
375
376
|
"Shape the graph by intent, not by ticket count. Do not create a full RED/GREEN/ACCEPTANCE/VERIFY chain for each backlog ticket unless each step needs ticket-specific evidence.",
|
|
376
377
|
"Use one RED node for a group of tickets when they share a test strategy, then fan out to parallel GREEN implementation nodes where the work can be implemented independently.",
|
|
377
378
|
"Use one acceptance or verifier node for multiple GREEN nodes when the same acceptance checklist or real repository commands prove the group.",
|
|
@@ -402,6 +403,7 @@ function validateScheduleArtifact(config, artifact, planningContext) {
|
|
|
402
403
|
...workflowReferenceNodeIssues(artifact),
|
|
403
404
|
...workflowAssignedWorkUnitIssues(artifact, planningContext.workUnits),
|
|
404
405
|
...missingAssignedWorkUnitIssues(artifact, planningContext.workUnits),
|
|
406
|
+
...workUnitDependencyIssues(artifact, planningContext.workUnits),
|
|
405
407
|
...invalidWorkflowPrimitiveIssues(config, artifact),
|
|
406
408
|
...implementationCoverageIssues(artifact)
|
|
407
409
|
];
|
|
@@ -464,6 +466,47 @@ function missingAssignedWorkUnitIssues(artifact, workUnits) {
|
|
|
464
466
|
const missing = workUnits.map((unit) => unit.id).filter((id) => !assigned.has(id));
|
|
465
467
|
return missing.length > 0 ? [`missing assigned backlog work units: ${missing.join(", ")}`] : [];
|
|
466
468
|
}
|
|
469
|
+
function workUnitDependencyIssues(artifact, workUnits) {
|
|
470
|
+
if (workUnits.length === 0) return [];
|
|
471
|
+
const workUnitIds = new Set(workUnits.map((unit) => unit.id));
|
|
472
|
+
const dependenciesByUnit = new Map(workUnits.map((unit) => [unit.id, (unit.dependencies ?? []).filter((id) => workUnitIds.has(id))]));
|
|
473
|
+
return Object.entries(artifact.workflows).flatMap(([workflowId, workflow]) => {
|
|
474
|
+
const nodes = workflow.nodes.flatMap(flattenWorkflowNode);
|
|
475
|
+
const dependentsByNeed = workflowDependentsByNeed(nodes);
|
|
476
|
+
const nodesByWorkUnit = nodesByAssignedWorkUnit(nodes);
|
|
477
|
+
return nodes.filter(isImplementationNode).flatMap((node) => {
|
|
478
|
+
const dependentId = node.task_context?.id;
|
|
479
|
+
if (!dependentId) return [];
|
|
480
|
+
return (dependenciesByUnit.get(dependentId) ?? []).flatMap((prerequisiteId) => {
|
|
481
|
+
const prerequisiteNodes = nodesByWorkUnit.get(prerequisiteId) ?? [];
|
|
482
|
+
return prerequisiteNodes.some((source) => hasPathToNode(source.id, node.id, dependentsByNeed)) ? [] : [`work unit dependency edge missing in '${workflowId}': '${dependentId}' node '${node.id}' must depend on prerequisite '${prerequisiteId}' nodes ${prerequisiteNodes.map((prerequisite) => `'${prerequisite.id}'`).join(", ")}`];
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function nodesByAssignedWorkUnit(nodes) {
|
|
488
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const node of nodes) {
|
|
490
|
+
const id = node.task_context?.id;
|
|
491
|
+
if (!id) continue;
|
|
492
|
+
const current = grouped.get(id) ?? [];
|
|
493
|
+
current.push(node);
|
|
494
|
+
grouped.set(id, current);
|
|
495
|
+
}
|
|
496
|
+
return grouped;
|
|
497
|
+
}
|
|
498
|
+
function hasPathToNode(sourceId, targetId, dependentsByNeed) {
|
|
499
|
+
const queue = [...dependentsByNeed.get(sourceId) ?? []];
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
while (queue.length > 0) {
|
|
502
|
+
const node = queue.shift();
|
|
503
|
+
if (!node || seen.has(node.id)) continue;
|
|
504
|
+
if (node.id === targetId) return true;
|
|
505
|
+
seen.add(node.id);
|
|
506
|
+
queue.push(...dependentsByNeed.get(node.id) ?? []);
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
467
510
|
function invalidWorkflowPrimitiveIssues(config, artifact) {
|
|
468
511
|
const allowed = new Set(Object.keys(config.workflows));
|
|
469
512
|
return allWorkflowNodes(artifact.workflows).flatMap((node) => {
|
|
@@ -543,6 +586,7 @@ function readBacklogTaskFile(path) {
|
|
|
543
586
|
parentTaskId: stringFrontmatter(parsed.data.parent_task_id),
|
|
544
587
|
workUnit: {
|
|
545
588
|
acceptance_criteria: acceptanceCriteriaFromMarkdown(parsed.content),
|
|
589
|
+
...optionalStringArrayField("dependencies", stringArrayFrontmatter(parsed.data.dependencies)),
|
|
546
590
|
...optionalStringField("description", descriptionFromMarkdown(parsed.content)),
|
|
547
591
|
id,
|
|
548
592
|
...optionalStringField("title", stringFrontmatter(parsed.data.title))
|
|
@@ -552,9 +596,16 @@ function readBacklogTaskFile(path) {
|
|
|
552
596
|
function stringFrontmatter(value) {
|
|
553
597
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
554
598
|
}
|
|
599
|
+
function stringArrayFrontmatter(value) {
|
|
600
|
+
if (!Array.isArray(value)) return [];
|
|
601
|
+
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
602
|
+
}
|
|
555
603
|
function optionalStringField(key, value) {
|
|
556
604
|
return value ? { [key]: value } : {};
|
|
557
605
|
}
|
|
606
|
+
function optionalStringArrayField(key, value) {
|
|
607
|
+
return value.length > 0 ? { [key]: value } : {};
|
|
608
|
+
}
|
|
558
609
|
function descriptionFromMarkdown(content) {
|
|
559
610
|
const marked = betweenMarkers(content, "<!-- SECTION:DESCRIPTION:BEGIN -->", "<!-- SECTION:DESCRIPTION:END -->");
|
|
560
611
|
if (marked) return marked;
|
package/docs/operator-guide.md
CHANGED
|
@@ -133,7 +133,7 @@ Local dry run:
|
|
|
133
133
|
```shell
|
|
134
134
|
export PIPELINE_TARGET_PATH=/path/to/target/repo
|
|
135
135
|
export OISIN_PIPELINE_EVENT_AUTH_TOKEN=dev-token
|
|
136
|
-
export OISIN_PIPELINE_RUNNER_PAYLOAD_JSON='{"eventSink":{"authHeader":"Authorization","url":"http://127.0.0.1:3000/api/pipeline/runs/run-uid-1/events"},"run":{"projectId":"alpha","requestedBy":"@agent","runId":"run-uid-1"},"selector":{"workflowId":"default"},"task":{"prompt":"PIPE-38","taskId":"PIPE-38"}}'
|
|
136
|
+
export OISIN_PIPELINE_RUNNER_PAYLOAD_JSON='{"contractVersion":"1","eventSink":{"authHeader":"Authorization","url":"http://127.0.0.1:3000/api/pipeline/runs/run-uid-1/events"},"run":{"projectId":"alpha","requestedBy":"@agent","runId":"run-uid-1"},"selector":{"allowCommandHooks":true,"workflowId":"default"},"task":{"prompt":"PIPE-38","taskId":"PIPE-38"}}'
|
|
137
137
|
pipe runner-job
|
|
138
138
|
```
|
|
139
139
|
|
|
@@ -161,7 +161,7 @@ spec:
|
|
|
161
161
|
image: ghcr.io/oisin-ee/pipeline-runner:latest
|
|
162
162
|
env:
|
|
163
163
|
- name: OISIN_PIPELINE_RUNNER_PAYLOAD_JSON
|
|
164
|
-
value: '{"eventSink":{"authHeader":"Authorization","url":"https://console.example/api/pipeline/runs/run-uid-1/events"},"run":{"projectId":"alpha","runId":"run-uid-1"},"selector":{"workflowId":"default"},"task":{"prompt":"PIPE-38","taskId":"PIPE-38"}}'
|
|
164
|
+
value: '{"contractVersion":"1","eventSink":{"authHeader":"Authorization","url":"https://console.example/api/pipeline/runs/run-uid-1/events"},"run":{"projectId":"alpha","runId":"run-uid-1"},"selector":{"allowCommandHooks":true,"workflowId":"default"},"task":{"prompt":"PIPE-38","taskId":"PIPE-38"}}'
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
The runner image is configured in `pipeline-console` as
|
|
@@ -171,9 +171,22 @@ event sink URL, and auth header. The runner-side
|
|
|
171
171
|
`OISIN_PIPELINE_EVENT_AUTH_TOKEN` or `PIPELINE_EVENT_API_TOKEN` must match the
|
|
172
172
|
console API `PIPELINE_EVENT_API_TOKEN`.
|
|
173
173
|
|
|
174
|
+
The runner payload contract lives at
|
|
175
|
+
`@oisincoveney/pipeline/runner-job-contract`. `pipeline-console` should create
|
|
176
|
+
payloads with `buildRunnerJobPayload` and can use
|
|
177
|
+
`runnerJobPayloadJsonSchema` for neutral validation or generated docs. The
|
|
178
|
+
runner image labels `pipeline.oisin.dev.runner-contract-version` and
|
|
179
|
+
`pipeline.oisin.dev.pipeline-package-version`, plus the console
|
|
180
|
+
`runner.expectedContractVersion` setting and
|
|
181
|
+
`pipeline.oisin.dev/runner-contract-version` Job label, give operators a fast
|
|
182
|
+
way to detect image/dependency skew before starting Jobs.
|
|
183
|
+
|
|
174
184
|
Troubleshooting:
|
|
175
185
|
|
|
176
186
|
- Missing payload: set `OISIN_PIPELINE_RUNNER_PAYLOAD_JSON`; exit code is `64`.
|
|
187
|
+
- Schema validation: unsupported selector fields or incompatible
|
|
188
|
+
`contractVersion` exit `64`; recoverable payloads also post
|
|
189
|
+
`runner.schema.validation` and a failing `workflow.finish` event.
|
|
177
190
|
- Invalid auth: confirm the runner token matches the console API token; 401/403
|
|
178
191
|
event sink responses are terminal.
|
|
179
192
|
- Missing target config: set `PIPELINE_TARGET_PATH` to a repo containing
|
|
@@ -10,8 +10,15 @@ console database, event store, Job builder, Kueue watcher, or UI.
|
|
|
10
10
|
`pipeline-console` starts the image with one environment variable:
|
|
11
11
|
`OISIN_PIPELINE_RUNNER_PAYLOAD_JSON`.
|
|
12
12
|
|
|
13
|
+
The executable payload contract lives in this package at
|
|
14
|
+
`@oisincoveney/pipeline/runner-job-contract`. Console code must build runner
|
|
15
|
+
payloads through `buildRunnerJobPayload` instead of hand-shaping JSON. The same
|
|
16
|
+
subpath exports `parseRunnerJobPayload`, `RUNNER_JOB_CONTRACT_VERSION`, and
|
|
17
|
+
`runnerJobPayloadJsonSchema` for validation, tests, and docs.
|
|
18
|
+
|
|
13
19
|
```json
|
|
14
20
|
{
|
|
21
|
+
"contractVersion": "1",
|
|
15
22
|
"eventSink": {
|
|
16
23
|
"authHeader": "Authorization",
|
|
17
24
|
"url": "https://console.example/api/pipeline/runs/run-uid-1/events"
|
|
@@ -22,6 +29,7 @@ console database, event store, Job builder, Kueue watcher, or UI.
|
|
|
22
29
|
"runId": "run-uid-1"
|
|
23
30
|
},
|
|
24
31
|
"selector": {
|
|
32
|
+
"allowCommandHooks": true,
|
|
25
33
|
"workflowId": "epic-drain"
|
|
26
34
|
},
|
|
27
35
|
"task": {
|
|
@@ -33,7 +41,19 @@ console database, event store, Job builder, Kueue watcher, or UI.
|
|
|
33
41
|
|
|
34
42
|
`eventSink.url` is the exact append endpoint the runner posts to. The console
|
|
35
43
|
resolves any requested entrypoint before creating the Job and sends
|
|
36
|
-
`selector.workflowId`; the runner rejects unsupported selector modes.
|
|
44
|
+
`selector.workflowId`; the runner rejects unsupported selector modes. The
|
|
45
|
+
`selector.allowCommandHooks` boolean is the runner-side hook policy for command
|
|
46
|
+
hooks in that job. It defaults to `true`, and console callers that disable hooks
|
|
47
|
+
must send `false` through the shared builder.
|
|
48
|
+
|
|
49
|
+
Payloads declare `contractVersion: "1"`. Runner images are labeled with
|
|
50
|
+
`pipeline.oisin.dev.runner-contract-version` and
|
|
51
|
+
`pipeline.oisin.dev.pipeline-package-version`; console deployment config records
|
|
52
|
+
the expected payload contract as `runner.expectedContractVersion` and labels
|
|
53
|
+
created Jobs with `pipeline.oisin.dev/runner-contract-version`. Operators should
|
|
54
|
+
keep the console package dependency, console expected version, and runner image
|
|
55
|
+
label version aligned. A future breaking payload change must increment the
|
|
56
|
+
contract version and ship a compatibility plan.
|
|
37
57
|
|
|
38
58
|
Console-created Jobs are labeled with `kueue.x-k8s.io/queue-name` and
|
|
39
59
|
`pipeline.oisin.dev/project`, `pipeline.oisin.dev/run-id`,
|
|
@@ -63,6 +83,12 @@ Each event has a strictly increasing integer `sequence`, a string `type`, an
|
|
|
63
83
|
`node`, `gate`, `artifact`, `log`, and `finalResult`. The console stores
|
|
64
84
|
non-reserved top-level fields as event payload.
|
|
65
85
|
|
|
86
|
+
If payload validation fails but the runner can recover the run identity and
|
|
87
|
+
event sink identity, it posts a `runner.schema.validation` warning event with
|
|
88
|
+
normalized issue details, then posts `workflow.finish` with outcome `FAIL`, and
|
|
89
|
+
exits `64`. If identity is not recoverable, it writes the validation error to
|
|
90
|
+
stderr and exits `64` without posting events.
|
|
91
|
+
|
|
66
92
|
## Authentication
|
|
67
93
|
|
|
68
94
|
The payload carries the header name, not the token. The runner sets
|
package/package.json
CHANGED
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"types": "./dist/runner.d.ts",
|
|
63
63
|
"import": "./dist/runner.js"
|
|
64
64
|
},
|
|
65
|
+
"./runner-job-contract": {
|
|
66
|
+
"types": "./dist/runner-job-contract.d.ts",
|
|
67
|
+
"import": "./dist/runner-job-contract.js"
|
|
68
|
+
},
|
|
65
69
|
"./runtime": {
|
|
66
70
|
"types": "./dist/pipeline-runtime.d.ts",
|
|
67
71
|
"import": "./dist/pipeline-runtime.js"
|
|
@@ -91,7 +95,7 @@
|
|
|
91
95
|
"prepack": "bun run build:cli"
|
|
92
96
|
},
|
|
93
97
|
"type": "module",
|
|
94
|
-
"version": "1.
|
|
98
|
+
"version": "1.13.0",
|
|
95
99
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
96
100
|
"main": "./dist/index.js",
|
|
97
101
|
"types": "./dist/index.d.ts",
|