@oisincoveney/pipeline 1.3.1 → 1.4.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/dist/index.js CHANGED
@@ -27948,8 +27948,14 @@ var filesystemSchema = exports_external.object({
27948
27948
  var networkSchema = exports_external.object({
27949
27949
  mode: exports_external.enum(NETWORK_MODES)
27950
27950
  }).strict();
27951
+ var outputRepairSchema = exports_external.object({
27952
+ enabled: exports_external.boolean().optional(),
27953
+ max_attempts: exports_external.number().int().positive().optional(),
27954
+ runner: exports_external.string().optional()
27955
+ }).strict();
27951
27956
  var outputSchema = exports_external.object({
27952
27957
  format: exports_external.enum(OUTPUT_FORMATS),
27958
+ repair: outputRepairSchema.optional(),
27953
27959
  schema_path: exports_external.string().min(1).optional()
27954
27960
  }).strict();
27955
27961
  var artifactSchema = exports_external.object({
@@ -28184,6 +28190,16 @@ function validateProfile(profileId, profile, runner, config2, issues, projectRoo
28184
28190
  message: `profile '${profileId}' must declare output.schema_path for json_schema output`
28185
28191
  });
28186
28192
  }
28193
+ const repairRunnerId = profile.output?.repair?.runner;
28194
+ if (repairRunnerId && !config2.runners[repairRunnerId]) {
28195
+ issues.push({
28196
+ path: `profiles.${profileId}.output.repair.runner`,
28197
+ message: `profile '${profileId}' references missing repair runner '${repairRunnerId}'`
28198
+ });
28199
+ }
28200
+ if (repairRunnerId && config2.runners[repairRunnerId]) {
28201
+ validateListCapability(`profiles.${profileId}.output.repair.runner`, ["text"], config2.runners[repairRunnerId].capabilities.output_formats, "repair output format", issues);
28202
+ }
28187
28203
  validatePath(`profiles.${profileId}.output.schema_path`, profile.output?.schema_path, projectRoot, issues);
28188
28204
  }
28189
28205
  function validateActor(label, path, actor, runner, config2, issues, projectRoot) {
@@ -35815,7 +35831,7 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options2 = {})
35815
35831
  ...mcpArgs,
35816
35832
  ...skillArgs,
35817
35833
  "--sandbox",
35818
- "workspace-write",
35834
+ codexSandboxFor(options2.actor),
35819
35835
  "--config",
35820
35836
  'approval_policy="never"',
35821
35837
  "--skip-git-repo-check",
@@ -35864,6 +35880,9 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options2 = {})
35864
35880
  }
35865
35881
  }
35866
35882
  }
