@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 +325 -29
- package/dist/mastra/config.d.ts +5 -0
- package/dist/mastra/runner.js +7 -4
- package/dist/pipeline-runtime.d.ts +34 -0
- package/docs/config-architecture.md +18 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
"
|
|
36408
|
-
"
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37068
|
+
const result3 = nodeFailure(node.id, attempt, successHook.evidence, last.output);
|
|
37069
|
+
emitNodeFinish(context, result3);
|
|
37070
|
+
return result3;
|
|
37005
37071
|
}
|
|
37006
|
-
|
|
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
|
|
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
|
-
|
|
37091
|
+
const result2 = nodeFailure(node.id, attempt, evidence, last.output);
|
|
37092
|
+
emitNodeFinish(context, result2);
|
|
37093
|
+
return result2;
|
|
37024
37094
|
}
|
|
37025
37095
|
}
|
|
37026
|
-
|
|
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
|
-
...
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
]
|
|
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) {
|
package/dist/mastra/config.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/mastra/runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|