@oisincoveney/pipeline 1.3.0 → 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) {
@@ -35809,17 +35825,17 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options2 = {})
35809
35825
  return [
35810
35826
  "exec",
35811
35827
  "--json",
35828
+ "-C",
35829
+ worktreePath,
35812
35830
  ...optionalModelArgs(harness, options2.runner, options2.actor),
35813
35831
  ...mcpArgs,
35814
35832
  ...skillArgs,
35815
35833
  "--sandbox",
35816
- "workspace-write",
35834
+ codexSandboxFor(options2.actor),
35817
35835
  "--config",
35818
35836
  'approval_policy="never"',
35819
35837
  "--skip-git-repo-check",
35820
- prompt,
35821
- "-C",
35822
- worktreePath
35838
+ prompt
35823
35839
  ];
35824
35840
  case "opencode":
35825
35841
  return contextFile ? [
@@ -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,
@@ -36404,8 +36432,11 @@ var SCAFFOLD_FILES = {
36404
36432
  `),
36405
36433
  ".pipeline/prompts/inspector.md": [
36406
36434
  "You are the read-only inspection phase for the pipeline.",
36407
- "Inspect first-party source, tests, docs, and task context.",
36408
- "Report the repository structure, available checks, important files, and notable risks.",
36435
+ "Use a bounded inspection: run at most 8 discovery commands and read at most 12 small, high-signal files.",
36436
+ "Prefer `pwd`, `rg --files -g '!*node_modules*' -g '!dist/**' -g '!build/**' | head -200`, package/workspace manifests, mise/turbo config, and test config files.",
36437
+ "When reading paths with shell metacharacters such as brackets, quote the whole path.",
36438
+ "Do not recursively inspect route trees or generated output.",
36439
+ "Report the app structure, available checks, important files, and notable risks from the sampled evidence.",
36409
36440
  "Do not modify files.",
36410
36441
  ""
36411
36442
  ].join(`
@@ -36932,17 +36963,29 @@ async function runPipelineFromConfig(options2) {
36932
36963
  plan,
36933
36964
  task: options2.task,
36934
36965
  workflowId,
36935
- worktreePath
36966
+ worktreePath,
36967
+ ...options2.reporter ? { reporter: options2.reporter } : {}
36936
36968
  };
36937
36969
  const nodes = [];
36970
+ emit(context, {
36971
+ nodeIds: plan.topologicalOrder.map((node) => node.id),
36972
+ type: "workflow.start",
36973
+ workflowId
36974
+ });
36938
36975
  const startHook = await dispatchHooks(context, "workflow.start");
36939
36976
  if (startHook) {
36940
- return failedRuntimeResult(context, nodes, startHook);
36977
+ const result2 = failedRuntimeResult(context, nodes, startHook);
36978
+ emit(context, {
36979
+ outcome: result2.outcome,
36980
+ type: "workflow.finish",
36981
+ workflowId
36982
+ });
36983
+ return result2;
36941
36984
  }
36942
36985
  for (const batch of plan.parallelBatches) {
36943
36986
  const results = await Promise.all(batch.map((node) => executeNode(node, context)));
36944
36987
  nodes.push(...results);
36945
- const failed = results.find((result) => result.status === "failed");
36988
+ const failed = results.find((result2) => result2.status === "failed");
36946
36989
  if (failed) {
36947
36990
  const failure = {
36948
36991
  evidence: failed.evidence,
@@ -36952,16 +36995,28 @@ async function runPipelineFromConfig(options2) {
36952
36995
  };
36953
36996
  await dispatchHooks(context, "workflow.failure", failure);
36954
36997
  await dispatchHooks(context, "workflow.complete", failure);
36955
- return failedRuntimeResult(context, nodes, failure);
36998
+ const result2 = failedRuntimeResult(context, nodes, failure);
36999
+ emit(context, {
37000
+ outcome: result2.outcome,
37001
+ type: "workflow.finish",
37002
+ workflowId
37003
+ });
37004
+ return result2;
36956
37005
  }
36957
37006
  }
36958
37007
  const successHook = await dispatchHooks(context, "workflow.success");
36959
37008
  const completeHook = await dispatchHooks(context, "workflow.complete");
36960
37009
  const hookFailure = successHook ?? completeHook;
36961
37010
  if (hookFailure) {
36962
- return failedRuntimeResult(context, nodes, hookFailure);
37011
+ const result2 = failedRuntimeResult(context, nodes, hookFailure);
37012
+ emit(context, {
37013
+ outcome: result2.outcome,
37014
+ type: "workflow.finish",
37015
+ workflowId
37016
+ });
37017
+ return result2;
36963
37018
  }
36964
- return {
37019
+ const result = {
36965
37020
  agentInvocations: context.agentInvocations,
36966
37021
  failureDetails: [],
36967
37022
  gates: context.gates,
@@ -36970,6 +37025,12 @@ async function runPipelineFromConfig(options2) {
36970
37025
  outcome: "PASS",
36971
37026
  plan
36972
37027
  };
37028
+ emit(context, {
37029
+ outcome: result.outcome,
37030
+ type: "workflow.finish",
37031
+ workflowId
37032
+ });
37033
+ return result;
36973
37034
  }
36974
37035
  function failedRuntimeResult(context, nodes, failure) {
36975
37036
  return {
@@ -36990,9 +37051,12 @@ async function executeNode(node, context) {
36990
37051
  output: ""
36991
37052
  };
36992
37053
  for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
37054
+ emitNodeStart(context, node, attempt);
36993
37055
  const startHook = await dispatchHooks(context, "node.start", undefined, node);
36994
37056
  if (startHook) {
36995
- return nodeFailure(node.id, attempt, startHook.evidence, last.output);
37057
+ const result2 = nodeFailure(node.id, attempt, startHook.evidence, last.output);
37058
+ emitNodeFinish(context, result2);
37059
+ return result2;
36996
37060
  }
36997
37061
  last = await executeNodeAttempt(node, context);
36998
37062
  context.lastOutputByNode.set(node.id, last.output);
@@ -37001,9 +37065,11 @@ async function executeNode(node, context) {
37001
37065
  if (!failedGate && last.exitCode === 0) {
37002
37066
  const successHook = await dispatchHooks(context, "node.success", undefined, node);
37003
37067
  if (successHook) {
37004
- return nodeFailure(node.id, attempt, successHook.evidence, last.output);
37068
+ const result3 = nodeFailure(node.id, attempt, successHook.evidence, last.output);
37069
+ emitNodeFinish(context, result3);
37070
+ return result3;
37005
37071
  }
37006
- return {
37072
+ const result2 = {
37007
37073
  attempts: attempt,
37008
37074
  evidence: last.evidence,
37009
37075
  exitCode: 0,
@@ -37011,8 +37077,10 @@ async function executeNode(node, context) {
37011
37077
  output: last.output,
37012
37078
  status: "passed"
37013
37079
  };
37080
+ emitNodeFinish(context, result2);
37081
+ return result2;
37014
37082
  }
37015
- 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}`);
37016
37084
  if (attempt === maxAttempts) {
37017
37085
  await dispatchHooks(context, "node.error", {
37018
37086
  evidence,
@@ -37020,10 +37088,14 @@ async function executeNode(node, context) {
37020
37088
  nodeId: node.id,
37021
37089
  reason: failedGate?.reason ?? `node exited with code ${last.exitCode}`
37022
37090
  }, node);
37023
- return nodeFailure(node.id, attempt, evidence, last.output);
37091
+ const result2 = nodeFailure(node.id, attempt, evidence, last.output);
37092
+ emitNodeFinish(context, result2);
37093
+ return result2;
37024
37094
  }
37025
37095
  }
37026
- return nodeFailure(node.id, maxAttempts, last.evidence, last.output);
37096
+ const result = nodeFailure(node.id, maxAttempts, last.evidence, last.output);
37097
+ emitNodeFinish(context, result);
37098
+ return result;
37027
37099
  }
37028
37100
  function nodeFailure(nodeId, attempts, evidence, output) {
37029
37101
  return {
@@ -37073,17 +37145,167 @@ async function executeAgentNode(node, context) {
37073
37145
  context.agentInvocations.push(plan);
37074
37146
  const result = await context.executor(plan);
37075
37147
  const normalized = normalizeAgentOutput(plan, result.stdout);
37148
+ const finalized = await finalizeAgentOutput({
37149
+ context,
37150
+ node,
37151
+ normalized,
37152
+ result
37153
+ });
37076
37154
  return {
37077
37155
  evidence: [
37078
37156
  `agent boundary node=${node.id} profile=${node.profile} runner=${plan.runnerId} strategy=${plan.strategy}`,
37079
- ...normalized.evidence,
37157
+ ...finalized.evidence,
37080
37158
  ...result.stderr ? [`stderr: ${result.stderr}`] : [],
37081
37159
  ...result.timedOut ? ["agent timed out"] : []
37082
37160
  ],
37083
37161
  exitCode: result.exitCode,
37084
- 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
37250
+ };
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 } : {}
37085
37258
  };
37086
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
+ }
37087
37309
  function normalizeAgentOutput(plan, stdout) {
37088
37310
  if (plan.type === "codex") {
37089
37311
  const text = lastJsonLineValue(stdout, (value) => {
@@ -37301,6 +37523,13 @@ async function evaluateNodeGates(node, context, attempt) {
37301
37523
  const result = await evaluateGate(gate, node.id, context, attempt);
37302
37524
  context.gates.push(result);
37303
37525
  results.push(result);
37526
+ emit(context, {
37527
+ gateId: result.gateId,
37528
+ nodeId: result.nodeId,
37529
+ passed: result.passed,
37530
+ type: "gate.finish",
37531
+ ...result.reason ? { reason: result.reason } : {}
37532
+ });
37304
37533
  if (!result.passed) {
37305
37534
  await dispatchHooks(context, "gate.failure", {
37306
37535
  evidence: result.evidence,
@@ -37315,6 +37544,28 @@ async function evaluateNodeGates(node, context, attempt) {
37315
37544
  }
37316
37545
  return results;
37317
37546
  }
37547
+ function emit(context, event) {
37548
+ context.reporter?.(event);
37549
+ }
37550
+ function emitNodeStart(context, node, attempt) {
37551
+ const profile = node.profile ? context.config.profiles[node.profile] : undefined;
37552
+ emit(context, {
37553
+ attempt,
37554
+ nodeId: node.id,
37555
+ type: "node.start",
37556
+ ...node.profile ? { profile: node.profile } : {},
37557
+ ...profile?.runner ? { runnerId: profile.runner } : {}
37558
+ });
37559
+ }
37560
+ function emitNodeFinish(context, result) {
37561
+ emit(context, {
37562
+ attempt: result.attempts,
37563
+ exitCode: result.exitCode,
37564
+ nodeId: result.nodeId,
37565
+ status: result.status,
37566
+ type: "node.finish"
37567
+ });
37568
+ }
37318
37569
  async function evaluateGate(gate, nodeId, context, attempt) {
37319
37570
  const gateId = gate.id ?? `${gate.kind}:${nodeId}`;
37320
37571
  if (gate.kind === "command") {
@@ -37369,24 +37620,29 @@ function evaluateJsonSchemaGate(gate, gateId, nodeId, context, attempt) {
37369
37620
  reason: `missing JSON artifact '${gate.path ?? ""}'`
37370
37621
  };
37371
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) {
37372
37634
  try {
37373
- const schema = JSON.parse(readFileSync7(join6(context.worktreePath, schemaPath), "utf8"));
37635
+ const schema = JSON.parse(readFileSync7(join6(worktreePath, schemaPath), "utf8"));
37374
37636
  const value = JSON.parse(source);
37375
37637
  const errors4 = validateJsonSchema(value, schema);
37376
37638
  return {
37377
37639
  evidence: errors4.length === 0 ? [`JSON schema passed: ${schemaPath}`] : errors4.map((error51) => `schema: ${error51}`),
37378
- gateId,
37379
- kind: gate.kind,
37380
- nodeId,
37381
37640
  passed: errors4.length === 0,
37382
37641
  reason: errors4.length === 0 ? undefined : "JSON schema validation failed"
37383
37642
  };
37384
37643
  } catch (err) {
37385
37644
  return {
37386
37645
  evidence: [err instanceof Error ? err.message : String(err)],
37387
- gateId,
37388
- kind: gate.kind,
37389
- nodeId,
37390
37646
  passed: false,
37391
37647
  reason: "JSON schema validation failed"
37392
37648
  };
@@ -37554,6 +37810,7 @@ function pipe2(description, options2 = {}) {
37554
37810
  async function runConfiguredPipeline(inputs) {
37555
37811
  const runner = inputs.pipelineRunner ?? runPipelineFromConfig;
37556
37812
  const result = await runner({
37813
+ reporter: formatRuntimeProgress,
37557
37814
  task: inputs.task,
37558
37815
  workflowId: inputs.workflow,
37559
37816
  worktreePath: inputs.worktreePath
@@ -37563,13 +37820,52 @@ async function runConfiguredPipeline(inputs) {
37563
37820
  throw new Error(formatRuntimeFailure(result));
37564
37821
  }
37565
37822
  }
37823
+ function formatRuntimeProgress(event) {
37824
+ switch (event.type) {
37825
+ case "workflow.start":
37826
+ console.error(`Pipeline starting: ${event.workflowId} (${event.nodeIds.join(" -> ")})`);
37827
+ return;
37828
+ case "node.start":
37829
+ console.error([
37830
+ `Node starting: ${event.nodeId}`,
37831
+ event.runnerId ? `runner=${event.runnerId}` : "",
37832
+ event.profile ? `profile=${event.profile}` : "",
37833
+ `attempt=${event.attempt}`
37834
+ ].filter(Boolean).join(" "));
37835
+ return;
37836
+ case "gate.finish":
37837
+ console.error(`Gate ${event.passed ? "passed" : "failed"}: ${event.nodeId}/${event.gateId}${event.reason ? ` (${event.reason})` : ""}`);
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;
37842
+ case "node.finish":
37843
+ console.error(`Node finished: ${event.nodeId} ${event.status} exit=${event.exitCode}`);
37844
+ return;
37845
+ case "workflow.finish":
37846
+ console.error(`Pipeline finished: ${event.workflowId} ${event.outcome}`);
37847
+ return;
37848
+ default: {
37849
+ const _exhaustive = event;
37850
+ throw new Error(`Unhandled runtime event: ${String(_exhaustive)}`);
37851
+ }
37852
+ }
37853
+ }
37566
37854
  function formatRuntimeResult(result) {
37567
- return [
37855
+ const lines = [
37568
37856
  `Pipeline complete: ${result.outcome}`,
37569
37857
  `Workflow: ${result.plan.workflowId}`,
37570
37858
  `Nodes: ${result.nodes.map((node) => `${node.nodeId}:${node.status}`).join(", ")}`,
37571
37859
  `Agent boundaries: ${result.agentInvocations.length}`
37572
- ].join(`
37860
+ ];
37861
+ const outputs = result.nodes.filter((node) => node.output.trim());
37862
+ if (outputs.length > 0) {
37863
+ lines.push("Node outputs:");
37864
+ for (const node of outputs) {
37865
+ appendIndentedSection(lines, node.nodeId, [node.output]);
37866
+ }
37867
+ }
37868
+ return lines.join(`
37573
37869
  `);
37574
37870
  }
37575
37871
  function formatRuntimeFailure(result) {
@@ -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>>;
@@ -7277,17 +7277,17 @@ function harnessArgv(harness, prompt, worktreePath, contextFile, options = {}) {
7277
7277
  return [
7278
7278
  "exec",
7279
7279
  "--json",
7280
+ "-C",
7281
+ worktreePath,
7280
7282
  ...optionalModelArgs(harness, options.runner, options.actor),
7281
7283
  ...mcpArgs,
7282
7284
  ...skillArgs,
7283
7285
  "--sandbox",
7284
- "workspace-write",
7286
+ codexSandboxFor(options.actor),
7285
7287
  "--config",
7286
7288
  'approval_policy="never"',
7287
7289
  "--skip-git-repo-check",
7288
- prompt,
7289
- "-C",
7290
- worktreePath
7290
+ prompt
7291
7291
  ];
7292
7292
  case "opencode":
7293
7293
  return contextFile ? [
@@ -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);
@@ -32,9 +32,43 @@ export interface PipelineRuntimeResult {
32
32
  outcome: "FAIL" | "PASS";
33
33
  plan: WorkflowExecutionPlan;
34
34
  }
35
+ export type PipelineRuntimeEvent = {
36
+ nodeIds: string[];
37
+ type: "workflow.start";
38
+ workflowId: string;
39
+ } | {
40
+ attempt: number;
41
+ nodeId: string;
42
+ profile?: string;
43
+ runnerId?: string;
44
+ type: "node.start";
45
+ } | {
46
+ attempt: number;
47
+ exitCode: number;
48
+ nodeId: string;
49
+ status: RuntimeNodeResult["status"];
50
+ type: "node.finish";
51
+ } | {
52
+ gateId: string;
53
+ nodeId: string;
54
+ passed: boolean;
55
+ reason?: string;
56
+ type: "gate.finish";
57
+ } | {
58
+ attempt: number;
59
+ nodeId: string;
60
+ passed: boolean;
61
+ reason?: string;
62
+ type: "output.repair";
63
+ } | {
64
+ outcome: PipelineRuntimeResult["outcome"];
65
+ type: "workflow.finish";
66
+ workflowId: string;
67
+ };
35
68
  export interface PipelineRuntimeOptions {
36
69
  config?: PipelineConfig;
37
70
  executor?: (plan: RunnerLaunchPlan) => AgentResult | Promise<AgentResult>;
71
+ reporter?: (event: PipelineRuntimeEvent) => void;
38
72
  task: string;
39
73
  workflowId?: string;
40
74
  worktreePath?: string;
@@ -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.0",
72
+ "version": "1.4.0",
73
73
  "description": "",
74
74
  "main": "index.js",
75
75
  "keywords": [],