dahrk-node 0.1.21 → 0.1.22
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/main.js +611 -451
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -74,6 +74,13 @@ import {
|
|
|
74
74
|
query
|
|
75
75
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
76
76
|
|
|
77
|
+
// ../../packages/executor-worktree/src/response-rule.ts
|
|
78
|
+
function decideResponse(bufferedText, endedOnTool, status) {
|
|
79
|
+
const text = bufferedText.trim();
|
|
80
|
+
if (status !== "ok" || !text || endedOnTool) return {};
|
|
81
|
+
return { responseText: text, event: { type: "response", text } };
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// ../../packages/executor-worktree/src/claude-mappers.ts
|
|
78
85
|
function mapClaudeMessage(msg) {
|
|
79
86
|
switch (msg.type) {
|
|
@@ -168,27 +175,21 @@ function consumeClaudeMessage(msg, state, suppressStageExit) {
|
|
|
168
175
|
}
|
|
169
176
|
if (msg.type === "result") {
|
|
170
177
|
const status = r.resultStatus === "fail" ? "fail" : "ok";
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
responseText = state.bufferedText;
|
|
174
|
-
events.push({ type: "response", text: state.bufferedText });
|
|
175
|
-
}
|
|
178
|
+
const decision = decideResponse(state.bufferedText ?? "", state.turnEndedOnTool, status);
|
|
179
|
+
if (decision.event) events.push(decision.event);
|
|
176
180
|
for (const e of r.events) {
|
|
177
181
|
if (suppressStageExit && e.type === "state") continue;
|
|
178
182
|
events.push(e);
|
|
179
183
|
}
|
|
180
184
|
state.bufferedText = null;
|
|
181
185
|
state.turnEndedOnTool = false;
|
|
182
|
-
return { events, isResult: true, status, responseText };
|
|
186
|
+
return { events, isResult: true, status, responseText: decision.responseText };
|
|
183
187
|
}
|
|
184
188
|
events.push(...r.events);
|
|
185
189
|
return { events, isResult: false };
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
// ../../packages/executor-worktree/src/
|
|
189
|
-
import { readFileSync } from "fs";
|
|
190
|
-
import { join } from "path";
|
|
191
|
-
import { attachedDocBasename } from "@dahrk/contracts";
|
|
192
|
+
// ../../packages/executor-worktree/src/runtime-session.ts
|
|
192
193
|
var SUMMARISE_PROMPT = "This stage is ending. In ONE concise sentence for a human reviewer reading the Linear ticket, state concretely what you DID or FOUND in this stage and the result (for example which functions or files changed, whether the tests pass, or what a review flagged). Be specific and lead with the outcome. Do not mention internal scratch files or a next-stage entry point. Reply with only that sentence.";
|
|
193
194
|
function makeEmit(runtime, onTrace, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
194
195
|
return (event, rawRef) => {
|
|
@@ -197,6 +198,11 @@ function makeEmit(runtime, onTrace, now = () => (/* @__PURE__ */ new Date()).toI
|
|
|
197
198
|
onTrace(full);
|
|
198
199
|
};
|
|
199
200
|
}
|
|
201
|
+
|
|
202
|
+
// ../../packages/executor-worktree/src/prompt-assembly.ts
|
|
203
|
+
import { readFileSync } from "fs";
|
|
204
|
+
import { join } from "path";
|
|
205
|
+
import { attachedDocBasename } from "@dahrk/contracts";
|
|
200
206
|
function stripFrontmatter(text) {
|
|
201
207
|
const lines = text.split("\n");
|
|
202
208
|
if (lines[0]?.trim() !== "---") return text;
|
|
@@ -319,6 +325,8 @@ var OPENING_KICKOFF = "Begin now. Using the ticket context and your instructions
|
|
|
319
325
|
function interactiveSeedText(ctx, instructionInSystemPrompt) {
|
|
320
326
|
return instructionInSystemPrompt ? OPENING_KICKOFF : resolveStagePrompt(ctx);
|
|
321
327
|
}
|
|
328
|
+
|
|
329
|
+
// ../../packages/executor-worktree/src/mailbox.ts
|
|
322
330
|
var ManagedMailbox = class {
|
|
323
331
|
q = [];
|
|
324
332
|
waiters = [];
|
|
@@ -344,36 +352,19 @@ var ManagedMailbox = class {
|
|
|
344
352
|
};
|
|
345
353
|
}
|
|
346
354
|
};
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
signal.removeEventListener("abort", onAbort);
|
|
361
|
-
resolve3(r);
|
|
362
|
-
}
|
|
363
|
-
function onAbort() {
|
|
364
|
-
finish({ kind: "cancelled" });
|
|
365
|
-
}
|
|
366
|
-
if (signal.aborted) {
|
|
367
|
-
finish({ kind: "cancelled" });
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
signal.addEventListener("abort", onAbort);
|
|
371
|
-
timer = setTimeout(() => finish({ kind: "idle-timeout" }), idleMs);
|
|
372
|
-
pending.then(
|
|
373
|
-
(res) => finish(res.done ? { kind: "turns-exhausted" } : { kind: "turn", value: res.value }),
|
|
374
|
-
() => finish({ kind: "turns-exhausted" })
|
|
375
|
-
);
|
|
376
|
-
});
|
|
355
|
+
|
|
356
|
+
// ../../packages/executor-worktree/src/elicit-router.ts
|
|
357
|
+
function elicitOutcomeReply(outcome) {
|
|
358
|
+
switch (outcome.kind) {
|
|
359
|
+
case "reply":
|
|
360
|
+
return `The user selected: ${outcome.text}`;
|
|
361
|
+
case "busy":
|
|
362
|
+
return "Only one question can be asked at a time; wait for the current one to be answered, then ask again.";
|
|
363
|
+
case "noreply":
|
|
364
|
+
return "No response from the user; proceed with your best judgement.";
|
|
365
|
+
case "cancel":
|
|
366
|
+
return "The question was cancelled.";
|
|
367
|
+
}
|
|
377
368
|
}
|
|
378
369
|
function createElicitTurnRouter(turns, opts) {
|
|
379
370
|
const conversation = new ManagedMailbox();
|
|
@@ -417,6 +408,147 @@ function createElicitTurnRouter(turns, opts) {
|
|
|
417
408
|
return { conversation, ask };
|
|
418
409
|
}
|
|
419
410
|
|
|
411
|
+
// ../../packages/executor-worktree/src/turn-loop.ts
|
|
412
|
+
function interactiveIdleWindows(ctx) {
|
|
413
|
+
const idleMs = ctx.config.idleMs ?? Number(process.env.DAHRK_INTERACTIVE_IDLE_MS ?? process.env.SKAKEL_INTERACTIVE_IDLE_MS ?? 12e4);
|
|
414
|
+
const firstReplyMs = ctx.config.firstReplyMs ?? Number(process.env.DAHRK_INTERACTIVE_FIRST_REPLY_MS ?? process.env.SKAKEL_INTERACTIVE_FIRST_REPLY_MS ?? 6e5);
|
|
415
|
+
return { firstReplyMs: Math.max(firstReplyMs, idleMs), idleMs };
|
|
416
|
+
}
|
|
417
|
+
function raceNextTurn(pending, idleMs, signal) {
|
|
418
|
+
return new Promise((resolve3) => {
|
|
419
|
+
let settled = false;
|
|
420
|
+
let timer;
|
|
421
|
+
function finish(r) {
|
|
422
|
+
if (settled) return;
|
|
423
|
+
settled = true;
|
|
424
|
+
if (timer) clearTimeout(timer);
|
|
425
|
+
signal.removeEventListener("abort", onAbort);
|
|
426
|
+
resolve3(r);
|
|
427
|
+
}
|
|
428
|
+
function onAbort() {
|
|
429
|
+
finish({ kind: "cancelled" });
|
|
430
|
+
}
|
|
431
|
+
if (signal.aborted) {
|
|
432
|
+
finish({ kind: "cancelled" });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
signal.addEventListener("abort", onAbort);
|
|
436
|
+
timer = setTimeout(() => finish({ kind: "idle-timeout" }), idleMs);
|
|
437
|
+
pending.then(
|
|
438
|
+
(res) => finish(res.done ? { kind: "turns-exhausted" } : { kind: "turn", value: res.value }),
|
|
439
|
+
() => finish({ kind: "turns-exhausted" })
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
444
|
+
async function runInteractiveLoop(ctx, turns, emit, makeSession, opts) {
|
|
445
|
+
const { signal, cancelled, cancel, instructionInSystemPrompt } = opts;
|
|
446
|
+
const exit = ctx.config.exit ?? "either";
|
|
447
|
+
const wantsTool = exit === "tool" || exit === "either";
|
|
448
|
+
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
449
|
+
const router = createElicitTurnRouter(turns, { signal, firstReplyMs, idleMs });
|
|
450
|
+
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
451
|
+
let awaitingFirstReply = true;
|
|
452
|
+
const ask = async (question) => {
|
|
453
|
+
const outcome = await router.ask(awaitingFirstReply, () => {
|
|
454
|
+
emit({ type: "elicitation", prompt: question.prompt, signal: "select", options: question.options });
|
|
455
|
+
ctx.emitElicit?.(question);
|
|
456
|
+
});
|
|
457
|
+
return elicitOutcomeReply(outcome);
|
|
458
|
+
};
|
|
459
|
+
const session = makeSession({ emit, ask });
|
|
460
|
+
let toolSummary;
|
|
461
|
+
let artifact;
|
|
462
|
+
let exited = "gate";
|
|
463
|
+
let pending = humanIter.next();
|
|
464
|
+
try {
|
|
465
|
+
const seed = await session.sendTurn(interactiveSeedText(ctx, instructionInSystemPrompt));
|
|
466
|
+
if (seed.stageComplete && wantsTool) {
|
|
467
|
+
exited = "tool";
|
|
468
|
+
toolSummary = seed.summary;
|
|
469
|
+
artifact = seed.artifact;
|
|
470
|
+
}
|
|
471
|
+
for (; ; ) {
|
|
472
|
+
if (exited === "tool") break;
|
|
473
|
+
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, signal);
|
|
474
|
+
awaitingFirstReply = false;
|
|
475
|
+
if (race.kind === "cancelled") {
|
|
476
|
+
exited = "cancelled";
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
if (race.kind === "idle-timeout") {
|
|
480
|
+
exited = "timeout";
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
if (race.kind === "turns-exhausted") {
|
|
484
|
+
exited = "gate";
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
const texts = [race.value.text];
|
|
488
|
+
pending = humanIter.next();
|
|
489
|
+
for (; ; ) {
|
|
490
|
+
const more = await raceNextTurn(pending, COALESCE_MS, signal);
|
|
491
|
+
if (more.kind === "turn") {
|
|
492
|
+
texts.push(more.value.text);
|
|
493
|
+
pending = humanIter.next();
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (more.kind === "cancelled") exited = "cancelled";
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
if (exited === "cancelled") break;
|
|
500
|
+
const tr = await session.sendTurn(texts.join("\n"));
|
|
501
|
+
if (tr.stageComplete && wantsTool) {
|
|
502
|
+
exited = "tool";
|
|
503
|
+
toolSummary = tr.summary;
|
|
504
|
+
artifact = tr.artifact;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
if (!cancelled()) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
510
|
+
exited = cancelled() ? "cancelled" : "gate";
|
|
511
|
+
}
|
|
512
|
+
let status = "ok";
|
|
513
|
+
let summary = "";
|
|
514
|
+
if (exited === "tool") {
|
|
515
|
+
summary = toolSummary ?? "(stage marked complete)";
|
|
516
|
+
} else if (exited === "gate") {
|
|
517
|
+
summary = await session.summariseTurn();
|
|
518
|
+
} else if (exited === "timeout") {
|
|
519
|
+
status = "timeout";
|
|
520
|
+
summary = "(stage timed out awaiting input)";
|
|
521
|
+
await cancel();
|
|
522
|
+
} else {
|
|
523
|
+
status = "fail";
|
|
524
|
+
summary = "(stage cancelled)";
|
|
525
|
+
}
|
|
526
|
+
const costUsd = session.cost();
|
|
527
|
+
const sessionId = session.sessionId;
|
|
528
|
+
const outArtifact = status === "ok" ? artifact : void 0;
|
|
529
|
+
return {
|
|
530
|
+
status,
|
|
531
|
+
summary,
|
|
532
|
+
...sessionId ? { sessionId } : {},
|
|
533
|
+
...costUsd !== void 0 ? { costUsd } : {},
|
|
534
|
+
...outArtifact ? { artifact: outArtifact } : {}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
async function runBatchLoop(session, ctx, hooks, opts) {
|
|
538
|
+
let status = "ok";
|
|
539
|
+
try {
|
|
540
|
+
const tr = await session.sendTurn(resolveStagePrompt(ctx));
|
|
541
|
+
if (tr.status) status = tr.status;
|
|
542
|
+
} catch (e) {
|
|
543
|
+
if (!opts.cancelled()) hooks.emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
544
|
+
status = "fail";
|
|
545
|
+
}
|
|
546
|
+
if (opts.cancelled()) status = "fail";
|
|
547
|
+
const costUsd = session.cost();
|
|
548
|
+
const sessionId = session.sessionId;
|
|
549
|
+
return { status, ...sessionId ? { sessionId } : {}, ...costUsd !== void 0 ? { costUsd } : {} };
|
|
550
|
+
}
|
|
551
|
+
|
|
420
552
|
// ../../packages/executor-worktree/src/stage-complete-tool.ts
|
|
421
553
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
422
554
|
import { z } from "zod";
|
|
@@ -424,6 +556,10 @@ var STAGE_COMPLETE_TOOL_NAME = "mcp__dahrk__dahrk_stage_complete";
|
|
|
424
556
|
function createStageCompleteTool() {
|
|
425
557
|
let captured = null;
|
|
426
558
|
let capturedDoc = null;
|
|
559
|
+
const capture = (args) => {
|
|
560
|
+
captured = args.summary;
|
|
561
|
+
if (args.document !== void 0) capturedDoc = args.document;
|
|
562
|
+
};
|
|
427
563
|
const completeTool = tool(
|
|
428
564
|
"dahrk_stage_complete",
|
|
429
565
|
"End the current stage and hand off a one-sentence summary of what was accomplished. When the stage's deliverable is a document (e.g. a specification or report) to be published, pass its full markdown body as `document`; this is the only way an interactive stage can emit a document, since it cannot write files.",
|
|
@@ -432,8 +568,7 @@ function createStageCompleteTool() {
|
|
|
432
568
|
document: z.string().optional().describe("The full markdown body of the stage's deliverable document, if any.")
|
|
433
569
|
},
|
|
434
570
|
async (args) => {
|
|
435
|
-
|
|
436
|
-
if (args.document !== void 0) capturedDoc = args.document;
|
|
571
|
+
capture(args);
|
|
437
572
|
return { content: [{ type: "text", text: "Stage marked complete." }] };
|
|
438
573
|
}
|
|
439
574
|
);
|
|
@@ -442,7 +577,8 @@ function createStageCompleteTool() {
|
|
|
442
577
|
allowedToolName: STAGE_COMPLETE_TOOL_NAME,
|
|
443
578
|
fired: () => captured !== null,
|
|
444
579
|
summary: () => captured,
|
|
445
|
-
document: () => capturedDoc
|
|
580
|
+
document: () => capturedDoc,
|
|
581
|
+
capture
|
|
446
582
|
};
|
|
447
583
|
}
|
|
448
584
|
|
|
@@ -496,7 +632,6 @@ function createAskUserQuestionTool(deps) {
|
|
|
496
632
|
}
|
|
497
633
|
|
|
498
634
|
// ../../packages/executor-worktree/src/claude-adapter.ts
|
|
499
|
-
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
500
635
|
var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
|
|
501
636
|
var HANDED_BACK_ARTIFACT_PATH = ".dahrk/scratch/output/document.md";
|
|
502
637
|
var CLAUDE_CODE_SYSTEM_PROMPT = { type: "preset", preset: "claude_code" };
|
|
@@ -505,6 +640,7 @@ var userMsg = (text) => ({
|
|
|
505
640
|
parent_tool_use_id: null,
|
|
506
641
|
message: { role: "user", content: text }
|
|
507
642
|
});
|
|
643
|
+
var defaultAsk = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
508
644
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
509
645
|
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
510
646
|
const verdict2 = ctx.authorizeToolUse?.(toolName, input);
|
|
@@ -544,7 +680,25 @@ function runtimeEnvOptions(ctx) {
|
|
|
544
680
|
if (!ctx.runtimeEnv) return {};
|
|
545
681
|
return { env: { ...process.env, ...ctx.runtimeEnv } };
|
|
546
682
|
}
|
|
547
|
-
|
|
683
|
+
var defaultCreateClaudeSession = ({ prompt, options }) => {
|
|
684
|
+
const mailbox = prompt === void 0 ? new ManagedMailbox() : void 0;
|
|
685
|
+
const q = query({ prompt: mailbox ?? prompt, options });
|
|
686
|
+
const it = q[Symbol.asyncIterator]();
|
|
687
|
+
return {
|
|
688
|
+
[Symbol.asyncIterator]: () => it,
|
|
689
|
+
push: (m) => mailbox?.push(m),
|
|
690
|
+
end: () => mailbox?.end(),
|
|
691
|
+
close: () => {
|
|
692
|
+
try {
|
|
693
|
+
q.close();
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
interrupt: () => q.interrupt()
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
function createClaudeRunner(deps = {}) {
|
|
701
|
+
const createSession = deps.createSession ?? defaultCreateClaudeSession;
|
|
548
702
|
const abortController = new AbortController();
|
|
549
703
|
let cancelled = false;
|
|
550
704
|
let active;
|
|
@@ -609,11 +763,86 @@ function createClaudeRunner() {
|
|
|
609
763
|
}
|
|
610
764
|
active = void 0;
|
|
611
765
|
};
|
|
766
|
+
const makeClaudeRuntimeSession = (ctx, hooks, mode) => {
|
|
767
|
+
const state = newBufferState();
|
|
768
|
+
const it = mode.interactive ? mode.session[Symbol.asyncIterator]() : void 0;
|
|
769
|
+
const consumeInteractiveTurn = async () => {
|
|
770
|
+
for (; ; ) {
|
|
771
|
+
const { value: msg, done } = await it.next();
|
|
772
|
+
if (done) return {};
|
|
773
|
+
const res = handleMessage(msg, hooks.emit, ctx, state, true);
|
|
774
|
+
if (res.isResult) return { status: res.status, responseText: res.responseText };
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
return {
|
|
778
|
+
get sessionId() {
|
|
779
|
+
return sessionId;
|
|
780
|
+
},
|
|
781
|
+
async sendTurn(text) {
|
|
782
|
+
if (!mode.interactive) {
|
|
783
|
+
const session = createSession({ prompt: text, options: mode.options });
|
|
784
|
+
active = session;
|
|
785
|
+
let status2;
|
|
786
|
+
try {
|
|
787
|
+
for await (const msg of session) {
|
|
788
|
+
const res = handleMessage(msg, hooks.emit, ctx, state, false);
|
|
789
|
+
if (res.isResult && res.status) status2 = res.status;
|
|
790
|
+
}
|
|
791
|
+
} finally {
|
|
792
|
+
closeActive();
|
|
793
|
+
}
|
|
794
|
+
return { stageComplete: false, ...status2 !== void 0 ? { status: status2 } : {} };
|
|
795
|
+
}
|
|
796
|
+
mode.session.push(userMsg(text));
|
|
797
|
+
const { status, responseText } = await consumeInteractiveTurn();
|
|
798
|
+
const stageComplete = mode.stageTool.fired();
|
|
799
|
+
const doc = stageComplete ? mode.stageTool.document() : null;
|
|
800
|
+
return {
|
|
801
|
+
stageComplete,
|
|
802
|
+
...stageComplete ? { summary: mode.stageTool.summary() ?? void 0 } : {},
|
|
803
|
+
...responseText !== void 0 ? { responseText } : {},
|
|
804
|
+
...status !== void 0 ? { status } : {},
|
|
805
|
+
...doc !== null ? { artifact: { path: ctx.config.emitArtifact ?? HANDED_BACK_ARTIFACT_PATH, content: doc } } : {}
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
async summariseTurn() {
|
|
809
|
+
if (!mode.interactive) return "(no summary produced)";
|
|
810
|
+
mode.summarising.value = true;
|
|
811
|
+
try {
|
|
812
|
+
mode.session.push(userMsg(SUMMARISE_PROMPT));
|
|
813
|
+
const { responseText } = await consumeInteractiveTurn();
|
|
814
|
+
return (responseText ?? "").trim() || "(no summary produced)";
|
|
815
|
+
} catch {
|
|
816
|
+
return "(no summary produced)";
|
|
817
|
+
} finally {
|
|
818
|
+
mode.summarising.value = false;
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
cost() {
|
|
822
|
+
return costUsd;
|
|
823
|
+
},
|
|
824
|
+
dispose() {
|
|
825
|
+
if (!mode.interactive) return;
|
|
826
|
+
const iter = it;
|
|
827
|
+
mode.session.end();
|
|
828
|
+
void (async () => {
|
|
829
|
+
try {
|
|
830
|
+
for (; ; ) {
|
|
831
|
+
const { done } = await iter.next();
|
|
832
|
+
if (done) break;
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
} finally {
|
|
836
|
+
closeActive();
|
|
837
|
+
}
|
|
838
|
+
})();
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
};
|
|
612
842
|
return {
|
|
613
843
|
runtime: "claude-code",
|
|
614
844
|
async runBatch(ctx, onTrace) {
|
|
615
|
-
const
|
|
616
|
-
const prompt = resolveStagePrompt(ctx);
|
|
845
|
+
const hooks = { emit: makeEmit("claude-code", onTrace), ask: defaultAsk };
|
|
617
846
|
const mcpServers = buildBrokeredMcpServers(ctx);
|
|
618
847
|
const options = {
|
|
619
848
|
...baseOptions(ctx),
|
|
@@ -624,171 +853,50 @@ function createClaudeRunner() {
|
|
|
624
853
|
// Headless: allow tools to run without an interactive permission prompt (M6 wires policy).
|
|
625
854
|
canUseTool: async (toolName, input) => policyCanUseTool(ctx, toolName, input)
|
|
626
855
|
};
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
try {
|
|
630
|
-
const q = query({ prompt, options });
|
|
631
|
-
active = q;
|
|
632
|
-
for await (const msg of q) {
|
|
633
|
-
const res = handleMessage(msg, emit, ctx, state, false);
|
|
634
|
-
if (res.isResult && res.status) status = res.status;
|
|
635
|
-
}
|
|
636
|
-
} catch (e) {
|
|
637
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
638
|
-
status = "fail";
|
|
639
|
-
} finally {
|
|
640
|
-
closeActive();
|
|
641
|
-
}
|
|
642
|
-
if (cancelled) status = "fail";
|
|
643
|
-
return { status, ...sessionId ? { sessionId } : {}, ...costUsd !== void 0 ? { costUsd } : {} };
|
|
856
|
+
const rt = makeClaudeRuntimeSession(ctx, hooks, { interactive: false, options });
|
|
857
|
+
return runBatchLoop(rt, ctx, hooks, { cancelled: () => cancelled });
|
|
644
858
|
},
|
|
645
859
|
async runInteractive(ctx, turns, onTrace) {
|
|
646
860
|
const emit = makeEmit("claude-code", onTrace);
|
|
647
861
|
const stageTool = createStageCompleteTool();
|
|
648
|
-
const {
|
|
649
|
-
let
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
677
|
-
// MCP servers (parity with batch).
|
|
678
|
-
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
679
|
-
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
680
|
-
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
681
|
-
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
682
|
-
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
683
|
-
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
684
|
-
// it does not restrict the other tools canUseTool allows.
|
|
685
|
-
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
686
|
-
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising, stageTool.allowedToolName, ctx, toolName, input),
|
|
687
|
-
maxTurns: MAX_TURNS,
|
|
688
|
-
includePartialMessages: false
|
|
689
|
-
};
|
|
690
|
-
const exit = ctx.config.exit ?? "either";
|
|
691
|
-
const wantsTool = exit === "tool" || exit === "either";
|
|
692
|
-
const mailbox = new ManagedMailbox();
|
|
693
|
-
const q = query({ prompt: mailbox, options });
|
|
694
|
-
active = q;
|
|
695
|
-
const it = q[Symbol.asyncIterator]();
|
|
696
|
-
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
697
|
-
const state = newBufferState();
|
|
698
|
-
const consumeTurn = async () => {
|
|
699
|
-
for (; ; ) {
|
|
700
|
-
const { value: msg, done } = await it.next();
|
|
701
|
-
if (done) return void 0;
|
|
702
|
-
const res = handleMessage(msg, emit, ctx, state, true);
|
|
703
|
-
if (res.isResult) return res.responseText;
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
let exited = null;
|
|
707
|
-
let pending = humanIter.next();
|
|
708
|
-
try {
|
|
709
|
-
mailbox.push(userMsg(interactiveSeedText(ctx, hasSystemPrompt(ctx))));
|
|
710
|
-
await consumeTurn();
|
|
711
|
-
if (stageTool.fired() && wantsTool) exited = "tool";
|
|
712
|
-
while (exited === null) {
|
|
713
|
-
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, abortController.signal);
|
|
714
|
-
awaitingFirstReply = false;
|
|
715
|
-
if (race.kind === "cancelled") {
|
|
716
|
-
exited = "cancelled";
|
|
717
|
-
break;
|
|
718
|
-
}
|
|
719
|
-
if (race.kind === "idle-timeout") {
|
|
720
|
-
exited = "timeout";
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
if (race.kind === "turns-exhausted") {
|
|
724
|
-
exited = "gate";
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
const texts = [race.value.text];
|
|
728
|
-
pending = humanIter.next();
|
|
729
|
-
for (; ; ) {
|
|
730
|
-
const more = await raceNextTurn(pending, COALESCE_MS, abortController.signal);
|
|
731
|
-
if (more.kind === "turn") {
|
|
732
|
-
texts.push(more.value.text);
|
|
733
|
-
pending = humanIter.next();
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (more.kind === "cancelled") exited = "cancelled";
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
739
|
-
if (exited === "cancelled") break;
|
|
740
|
-
mailbox.push(userMsg(texts.join("\n")));
|
|
741
|
-
await consumeTurn();
|
|
742
|
-
if (stageTool.fired() && wantsTool) {
|
|
743
|
-
exited = "tool";
|
|
744
|
-
break;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
} catch (e) {
|
|
748
|
-
if (!cancelled && !exited) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
749
|
-
exited = exited ?? (cancelled ? "cancelled" : "gate");
|
|
750
|
-
}
|
|
751
|
-
let status = "ok";
|
|
752
|
-
let summary = "";
|
|
753
|
-
if (exited === "tool") {
|
|
754
|
-
summary = stageTool.summary() ?? "(stage marked complete)";
|
|
755
|
-
} else if (exited === "gate") {
|
|
756
|
-
summarising = true;
|
|
757
|
-
try {
|
|
758
|
-
mailbox.push(userMsg(SUMMARISE_PROMPT));
|
|
759
|
-
const reply = await consumeTurn();
|
|
760
|
-
summary = (reply ?? "").trim() || "(no summary produced)";
|
|
761
|
-
} catch {
|
|
762
|
-
summary = "(no summary produced)";
|
|
763
|
-
} finally {
|
|
764
|
-
summarising = false;
|
|
765
|
-
}
|
|
766
|
-
} else if (exited === "timeout") {
|
|
767
|
-
status = "timeout";
|
|
768
|
-
summary = "(stage timed out awaiting input)";
|
|
769
|
-
await this.cancel();
|
|
770
|
-
} else {
|
|
771
|
-
status = "fail";
|
|
772
|
-
summary = "(stage cancelled)";
|
|
773
|
-
}
|
|
774
|
-
mailbox.end();
|
|
775
|
-
try {
|
|
776
|
-
for (; ; ) {
|
|
777
|
-
const { done } = await it.next();
|
|
778
|
-
if (done) break;
|
|
779
|
-
}
|
|
780
|
-
} catch {
|
|
781
|
-
}
|
|
782
|
-
closeActive();
|
|
783
|
-
const handedBackDoc = status === "ok" ? stageTool.document() : null;
|
|
784
|
-
const artifact = handedBackDoc !== null ? { path: ctx.config.emitArtifact ?? HANDED_BACK_ARTIFACT_PATH, content: handedBackDoc } : void 0;
|
|
785
|
-
return {
|
|
786
|
-
status,
|
|
787
|
-
summary,
|
|
788
|
-
...sessionId ? { sessionId } : {},
|
|
789
|
-
...costUsd !== void 0 ? { costUsd } : {},
|
|
790
|
-
...artifact ? { artifact } : {}
|
|
862
|
+
const summarising = { value: false };
|
|
863
|
+
let rt;
|
|
864
|
+
const makeSession = (hooks) => {
|
|
865
|
+
const askTool = createAskUserQuestionTool({ ask: (question) => hooks.ask(question) });
|
|
866
|
+
const brokered = buildBrokeredMcpServers(ctx);
|
|
867
|
+
const options = {
|
|
868
|
+
...baseOptions(ctx),
|
|
869
|
+
// Keep the cwd-anchoring preset; fold the stage instruction in via `append` so the
|
|
870
|
+
// interactive persona still gets it without losing the working-directory context.
|
|
871
|
+
systemPrompt: hasSystemPrompt(ctx) ? { type: "preset", preset: "claude_code", append: resolveStagePrompt(ctx) } : CLAUDE_CODE_SYSTEM_PROMPT,
|
|
872
|
+
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
873
|
+
// MCP servers (parity with batch).
|
|
874
|
+
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
875
|
+
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
876
|
+
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
877
|
+
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
878
|
+
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
879
|
+
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
880
|
+
// it does not restrict the other tools canUseTool allows.
|
|
881
|
+
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
882
|
+
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising.value, stageTool.allowedToolName, ctx, toolName, input),
|
|
883
|
+
maxTurns: MAX_TURNS,
|
|
884
|
+
includePartialMessages: false
|
|
885
|
+
};
|
|
886
|
+
const session = createSession({ options, stageTool });
|
|
887
|
+
active = session;
|
|
888
|
+
rt = makeClaudeRuntimeSession(ctx, hooks, { interactive: true, session, stageTool, summarising });
|
|
889
|
+
return rt;
|
|
791
890
|
};
|
|
891
|
+
const result = await runInteractiveLoop(ctx, turns, emit, makeSession, {
|
|
892
|
+
signal: abortController.signal,
|
|
893
|
+
cancelled: () => cancelled,
|
|
894
|
+
cancel: () => this.cancel(),
|
|
895
|
+
// Claude carries the stage instruction in its system prompt, so the seed can be a short kickoff.
|
|
896
|
+
instructionInSystemPrompt: hasSystemPrompt(ctx)
|
|
897
|
+
});
|
|
898
|
+
rt?.dispose();
|
|
899
|
+
return result;
|
|
792
900
|
},
|
|
793
901
|
async summarise(ctx) {
|
|
794
902
|
if (!sessionId) return "(no summary: session not established)";
|
|
@@ -803,9 +911,9 @@ function createClaudeRunner() {
|
|
|
803
911
|
};
|
|
804
912
|
try {
|
|
805
913
|
let out2 = "";
|
|
806
|
-
const
|
|
807
|
-
active =
|
|
808
|
-
for await (const msg of
|
|
914
|
+
const session = createSession({ prompt: SUMMARISE_PROMPT, options });
|
|
915
|
+
active = session;
|
|
916
|
+
for await (const msg of session) {
|
|
809
917
|
const found = sessionIdOf(msg);
|
|
810
918
|
if (found) sessionId = found;
|
|
811
919
|
if (msg.type === "result" && msg.subtype === "success") out2 += msg.result;
|
|
@@ -833,6 +941,16 @@ function createClaudeRunner() {
|
|
|
833
941
|
import { join as join4 } from "path";
|
|
834
942
|
|
|
835
943
|
// ../../packages/executor-worktree/src/pi-mappers.ts
|
|
944
|
+
function parsePiEvent(msg) {
|
|
945
|
+
if (typeof msg !== "object" || msg === null) return null;
|
|
946
|
+
const type = msg.type;
|
|
947
|
+
if (typeof type !== "string") return null;
|
|
948
|
+
if (type === "message_update") {
|
|
949
|
+
const ame = msg.assistantMessageEvent;
|
|
950
|
+
if (typeof ame !== "object" || ame === null) return null;
|
|
951
|
+
}
|
|
952
|
+
return msg;
|
|
953
|
+
}
|
|
836
954
|
function mapUsage2(u) {
|
|
837
955
|
return {
|
|
838
956
|
input: u?.input ?? 0,
|
|
@@ -929,12 +1047,8 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
929
1047
|
if (ev.type === "turn_end" || ev.type === "agent_end") {
|
|
930
1048
|
const r = mapPiEvent(ev);
|
|
931
1049
|
const status = settleStatus(settleMessage(ev));
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (status === "ok" && text && !state.turnEndedOnTool) {
|
|
935
|
-
responseText = text;
|
|
936
|
-
events.push({ type: "response", text });
|
|
937
|
-
}
|
|
1050
|
+
const decision = decideResponse(state.bufferedText, state.turnEndedOnTool, status);
|
|
1051
|
+
if (decision.event) events.push(decision.event);
|
|
938
1052
|
if (state.pendingThought) {
|
|
939
1053
|
events.push({ type: "thought", subtype: "reasoning_text", text: state.pendingThought });
|
|
940
1054
|
}
|
|
@@ -945,7 +1059,7 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
945
1059
|
state.bufferedText = "";
|
|
946
1060
|
state.pendingThought = "";
|
|
947
1061
|
state.turnEndedOnTool = false;
|
|
948
|
-
return { events, isResult: true, status, responseText };
|
|
1062
|
+
return { events, isResult: true, status, responseText: decision.responseText };
|
|
949
1063
|
}
|
|
950
1064
|
return { events, isResult: false };
|
|
951
1065
|
}
|
|
@@ -1007,7 +1121,6 @@ function writeStageCustomProviders(dir, hint2) {
|
|
|
1007
1121
|
}
|
|
1008
1122
|
|
|
1009
1123
|
// ../../packages/executor-worktree/src/pi-adapter.ts
|
|
1010
|
-
var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
1011
1124
|
var PI_STAGE_COMPLETE_TOOL = "dahrk_stage_complete";
|
|
1012
1125
|
var PI_ASK_USER_QUESTION_TOOL = "ask_user_question";
|
|
1013
1126
|
function piToolCallDecision(ctx, toolName, input) {
|
|
@@ -1061,20 +1174,82 @@ function createBrokeredMcpExtension(servers) {
|
|
|
1061
1174
|
}
|
|
1062
1175
|
};
|
|
1063
1176
|
}
|
|
1177
|
+
var defaultAsk2 = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
1178
|
+
function makePiRuntimeSession(s, ctx, hooks, interactive) {
|
|
1179
|
+
return {
|
|
1180
|
+
get sessionId() {
|
|
1181
|
+
return s.sessionId;
|
|
1182
|
+
},
|
|
1183
|
+
async sendTurn(text) {
|
|
1184
|
+
const state = newPiBufferState();
|
|
1185
|
+
let stageComplete = false;
|
|
1186
|
+
let summary;
|
|
1187
|
+
let stageCompleteCallId;
|
|
1188
|
+
let responseText;
|
|
1189
|
+
let status;
|
|
1190
|
+
const unsub = s.subscribe((ev) => {
|
|
1191
|
+
const rawRef = ctx.writeRaw?.(ev);
|
|
1192
|
+
if (ev.type === "tool_execution_start" && ev.toolName === PI_STAGE_COMPLETE_TOOL) {
|
|
1193
|
+
stageComplete = true;
|
|
1194
|
+
stageCompleteCallId = ev.toolCallId;
|
|
1195
|
+
const args = ev.args;
|
|
1196
|
+
if (args?.summary) summary = args.summary;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (ev.type === "tool_execution_end" && ev.toolCallId === stageCompleteCallId) return;
|
|
1200
|
+
const r = consumePiEvent(ev, state, interactive);
|
|
1201
|
+
for (const e of r.events) hooks.emit(e, rawRef);
|
|
1202
|
+
if (r.responseText) responseText = r.responseText;
|
|
1203
|
+
if (r.isResult && r.status) status = r.status;
|
|
1204
|
+
});
|
|
1205
|
+
try {
|
|
1206
|
+
await s.prompt(text);
|
|
1207
|
+
} finally {
|
|
1208
|
+
unsub();
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
stageComplete,
|
|
1212
|
+
...summary !== void 0 ? { summary } : {},
|
|
1213
|
+
...responseText !== void 0 ? { responseText } : {},
|
|
1214
|
+
...status !== void 0 ? { status } : {}
|
|
1215
|
+
};
|
|
1216
|
+
},
|
|
1217
|
+
async summariseTurn() {
|
|
1218
|
+
if (s.agent) s.agent.state.tools = [];
|
|
1219
|
+
const state = newPiBufferState();
|
|
1220
|
+
let out2;
|
|
1221
|
+
const unsub = s.subscribe((ev) => {
|
|
1222
|
+
const r = consumePiEvent(ev, state, true);
|
|
1223
|
+
if (r.responseText) out2 = r.responseText;
|
|
1224
|
+
});
|
|
1225
|
+
try {
|
|
1226
|
+
await s.prompt(SUMMARISE_PROMPT);
|
|
1227
|
+
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
return `(summary unavailable: ${e.message})`;
|
|
1230
|
+
} finally {
|
|
1231
|
+
unsub();
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
cost() {
|
|
1235
|
+
const cost = s.getSessionStats?.()?.cost;
|
|
1236
|
+
return typeof cost === "number" ? cost : void 0;
|
|
1237
|
+
},
|
|
1238
|
+
dispose() {
|
|
1239
|
+
s.dispose();
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1064
1243
|
function createPiRunner(deps = {}) {
|
|
1065
1244
|
const createSession = deps.createSession ?? defaultCreatePiSession;
|
|
1066
1245
|
const abortController = new AbortController();
|
|
1067
1246
|
const signal = abortController.signal;
|
|
1068
1247
|
let cancelled = false;
|
|
1069
1248
|
let session;
|
|
1070
|
-
let sessionId;
|
|
1071
1249
|
const openSession = async (ctx) => {
|
|
1072
1250
|
if (!session) session = await createSession(ctx);
|
|
1073
1251
|
return session;
|
|
1074
1252
|
};
|
|
1075
|
-
const captureSessionId = (s) => {
|
|
1076
|
-
if (s.sessionId) sessionId = s.sessionId;
|
|
1077
|
-
};
|
|
1078
1253
|
let sessionDisposed = false;
|
|
1079
1254
|
const disposeSession = () => {
|
|
1080
1255
|
if (sessionDisposed || !session) return;
|
|
@@ -1084,178 +1259,41 @@ function createPiRunner(deps = {}) {
|
|
|
1084
1259
|
} catch {
|
|
1085
1260
|
}
|
|
1086
1261
|
};
|
|
1087
|
-
const readSessionCost = (s) => {
|
|
1088
|
-
const cost = s.getSessionStats?.()?.cost;
|
|
1089
|
-
return typeof cost === "number" ? cost : void 0;
|
|
1090
|
-
};
|
|
1091
1262
|
return {
|
|
1092
1263
|
runtime: "pi",
|
|
1093
1264
|
async runBatch(ctx, onTrace) {
|
|
1094
|
-
const
|
|
1265
|
+
const hooks = { emit: makeEmit("pi", onTrace), ask: defaultAsk2 };
|
|
1095
1266
|
const s = await openSession(ctx);
|
|
1096
1267
|
registerToolCallGate(s, ctx);
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const r = consumePiEvent(ev, state, false);
|
|
1102
|
-
for (const e of r.events) emit(e, rawRef);
|
|
1103
|
-
if (r.isResult && r.status) status = r.status;
|
|
1104
|
-
});
|
|
1105
|
-
try {
|
|
1106
|
-
await s.prompt(resolveStagePrompt(ctx));
|
|
1107
|
-
} catch (e) {
|
|
1108
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
1109
|
-
status = "fail";
|
|
1110
|
-
} finally {
|
|
1111
|
-
unsub();
|
|
1112
|
-
}
|
|
1113
|
-
captureSessionId(s);
|
|
1114
|
-
if (cancelled) status = "fail";
|
|
1115
|
-
const costUsd = readSessionCost(s);
|
|
1116
|
-
if (status !== "ok") disposeSession();
|
|
1117
|
-
return { status, ...sessionId ? { sessionId } : {}, ...costUsd !== void 0 ? { costUsd } : {} };
|
|
1268
|
+
const rt = makePiRuntimeSession(s, ctx, hooks, false);
|
|
1269
|
+
const result = await runBatchLoop(rt, ctx, hooks, { cancelled: () => cancelled });
|
|
1270
|
+
if (result.status !== "ok") disposeSession();
|
|
1271
|
+
return result;
|
|
1118
1272
|
},
|
|
1119
1273
|
async runInteractive(ctx, turns, onTrace) {
|
|
1120
1274
|
const emit = makeEmit("pi", onTrace);
|
|
1121
1275
|
const s = await openSession(ctx);
|
|
1122
1276
|
registerToolCallGate(s, ctx);
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
let toolFired = false;
|
|
1127
|
-
let toolSummary = null;
|
|
1128
|
-
let stageCompleteCallId;
|
|
1129
|
-
let lastResponseText;
|
|
1130
|
-
const unsub = s.subscribe((ev) => {
|
|
1131
|
-
const rawRef = ctx.writeRaw?.(ev);
|
|
1132
|
-
if (ev.type === "tool_execution_start" && ev.toolName === PI_STAGE_COMPLETE_TOOL) {
|
|
1133
|
-
toolFired = true;
|
|
1134
|
-
stageCompleteCallId = ev.toolCallId;
|
|
1135
|
-
const args = ev.args;
|
|
1136
|
-
if (args?.summary) toolSummary = args.summary;
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
if (ev.type === "tool_execution_end" && ev.toolCallId === stageCompleteCallId) return;
|
|
1140
|
-
const r = consumePiEvent(ev, state, true);
|
|
1141
|
-
for (const e of r.events) emit(e, rawRef);
|
|
1142
|
-
if (r.responseText) lastResponseText = r.responseText;
|
|
1143
|
-
});
|
|
1144
|
-
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
1145
|
-
const router = createElicitTurnRouter(turns, { signal, firstReplyMs, idleMs });
|
|
1146
|
-
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
1147
|
-
let awaitingFirstReply = true;
|
|
1148
|
-
const elicitCtx = ctx;
|
|
1149
|
-
const ask = async (question) => {
|
|
1150
|
-
const outcome = await router.ask(awaitingFirstReply, () => {
|
|
1151
|
-
emit({ type: "elicitation", prompt: question.prompt, signal: "select", options: question.options });
|
|
1152
|
-
elicitCtx.emitElicit?.(question);
|
|
1153
|
-
});
|
|
1154
|
-
switch (outcome.kind) {
|
|
1155
|
-
case "reply":
|
|
1156
|
-
return `The user selected: ${outcome.text}`;
|
|
1157
|
-
case "busy":
|
|
1158
|
-
return "Only one question can be asked at a time; wait for the current one to be answered, then ask again.";
|
|
1159
|
-
case "noreply":
|
|
1160
|
-
return "No response from the user; proceed with your best judgement.";
|
|
1161
|
-
case "cancel":
|
|
1162
|
-
return "The question was cancelled.";
|
|
1163
|
-
}
|
|
1277
|
+
const makeSession = (hooks) => {
|
|
1278
|
+
s.setAskUserQuestionHandler?.((questions) => askQuestionsSequentially(questions, (q) => hooks.ask(q)));
|
|
1279
|
+
return makePiRuntimeSession(s, ctx, hooks, true);
|
|
1164
1280
|
};
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
for (; ; ) {
|
|
1172
|
-
if (exited === "tool") break;
|
|
1173
|
-
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, signal);
|
|
1174
|
-
awaitingFirstReply = false;
|
|
1175
|
-
if (race.kind === "cancelled") {
|
|
1176
|
-
exited = "cancelled";
|
|
1177
|
-
break;
|
|
1178
|
-
}
|
|
1179
|
-
if (race.kind === "idle-timeout") {
|
|
1180
|
-
exited = "timeout";
|
|
1181
|
-
break;
|
|
1182
|
-
}
|
|
1183
|
-
if (race.kind === "turns-exhausted") {
|
|
1184
|
-
exited = "gate";
|
|
1185
|
-
break;
|
|
1186
|
-
}
|
|
1187
|
-
const texts = [race.value.text];
|
|
1188
|
-
pending = humanIter.next();
|
|
1189
|
-
for (; ; ) {
|
|
1190
|
-
const more = await raceNextTurn(pending, COALESCE_MS2, signal);
|
|
1191
|
-
if (more.kind === "turn") {
|
|
1192
|
-
texts.push(more.value.text);
|
|
1193
|
-
pending = humanIter.next();
|
|
1194
|
-
continue;
|
|
1195
|
-
}
|
|
1196
|
-
if (more.kind === "cancelled") exited = "cancelled";
|
|
1197
|
-
break;
|
|
1198
|
-
}
|
|
1199
|
-
if (exited === "cancelled") break;
|
|
1200
|
-
await s.prompt(texts.join("\n"));
|
|
1201
|
-
if (toolFired && wantsTool) {
|
|
1202
|
-
exited = "tool";
|
|
1203
|
-
break;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
} catch (e) {
|
|
1207
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
1208
|
-
exited = cancelled ? "cancelled" : "gate";
|
|
1209
|
-
}
|
|
1210
|
-
let status = "ok";
|
|
1211
|
-
let summary = "";
|
|
1212
|
-
if (exited === "tool") {
|
|
1213
|
-
summary = toolSummary ?? "(stage marked complete)";
|
|
1214
|
-
} else if (exited === "gate") {
|
|
1215
|
-
lastResponseText = void 0;
|
|
1216
|
-
try {
|
|
1217
|
-
await s.prompt(SUMMARISE_PROMPT);
|
|
1218
|
-
summary = (lastResponseText ?? "").trim() || "(no summary produced)";
|
|
1219
|
-
} catch {
|
|
1220
|
-
summary = "(no summary produced)";
|
|
1221
|
-
}
|
|
1222
|
-
} else if (exited === "timeout") {
|
|
1223
|
-
status = "timeout";
|
|
1224
|
-
summary = "(stage timed out awaiting input)";
|
|
1225
|
-
await this.cancel();
|
|
1226
|
-
} else {
|
|
1227
|
-
status = "fail";
|
|
1228
|
-
summary = "(stage cancelled)";
|
|
1229
|
-
}
|
|
1230
|
-
unsub();
|
|
1231
|
-
captureSessionId(s);
|
|
1232
|
-
const costUsd = readSessionCost(s);
|
|
1281
|
+
const result = await runInteractiveLoop(ctx, turns, emit, makeSession, {
|
|
1282
|
+
signal,
|
|
1283
|
+
cancelled: () => cancelled,
|
|
1284
|
+
cancel: () => this.cancel(),
|
|
1285
|
+
instructionInSystemPrompt: false
|
|
1286
|
+
});
|
|
1233
1287
|
disposeSession();
|
|
1234
|
-
return
|
|
1235
|
-
status,
|
|
1236
|
-
summary,
|
|
1237
|
-
...sessionId ? { sessionId } : {},
|
|
1238
|
-
...costUsd !== void 0 ? { costUsd } : {}
|
|
1239
|
-
};
|
|
1288
|
+
return result;
|
|
1240
1289
|
},
|
|
1241
1290
|
async summarise(ctx) {
|
|
1242
1291
|
if (!session) return "(no summary: session not established)";
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
const state = newPiBufferState();
|
|
1246
|
-
let out2;
|
|
1247
|
-
const unsub = s.subscribe((ev) => {
|
|
1248
|
-
const r = consumePiEvent(ev, state, true);
|
|
1249
|
-
if (r.responseText) out2 = r.responseText;
|
|
1250
|
-
});
|
|
1292
|
+
const rt = makePiRuntimeSession(session, ctx, { emit: () => {
|
|
1293
|
+
}, ask: defaultAsk2 }, true);
|
|
1251
1294
|
try {
|
|
1252
|
-
await
|
|
1253
|
-
captureSessionId(s);
|
|
1254
|
-
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1255
|
-
} catch (e) {
|
|
1256
|
-
return `(summary unavailable: ${e.message})`;
|
|
1295
|
+
return await rt.summariseTurn();
|
|
1257
1296
|
} finally {
|
|
1258
|
-
unsub();
|
|
1259
1297
|
disposeSession();
|
|
1260
1298
|
}
|
|
1261
1299
|
},
|
|
@@ -1338,7 +1376,7 @@ async function defaultCreatePiSession(ctx) {
|
|
|
1338
1376
|
},
|
|
1339
1377
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1340
1378
|
execute: async (_toolCallId, params) => {
|
|
1341
|
-
const text = askHandler ? await askHandler(params.questions) :
|
|
1379
|
+
const text = askHandler ? await askHandler(params.questions) : elicitOutcomeReply({ kind: "noreply" });
|
|
1342
1380
|
return { content: [{ type: "text", text }], details: {} };
|
|
1343
1381
|
}
|
|
1344
1382
|
});
|
|
@@ -1557,7 +1595,8 @@ var PiRpcSession = class {
|
|
|
1557
1595
|
}
|
|
1558
1596
|
return;
|
|
1559
1597
|
}
|
|
1560
|
-
const ev = msg;
|
|
1598
|
+
const ev = parsePiEvent(msg);
|
|
1599
|
+
if (!ev) return;
|
|
1561
1600
|
for (const l of [...this.#listeners]) l(ev);
|
|
1562
1601
|
if (ev.type === "agent_end" && this.#pendingAgentEnd) {
|
|
1563
1602
|
const d = this.#pendingAgentEnd;
|
|
@@ -1620,10 +1659,67 @@ import { execFileSync } from "child_process";
|
|
|
1620
1659
|
import { existsSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1621
1660
|
import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
|
|
1622
1661
|
import { basename, dirname, isAbsolute, join as join5 } from "path";
|
|
1662
|
+
|
|
1663
|
+
// ../../packages/executor-worktree/src/footprint.ts
|
|
1664
|
+
function resolveRenamePath(raw) {
|
|
1665
|
+
const brace = /\{(.*?) => (.*?)\}/;
|
|
1666
|
+
if (brace.test(raw)) {
|
|
1667
|
+
return raw.replace(brace, (_m, _old, next) => next).replace(/\/{2,}/g, "/");
|
|
1668
|
+
}
|
|
1669
|
+
const arrow2 = raw.indexOf(" => ");
|
|
1670
|
+
if (arrow2 !== -1) return raw.slice(arrow2 + " => ".length);
|
|
1671
|
+
return raw;
|
|
1672
|
+
}
|
|
1673
|
+
function parseNumstat(raw) {
|
|
1674
|
+
const entries = [];
|
|
1675
|
+
for (const line of raw.split("\n")) {
|
|
1676
|
+
if (line.trim() === "") continue;
|
|
1677
|
+
const firstTab = line.indexOf(" ");
|
|
1678
|
+
const secondTab = line.indexOf(" ", firstTab + 1);
|
|
1679
|
+
if (firstTab === -1 || secondTab === -1) continue;
|
|
1680
|
+
const addedRaw = line.slice(0, firstTab);
|
|
1681
|
+
const removedRaw = line.slice(firstTab + 1, secondTab);
|
|
1682
|
+
const pathRaw = line.slice(secondTab + 1);
|
|
1683
|
+
const binary = addedRaw === "-" && removedRaw === "-";
|
|
1684
|
+
entries.push({
|
|
1685
|
+
path: resolveRenamePath(pathRaw),
|
|
1686
|
+
added: binary ? 0 : Number.parseInt(addedRaw, 10) || 0,
|
|
1687
|
+
removed: binary ? 0 : Number.parseInt(removedRaw, 10) || 0,
|
|
1688
|
+
binary
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return entries;
|
|
1692
|
+
}
|
|
1693
|
+
function topLevel(path) {
|
|
1694
|
+
const slash = path.indexOf("/");
|
|
1695
|
+
return slash === -1 ? path : path.slice(0, slash);
|
|
1696
|
+
}
|
|
1697
|
+
function deriveFootprint(entries, opts) {
|
|
1698
|
+
let added = 0;
|
|
1699
|
+
let removed = 0;
|
|
1700
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
1701
|
+
const allPaths = [];
|
|
1702
|
+
for (const e of entries) {
|
|
1703
|
+
added += e.added;
|
|
1704
|
+
removed += e.removed;
|
|
1705
|
+
scopeSet.add(topLevel(e.path));
|
|
1706
|
+
allPaths.push(e.path);
|
|
1707
|
+
}
|
|
1708
|
+
const changedPathsTruncated = allPaths.length > opts.cap;
|
|
1709
|
+
return {
|
|
1710
|
+
numstat: { files: entries.length, added, removed },
|
|
1711
|
+
scope: [...scopeSet].sort(),
|
|
1712
|
+
changedPaths: changedPathsTruncated ? allPaths.slice(0, opts.cap) : allPaths,
|
|
1713
|
+
changedPathsTruncated
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ../../packages/executor-worktree/src/git-service.ts
|
|
1623
1718
|
var noopLogger = { info: () => {
|
|
1624
1719
|
}, warn: () => {
|
|
1625
1720
|
} };
|
|
1626
1721
|
var SCRATCH_DIR = ".dahrk/scratch";
|
|
1722
|
+
var CHANGED_PATHS_CAP = 100;
|
|
1627
1723
|
var DEFAULT_MERGE_RESOLVE_RULES = [
|
|
1628
1724
|
{ path: "pnpm-lock.yaml", strategy: "theirs" },
|
|
1629
1725
|
{ path: "package-lock.json", strategy: "theirs" },
|
|
@@ -1956,6 +2052,7 @@ function createGitService(opts = {}) {
|
|
|
1956
2052
|
const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
|
|
1957
2053
|
let pushed = false;
|
|
1958
2054
|
let integration;
|
|
2055
|
+
let footprint;
|
|
1959
2056
|
try {
|
|
1960
2057
|
let fetched = false;
|
|
1961
2058
|
if (opts2.base) {
|
|
@@ -1975,6 +2072,12 @@ function createGitService(opts = {}) {
|
|
|
1975
2072
|
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1976
2073
|
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1977
2074
|
}
|
|
2075
|
+
const numstatEntries = parseNumstat(git(worktreePath, ["diff", "--numstat", "FETCH_HEAD...HEAD"])).filter(
|
|
2076
|
+
(e) => !isScratchPath(e.path)
|
|
2077
|
+
);
|
|
2078
|
+
if (numstatEntries.length > 0) {
|
|
2079
|
+
footprint = deriveFootprint(numstatEntries, { cap: CHANGED_PATHS_CAP });
|
|
2080
|
+
}
|
|
1978
2081
|
try {
|
|
1979
2082
|
git(worktreePath, [
|
|
1980
2083
|
"-c",
|
|
@@ -2009,7 +2112,7 @@ function createGitService(opts = {}) {
|
|
|
2009
2112
|
headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
2010
2113
|
} else {
|
|
2011
2114
|
git(worktreePath, ["merge", "--abort"]);
|
|
2012
|
-
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles };
|
|
2115
|
+
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles, ...footprint ? { footprint } : {} };
|
|
2013
2116
|
}
|
|
2014
2117
|
} else {
|
|
2015
2118
|
const msg = mergeErr.message;
|
|
@@ -2025,7 +2128,7 @@ function createGitService(opts = {}) {
|
|
|
2025
2128
|
} finally {
|
|
2026
2129
|
cleanup();
|
|
2027
2130
|
}
|
|
2028
|
-
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {} };
|
|
2131
|
+
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {}, ...footprint ? { footprint } : {} };
|
|
2029
2132
|
},
|
|
2030
2133
|
async backupPush(ref, opts2) {
|
|
2031
2134
|
const { worktreePath } = ref;
|
|
@@ -2120,12 +2223,16 @@ import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync3
|
|
|
2120
2223
|
import { join as join6 } from "path";
|
|
2121
2224
|
var MINUTE = 6e4;
|
|
2122
2225
|
var HOUR = 60 * MINUTE;
|
|
2226
|
+
var DAY = 24 * HOUR;
|
|
2123
2227
|
var DEFAULTS = {
|
|
2124
2228
|
// Deliberately non-optional defaults. "No policy configured" used to mean "never collect anything",
|
|
2125
2229
|
// which is precisely how the disk reached 65 GB. Absent config must mean sane collection, not none.
|
|
2126
2230
|
maxRuns: 20,
|
|
2127
2231
|
maxIdleMs: 6 * HOUR,
|
|
2128
|
-
activityGraceMs: 30 * MINUTE
|
|
2232
|
+
activityGraceMs: 30 * MINUTE,
|
|
2233
|
+
// 14 days: long enough that an operator who missed the park log can still recover unpushed work, but
|
|
2234
|
+
// bounded so parked tips cannot pile up for ever. Cited verbatim in docs/logging.md - keep in step.
|
|
2235
|
+
salvageTtlMs: 14 * DAY
|
|
2129
2236
|
};
|
|
2130
2237
|
var noop = { info: () => {
|
|
2131
2238
|
}, warn: () => {
|
|
@@ -2166,6 +2273,38 @@ function registeredWorktrees(mirror) {
|
|
|
2166
2273
|
}
|
|
2167
2274
|
return out2.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
2168
2275
|
}
|
|
2276
|
+
function salvageParkedMs(mirror, ref) {
|
|
2277
|
+
try {
|
|
2278
|
+
return statSync(join6(mirror, ref)).mtimeMs;
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
try {
|
|
2282
|
+
const ts = gitOut(mirror, ["log", "-g", "--format=%ct", "-1", ref]).trim();
|
|
2283
|
+
if (ts) return Number(ts) * 1e3;
|
|
2284
|
+
} catch {
|
|
2285
|
+
}
|
|
2286
|
+
return void 0;
|
|
2287
|
+
}
|
|
2288
|
+
function sweepSalvageRefs(mirror, ttlMs, dryRun, now, log) {
|
|
2289
|
+
let refs;
|
|
2290
|
+
try {
|
|
2291
|
+
refs = gitOut(mirror, ["for-each-ref", "--format=%(refname)", "refs/dahrk/salvage/"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2292
|
+
} catch {
|
|
2293
|
+
return 0;
|
|
2294
|
+
}
|
|
2295
|
+
let parked = 0;
|
|
2296
|
+
for (const ref of refs) {
|
|
2297
|
+
const at = salvageParkedMs(mirror, ref);
|
|
2298
|
+
const expired = at !== void 0 && now - at > ttlMs;
|
|
2299
|
+
if (expired && !dryRun) {
|
|
2300
|
+
if (!gitOk(mirror, ["update-ref", "-d", ref])) log.warn(`reaper: could not expire salvage ref ${ref}`);
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
if (expired) log.info(`reaper (dry-run): would expire salvage ref ${ref}`);
|
|
2304
|
+
parked++;
|
|
2305
|
+
}
|
|
2306
|
+
return parked;
|
|
2307
|
+
}
|
|
2169
2308
|
function createWorktreeReaper(opts) {
|
|
2170
2309
|
const log = opts.logger ?? noop;
|
|
2171
2310
|
const mirrors = () => {
|
|
@@ -2184,8 +2323,9 @@ function createWorktreeReaper(opts) {
|
|
|
2184
2323
|
const maxRuns = policy.maxRuns ?? DEFAULTS.maxRuns;
|
|
2185
2324
|
const maxIdleMs = policy.maxIdleMs ?? DEFAULTS.maxIdleMs;
|
|
2186
2325
|
const graceMs = policy.activityGraceMs ?? DEFAULTS.activityGraceMs;
|
|
2326
|
+
const salvageTtlMs = policy.salvageTtlMs ?? DEFAULTS.salvageTtlMs;
|
|
2187
2327
|
const dryRun = policy.dryRun ?? false;
|
|
2188
|
-
const report = { scanned: 0, reaped: [], skipped: 0, errors: [] };
|
|
2328
|
+
const report = { scanned: 0, reaped: [], skipped: 0, salvagedRefs: 0, errors: [] };
|
|
2189
2329
|
const now = Date.now();
|
|
2190
2330
|
const registered = /* @__PURE__ */ new Map();
|
|
2191
2331
|
for (const m of mirrors()) {
|
|
@@ -2247,6 +2387,14 @@ function createWorktreeReaper(opts) {
|
|
|
2247
2387
|
if (report.reaped.length) {
|
|
2248
2388
|
log.info(`reaper: reaped ${report.reaped.length} worktree(s), skipped ${report.skipped}`);
|
|
2249
2389
|
}
|
|
2390
|
+
for (const m of registered.keys()) {
|
|
2391
|
+
report.salvagedRefs += sweepSalvageRefs(m, salvageTtlMs, dryRun, now, log);
|
|
2392
|
+
}
|
|
2393
|
+
if (report.salvagedRefs) {
|
|
2394
|
+
log.info(
|
|
2395
|
+
`reaper: ${report.salvagedRefs} salvage ref(s) parked, expire ${Math.round(salvageTtlMs / DAY)}d after parking`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2250
2398
|
return report;
|
|
2251
2399
|
}
|
|
2252
2400
|
};
|
|
@@ -3520,6 +3668,77 @@ async function startMcpGateway(opts) {
|
|
|
3520
3668
|
};
|
|
3521
3669
|
}
|
|
3522
3670
|
|
|
3671
|
+
// ../../packages/edge/src/push-outcome.ts
|
|
3672
|
+
function resolveDeliverOutcome(r, job, pr) {
|
|
3673
|
+
const { jobId, branch, base } = job;
|
|
3674
|
+
if (r.integration === "noop") {
|
|
3675
|
+
return {
|
|
3676
|
+
jobId,
|
|
3677
|
+
status: "ok",
|
|
3678
|
+
branch,
|
|
3679
|
+
headSha: r.headSha,
|
|
3680
|
+
pushed: false,
|
|
3681
|
+
nothingToCommit: true,
|
|
3682
|
+
commitsAhead: r.commitsAhead,
|
|
3683
|
+
summary: `no changes to deliver on ${branch} - work already present on ${base}`
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
if (r.integration === "conflict") {
|
|
3687
|
+
return {
|
|
3688
|
+
jobId,
|
|
3689
|
+
status: "ok",
|
|
3690
|
+
branch,
|
|
3691
|
+
headSha: r.headSha,
|
|
3692
|
+
pushed: false,
|
|
3693
|
+
nothingToCommit: r.nothingToCommit,
|
|
3694
|
+
commitsAhead: r.commitsAhead,
|
|
3695
|
+
integration: "conflict",
|
|
3696
|
+
...r.conflictFiles ? { conflictFiles: r.conflictFiles } : {},
|
|
3697
|
+
...r.footprint ?? {},
|
|
3698
|
+
summary: `base advanced; merge conflict on ${branch} (manual merge needed)`
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
if (r.integration === "diverged") {
|
|
3702
|
+
return {
|
|
3703
|
+
jobId,
|
|
3704
|
+
status: "fail",
|
|
3705
|
+
branch,
|
|
3706
|
+
headSha: r.headSha,
|
|
3707
|
+
pushed: false,
|
|
3708
|
+
nothingToCommit: r.nothingToCommit,
|
|
3709
|
+
commitsAhead: r.commitsAhead,
|
|
3710
|
+
summary: `branch history diverged from ${base}; cannot auto-integrate on ${branch} (the branch likely needs rebuilding from ${base})`
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
return {
|
|
3714
|
+
jobId,
|
|
3715
|
+
status: "ok",
|
|
3716
|
+
branch,
|
|
3717
|
+
headSha: r.headSha,
|
|
3718
|
+
pushed: r.pushed,
|
|
3719
|
+
nothingToCommit: r.nothingToCommit,
|
|
3720
|
+
commitsAhead: r.commitsAhead,
|
|
3721
|
+
...r.integration ? { integration: r.integration } : {},
|
|
3722
|
+
...pr?.prUrl ? { prUrl: pr.prUrl } : {},
|
|
3723
|
+
...pr?.prNumber !== void 0 ? { prNumber: pr.prNumber } : {},
|
|
3724
|
+
...pr?.prError ? { prError: pr.prError } : {},
|
|
3725
|
+
...r.footprint ?? {},
|
|
3726
|
+
summary: r.nothingToCommit ? `no changes to commit; ${r.pushed ? "branch pushed" : "nothing pushed"}` : `committed ${r.headSha.slice(0, 7)} and pushed ${branch}`
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
function resolveBackupOutcome(r, job) {
|
|
3730
|
+
return {
|
|
3731
|
+
jobId: job.jobId,
|
|
3732
|
+
status: "ok",
|
|
3733
|
+
branch: job.branch,
|
|
3734
|
+
headSha: r.headSha,
|
|
3735
|
+
pushed: r.pushed,
|
|
3736
|
+
nothingToCommit: r.nothingToCommit,
|
|
3737
|
+
wipRef: r.wipRef,
|
|
3738
|
+
summary: `backup: preserved ${r.headSha.slice(0, 7)} on ${r.wipRef} (no base merge, no PR)`
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3523
3742
|
// ../../packages/edge/src/stage-runner.ts
|
|
3524
3743
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3525
3744
|
var PREVIEW = 500;
|
|
@@ -4135,17 +4354,7 @@ function createStageRunner(deps) {
|
|
|
4135
4354
|
branch: job.branch,
|
|
4136
4355
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
4137
4356
|
});
|
|
4138
|
-
|
|
4139
|
-
jobId,
|
|
4140
|
-
status: "ok",
|
|
4141
|
-
branch: job.branch,
|
|
4142
|
-
headSha: r.headSha,
|
|
4143
|
-
pushed: r.pushed,
|
|
4144
|
-
nothingToCommit: r.nothingToCommit,
|
|
4145
|
-
wipRef: r.wipRef,
|
|
4146
|
-
summary: `backup: preserved ${r.headSha.slice(0, 7)} on ${r.wipRef} (no base merge, no PR)`
|
|
4147
|
-
};
|
|
4148
|
-
return result;
|
|
4357
|
+
return resolveBackupOutcome(r, { jobId, branch: job.branch });
|
|
4149
4358
|
} catch (e) {
|
|
4150
4359
|
return { jobId, status: "fail", branch: job.branch, summary: `backup push failed: ${e.message}` };
|
|
4151
4360
|
}
|
|
@@ -4170,64 +4379,13 @@ function createStageRunner(deps) {
|
|
|
4170
4379
|
base: job.base,
|
|
4171
4380
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
4172
4381
|
});
|
|
4173
|
-
if (r.integration === "noop") {
|
|
4174
|
-
return {
|
|
4175
|
-
jobId,
|
|
4176
|
-
status: "ok",
|
|
4177
|
-
branch: job.branch,
|
|
4178
|
-
headSha: r.headSha,
|
|
4179
|
-
pushed: false,
|
|
4180
|
-
nothingToCommit: true,
|
|
4181
|
-
commitsAhead: r.commitsAhead,
|
|
4182
|
-
summary: `no changes to deliver on ${job.branch} - work already present on ${job.base}`
|
|
4183
|
-
};
|
|
4184
|
-
}
|
|
4185
|
-
if (r.integration === "conflict") {
|
|
4186
|
-
return {
|
|
4187
|
-
jobId,
|
|
4188
|
-
status: "ok",
|
|
4189
|
-
branch: job.branch,
|
|
4190
|
-
headSha: r.headSha,
|
|
4191
|
-
pushed: false,
|
|
4192
|
-
nothingToCommit: r.nothingToCommit,
|
|
4193
|
-
commitsAhead: r.commitsAhead,
|
|
4194
|
-
integration: "conflict",
|
|
4195
|
-
...r.conflictFiles ? { conflictFiles: r.conflictFiles } : {},
|
|
4196
|
-
summary: `base advanced; merge conflict on ${job.branch} (manual merge needed)`
|
|
4197
|
-
};
|
|
4198
|
-
}
|
|
4199
|
-
if (r.integration === "diverged") {
|
|
4200
|
-
return {
|
|
4201
|
-
jobId,
|
|
4202
|
-
status: "fail",
|
|
4203
|
-
branch: job.branch,
|
|
4204
|
-
headSha: r.headSha,
|
|
4205
|
-
pushed: false,
|
|
4206
|
-
nothingToCommit: r.nothingToCommit,
|
|
4207
|
-
commitsAhead: r.commitsAhead,
|
|
4208
|
-
summary: `branch history diverged from ${job.base}; cannot auto-integrate on ${job.branch} (the branch likely needs rebuilding from ${job.base})`
|
|
4209
|
-
};
|
|
4210
|
-
}
|
|
4211
4382
|
const pr = job.openPr && r.pushed ? await deps.gitService.openPrAmbient(ref, {
|
|
4212
4383
|
branch: job.branch,
|
|
4213
4384
|
base: job.base,
|
|
4214
4385
|
title: job.openPr.title,
|
|
4215
4386
|
body: job.openPr.body
|
|
4216
4387
|
}) : void 0;
|
|
4217
|
-
return {
|
|
4218
|
-
jobId,
|
|
4219
|
-
status: "ok",
|
|
4220
|
-
branch: job.branch,
|
|
4221
|
-
headSha: r.headSha,
|
|
4222
|
-
pushed: r.pushed,
|
|
4223
|
-
nothingToCommit: r.nothingToCommit,
|
|
4224
|
-
commitsAhead: r.commitsAhead,
|
|
4225
|
-
...r.integration ? { integration: r.integration } : {},
|
|
4226
|
-
...pr?.prUrl ? { prUrl: pr.prUrl } : {},
|
|
4227
|
-
...pr?.prNumber !== void 0 ? { prNumber: pr.prNumber } : {},
|
|
4228
|
-
...pr?.prError ? { prError: pr.prError } : {},
|
|
4229
|
-
summary: r.nothingToCommit ? `no changes to commit; ${r.pushed ? "branch pushed" : "nothing pushed"}` : `committed ${r.headSha.slice(0, 7)} and pushed ${job.branch}`
|
|
4230
|
-
};
|
|
4388
|
+
return resolveDeliverOutcome(r, { jobId, branch: job.branch, base: job.base }, pr);
|
|
4231
4389
|
} catch (e) {
|
|
4232
4390
|
return { jobId, status: "fail", summary: `push failed: ${e.message}` };
|
|
4233
4391
|
}
|
|
@@ -4414,10 +4572,10 @@ async function startEdgeNode(opts) {
|
|
|
4414
4572
|
await reconcileInterruptedJobs();
|
|
4415
4573
|
void stageRunner.reapWorktrees().then((r) => {
|
|
4416
4574
|
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
4417
|
-
if (r.reaped.length) {
|
|
4575
|
+
if (r.reaped.length || r.salvagedRefs) {
|
|
4418
4576
|
log.info(
|
|
4419
|
-
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped },
|
|
4420
|
-
`EDGE_REAPED:${r.reaped.length} worktrees (scanned ${r.scanned}, skipped ${r.skipped})`
|
|
4577
|
+
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped, salvagedRefs: r.salvagedRefs },
|
|
4578
|
+
`EDGE_REAPED:${r.reaped.length} worktrees (scanned ${r.scanned}, skipped ${r.skipped}, salvaged ${r.salvagedRefs})`
|
|
4421
4579
|
);
|
|
4422
4580
|
}
|
|
4423
4581
|
for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
|
|
@@ -5781,7 +5939,8 @@ function driftOf(stored, repo) {
|
|
|
5781
5939
|
const drift = {};
|
|
5782
5940
|
if (typeof stored.defaultBranch === "string" && stored.defaultBranch !== repo.defaultBranch) drift.branch = stored.defaultBranch;
|
|
5783
5941
|
if (typeof stored.name === "string" && stored.name !== repo.name) drift.name = stored.name;
|
|
5784
|
-
|
|
5942
|
+
if (typeof stored.gitUrl === "string" && stored.gitUrl !== repo.gitUrl) drift.gitUrl = stored.gitUrl;
|
|
5943
|
+
return drift.branch || drift.name || drift.gitUrl ? drift : void 0;
|
|
5785
5944
|
}
|
|
5786
5945
|
async function registerRepo(deps, args) {
|
|
5787
5946
|
const url = `${args.base}/config/api/repositories`;
|
|
@@ -7229,7 +7388,7 @@ async function runStatus(inputs, deps) {
|
|
|
7229
7388
|
}
|
|
7230
7389
|
|
|
7231
7390
|
// src/main.ts
|
|
7232
|
-
var CLIENT_VERSION = "0.1.
|
|
7391
|
+
var CLIENT_VERSION = "0.1.22";
|
|
7233
7392
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
7234
7393
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7235
7394
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -7622,6 +7781,7 @@ async function runRepoAdd(flags) {
|
|
|
7622
7781
|
const bits = [];
|
|
7623
7782
|
if (result.drift.branch) bits.push(`branch ${result.drift.branch} (this repo is on ${repo.defaultBranch})`);
|
|
7624
7783
|
if (result.drift.name) bits.push(`name ${result.drift.name}`);
|
|
7784
|
+
if (result.drift.gitUrl) bits.push(`URL ${result.drift.gitUrl} (this repo registers as ${repo.gitUrl})`);
|
|
7625
7785
|
out(hint(`The hub's stored record differs - ${bits.join(", ")} - and was left unchanged.`));
|
|
7626
7786
|
}
|
|
7627
7787
|
return 0;
|