@oisincoveney/pipeline 3.19.2 → 3.19.3
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.
|
@@ -20,7 +20,11 @@ function runScopedDynamicRunnerCommand(schema, rawOptions, runEffect) {
|
|
|
20
20
|
stderr.write(`${parsed.error.message}\n`);
|
|
21
21
|
return Promise.resolve(DYNAMIC_COMMAND_EXIT.validation);
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const options = {
|
|
24
|
+
...parsed.data,
|
|
25
|
+
stderr
|
|
26
|
+
};
|
|
27
|
+
return Effect.runPromise(Effect.provide(Effect.scoped(runEffect(options)), RunnerCommandIoServiceLive));
|
|
24
28
|
}
|
|
25
29
|
function dynamicRunnerContextEffect(options) {
|
|
26
30
|
return Effect.gen(function* () {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveFileReference } from "../path-refs.js";
|
|
1
2
|
import { parseTicketPlanEffect, ticketPlanSchema } from "../tickets/ticket-plan.js";
|
|
2
3
|
import { runLaunchPlan } from "../runner/subprocess.js";
|
|
3
4
|
import { createRunnerLaunchPlan } from "../runner.js";
|
|
@@ -9,6 +10,7 @@ import { runnerTaskTextEffect } from "./run.js";
|
|
|
9
10
|
import { DYNAMIC_COMMAND_EXIT, dynamicRunnerCommandErrorExit, dynamicRunnerContextEffect, runScopedDynamicRunnerCommand } from "./dynamic-command.js";
|
|
10
11
|
import { Effect } from "effect";
|
|
11
12
|
import { z } from "zod";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
12
14
|
//#region src/runner-command/pre-schedule.ts
|
|
13
15
|
const PRE_SCHEDULE_NODE_IDS = [
|
|
14
16
|
"pre-research",
|
|
@@ -30,6 +32,7 @@ const PHASE_PROFILES = {
|
|
|
30
32
|
"pre-research": "moka-researcher"
|
|
31
33
|
};
|
|
32
34
|
const MAX_DYNAMIC_SCHEDULE_WAVES = 90;
|
|
35
|
+
const JSON_CODE_FENCE_RE = /^```(?:json)?\s*([\s\S]*?)\s*```$/i;
|
|
33
36
|
const researchOutputSchema = z.object({
|
|
34
37
|
ac: z.array(z.string()),
|
|
35
38
|
files: z.array(z.string()).optional(),
|
|
@@ -89,7 +92,7 @@ function runAgentPhaseEffect(options, context) {
|
|
|
89
92
|
const plan = createRunnerLaunchPlan(context.config, {
|
|
90
93
|
nodeId: PHASE_NODE_IDS[phase],
|
|
91
94
|
profileId: PHASE_PROFILES[phase],
|
|
92
|
-
prompt:
|
|
95
|
+
prompt: yield* agentPhasePromptEffect(phase, context),
|
|
93
96
|
worktreePath: context.worktreePath
|
|
94
97
|
});
|
|
95
98
|
const executor = options.executor ?? runLaunchPlan;
|
|
@@ -98,7 +101,7 @@ function runAgentPhaseEffect(options, context) {
|
|
|
98
101
|
try: async () => await executor(plan, {})
|
|
99
102
|
});
|
|
100
103
|
const normalized = normalizeRunnerOutput(plan, agentResult.stdout);
|
|
101
|
-
const output = normalized.output;
|
|
104
|
+
const output = agentResult.exitCode === 0 ? yield* validatedAgentPhaseOutputEffect(phase, normalized.output) : normalized.output;
|
|
102
105
|
if (phase === "pre-planning" && agentResult.exitCode === 0) yield* parseTicketPlanEffect(output);
|
|
103
106
|
if (phase === "pre-research" && agentResult.exitCode === 0) yield* Effect.try({
|
|
104
107
|
catch: (error) => error,
|
|
@@ -177,8 +180,26 @@ function recordPhaseResultEffect(context, result) {
|
|
|
177
180
|
});
|
|
178
181
|
});
|
|
179
182
|
}
|
|
180
|
-
function
|
|
183
|
+
function agentPhasePromptEffect(phase, context) {
|
|
184
|
+
return Effect.gen(function* () {
|
|
185
|
+
const profileId = PHASE_PROFILES[phase];
|
|
186
|
+
const profile = context.config.profiles[profileId];
|
|
187
|
+
return agentPhasePrompt(phase, context, yield* profileInstructionsEffect(context.worktreePath, profile?.instructions));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function profileInstructionsEffect(worktreePath, instructions) {
|
|
191
|
+
if (!instructions) return Effect.succeed("");
|
|
192
|
+
if (instructions.inline) return Effect.succeed(instructions.inline);
|
|
193
|
+
const instructionPath = instructions.path;
|
|
194
|
+
if (!instructionPath) return Effect.succeed("");
|
|
195
|
+
return Effect.tryPromise({
|
|
196
|
+
catch: (error) => error,
|
|
197
|
+
try: () => readFile(resolveFileReference(worktreePath, instructionPath), { encoding: "utf8" })
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function agentPhasePrompt(phase, context, profileInstructions) {
|
|
181
201
|
if (phase === "pre-research") return [
|
|
202
|
+
...remotePhaseContract("research", profileInstructions),
|
|
182
203
|
"Research this task before scheduling.",
|
|
183
204
|
"Return only JSON matching .pipeline/schemas/research.schema.json.",
|
|
184
205
|
"",
|
|
@@ -187,6 +208,7 @@ function agentPhasePrompt(phase, context) {
|
|
|
187
208
|
].join("\n");
|
|
188
209
|
const research = context.persistence.durableStore.get(context.payload.run.id, "pre-research");
|
|
189
210
|
return [
|
|
211
|
+
...remotePhaseContract("ticket scoping", profileInstructions),
|
|
190
212
|
"Scope this task into an implementation-ready ticket plan before scheduling.",
|
|
191
213
|
"Return only JSON matching .pipeline/schemas/ticket-plan.schema.json.",
|
|
192
214
|
"",
|
|
@@ -197,6 +219,68 @@ function agentPhasePrompt(phase, context) {
|
|
|
197
219
|
research?.result.output ?? "No pre-research output recorded."
|
|
198
220
|
].join("\n");
|
|
199
221
|
}
|
|
222
|
+
function remotePhaseContract(label, profileInstructions) {
|
|
223
|
+
return [
|
|
224
|
+
`Automated remote pre-schedule ${label} phase.`,
|
|
225
|
+
"The phase contract below overrides any conflicting profile instruction.",
|
|
226
|
+
"",
|
|
227
|
+
"Phase contract:",
|
|
228
|
+
"- Do not edit files.",
|
|
229
|
+
"- Do not spawn subagents or delegate to task tools.",
|
|
230
|
+
"- Do not call goal or plan tools.",
|
|
231
|
+
"- Keep inspection bounded to files directly relevant to this task.",
|
|
232
|
+
"- Return exactly one JSON object with no Markdown fences and no prose.",
|
|
233
|
+
"",
|
|
234
|
+
"Profile instructions:",
|
|
235
|
+
profileInstructions.trim() || "(none)",
|
|
236
|
+
""
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
function validatedAgentPhaseOutputEffect(phase, output) {
|
|
240
|
+
return Effect.gen(function* () {
|
|
241
|
+
const json = yield* Effect.try({
|
|
242
|
+
catch: (error) => /* @__PURE__ */ new Error(`${phase} returned invalid JSON: ${errorMessage(error)}. Output excerpt: ${outputExcerpt(output)}`),
|
|
243
|
+
try: () => parseJsonObjectOutput(output)
|
|
244
|
+
});
|
|
245
|
+
if (phase === "pre-research") {
|
|
246
|
+
const parsed = researchOutputSchema.safeParse(json);
|
|
247
|
+
if (!parsed.success) return yield* Effect.fail(phaseSchemaError(phase, "research.schema.json", parsed.error, output));
|
|
248
|
+
return JSON.stringify(parsed.data);
|
|
249
|
+
}
|
|
250
|
+
const parsed = ticketPlanSchema.safeParse(json);
|
|
251
|
+
if (!parsed.success) return yield* Effect.fail(phaseSchemaError(phase, "ticket-plan.schema.json", parsed.error, output));
|
|
252
|
+
return JSON.stringify(parsed.data);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function phaseSchemaError(phase, schemaName, error, output) {
|
|
256
|
+
return /* @__PURE__ */ new Error(`${phase} returned JSON that does not match ${schemaName}: ${error.message}. Output excerpt: ${outputExcerpt(output)}`);
|
|
257
|
+
}
|
|
258
|
+
function parseJsonObjectOutput(output) {
|
|
259
|
+
const errors = [];
|
|
260
|
+
for (const candidate of jsonObjectCandidates(output)) try {
|
|
261
|
+
return JSON.parse(candidate);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
errors.push(errorMessage(error));
|
|
264
|
+
}
|
|
265
|
+
throw new Error(errors.at(-1) ?? "no JSON object candidate found");
|
|
266
|
+
}
|
|
267
|
+
function jsonObjectCandidates(output) {
|
|
268
|
+
const trimmed = output.trim();
|
|
269
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
270
|
+
if (trimmed.length > 0) candidates.add(trimmed);
|
|
271
|
+
const fenced = trimmed.match(JSON_CODE_FENCE_RE)?.[1];
|
|
272
|
+
if (fenced?.trim()) candidates.add(fenced.trim());
|
|
273
|
+
const firstBrace = trimmed.indexOf("{");
|
|
274
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
275
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) candidates.add(trimmed.slice(firstBrace, lastBrace + 1));
|
|
276
|
+
return [...candidates];
|
|
277
|
+
}
|
|
278
|
+
function errorMessage(error) {
|
|
279
|
+
return error instanceof Error ? error.message : String(error);
|
|
280
|
+
}
|
|
281
|
+
function outputExcerpt(output) {
|
|
282
|
+
return output.trim().replace(/\s+/g, " ").slice(0, 500);
|
|
283
|
+
}
|
|
200
284
|
function scheduleEntrypointId(payload) {
|
|
201
285
|
if (payload.submission.kind !== "graph") throw new Error("Pre-schedule generation requires a graph submission.");
|
|
202
286
|
return payload.submission.mode === "quick" ? "quick" : "execute";
|
|
@@ -201,10 +201,12 @@ function promptBody(plan) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
const FLAGS_TAKING_VALUE = new Set([
|
|
204
|
-
"--
|
|
204
|
+
"--agent",
|
|
205
205
|
"--dir",
|
|
206
206
|
"--file",
|
|
207
|
-
"--format"
|
|
207
|
+
"--format",
|
|
208
|
+
"--model",
|
|
209
|
+
"--variant"
|
|
208
210
|
]);
|
|
209
211
|
/**
|
|
210
212
|
* The launch plan carries the prompt inside the CLI argv (`run <prompt>` or
|
package/package.json
CHANGED
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"prepack": "nub run build:cli"
|
|
133
133
|
},
|
|
134
134
|
"type": "module",
|
|
135
|
-
"version": "3.19.
|
|
135
|
+
"version": "3.19.3",
|
|
136
136
|
"description": "Config-driven multi-agent pipeline runner for repository work",
|
|
137
137
|
"main": "./dist/index.js",
|
|
138
138
|
"types": "./dist/index.d.ts",
|