35883
+ function codexSandboxFor(actor) {
35884
+ return actor?.filesystem?.mode === "read-only" ? "read-only" : "workspace-write";
35885
+ }
35867
35886
  function createRunnerLaunchPlan(config2, input) {
35868
35887
  const profile = input.profileId ? config2.profiles[input.profileId] : undefined;
35869
35888
  if (input.profileId && !profile) {
@@ -36267,6 +36286,9 @@ profiles:
36267
36286
  output:
36268
36287
  format: json_schema
36269
36288
  schema_path: .pipeline/schemas/research.schema.json
36289
+ repair:
36290
+ enabled: true
36291
+ max_attempts: 1
36270
36292
  pipeline-inspector:
36271
36293
  runner: codex
36272
36294
  description: Inspect the repository without modifying files.
@@ -36327,6 +36349,9 @@ profiles:
36327
36349
  output:
36328
36350
  format: json_schema
36329
36351
  schema_path: .pipeline/schemas/verify.schema.json
36352
+ repair:
36353
+ enabled: true
36354
+ max_attempts: 1
36330
36355
  pipeline-learner:
36331
36356
  runner: codex
36332
36357
  description: Store durable lessons from the completed run.
@@ -36342,6 +36367,9 @@ profiles:
36342
36367
  output:
36343
36368
  format: json_schema
36344
36369
  schema_path: .pipeline/schemas/learn.schema.json
36370
+ repair:
36371
+ enabled: true
36372
+ max_attempts: 1
36345
36373
  `;
36346
36374
  var RESEARCH_SCHEMA = JSON.stringify({
36347
36375
  additionalProperties: false,
@@ -37052,7 +37080,7 @@ async function executeNode(node, context) {
37052
37080
  emitNodeFinish(context, result2);
37053
37081
  return result2;
37054
37082
  }
37055
- const evidence = failedGate?.evidence ?? last.evidence.concat(`node exited with code ${last.exitCode}`);
37083
+ const evidence = failedGate ? [...last.evidence, ...failedGate.evidence] : last.evidence.concat(`node exited with code ${last.exitCode}`);
37056
37084
  if (attempt === maxAttempts) {
37057
37085
  await dispatchHooks(context, "node.error", {
37058
37086
  evidence,
@@ -37117,17 +37145,167 @@ async function executeAgentNode(node, context) {
37117
37145
  context.agentInvocations.push(plan);
37118
37146
  const result = await context.executor(plan);
37119
37147
  const normalized = normalizeAgentOutput(plan, result.stdout);
37148
+ const finalized = await finalizeAgentOutput({
37149
+ context,
37150
+ node,
37151
+ normalized,
37152
+ result
37153
+ });
37120
37154
  return {
37121
37155
  evidence: [
37122
37156
  `agent boundary node=${node.id} profile=${node.profile} runner=${plan.runnerId} strategy=${plan.strategy}`,
37123
- ...normalized.evidence,
37157
+ ...finalized.evidence,
37124
37158
  ...result.stderr ? [`stderr: ${result.stderr}`] : [],
37125
37159
  ...result.timedOut ? ["agent timed out"] : []
37126
37160
  ],
37127
37161
  exitCode: result.exitCode,
37128
- output: normalized.output
37162
+ output: finalized.output
37163
+ };
37164
+ }
37165
+ async function finalizeAgentOutput(inputs) {
37166
+ const { context, node, normalized, result } = inputs;
37167
+ const repairContext = outputRepairContext(context, node, normalized, result);
37168
+ if (!repairContext) {
37169
+ return normalized;
37170
+ }
37171
+ return await runOutputRepair(context, node, normalized, repairContext);
37172
+ }
37173
+ function outputRepairContext(context, node, normalized, result) {
37174
+ if (result.exitCode !== 0 || result.timedOut) {
37175
+ return null;
37176
+ }
37177
+ const profile = node.profile ? context.config.profiles[node.profile] : undefined;
37178
+ if (!profile) {
37179
+ return null;
37180
+ }
37181
+ const output = profile?.output;
37182
+ if (output?.format !== "json_schema" || !output.schema_path) {
37183
+ return null;
37184
+ }
37185
+ const firstValidation = validateJsonSchemaSource(normalized.output, output.schema_path, context.worktreePath);
37186
+ if (firstValidation.passed) {
37187
+ return null;
37188
+ }
37189
+ const repair = outputRepairOptions(output);
37190
+ if (!repair.enabled) {
37191
+ return null;
37192
+ }
37193
+ return {
37194
+ evidence: [
37195
+ ...normalized.evidence,
37196
+ "output repair triggered",
37197
+ ...firstValidation.evidence.map((item) => `original output: ${item}`)
37198
+ ],
37199
+ maxAttempts: repair.maxAttempts,
37200
+ runner: repair.runner ?? profile.runner,
37201
+ schemaPath: output.schema_path,
37202
+ validation: firstValidation
37203
+ };
37204
+ }
37205
+ async function runOutputRepair(context, node, normalized, repairContext) {
37206
+ let latest = normalized;
37207
+ let latestValidation = repairContext.validation;
37208
+ const evidence = [...repairContext.evidence];
37209
+ for (let attempt = 1;attempt <= repairContext.maxAttempts; attempt += 1) {
37210
+ const repairPlan = createOutputRepairPlan({
37211
+ context,
37212
+ node,
37213
+ originalOutput: latest.output,
37214
+ repairRunner: repairContext.runner,
37215
+ schemaPath: repairContext.schemaPath,
37216
+ validation: latestValidation
37217
+ });
37218
+ context.agentInvocations.push(repairPlan);
37219
+ const repairResult = await context.executor(repairPlan);
37220
+ const repaired = normalizeAgentOutput(repairPlan, repairResult.stdout);
37221
+ const repairedValidation = validateJsonSchemaSource(repaired.output, repairContext.schemaPath, context.worktreePath);
37222
+ latest = {
37223
+ evidence: [
37224
+ ...repaired.evidence,
37225
+ ...repairResult.stderr ? [`repair stderr: ${repairResult.stderr}`] : [],
37226
+ ...repairResult.timedOut ? ["output repair timed out"] : []
37227
+ ],
37228
+ output: repaired.output
37229
+ };
37230
+ latestValidation = repairedValidation;
37231
+ const passed = repairResult.exitCode === 0 && repairedValidation.passed;
37232
+ evidence.push(...repaired.evidence, passed ? `output repair passed for ${node.id} after attempt ${attempt}` : `output repair failed for ${node.id} after attempt ${attempt}`, ...repairedValidation.evidence.map((item) => `repaired output: ${item}`));
37233
+ emit(context, {
37234
+ attempt,
37235
+ nodeId: node.id,
37236
+ passed,
37237
+ type: "output.repair",
37238
+ ...passed ? {} : { reason: repairedValidation.reason ?? "repair failed" }
37239
+ });
37240
+ if (passed) {
37241
+ return {
37242
+ evidence,
37243
+ output: repaired.output
37244
+ };
37245
+ }
37246
+ }
37247
+ return {
37248
+ evidence,
37249
+ output: latest.output
37129
37250
  };
37130
37251
  }
37252
+ function outputRepairOptions(output) {
37253
+ const repair = output.repair;
37254
+ return {
37255
+ enabled: repair?.enabled ?? true,
37256
+ maxAttempts: repair?.max_attempts ?? 1,
37257
+ ...repair?.runner ? { runner: repair.runner } : {}
37258
+ };
37259
+ }
37260
+ function createOutputRepairPlan(inputs) {
37261
+ const {
37262
+ context,
37263
+ node,
37264
+ originalOutput,
37265
+ repairRunner,
37266
+ schemaPath,
37267
+ validation
37268
+ } = inputs;
37269
+ const schema = readFileSync7(join6(context.worktreePath, schemaPath), "utf8");
37270
+ const repairProfileId = `${node.id}:output-repair`;
37271
+ const repairConfig = {
37272
+ ...context.config,
37273
+ profiles: {
37274
+ ...context.config.profiles,
37275
+ [repairProfileId]: {
37276
+ filesystem: { mode: "read-only" },
37277
+ instructions: { inline: "Repair invalid structured output." },
37278
+ network: { mode: "disabled" },
37279
+ output: { format: "text" },
37280
+ runner: repairRunner,
37281
+ tools: []
37282
+ }
37283
+ }
37284
+ };
37285
+ const prompt = [
37286
+ "You are an output finalizer for a pipeline agent.",
37287
+ "Return only valid JSON matching the expected schema.",
37288
+ "Do not use Markdown fences or add prose outside the JSON value.",
37289
+ "Preserve facts from the original output. If required information is missing, use empty arrays or nulls only where the schema permits.",
37290
+ "",
37291
+ "Expected schema:",
37292
+ schema,
37293
+ "",
37294
+ "Validation error:",
37295
+ validation.evidence.join(`
37296
+ `),
37297
+ "",
37298
+ "Original output:",
37299
+ originalOutput
37300
+ ].join(`
37301
+ `);
37302
+ return createRunnerLaunchPlan(repairConfig, {
37303
+ nodeId: repairProfileId,
37304
+ profileId: repairProfileId,
37305
+ prompt,
37306
+ worktreePath: context.worktreePath
37307
+ });
37308
+ }
37131
37309
  function normalizeAgentOutput(plan, stdout) {
37132
37310
  if (plan.type === "codex") {
37133
37311
  const text = lastJsonLineValue(stdout, (value) => {
@@ -37442,24 +37620,29 @@ function evaluateJsonSchemaGate(gate, gateId, nodeId, context, attempt) {
37442
37620
  reason: `missing JSON artifact '${gate.path ?? ""}'`
37443
37621
  };
37444
37622
  }
37623
+ const result = validateJsonSchemaSource(source, schemaPath, context.worktreePath);
37624
+ return {
37625
+ evidence: result.evidence,
37626
+ gateId,
37627
+ kind: gate.kind,
37628
+ nodeId,
37629
+ passed: result.passed,
37630
+ reason: result.reason
37631
+ };
37632
+ }
37633
+ function validateJsonSchemaSource(source, schemaPath, worktreePath) {
37445
37634
  try {
37446
- const schema = JSON.parse(readFileSync7(join6(context.worktreePath, schemaPath), "utf8"));
37635
+ const schema = JSON.parse(readFileSync7(join6(worktreePath, schemaPath), "utf8"));
37447
37636
  const value = JSON.parse(source);
37448
37637
  const errors4 = validateJsonSchema(value, schema);
37449
37638
  return {
37450
37639
  evidence: errors4.length === 0 ? [`JSON schema passed: ${schemaPath}`] : errors4.map((error51) => `schema: ${error51}`),
37451
- gateId,
37452
- kind: gate.kind,
37453
- nodeId,
37454
37640
  passed: errors4.length === 0,
37455
37641
  reason: errors4.length === 0 ? undefined : "JSON schema validation failed"
37456
37642
  };
37457
37643
  } catch (err) {
37458
37644
  return {
37459
37645
  evidence: [err instanceof Error ? err.message : String(err)],
37460
- gateId,
37461
- kind: gate.kind,
37462
- nodeId,
37463
37646
  passed: false,
37464
37647
  reason: "JSON schema validation failed"
37465
37648
  };
@@ -37653,6 +37836,9 @@ function formatRuntimeProgress(event) {
37653
37836
  case "gate.finish":
37654
37837
  console.error(`Gate ${event.passed ? "passed" : "failed"}: ${event.nodeId}/${event.gateId}${event.reason ? ` (${event.reason})` : ""}`);
37655
37838
  return;
37839
+ case "output.repair":
37840
+ console.error(`Output repair ${event.passed ? "passed" : "failed"}: ${event.nodeId} attempt=${event.attempt}${event.reason ? ` (${event.reason})` : ""}`);
37841
+ return;
37656
37842
  case "node.finish":
37657
37843
  console.error(`Node finished: ${event.nodeId} ${event.status} exit=${event.exitCode}`);
37658
37844
  return;
@@ -76,6 +76,11 @@ declare const configSchema: z.ZodObject<{
76
76
  jsonl: "jsonl";
77
77
  json_schema: "json_schema";
78
78
  }>;
79
+ repair: z.ZodOptional<z.ZodObject<{
80
+ enabled: z.ZodOptional<z.ZodBoolean>;
81
+ max_attempts: z.ZodOptional<z.ZodNumber>;
82
+ runner: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strict>>;
79
84
  schema_path: z.ZodOptional<z.ZodString>;
80
85
  }, z.core.$strict>>;
81
86
  rules: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -7283,7 +7283,7 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
7283
7283
  ...mcpArgs,
7284
7284
  ...skillArgs,
7285
7285
  "--sandbox",
7286
- "workspace-write",
7286
+ codexSandboxFor(options.actor),
7287
7287
  "--config",
7288
7288
  'approval_policy="never"',
7289
7289
  "--skip-git-repo-check",
@@ -7332,6 +7332,9 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
7332
7332
  }
7333
7333
  }
7334
7334
  }
7335
+ function codexSandboxFor(actor) {
7336
+ return actor?.filesystem?.mode === "read-only" ? "read-only" : "workspace-write";
7337
+ }
7335
7338
  async function execaHarness(harness, prompt, contextFile, worktreePath) {
7336
7339
  if (harness === "pi") {
7337
7340
  return execaHarnessPi(prompt, contextFile, worktreePath);
@@ -54,6 +54,12 @@ export type PipelineRuntimeEvent = {
54
54
  passed: boolean;
55
55
  reason?: string;
56
56
  type: "gate.finish";
57
+ } | {
58
+ attempt: number;
59
+ nodeId: string;
60
+ passed: boolean;
61
+ reason?: string;
62
+ type: "output.repair";
57
63
  } | {
58
64
  outcome: PipelineRuntimeResult["outcome"];
59
65
  type: "workflow.finish";
@@ -109,6 +109,24 @@ receive explicit grants:
109
109
  - `network`: inherited or disabled.
110
110
  - `output`: text, JSON, JSONL, or JSON Schema output.
111
111
 
112
+ JSON Schema outputs are hard contracts. The runtime validates normalized agent
113
+ output before the node can pass. Schema outputs also get a bounded repair pass
114
+ by default:
115
+
116
+ ```yaml
117
+ output:
118
+ format: json_schema
119
+ schema_path: .pipeline/schemas/research.schema.json
120
+ repair:
121
+ enabled: true
122
+ max_attempts: 1
123
+ ```
124
+
125
+ The repair pass receives only the schema, invalid output, and validation error.
126
+ It uses a no-tools, read-only profile, then the runtime validates the repaired
127
+ output again. If repair still fails, the node fails with both original and
128
+ repair evidence.
129
+
112
130
  Hooks live in `pipeline.yaml` and can be attached to the orchestrator, workflow,
113
131
  or workflow nodes.
114
132
 
package/package.json CHANGED
@@ -69,7 +69,7 @@
69
69
  "prepack": "bun run build:cli"
70
70
  },
71
71
  "type": "module",
72
- "version": "1.3.1",
72
+ "version": "1.4.0",
73
73
  "description": "",
74
74
  "main": "index.js",
75
75
  "keywords": [],