lalph 0.1.96 → 0.1.98
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/cli.mjs +215 -91
- package/package.json +1 -1
- package/src/Agents/chooser.ts +8 -0
- package/src/Agents/instructor.ts +8 -0
- package/src/CliAgent/claude.ts +179 -34
- package/src/commands/root.ts +175 -161
- package/src/domain/CliAgent.ts +1 -1
- package/src/shared/fs.ts +12 -0
package/dist/cli.mjs
CHANGED
|
@@ -28564,6 +28564,17 @@ const provideServiceEffect = /* @__PURE__ */ dual(3, (self$1, key, service$2) =>
|
|
|
28564
28564
|
*/
|
|
28565
28565
|
const runForEach$1 = /* @__PURE__ */ dual(2, (self$1, f) => runWith$1(self$1, (pull) => forever(flatMap(pull, f), { autoYield: false })));
|
|
28566
28566
|
/**
|
|
28567
|
+
* @since 2.0.0
|
|
28568
|
+
* @category execution
|
|
28569
|
+
*/
|
|
28570
|
+
const runHead$1 = (self$1) => suspend$3(() => {
|
|
28571
|
+
let head = none$3();
|
|
28572
|
+
return runWith$1(self$1, (pull) => pull.pipe(asSome, flatMap((head_$1) => {
|
|
28573
|
+
head = head_$1;
|
|
28574
|
+
return done();
|
|
28575
|
+
})), () => succeed$1(head));
|
|
28576
|
+
});
|
|
28577
|
+
/**
|
|
28567
28578
|
* Runs a channel and folds over all output elements with an accumulator.
|
|
28568
28579
|
*
|
|
28569
28580
|
* @example
|
|
@@ -30247,6 +30258,11 @@ const runCollect = (self$1) => runFold(self$1.channel, () => [], (acc, chunk) =>
|
|
|
30247
30258
|
return acc;
|
|
30248
30259
|
});
|
|
30249
30260
|
/**
|
|
30261
|
+
* @since 2.0.0
|
|
30262
|
+
* @category destructors
|
|
30263
|
+
*/
|
|
30264
|
+
const runHead = (self$1) => map$6(runHead$1(self$1.channel), map$12(getUnsafe$1(0)));
|
|
30265
|
+
/**
|
|
30250
30266
|
* Consumes all elements of the stream, passing them to the specified
|
|
30251
30267
|
* callback.
|
|
30252
30268
|
*
|
|
@@ -46145,15 +46161,15 @@ const black = `${ESC}30m`;
|
|
|
46145
46161
|
/** @internal */
|
|
46146
46162
|
const red = `${ESC}31m`;
|
|
46147
46163
|
/** @internal */
|
|
46148
|
-
const green = `${ESC}32m`;
|
|
46164
|
+
const green$1 = `${ESC}32m`;
|
|
46149
46165
|
/** @internal */
|
|
46150
|
-
const yellow = `${ESC}33m`;
|
|
46166
|
+
const yellow$1 = `${ESC}33m`;
|
|
46151
46167
|
/** @internal */
|
|
46152
46168
|
const blue = `${ESC}34m`;
|
|
46153
46169
|
/** @internal */
|
|
46154
46170
|
const magenta = `${ESC}35m`;
|
|
46155
46171
|
/** @internal */
|
|
46156
|
-
const cyan = `${ESC}36m`;
|
|
46172
|
+
const cyan$1 = `${ESC}36m`;
|
|
46157
46173
|
/** @internal */
|
|
46158
46174
|
const white = `${ESC}37m`;
|
|
46159
46175
|
/** @internal */
|
|
@@ -46543,12 +46559,12 @@ const renderAutoCompleteNextFrame = /* @__PURE__ */ fnUntraced(function* (state,
|
|
|
46543
46559
|
const renderSelectSubmission = /* @__PURE__ */ fnUntraced(function* (state, options) {
|
|
46544
46560
|
const figures = yield* platformFigures;
|
|
46545
46561
|
const selected = options.choices[state].title;
|
|
46546
|
-
return renderSelectOutput(annotate(figures.tick, green), annotate(figures.ellipsis, blackBright), options) + " " + annotate(selected, white) + "\n";
|
|
46562
|
+
return renderSelectOutput(annotate(figures.tick, green$1), annotate(figures.ellipsis, blackBright), options) + " " + annotate(selected, white) + "\n";
|
|
46547
46563
|
});
|
|
46548
46564
|
const renderAutoCompleteSubmission = /* @__PURE__ */ fnUntraced(function* (state, options) {
|
|
46549
46565
|
const figures = yield* platformFigures;
|
|
46550
46566
|
const selected = options.choices[state.index].title;
|
|
46551
|
-
return renderAutoCompleteOutput(state, annotate(figures.tick, green), annotate(figures.ellipsis, blackBright), options) + " " + annotate(selected, white) + "\n";
|
|
46567
|
+
return renderAutoCompleteOutput(state, annotate(figures.tick, green$1), annotate(figures.ellipsis, blackBright), options) + " " + annotate(selected, white) + "\n";
|
|
46552
46568
|
});
|
|
46553
46569
|
const processSelectCursorUp = (state, choices) => {
|
|
46554
46570
|
if (state === 0) return succeed$1(Action.NextFrame({ state: choices.length - 1 }));
|
|
@@ -46701,7 +46717,7 @@ const renderTextNextFrame = /* @__PURE__ */ fnUntraced(function* (state, options
|
|
|
46701
46717
|
});
|
|
46702
46718
|
const renderTextSubmission = /* @__PURE__ */ fnUntraced(function* (state, options) {
|
|
46703
46719
|
const figures = yield* platformFigures;
|
|
46704
|
-
return renderTextOutput(state, annotate(figures.tick, green), annotate(figures.ellipsis, blackBright), options, true) + "\n";
|
|
46720
|
+
return renderTextOutput(state, annotate(figures.tick, green$1), annotate(figures.ellipsis, blackBright), options, true) + "\n";
|
|
46705
46721
|
});
|
|
46706
46722
|
const processTextBackspace = (state) => {
|
|
46707
46723
|
if (state.cursor <= 0) return succeed$1(Action.Beep());
|
|
@@ -62208,7 +62224,7 @@ const symlink = /* @__PURE__ */ (() => {
|
|
|
62208
62224
|
const nodeSymlink = /* @__PURE__ */ effectify(NFS.symlink, /* @__PURE__ */ handleErrnoException("FileSystem", "symlink"), /* @__PURE__ */ handleBadArgument("symlink"));
|
|
62209
62225
|
return (target, path$2) => nodeSymlink(target, path$2);
|
|
62210
62226
|
})();
|
|
62211
|
-
const truncate = /* @__PURE__ */ (() => {
|
|
62227
|
+
const truncate$1 = /* @__PURE__ */ (() => {
|
|
62212
62228
|
const nodeTruncate = /* @__PURE__ */ effectify(NFS.truncate, /* @__PURE__ */ handleErrnoException("FileSystem", "truncate"), /* @__PURE__ */ handleBadArgument("truncate"));
|
|
62213
62229
|
return (path$2, length) => nodeTruncate(path$2, length !== void 0 ? Number(length) : void 0);
|
|
62214
62230
|
})();
|
|
@@ -62296,7 +62312,7 @@ const makeFileSystem = /* @__PURE__ */ map$6(/* @__PURE__ */ serviceOption(Watch
|
|
|
62296
62312
|
rename,
|
|
62297
62313
|
stat,
|
|
62298
62314
|
symlink,
|
|
62299
|
-
truncate,
|
|
62315
|
+
truncate: truncate$1,
|
|
62300
62316
|
utimes,
|
|
62301
62317
|
watch(path$2) {
|
|
62302
62318
|
return watch(getOrUndefined(backend), path$2);
|
|
@@ -63229,14 +63245,6 @@ const runMain = runMain$1;
|
|
|
63229
63245
|
//#region src/Kvs.ts
|
|
63230
63246
|
const layerKvs = layerFileSystem(".lalph/config");
|
|
63231
63247
|
|
|
63232
|
-
//#endregion
|
|
63233
|
-
//#region src/shared/stream.ts
|
|
63234
|
-
const streamFilterJson = (schema$1) => {
|
|
63235
|
-
const fromString = fromJsonString(schema$1);
|
|
63236
|
-
const decode$2 = decodeEffect(fromString);
|
|
63237
|
-
return flow(splitLines, filterMapEffect((line) => decode$2(line).pipe(catch_$1(() => succeed$1(failVoid)))));
|
|
63238
|
-
};
|
|
63239
|
-
|
|
63240
63248
|
//#endregion
|
|
63241
63249
|
//#region src/shared/ansi-colors.ts
|
|
63242
63250
|
const ansiColors = {
|
|
@@ -63247,45 +63255,148 @@ const ansiColors = {
|
|
|
63247
63255
|
yellow: "\x1B[33m"
|
|
63248
63256
|
};
|
|
63249
63257
|
|
|
63258
|
+
//#endregion
|
|
63259
|
+
//#region src/shared/stream.ts
|
|
63260
|
+
const streamFilterJson = (schema$1) => {
|
|
63261
|
+
const fromString = fromJsonString(schema$1);
|
|
63262
|
+
const decode$2 = decodeEffect(fromString);
|
|
63263
|
+
return flow(splitLines, filterMapEffect((line) => decode$2(line).pipe(catch_$1(() => succeed$1(failVoid)))));
|
|
63264
|
+
};
|
|
63265
|
+
|
|
63250
63266
|
//#endregion
|
|
63251
63267
|
//#region src/CliAgent/claude.ts
|
|
63252
63268
|
const claudeOutputTransformer = (stream$3) => stream$3.pipe(streamFilterJson(StreamJsonMessage), map$4((m) => m.format()));
|
|
63253
63269
|
const ContentBlock = Struct({
|
|
63254
63270
|
type: String$1,
|
|
63255
63271
|
text: optional$2(String$1),
|
|
63256
|
-
name: optional$2(String$1)
|
|
63272
|
+
name: optional$2(String$1),
|
|
63273
|
+
input: optional$2(Unknown),
|
|
63274
|
+
content: optional$2(String$1),
|
|
63275
|
+
is_error: optional$2(Boolean$2)
|
|
63276
|
+
});
|
|
63277
|
+
const ToolUseResult = Struct({
|
|
63278
|
+
stdout: optional$2(String$1),
|
|
63279
|
+
stderr: optional$2(String$1),
|
|
63280
|
+
interrupted: optional$2(Boolean$2),
|
|
63281
|
+
isImage: optional$2(Boolean$2)
|
|
63257
63282
|
});
|
|
63258
63283
|
var StreamJsonMessage = class extends Class("claude/StreamJsonMessage")({
|
|
63259
63284
|
type: String$1,
|
|
63260
63285
|
subtype: optional$2(String$1),
|
|
63261
63286
|
message: optional$2(Struct({ content: optional$2(Array$1(ContentBlock)) })),
|
|
63287
|
+
tool_use_result: optional$2(ToolUseResult),
|
|
63262
63288
|
duration_ms: optional$2(Number$1),
|
|
63263
63289
|
total_cost_usd: optional$2(Number$1)
|
|
63264
63290
|
}) {
|
|
63265
63291
|
format() {
|
|
63266
63292
|
switch (this.type) {
|
|
63267
|
-
case "system": return this.subtype === "init" ?
|
|
63293
|
+
case "system": return this.subtype === "init" ? dim("[Session started]") + "\n" : "";
|
|
63268
63294
|
case "assistant": return formatAssistantMessage(this);
|
|
63295
|
+
case "user": return formatToolResult(this);
|
|
63269
63296
|
case "result": return formatResult(this);
|
|
63270
63297
|
default: return "";
|
|
63271
63298
|
}
|
|
63272
63299
|
}
|
|
63273
63300
|
};
|
|
63301
|
+
const BashInput = Struct({ command: optional$2(String$1) });
|
|
63302
|
+
const FileInput = Struct({ file_path: optional$2(String$1) });
|
|
63303
|
+
const PatternInput = Struct({ pattern: optional$2(String$1) });
|
|
63304
|
+
const QuestionOption = Struct({
|
|
63305
|
+
label: String$1,
|
|
63306
|
+
description: optional$2(String$1)
|
|
63307
|
+
});
|
|
63308
|
+
const Question = Struct({
|
|
63309
|
+
question: String$1,
|
|
63310
|
+
header: optional$2(String$1),
|
|
63311
|
+
options: optional$2(Array$1(QuestionOption))
|
|
63312
|
+
});
|
|
63313
|
+
const AskUserQuestionInput = Struct({ questions: optional$2(Array$1(Question)) });
|
|
63314
|
+
const McpInputFields = [
|
|
63315
|
+
"query",
|
|
63316
|
+
"documentId",
|
|
63317
|
+
"page",
|
|
63318
|
+
"pattern",
|
|
63319
|
+
"relative_path",
|
|
63320
|
+
"name_path",
|
|
63321
|
+
"file_path",
|
|
63322
|
+
"root_path"
|
|
63323
|
+
];
|
|
63324
|
+
const truncate = (s, max$3) => s.length > max$3 ? s.slice(0, max$3) + "..." : s;
|
|
63325
|
+
const dim = (s) => ansiColors.dim + s + ansiColors.reset;
|
|
63326
|
+
const cyan = (s) => ansiColors.cyan + s + ansiColors.reset;
|
|
63327
|
+
const yellow = (s) => ansiColors.yellow + s + ansiColors.reset;
|
|
63328
|
+
const green = (s) => ansiColors.green + s + ansiColors.reset;
|
|
63274
63329
|
const formatToolName = (name) => name.replace("mcp__", "").replace(/__/g, ":");
|
|
63330
|
+
const withDetail = (display, detail) => display + getOrElse(detail, () => "");
|
|
63331
|
+
const formatBashInput = (input) => pipe(decodeUnknownOption(BashInput)(input), flatMap$3((data) => fromNullishOr$2(data.command)), filter$6((cmd) => cmd.length > 0), map$12((cmd) => dim("$ " + truncate(cmd, 100)) + "\n"));
|
|
63332
|
+
const formatFileInput = (input) => pipe(decodeUnknownOption(FileInput)(input), flatMap$3((data) => fromNullishOr$2(data.file_path)), filter$6((path$2) => path$2.length > 0), map$12((path$2) => dim(path$2) + "\n"));
|
|
63333
|
+
const formatPatternInput = (input) => pipe(decodeUnknownOption(PatternInput)(input), flatMap$3((data) => fromNullishOr$2(data.pattern)), filter$6((pattern) => pattern.length > 0), map$12((pattern) => dim(pattern) + "\n"));
|
|
63334
|
+
const formatMcpInput = (input) => {
|
|
63335
|
+
if (typeof input !== "object" || input === null) return none$3();
|
|
63336
|
+
const data = input;
|
|
63337
|
+
const parts$1 = McpInputFields.flatMap((field) => match$7(fromNullishOr$2(data[field]), {
|
|
63338
|
+
onNone: () => [],
|
|
63339
|
+
onSome: (value) => [`${field}=${truncate(String(value), 50)}`]
|
|
63340
|
+
}));
|
|
63341
|
+
return parts$1.length > 0 ? some(dim(parts$1.join(" ")) + "\n") : none$3();
|
|
63342
|
+
};
|
|
63343
|
+
const formatGenericInput = (input) => pipe(fromNullishOr$2(input), map$12((v) => dim(truncate(JSON.stringify(v), 100)) + "\n"));
|
|
63344
|
+
const formatUserQuestion = (input) => pipe(decodeUnknownOption(AskUserQuestionInput)(input), flatMap$3((data) => fromNullishOr$2(data.questions)), map$12((questions) => questions.map((q) => {
|
|
63345
|
+
let result$2 = "\n" + yellow("⚠ WAITING FOR INPUT") + "\n";
|
|
63346
|
+
result$2 += cyan((q.header ? `[${q.header}] ` : "") + q.question) + "\n";
|
|
63347
|
+
if (q.options) result$2 += q.options.map((opt, i) => ` ${i + 1}. ${opt.label}${opt.description ? dim(` - ${opt.description}`) : ""}`).join("\n") + "\n";
|
|
63348
|
+
return result$2;
|
|
63349
|
+
}).join("\n")), getOrElse(() => ""));
|
|
63350
|
+
const formatToolInput = (name, input) => {
|
|
63351
|
+
const display = "\n" + cyan("▶ " + formatToolName(name)) + "\n";
|
|
63352
|
+
if (name === "Bash") return withDetail(display, formatBashInput(input));
|
|
63353
|
+
if (name === "AskUserQuestion") return display + formatUserQuestion(input);
|
|
63354
|
+
if (name === "Read" || name === "Write" || name === "Edit") return withDetail(display, formatFileInput(input));
|
|
63355
|
+
if (name === "Grep" || name === "Glob") return withDetail(display, formatPatternInput(input));
|
|
63356
|
+
if (name.startsWith("mcp__")) return withDetail(display, formatMcpInput(input));
|
|
63357
|
+
return withDetail(display, formatGenericInput(input));
|
|
63358
|
+
};
|
|
63275
63359
|
const formatAssistantMessage = (msg) => {
|
|
63276
63360
|
const content = msg.message?.content;
|
|
63277
63361
|
if (!content) return "";
|
|
63278
63362
|
return content.map((block) => {
|
|
63279
63363
|
if (block.type === "text" && block.text) return block.text;
|
|
63280
|
-
|
|
63364
|
+
if (block.type === "tool_use" && block.name) return formatToolInput(block.name, block.input);
|
|
63281
63365
|
return "";
|
|
63282
63366
|
}).join("");
|
|
63283
63367
|
};
|
|
63368
|
+
const formatLongOutput = (text$3) => {
|
|
63369
|
+
const lines$1 = text$3.trim().split("\n");
|
|
63370
|
+
if (lines$1.length > 8) return dim([
|
|
63371
|
+
...lines$1.slice(0, 4),
|
|
63372
|
+
`... (${lines$1.length - 7} more lines)`,
|
|
63373
|
+
...lines$1.slice(-3)
|
|
63374
|
+
].join("\n")) + "\n";
|
|
63375
|
+
return text$3.length > 500 ? dim(truncate(text$3, 500)) + "\n" : dim(text$3) + "\n";
|
|
63376
|
+
};
|
|
63377
|
+
const formatToolResult = (msg) => {
|
|
63378
|
+
let output = "";
|
|
63379
|
+
const result$2 = msg.tool_use_result;
|
|
63380
|
+
if (result$2) {
|
|
63381
|
+
if (result$2.stderr?.trim()) output += yellow("stderr: ") + truncate(result$2.stderr.trim(), 500) + "\n";
|
|
63382
|
+
if (result$2.interrupted) output += yellow("[interrupted]") + "\n";
|
|
63383
|
+
if (result$2.stdout?.trim()) output += formatLongOutput(result$2.stdout.trim());
|
|
63384
|
+
}
|
|
63385
|
+
const content = msg.message?.content;
|
|
63386
|
+
if (content) {
|
|
63387
|
+
for (const block of content) if (block.type === "tool_result") {
|
|
63388
|
+
if (block.is_error) output += yellow("✗ Tool error") + "\n";
|
|
63389
|
+
if (block.content) output += formatLongOutput(block.content);
|
|
63390
|
+
}
|
|
63391
|
+
}
|
|
63392
|
+
return output;
|
|
63393
|
+
};
|
|
63284
63394
|
const formatResult = (msg) => {
|
|
63285
63395
|
if (msg.subtype === "success") {
|
|
63286
63396
|
const info = [msg.duration_ms ? (msg.duration_ms / 1e3).toFixed(1) + "s" : "", msg.total_cost_usd ? "$" + msg.total_cost_usd.toFixed(4) : ""].filter(Boolean).join(" | ");
|
|
63287
|
-
return "\n" +
|
|
63288
|
-
}
|
|
63397
|
+
return "\n" + green("✓ Done") + " " + dim(info) + "\n";
|
|
63398
|
+
}
|
|
63399
|
+
if (msg.subtype === "error") return "\n" + yellow("✗ Error") + "\n";
|
|
63289
63400
|
return "";
|
|
63290
63401
|
};
|
|
63291
63402
|
|
|
@@ -63326,7 +63437,7 @@ const claude = new CliAgent({
|
|
|
63326
63437
|
stdout: outputMode,
|
|
63327
63438
|
stderr: outputMode,
|
|
63328
63439
|
stdin: "inherit"
|
|
63329
|
-
})`claude --dangerously-skip-permissions --output-format stream-json --verbose -p ${`@${prdFilePath}
|
|
63440
|
+
})`claude --dangerously-skip-permissions --output-format stream-json --verbose --disallowed-tools AskUserQuestion -p ${`@${prdFilePath}
|
|
63330
63441
|
|
|
63331
63442
|
${prompt}`}`,
|
|
63332
63443
|
outputTransformer: claudeOutputTransformer,
|
|
@@ -145054,6 +145165,14 @@ const getOrSelectCliAgent = gen(function* () {
|
|
|
145054
145165
|
});
|
|
145055
145166
|
const commandAgent = make$28("agent").pipe(withDescription("Select the CLI agent to use"), withHandler(() => selectCliAgent));
|
|
145056
145167
|
|
|
145168
|
+
//#endregion
|
|
145169
|
+
//#region src/shared/fs.ts
|
|
145170
|
+
const makeWaitForFile = gen(function* () {
|
|
145171
|
+
const fs = yield* FileSystem;
|
|
145172
|
+
const pathService = yield* Path$1;
|
|
145173
|
+
return (directory$2, name) => pipe(fs.watch(directory$2), filter((e) => pathService.basename(e.path) === name), runHead);
|
|
145174
|
+
});
|
|
145175
|
+
|
|
145057
145176
|
//#endregion
|
|
145058
145177
|
//#region src/Agents/instructor.ts
|
|
145059
145178
|
const agentInstructor = fnUntraced(function* (options) {
|
|
@@ -145061,6 +145180,7 @@ const agentInstructor = fnUntraced(function* (options) {
|
|
|
145061
145180
|
const pathService = yield* Path$1;
|
|
145062
145181
|
const worktree = yield* Worktree;
|
|
145063
145182
|
const promptGen = yield* PromptGen;
|
|
145183
|
+
const waitForFile = yield* makeWaitForFile;
|
|
145064
145184
|
yield* pipe(options.cliAgent.command({
|
|
145065
145185
|
outputMode: "pipe",
|
|
145066
145186
|
prompt: promptGen.promptInstructions({
|
|
@@ -145073,7 +145193,7 @@ const agentInstructor = fnUntraced(function* (options) {
|
|
|
145073
145193
|
}), setCwd(worktree.directory), options.commandPrefix, worktree.execWithStallTimeout({
|
|
145074
145194
|
cliAgent: options.cliAgent,
|
|
145075
145195
|
stallTimeout: options.stallTimeout
|
|
145076
|
-
}));
|
|
145196
|
+
}), raceFirst(waitForFile(pathService.join(worktree.directory, ".lalph"), "instructions.md")));
|
|
145077
145197
|
return yield* fs.readFileString(pathService.join(worktree.directory, ".lalph", "instructions.md"));
|
|
145078
145198
|
});
|
|
145079
145199
|
|
|
@@ -145104,13 +145224,14 @@ const agentChooser = fnUntraced(function* (options) {
|
|
|
145104
145224
|
const worktree = yield* Worktree;
|
|
145105
145225
|
const promptGen = yield* PromptGen;
|
|
145106
145226
|
const prd = yield* Prd;
|
|
145227
|
+
const taskJsonCreated = (yield* makeWaitForFile)(pathService.join(worktree.directory, ".lalph"), "task.json");
|
|
145107
145228
|
yield* pipe(options.cliAgent.resolveCommandChoose({
|
|
145108
145229
|
prompt: promptGen.promptChoose,
|
|
145109
145230
|
prdFilePath: pathService.join(".lalph", "prd.yml")
|
|
145110
145231
|
}), setCwd(worktree.directory), options.commandPrefix, exitCode, timeoutOrElse({
|
|
145111
145232
|
duration: options.stallTimeout,
|
|
145112
145233
|
onTimeout: () => fail$4(new RunnerStalled())
|
|
145113
|
-
}));
|
|
145234
|
+
}), raceFirst(taskJsonCreated));
|
|
145114
145235
|
const taskJson = yield* fs.readFileString(pathService.join(worktree.directory, ".lalph", "task.json"));
|
|
145115
145236
|
return yield* decodeEffect(ChosenTask)(taskJson).pipe(mapError$2(() => new ChosenTaskNotFound()), flatMap(fnUntraced(function* (task) {
|
|
145116
145237
|
const prdTask = yield* prd.findById(task.id);
|
|
@@ -145169,71 +145290,6 @@ const agentTimeout = fnUntraced(function* (options) {
|
|
|
145169
145290
|
|
|
145170
145291
|
//#endregion
|
|
145171
145292
|
//#region src/commands/root.ts
|
|
145172
|
-
const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
|
|
145173
|
-
const concurrency = integer("concurrency").pipe(withDescription$1("Number of concurrent agents, defaults to 1"), withAlias("c"), withDefault(1));
|
|
145174
|
-
const targetBranch = string$1("target-branch").pipe(withDescription$1("Target branch for PRs. Defaults to current branch. Env variable: LALPH_TARGET_BRANCH"), withAlias("b"), withFallbackConfig(string$4("LALPH_TARGET_BRANCH")), withDefault(make$21`git branch --show-current`.pipe(string, orDie$2, flatMap((output) => {
|
|
145175
|
-
const branch = output.trim();
|
|
145176
|
-
return branch === "" ? fail$4(new MissingOption({ option: "--target-branch" })) : succeed$1(branch);
|
|
145177
|
-
}))), optional);
|
|
145178
|
-
const maxIterationMinutes = integer("max-minutes").pipe(withDescription$1("Maximum number of minutes to allow an iteration to run. Defaults to 90 minutes. Env variable: LALPH_MAX_MINUTES"), withFallbackConfig(int("LALPH_MAX_MINUTES")), withDefault(90));
|
|
145179
|
-
const stallMinutes = integer("stall-minutes").pipe(withDescription$1("If no activity occurs for this many minutes, the iteration will be stopped. Defaults to 5 minutes. Env variable: LALPH_STALL_MINUTES"), withFallbackConfig(int("LALPH_STALL_MINUTES")), withDefault(5));
|
|
145180
|
-
const specsDirectory = directory("specs").pipe(withDescription$1("Directory to store plan specifications. Env variable: LALPH_SPECS"), withAlias("s"), withFallbackConfig(string$4("LALPH_SPECS")), withDefault(".specs"));
|
|
145181
|
-
const verbose = boolean("verbose").pipe(withDescription$1("Enable verbose logging"), withAlias("v"));
|
|
145182
|
-
const reset = boolean("reset").pipe(withDescription$1("Reset the current issue source before running"), withAlias("r"));
|
|
145183
|
-
const commandRoot = make$28("lalph", {
|
|
145184
|
-
iterations,
|
|
145185
|
-
concurrency,
|
|
145186
|
-
targetBranch,
|
|
145187
|
-
maxIterationMinutes,
|
|
145188
|
-
stallMinutes,
|
|
145189
|
-
reset,
|
|
145190
|
-
specsDirectory,
|
|
145191
|
-
verbose
|
|
145192
|
-
}).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1, concurrency: concurrency$1, targetBranch: targetBranch$1, maxIterationMinutes: maxIterationMinutes$1, stallMinutes: stallMinutes$1, specsDirectory: specsDirectory$1 }) {
|
|
145193
|
-
const source = yield* build(CurrentIssueSource.layer);
|
|
145194
|
-
const commandPrefix = yield* getCommandPrefix;
|
|
145195
|
-
yield* getOrSelectCliAgent;
|
|
145196
|
-
const isFinite$3 = Number.isFinite(iterations$1);
|
|
145197
|
-
const iterationsDisplay = isFinite$3 ? iterations$1 : "unlimited";
|
|
145198
|
-
const runConcurrency = Math.max(1, concurrency$1);
|
|
145199
|
-
const semaphore = makeSemaphoreUnsafe(runConcurrency);
|
|
145200
|
-
const fibers = yield* make$25();
|
|
145201
|
-
yield* resetInProgress.pipe(provide$1(source), withSpan("Main.resetInProgress"));
|
|
145202
|
-
yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
|
|
145203
|
-
if (isSome(targetBranch$1)) yield* log$1(`Using target branch: ${targetBranch$1.value}`);
|
|
145204
|
-
let iteration = 0;
|
|
145205
|
-
let quit = false;
|
|
145206
|
-
while (true) {
|
|
145207
|
-
yield* semaphore.take(1);
|
|
145208
|
-
if (quit || isFinite$3 && iteration >= iterations$1) break;
|
|
145209
|
-
const currentIteration = iteration;
|
|
145210
|
-
const startedDeferred = yield* make$49();
|
|
145211
|
-
yield* checkForWork.pipe(andThen(run({
|
|
145212
|
-
startedDeferred,
|
|
145213
|
-
targetBranch: targetBranch$1,
|
|
145214
|
-
specsDirectory: specsDirectory$1,
|
|
145215
|
-
stallTimeout: minutes(stallMinutes$1),
|
|
145216
|
-
runTimeout: minutes(maxIterationMinutes$1),
|
|
145217
|
-
commandPrefix
|
|
145218
|
-
})), catchFilter((e) => e._tag === "NoMoreWork" || e._tag === "QuitError" ? fail$8(e) : e, (e) => logWarning(fail$5(e))), catchTags({
|
|
145219
|
-
NoMoreWork(_) {
|
|
145220
|
-
if (isFinite$3) {
|
|
145221
|
-
iterations$1 = currentIteration;
|
|
145222
|
-
return log$1(`No more work to process, ending after ${currentIteration} iteration(s).`);
|
|
145223
|
-
}
|
|
145224
|
-
const log$2 = size$1(fibers) <= 1 ? log$1("No more work to process, waiting 30 seconds...") : void_$1;
|
|
145225
|
-
return andThen(log$2, sleep(seconds(30)));
|
|
145226
|
-
},
|
|
145227
|
-
QuitError(_) {
|
|
145228
|
-
quit = true;
|
|
145229
|
-
return void_$1;
|
|
145230
|
-
}
|
|
145231
|
-
}), annotateLogs({ iteration: currentIteration }), ensuring(semaphore.release(1)), ensuring(completeWith(startedDeferred, void_$1)), provide$1(source), run$1(fibers));
|
|
145232
|
-
yield* _await(startedDeferred);
|
|
145233
|
-
iteration++;
|
|
145234
|
-
}
|
|
145235
|
-
yield* awaitEmpty(fibers);
|
|
145236
|
-
}, scoped$1)));
|
|
145237
145293
|
const run = fnUntraced(function* (options) {
|
|
145238
145294
|
const fs = yield* FileSystem;
|
|
145239
145295
|
const pathService = yield* Path$1;
|
|
@@ -145294,7 +145350,7 @@ const run = fnUntraced(function* (options) {
|
|
|
145294
145350
|
instructions
|
|
145295
145351
|
});
|
|
145296
145352
|
yield* log$1(`Agent exited with code: ${exitCode$1}`);
|
|
145297
|
-
yield* agentReviewer({
|
|
145353
|
+
if (options.review) yield* agentReviewer({
|
|
145298
145354
|
specsDirectory: options.specsDirectory,
|
|
145299
145355
|
stallTimeout: options.stallTimeout,
|
|
145300
145356
|
cliAgent,
|
|
@@ -145324,6 +145380,74 @@ const run = fnUntraced(function* (options) {
|
|
|
145324
145380
|
if ((yield* prd.findById(taskId))?.autoMerge) yield* autoMerge;
|
|
145325
145381
|
else yield* prd.maybeRevertIssue({ issueId: taskId });
|
|
145326
145382
|
}, scoped$1, provide$1([PromptGen.layer, Prd.layer]));
|
|
145383
|
+
const iterations = integer("iterations").pipe(withDescription$1("Number of iterations to run, defaults to unlimited"), withAlias("i"), withDefault(Number.POSITIVE_INFINITY));
|
|
145384
|
+
const concurrency = integer("concurrency").pipe(withDescription$1("Number of concurrent agents, defaults to 1"), withAlias("c"), withDefault(1));
|
|
145385
|
+
const targetBranch = string$1("target-branch").pipe(withDescription$1("Target branch for PRs. Defaults to current branch. Env variable: LALPH_TARGET_BRANCH"), withAlias("b"), withFallbackConfig(string$4("LALPH_TARGET_BRANCH")), withDefault(make$21`git branch --show-current`.pipe(string, orDie$2, flatMap((output) => {
|
|
145386
|
+
const branch = output.trim();
|
|
145387
|
+
return branch === "" ? fail$4(new MissingOption({ option: "--target-branch" })) : succeed$1(branch);
|
|
145388
|
+
}))), optional);
|
|
145389
|
+
const maxIterationMinutes = integer("max-minutes").pipe(withDescription$1("Maximum number of minutes to allow an iteration to run. Defaults to 90 minutes. Env variable: LALPH_MAX_MINUTES"), withFallbackConfig(int("LALPH_MAX_MINUTES")), withDefault(90));
|
|
145390
|
+
const stallMinutes = integer("stall-minutes").pipe(withDescription$1("If no activity occurs for this many minutes, the iteration will be stopped. Defaults to 5 minutes. Env variable: LALPH_STALL_MINUTES"), withFallbackConfig(int("LALPH_STALL_MINUTES")), withDefault(5));
|
|
145391
|
+
const specsDirectory = directory("specs").pipe(withDescription$1("Directory to store plan specifications. Env variable: LALPH_SPECS"), withAlias("s"), withFallbackConfig(string$4("LALPH_SPECS")), withDefault(".specs"));
|
|
145392
|
+
const verbose = boolean("verbose").pipe(withDescription$1("Enable verbose logging"), withAlias("v"));
|
|
145393
|
+
const review = boolean("review").pipe(withDescription$1("Enabled the AI peer-review step"));
|
|
145394
|
+
const reset = boolean("reset").pipe(withDescription$1("Reset the current issue source before running"), withAlias("r"));
|
|
145395
|
+
const commandRoot = make$28("lalph", {
|
|
145396
|
+
iterations,
|
|
145397
|
+
concurrency,
|
|
145398
|
+
targetBranch,
|
|
145399
|
+
maxIterationMinutes,
|
|
145400
|
+
stallMinutes,
|
|
145401
|
+
reset,
|
|
145402
|
+
review,
|
|
145403
|
+
specsDirectory,
|
|
145404
|
+
verbose
|
|
145405
|
+
}).pipe(withHandler(fnUntraced(function* ({ iterations: iterations$1, concurrency: concurrency$1, targetBranch: targetBranch$1, maxIterationMinutes: maxIterationMinutes$1, stallMinutes: stallMinutes$1, specsDirectory: specsDirectory$1, review: review$1 }) {
|
|
145406
|
+
const source = yield* build(CurrentIssueSource.layer);
|
|
145407
|
+
const commandPrefix = yield* getCommandPrefix;
|
|
145408
|
+
yield* getOrSelectCliAgent;
|
|
145409
|
+
const isFinite$3 = Number.isFinite(iterations$1);
|
|
145410
|
+
const iterationsDisplay = isFinite$3 ? iterations$1 : "unlimited";
|
|
145411
|
+
const runConcurrency = Math.max(1, concurrency$1);
|
|
145412
|
+
const semaphore = makeSemaphoreUnsafe(runConcurrency);
|
|
145413
|
+
const fibers = yield* make$25();
|
|
145414
|
+
yield* resetInProgress.pipe(provide$1(source), withSpan("Main.resetInProgress"));
|
|
145415
|
+
yield* log$1(`Executing ${iterationsDisplay} iteration(s) with concurrency ${runConcurrency}`);
|
|
145416
|
+
if (isSome(targetBranch$1)) yield* log$1(`Using target branch: ${targetBranch$1.value}`);
|
|
145417
|
+
let iteration = 0;
|
|
145418
|
+
let quit = false;
|
|
145419
|
+
while (true) {
|
|
145420
|
+
yield* semaphore.take(1);
|
|
145421
|
+
if (quit || isFinite$3 && iteration >= iterations$1) break;
|
|
145422
|
+
const currentIteration = iteration;
|
|
145423
|
+
const startedDeferred = yield* make$49();
|
|
145424
|
+
yield* checkForWork.pipe(andThen(run({
|
|
145425
|
+
startedDeferred,
|
|
145426
|
+
targetBranch: targetBranch$1,
|
|
145427
|
+
specsDirectory: specsDirectory$1,
|
|
145428
|
+
stallTimeout: minutes(stallMinutes$1),
|
|
145429
|
+
runTimeout: minutes(maxIterationMinutes$1),
|
|
145430
|
+
commandPrefix,
|
|
145431
|
+
review: review$1
|
|
145432
|
+
})), catchFilter((e) => e._tag === "NoMoreWork" || e._tag === "QuitError" ? fail$8(e) : e, (e) => logWarning(fail$5(e))), catchTags({
|
|
145433
|
+
NoMoreWork(_) {
|
|
145434
|
+
if (isFinite$3) {
|
|
145435
|
+
iterations$1 = currentIteration;
|
|
145436
|
+
return log$1(`No more work to process, ending after ${currentIteration} iteration(s).`);
|
|
145437
|
+
}
|
|
145438
|
+
const log$2 = size$1(fibers) <= 1 ? log$1("No more work to process, waiting 30 seconds...") : void_$1;
|
|
145439
|
+
return andThen(log$2, sleep(seconds(30)));
|
|
145440
|
+
},
|
|
145441
|
+
QuitError(_) {
|
|
145442
|
+
quit = true;
|
|
145443
|
+
return void_$1;
|
|
145444
|
+
}
|
|
145445
|
+
}), annotateLogs({ iteration: currentIteration }), ensuring(semaphore.release(1)), ensuring(completeWith(startedDeferred, void_$1)), provide$1(source), run$1(fibers));
|
|
145446
|
+
yield* _await(startedDeferred);
|
|
145447
|
+
iteration++;
|
|
145448
|
+
}
|
|
145449
|
+
yield* awaitEmpty(fibers);
|
|
145450
|
+
}, scoped$1)));
|
|
145327
145451
|
|
|
145328
145452
|
//#endregion
|
|
145329
145453
|
//#region src/commands/plan.ts
|
|
@@ -145455,7 +145579,7 @@ const commandSource = make$28("source").pipe(withDescription("Select the issue s
|
|
|
145455
145579
|
|
|
145456
145580
|
//#endregion
|
|
145457
145581
|
//#region package.json
|
|
145458
|
-
var version = "0.1.
|
|
145582
|
+
var version = "0.1.98";
|
|
145459
145583
|
|
|
145460
145584
|
//#endregion
|
|
145461
145585
|
//#region src/Tracing.ts
|
package/package.json
CHANGED
package/src/Agents/chooser.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ChildProcess } from "effect/unstable/process"
|
|
|
5
5
|
import { Worktree } from "../Worktree.ts"
|
|
6
6
|
import { RunnerStalled } from "../domain/Errors.ts"
|
|
7
7
|
import type { CliAgent } from "../domain/CliAgent.ts"
|
|
8
|
+
import { makeWaitForFile } from "../shared/fs.ts"
|
|
8
9
|
|
|
9
10
|
export const agentChooser = Effect.fnUntraced(function* (options: {
|
|
10
11
|
readonly stallTimeout: Duration.Duration
|
|
@@ -18,6 +19,12 @@ export const agentChooser = Effect.fnUntraced(function* (options: {
|
|
|
18
19
|
const worktree = yield* Worktree
|
|
19
20
|
const promptGen = yield* PromptGen
|
|
20
21
|
const prd = yield* Prd
|
|
22
|
+
const waitForFile = yield* makeWaitForFile
|
|
23
|
+
|
|
24
|
+
const taskJsonCreated = waitForFile(
|
|
25
|
+
pathService.join(worktree.directory, ".lalph"),
|
|
26
|
+
"task.json",
|
|
27
|
+
)
|
|
21
28
|
|
|
22
29
|
yield* pipe(
|
|
23
30
|
options.cliAgent.resolveCommandChoose({
|
|
@@ -31,6 +38,7 @@ export const agentChooser = Effect.fnUntraced(function* (options: {
|
|
|
31
38
|
duration: options.stallTimeout,
|
|
32
39
|
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
33
40
|
}),
|
|
41
|
+
Effect.raceFirst(taskJsonCreated),
|
|
34
42
|
)
|
|
35
43
|
|
|
36
44
|
const taskJson = yield* fs.readFileString(
|
package/src/Agents/instructor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ChildProcess } from "effect/unstable/process"
|
|
|
4
4
|
import { Worktree } from "../Worktree.ts"
|
|
5
5
|
import type { CliAgent } from "../domain/CliAgent.ts"
|
|
6
6
|
import type { PrdIssue } from "../domain/PrdIssue.ts"
|
|
7
|
+
import { makeWaitForFile } from "../shared/fs.ts"
|
|
7
8
|
|
|
8
9
|
export const agentInstructor = Effect.fnUntraced(function* (options: {
|
|
9
10
|
readonly targetBranch: Option.Option<string>
|
|
@@ -20,6 +21,7 @@ export const agentInstructor = Effect.fnUntraced(function* (options: {
|
|
|
20
21
|
const pathService = yield* Path.Path
|
|
21
22
|
const worktree = yield* Worktree
|
|
22
23
|
const promptGen = yield* PromptGen
|
|
24
|
+
const waitForFile = yield* makeWaitForFile
|
|
23
25
|
|
|
24
26
|
yield* pipe(
|
|
25
27
|
options.cliAgent.command({
|
|
@@ -38,6 +40,12 @@ export const agentInstructor = Effect.fnUntraced(function* (options: {
|
|
|
38
40
|
cliAgent: options.cliAgent,
|
|
39
41
|
stallTimeout: options.stallTimeout,
|
|
40
42
|
}),
|
|
43
|
+
Effect.raceFirst(
|
|
44
|
+
waitForFile(
|
|
45
|
+
pathService.join(worktree.directory, ".lalph"),
|
|
46
|
+
"instructions.md",
|
|
47
|
+
),
|
|
48
|
+
),
|
|
41
49
|
)
|
|
42
50
|
return yield* fs.readFileString(
|
|
43
51
|
pathService.join(worktree.directory, ".lalph", "instructions.md"),
|
package/src/CliAgent/claude.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Schema, Stream } from "effect"
|
|
2
1
|
import type { OutputTransformer } from "../domain/CliAgent.ts"
|
|
3
|
-
import {
|
|
2
|
+
import { Option, pipe, Schema, Stream } from "effect"
|
|
4
3
|
import { ansiColors } from "../shared/ansi-colors.ts"
|
|
4
|
+
import { streamFilterJson } from "../shared/stream.ts"
|
|
5
5
|
|
|
6
6
|
export const claudeOutputTransformer: OutputTransformer = (stream) =>
|
|
7
7
|
stream.pipe(
|
|
@@ -9,12 +9,20 @@ export const claudeOutputTransformer: OutputTransformer = (stream) =>
|
|
|
9
9
|
Stream.map((m) => m.format()),
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
// Schema definitions
|
|
13
|
-
|
|
14
12
|
const ContentBlock = Schema.Struct({
|
|
15
13
|
type: Schema.String,
|
|
16
14
|
text: Schema.optional(Schema.String),
|
|
17
15
|
name: Schema.optional(Schema.String),
|
|
16
|
+
input: Schema.optional(Schema.Unknown),
|
|
17
|
+
content: Schema.optional(Schema.String),
|
|
18
|
+
is_error: Schema.optional(Schema.Boolean),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const ToolUseResult = Schema.Struct({
|
|
22
|
+
stdout: Schema.optional(Schema.String),
|
|
23
|
+
stderr: Schema.optional(Schema.String),
|
|
24
|
+
interrupted: Schema.optional(Schema.Boolean),
|
|
25
|
+
isImage: Schema.optional(Schema.Boolean),
|
|
18
26
|
})
|
|
19
27
|
|
|
20
28
|
class StreamJsonMessage extends Schema.Class<StreamJsonMessage>(
|
|
@@ -27,17 +35,18 @@ class StreamJsonMessage extends Schema.Class<StreamJsonMessage>(
|
|
|
27
35
|
content: Schema.optional(Schema.Array(ContentBlock)),
|
|
28
36
|
}),
|
|
29
37
|
),
|
|
38
|
+
tool_use_result: Schema.optional(ToolUseResult),
|
|
30
39
|
duration_ms: Schema.optional(Schema.Number),
|
|
31
40
|
total_cost_usd: Schema.optional(Schema.Number),
|
|
32
41
|
}) {
|
|
33
42
|
format(): string {
|
|
34
43
|
switch (this.type) {
|
|
35
44
|
case "system":
|
|
36
|
-
return this.subtype === "init"
|
|
37
|
-
? ansiColors.dim + "[Session started]" + ansiColors.reset + "\n"
|
|
38
|
-
: ""
|
|
45
|
+
return this.subtype === "init" ? dim("[Session started]") + "\n" : ""
|
|
39
46
|
case "assistant":
|
|
40
47
|
return formatAssistantMessage(this)
|
|
48
|
+
case "user":
|
|
49
|
+
return formatToolResult(this)
|
|
41
50
|
case "result":
|
|
42
51
|
return formatResult(this)
|
|
43
52
|
default:
|
|
@@ -46,32 +55,179 @@ class StreamJsonMessage extends Schema.Class<StreamJsonMessage>(
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
const
|
|
58
|
+
const BashInput = Schema.Struct({ command: Schema.optional(Schema.String) })
|
|
59
|
+
const FileInput = Schema.Struct({ file_path: Schema.optional(Schema.String) })
|
|
60
|
+
const PatternInput = Schema.Struct({ pattern: Schema.optional(Schema.String) })
|
|
61
|
+
const QuestionOption = Schema.Struct({
|
|
62
|
+
label: Schema.String,
|
|
63
|
+
description: Schema.optional(Schema.String),
|
|
64
|
+
})
|
|
65
|
+
const Question = Schema.Struct({
|
|
66
|
+
question: Schema.String,
|
|
67
|
+
header: Schema.optional(Schema.String),
|
|
68
|
+
options: Schema.optional(Schema.Array(QuestionOption)),
|
|
69
|
+
})
|
|
70
|
+
const AskUserQuestionInput = Schema.Struct({
|
|
71
|
+
questions: Schema.optional(Schema.Array(Question)),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const McpInputFields = [
|
|
75
|
+
"query",
|
|
76
|
+
"documentId",
|
|
77
|
+
"page",
|
|
78
|
+
"pattern",
|
|
79
|
+
"relative_path",
|
|
80
|
+
"name_path",
|
|
81
|
+
"file_path",
|
|
82
|
+
"root_path",
|
|
83
|
+
] as const
|
|
84
|
+
|
|
85
|
+
const truncate = (s: string, max: number) =>
|
|
86
|
+
s.length > max ? s.slice(0, max) + "..." : s
|
|
87
|
+
|
|
88
|
+
const dim = (s: string) => ansiColors.dim + s + ansiColors.reset
|
|
89
|
+
const cyan = (s: string) => ansiColors.cyan + s + ansiColors.reset
|
|
90
|
+
const yellow = (s: string) => ansiColors.yellow + s + ansiColors.reset
|
|
91
|
+
const green = (s: string) => ansiColors.green + s + ansiColors.reset
|
|
92
|
+
|
|
93
|
+
const formatToolName = (name: string) =>
|
|
50
94
|
name.replace("mcp__", "").replace(/__/g, ":")
|
|
51
95
|
|
|
96
|
+
const withDetail = (display: string, detail: Option.Option<string>) =>
|
|
97
|
+
display + Option.getOrElse(detail, () => "")
|
|
98
|
+
|
|
99
|
+
const formatBashInput = (input: unknown) =>
|
|
100
|
+
pipe(
|
|
101
|
+
Schema.decodeUnknownOption(BashInput)(input),
|
|
102
|
+
Option.flatMap((data) => Option.fromNullishOr(data.command)),
|
|
103
|
+
Option.filter((cmd) => cmd.length > 0),
|
|
104
|
+
Option.map((cmd) => dim("$ " + truncate(cmd, 100)) + "\n"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const formatFileInput = (input: unknown) =>
|
|
108
|
+
pipe(
|
|
109
|
+
Schema.decodeUnknownOption(FileInput)(input),
|
|
110
|
+
Option.flatMap((data) => Option.fromNullishOr(data.file_path)),
|
|
111
|
+
Option.filter((path) => path.length > 0),
|
|
112
|
+
Option.map((path) => dim(path) + "\n"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const formatPatternInput = (input: unknown) =>
|
|
116
|
+
pipe(
|
|
117
|
+
Schema.decodeUnknownOption(PatternInput)(input),
|
|
118
|
+
Option.flatMap((data) => Option.fromNullishOr(data.pattern)),
|
|
119
|
+
Option.filter((pattern) => pattern.length > 0),
|
|
120
|
+
Option.map((pattern) => dim(pattern) + "\n"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const formatMcpInput = (input: unknown): Option.Option<string> => {
|
|
124
|
+
if (typeof input !== "object" || input === null) return Option.none()
|
|
125
|
+
const data = input as Record<string, unknown>
|
|
126
|
+
const parts = McpInputFields.flatMap((field) =>
|
|
127
|
+
Option.match(Option.fromNullishOr(data[field]), {
|
|
128
|
+
onNone: () => [],
|
|
129
|
+
onSome: (value) => [`${field}=${truncate(String(value), 50)}`],
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
return parts.length > 0
|
|
133
|
+
? Option.some(dim(parts.join(" ")) + "\n")
|
|
134
|
+
: Option.none()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const formatGenericInput = (input: unknown) =>
|
|
138
|
+
pipe(
|
|
139
|
+
Option.fromNullishOr(input),
|
|
140
|
+
Option.map((v) => dim(truncate(JSON.stringify(v), 100)) + "\n"),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
type DecodedQuestion = typeof Question.Encoded
|
|
144
|
+
|
|
145
|
+
const formatUserQuestion = (input: unknown) =>
|
|
146
|
+
pipe(
|
|
147
|
+
Schema.decodeUnknownOption(AskUserQuestionInput)(input),
|
|
148
|
+
Option.flatMap((data) => Option.fromNullishOr(data.questions)),
|
|
149
|
+
Option.map((questions) =>
|
|
150
|
+
questions
|
|
151
|
+
.map((q: DecodedQuestion) => {
|
|
152
|
+
let result = "\n" + yellow("⚠ WAITING FOR INPUT") + "\n"
|
|
153
|
+
result += cyan((q.header ? `[${q.header}] ` : "") + q.question) + "\n"
|
|
154
|
+
if (q.options) {
|
|
155
|
+
result +=
|
|
156
|
+
q.options
|
|
157
|
+
.map(
|
|
158
|
+
(opt, i) =>
|
|
159
|
+
` ${i + 1}. ${opt.label}${opt.description ? dim(` - ${opt.description}`) : ""}`,
|
|
160
|
+
)
|
|
161
|
+
.join("\n") + "\n"
|
|
162
|
+
}
|
|
163
|
+
return result
|
|
164
|
+
})
|
|
165
|
+
.join("\n"),
|
|
166
|
+
),
|
|
167
|
+
Option.getOrElse(() => ""),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const formatToolInput = (name: string, input: unknown): string => {
|
|
171
|
+
const display = "\n" + cyan("▶ " + formatToolName(name)) + "\n"
|
|
172
|
+
|
|
173
|
+
if (name === "Bash") return withDetail(display, formatBashInput(input))
|
|
174
|
+
if (name === "AskUserQuestion") return display + formatUserQuestion(input)
|
|
175
|
+
if (name === "Read" || name === "Write" || name === "Edit")
|
|
176
|
+
return withDetail(display, formatFileInput(input))
|
|
177
|
+
if (name === "Grep" || name === "Glob")
|
|
178
|
+
return withDetail(display, formatPatternInput(input))
|
|
179
|
+
if (name.startsWith("mcp__"))
|
|
180
|
+
return withDetail(display, formatMcpInput(input))
|
|
181
|
+
return withDetail(display, formatGenericInput(input))
|
|
182
|
+
}
|
|
183
|
+
|
|
52
184
|
const formatAssistantMessage = (msg: StreamJsonMessage): string => {
|
|
53
185
|
const content = msg.message?.content
|
|
54
186
|
if (!content) return ""
|
|
55
|
-
|
|
56
187
|
return content
|
|
57
188
|
.map((block) => {
|
|
58
|
-
if (block.type === "text" && block.text)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
"\n" +
|
|
63
|
-
ansiColors.cyan +
|
|
64
|
-
"▶ " +
|
|
65
|
-
formatToolName(block.name) +
|
|
66
|
-
ansiColors.reset +
|
|
67
|
-
"\n"
|
|
68
|
-
)
|
|
69
|
-
}
|
|
189
|
+
if (block.type === "text" && block.text) return block.text
|
|
190
|
+
if (block.type === "tool_use" && block.name)
|
|
191
|
+
return formatToolInput(block.name, block.input)
|
|
70
192
|
return ""
|
|
71
193
|
})
|
|
72
194
|
.join("")
|
|
73
195
|
}
|
|
74
196
|
|
|
197
|
+
const formatLongOutput = (text: string): string => {
|
|
198
|
+
const lines = text.trim().split("\n")
|
|
199
|
+
if (lines.length > 8) {
|
|
200
|
+
const preview = [
|
|
201
|
+
...lines.slice(0, 4),
|
|
202
|
+
`... (${lines.length - 7} more lines)`,
|
|
203
|
+
...lines.slice(-3),
|
|
204
|
+
].join("\n")
|
|
205
|
+
return dim(preview) + "\n"
|
|
206
|
+
}
|
|
207
|
+
return text.length > 500 ? dim(truncate(text, 500)) + "\n" : dim(text) + "\n"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const formatToolResult = (msg: StreamJsonMessage): string => {
|
|
211
|
+
let output = ""
|
|
212
|
+
const result = msg.tool_use_result
|
|
213
|
+
if (result) {
|
|
214
|
+
if (result.stderr?.trim())
|
|
215
|
+
output += yellow("stderr: ") + truncate(result.stderr.trim(), 500) + "\n"
|
|
216
|
+
if (result.interrupted) output += yellow("[interrupted]") + "\n"
|
|
217
|
+
if (result.stdout?.trim()) output += formatLongOutput(result.stdout.trim())
|
|
218
|
+
}
|
|
219
|
+
const content = msg.message?.content
|
|
220
|
+
if (content) {
|
|
221
|
+
for (const block of content) {
|
|
222
|
+
if (block.type === "tool_result") {
|
|
223
|
+
if (block.is_error) output += yellow("✗ Tool error") + "\n"
|
|
224
|
+
if (block.content) output += formatLongOutput(block.content)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return output
|
|
229
|
+
}
|
|
230
|
+
|
|
75
231
|
const formatResult = (msg: StreamJsonMessage): string => {
|
|
76
232
|
if (msg.subtype === "success") {
|
|
77
233
|
const duration = msg.duration_ms
|
|
@@ -79,19 +235,8 @@ const formatResult = (msg: StreamJsonMessage): string => {
|
|
|
79
235
|
: ""
|
|
80
236
|
const cost = msg.total_cost_usd ? "$" + msg.total_cost_usd.toFixed(4) : ""
|
|
81
237
|
const info = [duration, cost].filter(Boolean).join(" | ")
|
|
82
|
-
return (
|
|
83
|
-
"\n" +
|
|
84
|
-
ansiColors.green +
|
|
85
|
-
"✓ Done" +
|
|
86
|
-
ansiColors.reset +
|
|
87
|
-
" " +
|
|
88
|
-
ansiColors.dim +
|
|
89
|
-
info +
|
|
90
|
-
ansiColors.reset +
|
|
91
|
-
"\n"
|
|
92
|
-
)
|
|
93
|
-
} else if (msg.subtype === "error") {
|
|
94
|
-
return "\n" + ansiColors.yellow + "✗ Error" + ansiColors.reset + "\n"
|
|
238
|
+
return "\n" + green("✓ Done") + " " + dim(info) + "\n"
|
|
95
239
|
}
|
|
240
|
+
if (msg.subtype === "error") return "\n" + yellow("✗ Error") + "\n"
|
|
96
241
|
return ""
|
|
97
242
|
}
|
package/src/commands/root.ts
CHANGED
|
@@ -28,6 +28,174 @@ import { RunnerStalled } from "../domain/Errors.ts"
|
|
|
28
28
|
import { agentReviewer } from "../Agents/reviewer.ts"
|
|
29
29
|
import { agentTimeout } from "../Agents/timeout.ts"
|
|
30
30
|
|
|
31
|
+
// Main iteration run logic
|
|
32
|
+
|
|
33
|
+
const run = Effect.fnUntraced(
|
|
34
|
+
function* (options: {
|
|
35
|
+
readonly startedDeferred: Deferred.Deferred<void>
|
|
36
|
+
readonly targetBranch: Option.Option<string>
|
|
37
|
+
readonly specsDirectory: string
|
|
38
|
+
readonly stallTimeout: Duration.Duration
|
|
39
|
+
readonly runTimeout: Duration.Duration
|
|
40
|
+
readonly commandPrefix: (
|
|
41
|
+
command: ChildProcess.Command,
|
|
42
|
+
) => ChildProcess.Command
|
|
43
|
+
readonly review: boolean
|
|
44
|
+
}) {
|
|
45
|
+
const fs = yield* FileSystem.FileSystem
|
|
46
|
+
const pathService = yield* Path.Path
|
|
47
|
+
const worktree = yield* Worktree
|
|
48
|
+
const gh = yield* GithubCli
|
|
49
|
+
const cliAgent = yield* getOrSelectCliAgent
|
|
50
|
+
const prd = yield* Prd
|
|
51
|
+
const source = yield* IssueSource
|
|
52
|
+
|
|
53
|
+
if (Option.isSome(options.targetBranch)) {
|
|
54
|
+
const targetWithRemote = options.targetBranch.value.includes("/")
|
|
55
|
+
? options.targetBranch.value
|
|
56
|
+
: `origin/${options.targetBranch.value}`
|
|
57
|
+
yield* worktree.exec`git checkout ${targetWithRemote}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ensure cleanup of branch after run
|
|
61
|
+
yield* Effect.addFinalizer(
|
|
62
|
+
Effect.fnUntraced(function* () {
|
|
63
|
+
const currentBranchName = yield* worktree
|
|
64
|
+
.currentBranch(worktree.directory)
|
|
65
|
+
.pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
|
66
|
+
if (!currentBranchName) return
|
|
67
|
+
|
|
68
|
+
// enter detached state
|
|
69
|
+
yield* worktree.exec`git checkout --detach ${currentBranchName}`
|
|
70
|
+
// delete the branch
|
|
71
|
+
yield* worktree.exec`git branch -D ${currentBranchName}`
|
|
72
|
+
}, Effect.ignore),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
let taskId: string | undefined = undefined
|
|
76
|
+
|
|
77
|
+
// setup finalizer to revert issue if we fail
|
|
78
|
+
yield* Effect.addFinalizer(
|
|
79
|
+
Effect.fnUntraced(function* (exit) {
|
|
80
|
+
if (exit._tag === "Success") return
|
|
81
|
+
const prd = yield* Prd
|
|
82
|
+
if (taskId) {
|
|
83
|
+
yield* prd.maybeRevertIssue({
|
|
84
|
+
issueId: taskId,
|
|
85
|
+
})
|
|
86
|
+
} else {
|
|
87
|
+
yield* prd.revertUpdatedIssues
|
|
88
|
+
}
|
|
89
|
+
}, Effect.ignore),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// 1. Choose task
|
|
93
|
+
const chosenTask = yield* agentChooser({
|
|
94
|
+
stallTimeout: options.stallTimeout,
|
|
95
|
+
commandPrefix: options.commandPrefix,
|
|
96
|
+
cliAgent,
|
|
97
|
+
})
|
|
98
|
+
taskId = chosenTask.id
|
|
99
|
+
yield* prd.setChosenIssueId(taskId)
|
|
100
|
+
|
|
101
|
+
yield* source.ensureInProgress(taskId).pipe(
|
|
102
|
+
Effect.timeoutOrElse({
|
|
103
|
+
duration: "1 minute",
|
|
104
|
+
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
yield* Deferred.completeWith(options.startedDeferred, Effect.void)
|
|
109
|
+
|
|
110
|
+
if (chosenTask.githubPrNumber) {
|
|
111
|
+
yield* worktree.exec`gh pr checkout ${chosenTask.githubPrNumber}`
|
|
112
|
+
const feedback = yield* gh.prFeedbackMd(chosenTask.githubPrNumber)
|
|
113
|
+
yield* fs.writeFileString(
|
|
114
|
+
pathService.join(worktree.directory, ".lalph", "feedback.md"),
|
|
115
|
+
feedback,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 2. Generate instructions
|
|
120
|
+
const instructions = yield* agentInstructor({
|
|
121
|
+
stallTimeout: options.stallTimeout,
|
|
122
|
+
commandPrefix: options.commandPrefix,
|
|
123
|
+
specsDirectory: options.specsDirectory,
|
|
124
|
+
targetBranch: options.targetBranch,
|
|
125
|
+
task: chosenTask.prd,
|
|
126
|
+
cliAgent,
|
|
127
|
+
githubPrNumber: chosenTask.githubPrNumber ?? undefined,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
yield* Effect.gen(function* () {
|
|
131
|
+
// 3. Work on task
|
|
132
|
+
const exitCode = yield* agentWorker({
|
|
133
|
+
specsDirectory: options.specsDirectory,
|
|
134
|
+
stallTimeout: options.stallTimeout,
|
|
135
|
+
cliAgent,
|
|
136
|
+
commandPrefix: options.commandPrefix,
|
|
137
|
+
instructions,
|
|
138
|
+
})
|
|
139
|
+
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
140
|
+
|
|
141
|
+
// 4. Review task
|
|
142
|
+
if (options.review) {
|
|
143
|
+
yield* agentReviewer({
|
|
144
|
+
specsDirectory: options.specsDirectory,
|
|
145
|
+
stallTimeout: options.stallTimeout,
|
|
146
|
+
cliAgent,
|
|
147
|
+
commandPrefix: options.commandPrefix,
|
|
148
|
+
instructions,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}).pipe(
|
|
152
|
+
Effect.timeout(options.runTimeout),
|
|
153
|
+
Effect.tapErrorTag("TimeoutError", () =>
|
|
154
|
+
agentTimeout({
|
|
155
|
+
specsDirectory: options.specsDirectory,
|
|
156
|
+
stallTimeout: options.stallTimeout,
|
|
157
|
+
cliAgent,
|
|
158
|
+
commandPrefix: options.commandPrefix,
|
|
159
|
+
task: chosenTask.prd,
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Auto-merge logic
|
|
165
|
+
|
|
166
|
+
const autoMerge = Effect.gen(function* () {
|
|
167
|
+
let prState = yield* worktree.viewPrState()
|
|
168
|
+
yield* Effect.log("PR state", prState)
|
|
169
|
+
if (Option.isNone(prState)) {
|
|
170
|
+
return yield* prd.maybeRevertIssue({ issueId: taskId })
|
|
171
|
+
}
|
|
172
|
+
if (Option.isSome(options.targetBranch)) {
|
|
173
|
+
yield* worktree.exec`gh pr edit --base ${options.targetBranch.value}`
|
|
174
|
+
}
|
|
175
|
+
yield* worktree.exec`gh pr merge -sd`
|
|
176
|
+
yield* Effect.sleep(Duration.seconds(3))
|
|
177
|
+
prState = yield* worktree.viewPrState(prState.value.number)
|
|
178
|
+
yield* Effect.log("PR state after merge", prState)
|
|
179
|
+
if (Option.isSome(prState) && prState.value.state === "MERGED") {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
yield* Effect.log("Flagging unmergable PR")
|
|
183
|
+
yield* prd.flagUnmergable({ issueId: taskId })
|
|
184
|
+
}).pipe(Effect.annotateLogs({ phase: "autoMerge" }))
|
|
185
|
+
|
|
186
|
+
const task = yield* prd.findById(taskId)
|
|
187
|
+
if (task?.autoMerge) {
|
|
188
|
+
yield* autoMerge
|
|
189
|
+
} else {
|
|
190
|
+
yield* prd.maybeRevertIssue({ issueId: taskId })
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
Effect.scoped,
|
|
194
|
+
Effect.provide([PromptGen.layer, Prd.layer]),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Command
|
|
198
|
+
|
|
31
199
|
const iterations = Flag.integer("iterations").pipe(
|
|
32
200
|
Flag.withDescription("Number of iterations to run, defaults to unlimited"),
|
|
33
201
|
Flag.withAlias("i"),
|
|
@@ -95,6 +263,10 @@ const verbose = Flag.boolean("verbose").pipe(
|
|
|
95
263
|
Flag.withAlias("v"),
|
|
96
264
|
)
|
|
97
265
|
|
|
266
|
+
const review = Flag.boolean("review").pipe(
|
|
267
|
+
Flag.withDescription("Enabled the AI peer-review step"),
|
|
268
|
+
)
|
|
269
|
+
|
|
98
270
|
// handled in cli.ts
|
|
99
271
|
const reset = Flag.boolean("reset").pipe(
|
|
100
272
|
Flag.withDescription("Reset the current issue source before running"),
|
|
@@ -108,6 +280,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
108
280
|
maxIterationMinutes,
|
|
109
281
|
stallMinutes,
|
|
110
282
|
reset,
|
|
283
|
+
review,
|
|
111
284
|
specsDirectory,
|
|
112
285
|
verbose,
|
|
113
286
|
}).pipe(
|
|
@@ -119,6 +292,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
119
292
|
maxIterationMinutes,
|
|
120
293
|
stallMinutes,
|
|
121
294
|
specsDirectory,
|
|
295
|
+
review,
|
|
122
296
|
}) {
|
|
123
297
|
const source = yield* Layer.build(CurrentIssueSource.layer)
|
|
124
298
|
const commandPrefix = yield* getCommandPrefix
|
|
@@ -164,6 +338,7 @@ export const commandRoot = Command.make("lalph", {
|
|
|
164
338
|
stallTimeout: Duration.minutes(stallMinutes),
|
|
165
339
|
runTimeout: Duration.minutes(maxIterationMinutes),
|
|
166
340
|
commandPrefix,
|
|
341
|
+
review,
|
|
167
342
|
}),
|
|
168
343
|
),
|
|
169
344
|
Effect.catchFilter(
|
|
@@ -212,164 +387,3 @@ export const commandRoot = Command.make("lalph", {
|
|
|
212
387
|
}, Effect.scoped),
|
|
213
388
|
),
|
|
214
389
|
)
|
|
215
|
-
|
|
216
|
-
const run = Effect.fnUntraced(
|
|
217
|
-
function* (options: {
|
|
218
|
-
readonly startedDeferred: Deferred.Deferred<void>
|
|
219
|
-
readonly targetBranch: Option.Option<string>
|
|
220
|
-
readonly specsDirectory: string
|
|
221
|
-
readonly stallTimeout: Duration.Duration
|
|
222
|
-
readonly runTimeout: Duration.Duration
|
|
223
|
-
readonly commandPrefix: (
|
|
224
|
-
command: ChildProcess.Command,
|
|
225
|
-
) => ChildProcess.Command
|
|
226
|
-
}) {
|
|
227
|
-
const fs = yield* FileSystem.FileSystem
|
|
228
|
-
const pathService = yield* Path.Path
|
|
229
|
-
const worktree = yield* Worktree
|
|
230
|
-
const gh = yield* GithubCli
|
|
231
|
-
const cliAgent = yield* getOrSelectCliAgent
|
|
232
|
-
const prd = yield* Prd
|
|
233
|
-
const source = yield* IssueSource
|
|
234
|
-
|
|
235
|
-
if (Option.isSome(options.targetBranch)) {
|
|
236
|
-
const targetWithRemote = options.targetBranch.value.includes("/")
|
|
237
|
-
? options.targetBranch.value
|
|
238
|
-
: `origin/${options.targetBranch.value}`
|
|
239
|
-
yield* worktree.exec`git checkout ${targetWithRemote}`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ensure cleanup of branch after run
|
|
243
|
-
yield* Effect.addFinalizer(
|
|
244
|
-
Effect.fnUntraced(function* () {
|
|
245
|
-
const currentBranchName = yield* worktree
|
|
246
|
-
.currentBranch(worktree.directory)
|
|
247
|
-
.pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
|
248
|
-
if (!currentBranchName) return
|
|
249
|
-
|
|
250
|
-
// enter detached state
|
|
251
|
-
yield* worktree.exec`git checkout --detach ${currentBranchName}`
|
|
252
|
-
// delete the branch
|
|
253
|
-
yield* worktree.exec`git branch -D ${currentBranchName}`
|
|
254
|
-
}, Effect.ignore),
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
let taskId: string | undefined = undefined
|
|
258
|
-
|
|
259
|
-
// setup finalizer to revert issue if we fail
|
|
260
|
-
yield* Effect.addFinalizer(
|
|
261
|
-
Effect.fnUntraced(function* (exit) {
|
|
262
|
-
if (exit._tag === "Success") return
|
|
263
|
-
const prd = yield* Prd
|
|
264
|
-
if (taskId) {
|
|
265
|
-
yield* prd.maybeRevertIssue({
|
|
266
|
-
issueId: taskId,
|
|
267
|
-
})
|
|
268
|
-
} else {
|
|
269
|
-
yield* prd.revertUpdatedIssues
|
|
270
|
-
}
|
|
271
|
-
}, Effect.ignore),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
// 1. Choose task
|
|
275
|
-
const chosenTask = yield* agentChooser({
|
|
276
|
-
stallTimeout: options.stallTimeout,
|
|
277
|
-
commandPrefix: options.commandPrefix,
|
|
278
|
-
cliAgent,
|
|
279
|
-
})
|
|
280
|
-
taskId = chosenTask.id
|
|
281
|
-
yield* prd.setChosenIssueId(taskId)
|
|
282
|
-
|
|
283
|
-
yield* source.ensureInProgress(taskId).pipe(
|
|
284
|
-
Effect.timeoutOrElse({
|
|
285
|
-
duration: "1 minute",
|
|
286
|
-
onTimeout: () => Effect.fail(new RunnerStalled()),
|
|
287
|
-
}),
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
yield* Deferred.completeWith(options.startedDeferred, Effect.void)
|
|
291
|
-
|
|
292
|
-
if (chosenTask.githubPrNumber) {
|
|
293
|
-
yield* worktree.exec`gh pr checkout ${chosenTask.githubPrNumber}`
|
|
294
|
-
const feedback = yield* gh.prFeedbackMd(chosenTask.githubPrNumber)
|
|
295
|
-
yield* fs.writeFileString(
|
|
296
|
-
pathService.join(worktree.directory, ".lalph", "feedback.md"),
|
|
297
|
-
feedback,
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 2. Generate instructions
|
|
302
|
-
const instructions = yield* agentInstructor({
|
|
303
|
-
stallTimeout: options.stallTimeout,
|
|
304
|
-
commandPrefix: options.commandPrefix,
|
|
305
|
-
specsDirectory: options.specsDirectory,
|
|
306
|
-
targetBranch: options.targetBranch,
|
|
307
|
-
task: chosenTask.prd,
|
|
308
|
-
cliAgent,
|
|
309
|
-
githubPrNumber: chosenTask.githubPrNumber ?? undefined,
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
yield* Effect.gen(function* () {
|
|
313
|
-
// 3. Work on task
|
|
314
|
-
const exitCode = yield* agentWorker({
|
|
315
|
-
specsDirectory: options.specsDirectory,
|
|
316
|
-
stallTimeout: options.stallTimeout,
|
|
317
|
-
cliAgent,
|
|
318
|
-
commandPrefix: options.commandPrefix,
|
|
319
|
-
instructions,
|
|
320
|
-
})
|
|
321
|
-
yield* Effect.log(`Agent exited with code: ${exitCode}`)
|
|
322
|
-
|
|
323
|
-
// 4. Review task
|
|
324
|
-
yield* agentReviewer({
|
|
325
|
-
specsDirectory: options.specsDirectory,
|
|
326
|
-
stallTimeout: options.stallTimeout,
|
|
327
|
-
cliAgent,
|
|
328
|
-
commandPrefix: options.commandPrefix,
|
|
329
|
-
instructions,
|
|
330
|
-
})
|
|
331
|
-
}).pipe(
|
|
332
|
-
Effect.timeout(options.runTimeout),
|
|
333
|
-
Effect.tapErrorTag("TimeoutError", () =>
|
|
334
|
-
agentTimeout({
|
|
335
|
-
specsDirectory: options.specsDirectory,
|
|
336
|
-
stallTimeout: options.stallTimeout,
|
|
337
|
-
cliAgent,
|
|
338
|
-
commandPrefix: options.commandPrefix,
|
|
339
|
-
task: chosenTask.prd,
|
|
340
|
-
}),
|
|
341
|
-
),
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
// Auto-merge logic
|
|
345
|
-
|
|
346
|
-
const autoMerge = Effect.gen(function* () {
|
|
347
|
-
let prState = yield* worktree.viewPrState()
|
|
348
|
-
yield* Effect.log("PR state", prState)
|
|
349
|
-
if (Option.isNone(prState)) {
|
|
350
|
-
return yield* prd.maybeRevertIssue({ issueId: taskId })
|
|
351
|
-
}
|
|
352
|
-
if (Option.isSome(options.targetBranch)) {
|
|
353
|
-
yield* worktree.exec`gh pr edit --base ${options.targetBranch.value}`
|
|
354
|
-
}
|
|
355
|
-
yield* worktree.exec`gh pr merge -sd`
|
|
356
|
-
yield* Effect.sleep(Duration.seconds(3))
|
|
357
|
-
prState = yield* worktree.viewPrState(prState.value.number)
|
|
358
|
-
yield* Effect.log("PR state after merge", prState)
|
|
359
|
-
if (Option.isSome(prState) && prState.value.state === "MERGED") {
|
|
360
|
-
return
|
|
361
|
-
}
|
|
362
|
-
yield* Effect.log("Flagging unmergable PR")
|
|
363
|
-
yield* prd.flagUnmergable({ issueId: taskId })
|
|
364
|
-
}).pipe(Effect.annotateLogs({ phase: "autoMerge" }))
|
|
365
|
-
|
|
366
|
-
const task = yield* prd.findById(taskId)
|
|
367
|
-
if (task?.autoMerge) {
|
|
368
|
-
yield* autoMerge
|
|
369
|
-
} else {
|
|
370
|
-
yield* prd.maybeRevertIssue({ issueId: taskId })
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
Effect.scoped,
|
|
374
|
-
Effect.provide([PromptGen.layer, Prd.layer]),
|
|
375
|
-
)
|
package/src/domain/CliAgent.ts
CHANGED
|
@@ -77,7 +77,7 @@ const claude = new CliAgent({
|
|
|
77
77
|
stdout: outputMode,
|
|
78
78
|
stderr: outputMode,
|
|
79
79
|
stdin: "inherit",
|
|
80
|
-
})`claude --dangerously-skip-permissions --output-format stream-json --verbose -p ${`@${prdFilePath}
|
|
80
|
+
})`claude --dangerously-skip-permissions --output-format stream-json --verbose --disallowed-tools AskUserQuestion -p ${`@${prdFilePath}
|
|
81
81
|
|
|
82
82
|
${prompt}`}`,
|
|
83
83
|
outputTransformer: claudeOutputTransformer,
|
package/src/shared/fs.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, pipe, Stream } from "effect"
|
|
2
|
+
|
|
3
|
+
export const makeWaitForFile = Effect.gen(function* () {
|
|
4
|
+
const fs = yield* FileSystem.FileSystem
|
|
5
|
+
const pathService = yield* Path.Path
|
|
6
|
+
return (directory: string, name: string) =>
|
|
7
|
+
pipe(
|
|
8
|
+
fs.watch(directory),
|
|
9
|
+
Stream.filter((e) => pathService.basename(e.path) === name),
|
|
10
|
+
Stream.runHead,
|
|
11
|
+
)
|
|
12
|
+
})
|