@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
- return Effect.runPromise(Effect.provide(Effect.scoped(runEffect(parsed.data)), RunnerCommandIoServiceLive));
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: agentPhasePrompt(phase, context),
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 agentPhasePrompt(phase, context) {
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
- "--model",
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.2",
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",