dahrk-node 0.1.21 → 0.1.23
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 +668 -457
- 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,165 @@ 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
|
+
function classifyRuntimeError(message) {
|
|
538
|
+
const m = message.toLowerCase();
|
|
539
|
+
const transient = m.includes("stream idle timeout") || m.includes("partial response received") || m.includes("overloaded") || /\b529\b/.test(m) || m.includes("rate limit") || m.includes("rate_limit") || m.includes("too many requests") || /\b429\b/.test(m) || m.includes("gateway timeout") || m.includes("bad gateway") || m.includes("service unavailable") || m.includes("internal server error") || /\b50[0234]\b/.test(m) || m.includes("econnreset") || m.includes("connection reset") || m.includes("socket hang up") || m.includes("connection error");
|
|
540
|
+
return transient ? "external" : void 0;
|
|
541
|
+
}
|
|
542
|
+
async function runBatchLoop(session, ctx, hooks, opts) {
|
|
543
|
+
let status = "ok";
|
|
544
|
+
let failureClass;
|
|
545
|
+
let summary;
|
|
546
|
+
try {
|
|
547
|
+
const tr = await session.sendTurn(resolveStagePrompt(ctx));
|
|
548
|
+
if (tr.status) status = tr.status;
|
|
549
|
+
} catch (e) {
|
|
550
|
+
const message = e.message;
|
|
551
|
+
if (!opts.cancelled()) {
|
|
552
|
+
hooks.emit({ type: "error", kind: "runtime_error", message });
|
|
553
|
+
failureClass = classifyRuntimeError(message);
|
|
554
|
+
if (failureClass) summary = `upstream API transient: ${message}`;
|
|
555
|
+
}
|
|
556
|
+
status = "fail";
|
|
557
|
+
}
|
|
558
|
+
if (opts.cancelled()) status = "fail";
|
|
559
|
+
const costUsd = session.cost();
|
|
560
|
+
const sessionId = session.sessionId;
|
|
561
|
+
return {
|
|
562
|
+
status,
|
|
563
|
+
...summary !== void 0 ? { summary } : {},
|
|
564
|
+
...failureClass ? { failureClass } : {},
|
|
565
|
+
...sessionId ? { sessionId } : {},
|
|
566
|
+
...costUsd !== void 0 ? { costUsd } : {}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
420
570
|
// ../../packages/executor-worktree/src/stage-complete-tool.ts
|
|
421
571
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
422
572
|
import { z } from "zod";
|
|
@@ -424,6 +574,10 @@ var STAGE_COMPLETE_TOOL_NAME = "mcp__dahrk__dahrk_stage_complete";
|
|
|
424
574
|
function createStageCompleteTool() {
|
|
425
575
|
let captured = null;
|
|
426
576
|
let capturedDoc = null;
|
|
577
|
+
const capture = (args) => {
|
|
578
|
+
captured = args.summary;
|
|
579
|
+
if (args.document !== void 0) capturedDoc = args.document;
|
|
580
|
+
};
|
|
427
581
|
const completeTool = tool(
|
|
428
582
|
"dahrk_stage_complete",
|
|
429
583
|
"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 +586,7 @@ function createStageCompleteTool() {
|
|
|
432
586
|
document: z.string().optional().describe("The full markdown body of the stage's deliverable document, if any.")
|
|
433
587
|
},
|
|
434
588
|
async (args) => {
|
|
435
|
-
|
|
436
|
-
if (args.document !== void 0) capturedDoc = args.document;
|
|
589
|
+
capture(args);
|
|
437
590
|
return { content: [{ type: "text", text: "Stage marked complete." }] };
|
|
438
591
|
}
|
|
439
592
|
);
|
|
@@ -442,7 +595,8 @@ function createStageCompleteTool() {
|
|
|
442
595
|
allowedToolName: STAGE_COMPLETE_TOOL_NAME,
|
|
443
596
|
fired: () => captured !== null,
|
|
444
597
|
summary: () => captured,
|
|
445
|
-
document: () => capturedDoc
|
|
598
|
+
document: () => capturedDoc,
|
|
599
|
+
capture
|
|
446
600
|
};
|
|
447
601
|
}
|
|
448
602
|
|
|
@@ -496,7 +650,6 @@ function createAskUserQuestionTool(deps) {
|
|
|
496
650
|
}
|
|
497
651
|
|
|
498
652
|
// ../../packages/executor-worktree/src/claude-adapter.ts
|
|
499
|
-
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
500
653
|
var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
|
|
501
654
|
var HANDED_BACK_ARTIFACT_PATH = ".dahrk/scratch/output/document.md";
|
|
502
655
|
var CLAUDE_CODE_SYSTEM_PROMPT = { type: "preset", preset: "claude_code" };
|
|
@@ -505,6 +658,7 @@ var userMsg = (text) => ({
|
|
|
505
658
|
parent_tool_use_id: null,
|
|
506
659
|
message: { role: "user", content: text }
|
|
507
660
|
});
|
|
661
|
+
var defaultAsk = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
508
662
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
509
663
|
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
510
664
|
const verdict2 = ctx.authorizeToolUse?.(toolName, input);
|
|
@@ -544,7 +698,25 @@ function runtimeEnvOptions(ctx) {
|
|
|
544
698
|
if (!ctx.runtimeEnv) return {};
|
|
545
699
|
return { env: { ...process.env, ...ctx.runtimeEnv } };
|
|
546
700
|
}
|
|
547
|
-
|
|
701
|
+
var defaultCreateClaudeSession = ({ prompt, options }) => {
|
|
702
|
+
const mailbox = prompt === void 0 ? new ManagedMailbox() : void 0;
|
|
703
|
+
const q = query({ prompt: mailbox ?? prompt, options });
|
|
704
|
+
const it = q[Symbol.asyncIterator]();
|
|
705
|
+
return {
|
|
706
|
+
[Symbol.asyncIterator]: () => it,
|
|
707
|
+
push: (m) => mailbox?.push(m),
|
|
708
|
+
end: () => mailbox?.end(),
|
|
709
|
+
close: () => {
|
|
710
|
+
try {
|
|
711
|
+
q.close();
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
interrupt: () => q.interrupt()
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
function createClaudeRunner(deps = {}) {
|
|
719
|
+
const createSession = deps.createSession ?? defaultCreateClaudeSession;
|
|
548
720
|
const abortController = new AbortController();
|
|
549
721
|
let cancelled = false;
|
|
550
722
|
let active;
|
|
@@ -609,11 +781,86 @@ function createClaudeRunner() {
|
|
|
609
781
|
}
|
|
610
782
|
active = void 0;
|
|
611
783
|
};
|
|
784
|
+
const makeClaudeRuntimeSession = (ctx, hooks, mode) => {
|
|
785
|
+
const state = newBufferState();
|
|
786
|
+
const it = mode.interactive ? mode.session[Symbol.asyncIterator]() : void 0;
|
|
787
|
+
const consumeInteractiveTurn = async () => {
|
|
788
|
+
for (; ; ) {
|
|
789
|
+
const { value: msg, done } = await it.next();
|
|
790
|
+
if (done) return {};
|
|
791
|
+
const res = handleMessage(msg, hooks.emit, ctx, state, true);
|
|
792
|
+
if (res.isResult) return { status: res.status, responseText: res.responseText };
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
return {
|
|
796
|
+
get sessionId() {
|
|
797
|
+
return sessionId;
|
|
798
|
+
},
|
|
799
|
+
async sendTurn(text) {
|
|
800
|
+
if (!mode.interactive) {
|
|
801
|
+
const session = createSession({ prompt: text, options: mode.options });
|
|
802
|
+
active = session;
|
|
803
|
+
let status2;
|
|
804
|
+
try {
|
|
805
|
+
for await (const msg of session) {
|
|
806
|
+
const res = handleMessage(msg, hooks.emit, ctx, state, false);
|
|
807
|
+
if (res.isResult && res.status) status2 = res.status;
|
|
808
|
+
}
|
|
809
|
+
} finally {
|
|
810
|
+
closeActive();
|
|
811
|
+
}
|
|
812
|
+
return { stageComplete: false, ...status2 !== void 0 ? { status: status2 } : {} };
|
|
813
|
+
}
|
|
814
|
+
mode.session.push(userMsg(text));
|
|
815
|
+
const { status, responseText } = await consumeInteractiveTurn();
|
|
816
|
+
const stageComplete = mode.stageTool.fired();
|
|
817
|
+
const doc = stageComplete ? mode.stageTool.document() : null;
|
|
818
|
+
return {
|
|
819
|
+
stageComplete,
|
|
820
|
+
...stageComplete ? { summary: mode.stageTool.summary() ?? void 0 } : {},
|
|
821
|
+
...responseText !== void 0 ? { responseText } : {},
|
|
822
|
+
...status !== void 0 ? { status } : {},
|
|
823
|
+
...doc !== null ? { artifact: { path: ctx.config.emitArtifact ?? HANDED_BACK_ARTIFACT_PATH, content: doc } } : {}
|
|
824
|
+
};
|
|
825
|
+
},
|
|
826
|
+
async summariseTurn() {
|
|
827
|
+
if (!mode.interactive) return "(no summary produced)";
|
|
828
|
+
mode.summarising.value = true;
|
|
829
|
+
try {
|
|
830
|
+
mode.session.push(userMsg(SUMMARISE_PROMPT));
|
|
831
|
+
const { responseText } = await consumeInteractiveTurn();
|
|
832
|
+
return (responseText ?? "").trim() || "(no summary produced)";
|
|
833
|
+
} catch {
|
|
834
|
+
return "(no summary produced)";
|
|
835
|
+
} finally {
|
|
836
|
+
mode.summarising.value = false;
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
cost() {
|
|
840
|
+
return costUsd;
|
|
841
|
+
},
|
|
842
|
+
dispose() {
|
|
843
|
+
if (!mode.interactive) return;
|
|
844
|
+
const iter = it;
|
|
845
|
+
mode.session.end();
|
|
846
|
+
void (async () => {
|
|
847
|
+
try {
|
|
848
|
+
for (; ; ) {
|
|
849
|
+
const { done } = await iter.next();
|
|
850
|
+
if (done) break;
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
} finally {
|
|
854
|
+
closeActive();
|
|
855
|
+
}
|
|
856
|
+
})();
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
};
|
|
612
860
|
return {
|
|
613
861
|
runtime: "claude-code",
|
|
614
862
|
async runBatch(ctx, onTrace) {
|
|
615
|
-
const
|
|
616
|
-
const prompt = resolveStagePrompt(ctx);
|
|
863
|
+
const hooks = { emit: makeEmit("claude-code", onTrace), ask: defaultAsk };
|
|
617
864
|
const mcpServers = buildBrokeredMcpServers(ctx);
|
|
618
865
|
const options = {
|
|
619
866
|
...baseOptions(ctx),
|
|
@@ -624,171 +871,50 @@ function createClaudeRunner() {
|
|
|
624
871
|
// Headless: allow tools to run without an interactive permission prompt (M6 wires policy).
|
|
625
872
|
canUseTool: async (toolName, input) => policyCanUseTool(ctx, toolName, input)
|
|
626
873
|
};
|
|
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 } : {} };
|
|
874
|
+
const rt = makeClaudeRuntimeSession(ctx, hooks, { interactive: false, options });
|
|
875
|
+
return runBatchLoop(rt, ctx, hooks, { cancelled: () => cancelled });
|
|
644
876
|
},
|
|
645
877
|
async runInteractive(ctx, turns, onTrace) {
|
|
646
878
|
const emit = makeEmit("claude-code", onTrace);
|
|
647
879
|
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 } : {}
|
|
880
|
+
const summarising = { value: false };
|
|
881
|
+
let rt;
|
|
882
|
+
const makeSession = (hooks) => {
|
|
883
|
+
const askTool = createAskUserQuestionTool({ ask: (question) => hooks.ask(question) });
|
|
884
|
+
const brokered = buildBrokeredMcpServers(ctx);
|
|
885
|
+
const options = {
|
|
886
|
+
...baseOptions(ctx),
|
|
887
|
+
// Keep the cwd-anchoring preset; fold the stage instruction in via `append` so the
|
|
888
|
+
// interactive persona still gets it without losing the working-directory context.
|
|
889
|
+
systemPrompt: hasSystemPrompt(ctx) ? { type: "preset", preset: "claude_code", append: resolveStagePrompt(ctx) } : CLAUDE_CODE_SYSTEM_PROMPT,
|
|
890
|
+
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
891
|
+
// MCP servers (parity with batch).
|
|
892
|
+
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
893
|
+
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
894
|
+
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
895
|
+
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
896
|
+
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
897
|
+
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
898
|
+
// it does not restrict the other tools canUseTool allows.
|
|
899
|
+
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
900
|
+
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising.value, stageTool.allowedToolName, ctx, toolName, input),
|
|
901
|
+
maxTurns: MAX_TURNS,
|
|
902
|
+
includePartialMessages: false
|
|
903
|
+
};
|
|
904
|
+
const session = createSession({ options, stageTool });
|
|
905
|
+
active = session;
|
|
906
|
+
rt = makeClaudeRuntimeSession(ctx, hooks, { interactive: true, session, stageTool, summarising });
|
|
907
|
+
return rt;
|
|
791
908
|
};
|
|
909
|
+
const result = await runInteractiveLoop(ctx, turns, emit, makeSession, {
|
|
910
|
+
signal: abortController.signal,
|
|
911
|
+
cancelled: () => cancelled,
|
|
912
|
+
cancel: () => this.cancel(),
|
|
913
|
+
// Claude carries the stage instruction in its system prompt, so the seed can be a short kickoff.
|
|
914
|
+
instructionInSystemPrompt: hasSystemPrompt(ctx)
|
|
915
|
+
});
|
|
916
|
+
rt?.dispose();
|
|
917
|
+
return result;
|
|
792
918
|
},
|
|
793
919
|
async summarise(ctx) {
|
|
794
920
|
if (!sessionId) return "(no summary: session not established)";
|
|
@@ -803,9 +929,9 @@ function createClaudeRunner() {
|
|
|
803
929
|
};
|
|
804
930
|
try {
|
|
805
931
|
let out2 = "";
|
|
806
|
-
const
|
|
807
|
-
active =
|
|
808
|
-
for await (const msg of
|
|
932
|
+
const session = createSession({ prompt: SUMMARISE_PROMPT, options });
|
|
933
|
+
active = session;
|
|
934
|
+
for await (const msg of session) {
|
|
809
935
|
const found = sessionIdOf(msg);
|
|
810
936
|
if (found) sessionId = found;
|
|
811
937
|
if (msg.type === "result" && msg.subtype === "success") out2 += msg.result;
|
|
@@ -833,6 +959,16 @@ function createClaudeRunner() {
|
|
|
833
959
|
import { join as join4 } from "path";
|
|
834
960
|
|
|
835
961
|
// ../../packages/executor-worktree/src/pi-mappers.ts
|
|
962
|
+
function parsePiEvent(msg) {
|
|
963
|
+
if (typeof msg !== "object" || msg === null) return null;
|
|
964
|
+
const type = msg.type;
|
|
965
|
+
if (typeof type !== "string") return null;
|
|
966
|
+
if (type === "message_update") {
|
|
967
|
+
const ame = msg.assistantMessageEvent;
|
|
968
|
+
if (typeof ame !== "object" || ame === null) return null;
|
|
969
|
+
}
|
|
970
|
+
return msg;
|
|
971
|
+
}
|
|
836
972
|
function mapUsage2(u) {
|
|
837
973
|
return {
|
|
838
974
|
input: u?.input ?? 0,
|
|
@@ -929,12 +1065,8 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
929
1065
|
if (ev.type === "turn_end" || ev.type === "agent_end") {
|
|
930
1066
|
const r = mapPiEvent(ev);
|
|
931
1067
|
const status = settleStatus(settleMessage(ev));
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (status === "ok" && text && !state.turnEndedOnTool) {
|
|
935
|
-
responseText = text;
|
|
936
|
-
events.push({ type: "response", text });
|
|
937
|
-
}
|
|
1068
|
+
const decision = decideResponse(state.bufferedText, state.turnEndedOnTool, status);
|
|
1069
|
+
if (decision.event) events.push(decision.event);
|
|
938
1070
|
if (state.pendingThought) {
|
|
939
1071
|
events.push({ type: "thought", subtype: "reasoning_text", text: state.pendingThought });
|
|
940
1072
|
}
|
|
@@ -945,7 +1077,7 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
945
1077
|
state.bufferedText = "";
|
|
946
1078
|
state.pendingThought = "";
|
|
947
1079
|
state.turnEndedOnTool = false;
|
|
948
|
-
return { events, isResult: true, status, responseText };
|
|
1080
|
+
return { events, isResult: true, status, responseText: decision.responseText };
|
|
949
1081
|
}
|
|
950
1082
|
return { events, isResult: false };
|
|
951
1083
|
}
|
|
@@ -1007,7 +1139,6 @@ function writeStageCustomProviders(dir, hint2) {
|
|
|
1007
1139
|
}
|
|
1008
1140
|
|
|
1009
1141
|
// ../../packages/executor-worktree/src/pi-adapter.ts
|
|
1010
|
-
var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
1011
1142
|
var PI_STAGE_COMPLETE_TOOL = "dahrk_stage_complete";
|
|
1012
1143
|
var PI_ASK_USER_QUESTION_TOOL = "ask_user_question";
|
|
1013
1144
|
function piToolCallDecision(ctx, toolName, input) {
|
|
@@ -1061,20 +1192,82 @@ function createBrokeredMcpExtension(servers) {
|
|
|
1061
1192
|
}
|
|
1062
1193
|
};
|
|
1063
1194
|
}
|
|
1195
|
+
var defaultAsk2 = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
1196
|
+
function makePiRuntimeSession(s, ctx, hooks, interactive) {
|
|
1197
|
+
return {
|
|
1198
|
+
get sessionId() {
|
|
1199
|
+
return s.sessionId;
|
|
1200
|
+
},
|
|
1201
|
+
async sendTurn(text) {
|
|
1202
|
+
const state = newPiBufferState();
|
|
1203
|
+
let stageComplete = false;
|
|
1204
|
+
let summary;
|
|
1205
|
+
let stageCompleteCallId;
|
|
1206
|
+
let responseText;
|
|
1207
|
+
let status;
|
|
1208
|
+
const unsub = s.subscribe((ev) => {
|
|
1209
|
+
const rawRef = ctx.writeRaw?.(ev);
|
|
1210
|
+
if (ev.type === "tool_execution_start" && ev.toolName === PI_STAGE_COMPLETE_TOOL) {
|
|
1211
|
+
stageComplete = true;
|
|
1212
|
+
stageCompleteCallId = ev.toolCallId;
|
|
1213
|
+
const args = ev.args;
|
|
1214
|
+
if (args?.summary) summary = args.summary;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (ev.type === "tool_execution_end" && ev.toolCallId === stageCompleteCallId) return;
|
|
1218
|
+
const r = consumePiEvent(ev, state, interactive);
|
|
1219
|
+
for (const e of r.events) hooks.emit(e, rawRef);
|
|
1220
|
+
if (r.responseText) responseText = r.responseText;
|
|
1221
|
+
if (r.isResult && r.status) status = r.status;
|
|
1222
|
+
});
|
|
1223
|
+
try {
|
|
1224
|
+
await s.prompt(text);
|
|
1225
|
+
} finally {
|
|
1226
|
+
unsub();
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
stageComplete,
|
|
1230
|
+
...summary !== void 0 ? { summary } : {},
|
|
1231
|
+
...responseText !== void 0 ? { responseText } : {},
|
|
1232
|
+
...status !== void 0 ? { status } : {}
|
|
1233
|
+
};
|
|
1234
|
+
},
|
|
1235
|
+
async summariseTurn() {
|
|
1236
|
+
if (s.agent) s.agent.state.tools = [];
|
|
1237
|
+
const state = newPiBufferState();
|
|
1238
|
+
let out2;
|
|
1239
|
+
const unsub = s.subscribe((ev) => {
|
|
1240
|
+
const r = consumePiEvent(ev, state, true);
|
|
1241
|
+
if (r.responseText) out2 = r.responseText;
|
|
1242
|
+
});
|
|
1243
|
+
try {
|
|
1244
|
+
await s.prompt(SUMMARISE_PROMPT);
|
|
1245
|
+
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
return `(summary unavailable: ${e.message})`;
|
|
1248
|
+
} finally {
|
|
1249
|
+
unsub();
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
cost() {
|
|
1253
|
+
const cost = s.getSessionStats?.()?.cost;
|
|
1254
|
+
return typeof cost === "number" ? cost : void 0;
|
|
1255
|
+
},
|
|
1256
|
+
dispose() {
|
|
1257
|
+
s.dispose();
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1064
1261
|
function createPiRunner(deps = {}) {
|
|
1065
1262
|
const createSession = deps.createSession ?? defaultCreatePiSession;
|
|
1066
1263
|
const abortController = new AbortController();
|
|
1067
1264
|
const signal = abortController.signal;
|
|
1068
1265
|
let cancelled = false;
|
|
1069
1266
|
let session;
|
|
1070
|
-
let sessionId;
|
|
1071
1267
|
const openSession = async (ctx) => {
|
|
1072
1268
|
if (!session) session = await createSession(ctx);
|
|
1073
1269
|
return session;
|
|
1074
1270
|
};
|
|
1075
|
-
const captureSessionId = (s) => {
|
|
1076
|
-
if (s.sessionId) sessionId = s.sessionId;
|
|
1077
|
-
};
|
|
1078
1271
|
let sessionDisposed = false;
|
|
1079
1272
|
const disposeSession = () => {
|
|
1080
1273
|
if (sessionDisposed || !session) return;
|
|
@@ -1084,178 +1277,41 @@ function createPiRunner(deps = {}) {
|
|
|
1084
1277
|
} catch {
|
|
1085
1278
|
}
|
|
1086
1279
|
};
|
|
1087
|
-
const readSessionCost = (s) => {
|
|
1088
|
-
const cost = s.getSessionStats?.()?.cost;
|
|
1089
|
-
return typeof cost === "number" ? cost : void 0;
|
|
1090
|
-
};
|
|
1091
1280
|
return {
|
|
1092
1281
|
runtime: "pi",
|
|
1093
1282
|
async runBatch(ctx, onTrace) {
|
|
1094
|
-
const
|
|
1283
|
+
const hooks = { emit: makeEmit("pi", onTrace), ask: defaultAsk2 };
|
|
1095
1284
|
const s = await openSession(ctx);
|
|
1096
1285
|
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 } : {} };
|
|
1286
|
+
const rt = makePiRuntimeSession(s, ctx, hooks, false);
|
|
1287
|
+
const result = await runBatchLoop(rt, ctx, hooks, { cancelled: () => cancelled });
|
|
1288
|
+
if (result.status !== "ok") disposeSession();
|
|
1289
|
+
return result;
|
|
1118
1290
|
},
|
|
1119
1291
|
async runInteractive(ctx, turns, onTrace) {
|
|
1120
1292
|
const emit = makeEmit("pi", onTrace);
|
|
1121
1293
|
const s = await openSession(ctx);
|
|
1122
1294
|
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
|
-
}
|
|
1295
|
+
const makeSession = (hooks) => {
|
|
1296
|
+
s.setAskUserQuestionHandler?.((questions) => askQuestionsSequentially(questions, (q) => hooks.ask(q)));
|
|
1297
|
+
return makePiRuntimeSession(s, ctx, hooks, true);
|
|
1164
1298
|
};
|
|
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);
|
|
1299
|
+
const result = await runInteractiveLoop(ctx, turns, emit, makeSession, {
|
|
1300
|
+
signal,
|
|
1301
|
+
cancelled: () => cancelled,
|
|
1302
|
+
cancel: () => this.cancel(),
|
|
1303
|
+
instructionInSystemPrompt: false
|
|
1304
|
+
});
|
|
1233
1305
|
disposeSession();
|
|
1234
|
-
return
|
|
1235
|
-
status,
|
|
1236
|
-
summary,
|
|
1237
|
-
...sessionId ? { sessionId } : {},
|
|
1238
|
-
...costUsd !== void 0 ? { costUsd } : {}
|
|
1239
|
-
};
|
|
1306
|
+
return result;
|
|
1240
1307
|
},
|
|
1241
1308
|
async summarise(ctx) {
|
|
1242
1309
|
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
|
-
});
|
|
1310
|
+
const rt = makePiRuntimeSession(session, ctx, { emit: () => {
|
|
1311
|
+
}, ask: defaultAsk2 }, true);
|
|
1251
1312
|
try {
|
|
1252
|
-
await
|
|
1253
|
-
captureSessionId(s);
|
|
1254
|
-
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1255
|
-
} catch (e) {
|
|
1256
|
-
return `(summary unavailable: ${e.message})`;
|
|
1313
|
+
return await rt.summariseTurn();
|
|
1257
1314
|
} finally {
|
|
1258
|
-
unsub();
|
|
1259
1315
|
disposeSession();
|
|
1260
1316
|
}
|
|
1261
1317
|
},
|
|
@@ -1296,8 +1352,11 @@ async function defaultCreatePiSession(ctx) {
|
|
|
1296
1352
|
if (ctx.config.model) {
|
|
1297
1353
|
const resolved = resolveCliModel({ cliModel: ctx.config.model, modelRegistry });
|
|
1298
1354
|
if (!resolved?.error) {
|
|
1299
|
-
model = pickAuthedModel(resolved?.model, modelRegistry.getAvailable());
|
|
1355
|
+
model = pickAuthedModel(resolved?.model, modelRegistry.getAvailable(), hint2?.defaultModel);
|
|
1300
1356
|
}
|
|
1357
|
+
} else if (hint2?.defaultModel) {
|
|
1358
|
+
const resolved = resolveCliModel({ cliModel: hint2.defaultModel, modelRegistry });
|
|
1359
|
+
if (!resolved?.error) model = pickAuthedModel(resolved?.model, modelRegistry.getAvailable());
|
|
1301
1360
|
}
|
|
1302
1361
|
const stageComplete = defineTool({
|
|
1303
1362
|
name: PI_STAGE_COMPLETE_TOOL,
|
|
@@ -1338,7 +1397,7 @@ async function defaultCreatePiSession(ctx) {
|
|
|
1338
1397
|
},
|
|
1339
1398
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1340
1399
|
execute: async (_toolCallId, params) => {
|
|
1341
|
-
const text = askHandler ? await askHandler(params.questions) :
|
|
1400
|
+
const text = askHandler ? await askHandler(params.questions) : elicitOutcomeReply({ kind: "noreply" });
|
|
1342
1401
|
return { content: [{ type: "text", text }], details: {} };
|
|
1343
1402
|
}
|
|
1344
1403
|
});
|
|
@@ -1393,12 +1452,20 @@ function modelFamily(id) {
|
|
|
1393
1452
|
const last = id.split(".").pop() ?? id;
|
|
1394
1453
|
return last.replace(/-v\d+:\d+$/, "").toLowerCase();
|
|
1395
1454
|
}
|
|
1396
|
-
function pickAuthedModel(resolved, available) {
|
|
1455
|
+
function pickAuthedModel(resolved, available, fallbackModel) {
|
|
1397
1456
|
if (!resolved || !available?.length) return resolved;
|
|
1398
1457
|
const providers = new Set(available.map((m) => m.provider));
|
|
1399
1458
|
if (providers.has(resolved.provider)) return resolved;
|
|
1400
1459
|
const family = modelFamily(resolved.id);
|
|
1401
|
-
|
|
1460
|
+
const sameFamily = available.find((m) => modelFamily(m.id) === family);
|
|
1461
|
+
if (sameFamily) return sameFamily;
|
|
1462
|
+
if (fallbackModel) {
|
|
1463
|
+
const byId = available.find((m) => m.id === fallbackModel);
|
|
1464
|
+
if (byId) return byId;
|
|
1465
|
+
const byFamily = available.find((m) => modelFamily(m.id) === modelFamily(fallbackModel));
|
|
1466
|
+
if (byFamily) return byFamily;
|
|
1467
|
+
}
|
|
1468
|
+
return resolved;
|
|
1402
1469
|
}
|
|
1403
1470
|
|
|
1404
1471
|
// ../../packages/executor-worktree/src/pi-container.ts
|
|
@@ -1557,7 +1624,8 @@ var PiRpcSession = class {
|
|
|
1557
1624
|
}
|
|
1558
1625
|
return;
|
|
1559
1626
|
}
|
|
1560
|
-
const ev = msg;
|
|
1627
|
+
const ev = parsePiEvent(msg);
|
|
1628
|
+
if (!ev) return;
|
|
1561
1629
|
for (const l of [...this.#listeners]) l(ev);
|
|
1562
1630
|
if (ev.type === "agent_end" && this.#pendingAgentEnd) {
|
|
1563
1631
|
const d = this.#pendingAgentEnd;
|
|
@@ -1620,10 +1688,67 @@ import { execFileSync } from "child_process";
|
|
|
1620
1688
|
import { existsSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1621
1689
|
import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
|
|
1622
1690
|
import { basename, dirname, isAbsolute, join as join5 } from "path";
|
|
1691
|
+
|
|
1692
|
+
// ../../packages/executor-worktree/src/footprint.ts
|
|
1693
|
+
function resolveRenamePath(raw) {
|
|
1694
|
+
const brace = /\{(.*?) => (.*?)\}/;
|
|
1695
|
+
if (brace.test(raw)) {
|
|
1696
|
+
return raw.replace(brace, (_m, _old, next) => next).replace(/\/{2,}/g, "/");
|
|
1697
|
+
}
|
|
1698
|
+
const arrow2 = raw.indexOf(" => ");
|
|
1699
|
+
if (arrow2 !== -1) return raw.slice(arrow2 + " => ".length);
|
|
1700
|
+
return raw;
|
|
1701
|
+
}
|
|
1702
|
+
function parseNumstat(raw) {
|
|
1703
|
+
const entries = [];
|
|
1704
|
+
for (const line of raw.split("\n")) {
|
|
1705
|
+
if (line.trim() === "") continue;
|
|
1706
|
+
const firstTab = line.indexOf(" ");
|
|
1707
|
+
const secondTab = line.indexOf(" ", firstTab + 1);
|
|
1708
|
+
if (firstTab === -1 || secondTab === -1) continue;
|
|
1709
|
+
const addedRaw = line.slice(0, firstTab);
|
|
1710
|
+
const removedRaw = line.slice(firstTab + 1, secondTab);
|
|
1711
|
+
const pathRaw = line.slice(secondTab + 1);
|
|
1712
|
+
const binary = addedRaw === "-" && removedRaw === "-";
|
|
1713
|
+
entries.push({
|
|
1714
|
+
path: resolveRenamePath(pathRaw),
|
|
1715
|
+
added: binary ? 0 : Number.parseInt(addedRaw, 10) || 0,
|
|
1716
|
+
removed: binary ? 0 : Number.parseInt(removedRaw, 10) || 0,
|
|
1717
|
+
binary
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return entries;
|
|
1721
|
+
}
|
|
1722
|
+
function topLevel(path) {
|
|
1723
|
+
const slash = path.indexOf("/");
|
|
1724
|
+
return slash === -1 ? path : path.slice(0, slash);
|
|
1725
|
+
}
|
|
1726
|
+
function deriveFootprint(entries, opts) {
|
|
1727
|
+
let added = 0;
|
|
1728
|
+
let removed = 0;
|
|
1729
|
+
const scopeSet = /* @__PURE__ */ new Set();
|
|
1730
|
+
const allPaths = [];
|
|
1731
|
+
for (const e of entries) {
|
|
1732
|
+
added += e.added;
|
|
1733
|
+
removed += e.removed;
|
|
1734
|
+
scopeSet.add(topLevel(e.path));
|
|
1735
|
+
allPaths.push(e.path);
|
|
1736
|
+
}
|
|
1737
|
+
const changedPathsTruncated = allPaths.length > opts.cap;
|
|
1738
|
+
return {
|
|
1739
|
+
numstat: { files: entries.length, added, removed },
|
|
1740
|
+
scope: [...scopeSet].sort(),
|
|
1741
|
+
changedPaths: changedPathsTruncated ? allPaths.slice(0, opts.cap) : allPaths,
|
|
1742
|
+
changedPathsTruncated
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// ../../packages/executor-worktree/src/git-service.ts
|
|
1623
1747
|
var noopLogger = { info: () => {
|
|
1624
1748
|
}, warn: () => {
|
|
1625
1749
|
} };
|
|
1626
1750
|
var SCRATCH_DIR = ".dahrk/scratch";
|
|
1751
|
+
var CHANGED_PATHS_CAP = 100;
|
|
1627
1752
|
var DEFAULT_MERGE_RESOLVE_RULES = [
|
|
1628
1753
|
{ path: "pnpm-lock.yaml", strategy: "theirs" },
|
|
1629
1754
|
{ path: "package-lock.json", strategy: "theirs" },
|
|
@@ -1820,6 +1945,17 @@ function createGitService(opts = {}) {
|
|
|
1820
1945
|
}
|
|
1821
1946
|
git(mirror, ["config", "--replace-all", "remote.origin.fetch", TRACKING_REFSPEC]);
|
|
1822
1947
|
};
|
|
1948
|
+
const reconcileMirrorUrl = (mirror, repoId, gitUrl, authEnv) => {
|
|
1949
|
+
const expected = authEnv ? withTokenUser(gitUrl) : gitUrl;
|
|
1950
|
+
let current = "";
|
|
1951
|
+
try {
|
|
1952
|
+
current = git(mirror, ["config", "--get", "remote.origin.url"]).trim();
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
if (current === expected) return;
|
|
1956
|
+
log.info(`mirror ${repoId}: origin url drifted (${current} -> ${expected}); updating in place`);
|
|
1957
|
+
git(mirror, ["remote", "set-url", "origin", expected]);
|
|
1958
|
+
};
|
|
1823
1959
|
const salvageOrphanedTip = (mirror, branchName, start2) => {
|
|
1824
1960
|
try {
|
|
1825
1961
|
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", `refs/heads/${branchName}`])) return;
|
|
@@ -1860,6 +1996,7 @@ function createGitService(opts = {}) {
|
|
|
1860
1996
|
log.info(`refreshing mirror ${repoId}`);
|
|
1861
1997
|
try {
|
|
1862
1998
|
migrateMirrorConfig(mirror, repoId);
|
|
1999
|
+
reconcileMirrorUrl(mirror, repoId, gitUrl, authEnv);
|
|
1863
2000
|
git(mirror, ["fetch", "--prune", "origin"], netEnv(authEnv));
|
|
1864
2001
|
gcShadowHeads(mirror);
|
|
1865
2002
|
return { mirror, refreshed: true };
|
|
@@ -1956,6 +2093,7 @@ function createGitService(opts = {}) {
|
|
|
1956
2093
|
const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
|
|
1957
2094
|
let pushed = false;
|
|
1958
2095
|
let integration;
|
|
2096
|
+
let footprint;
|
|
1959
2097
|
try {
|
|
1960
2098
|
let fetched = false;
|
|
1961
2099
|
if (opts2.base) {
|
|
@@ -1975,6 +2113,12 @@ function createGitService(opts = {}) {
|
|
|
1975
2113
|
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1976
2114
|
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1977
2115
|
}
|
|
2116
|
+
const numstatEntries = parseNumstat(git(worktreePath, ["diff", "--numstat", "FETCH_HEAD...HEAD"])).filter(
|
|
2117
|
+
(e) => !isScratchPath(e.path)
|
|
2118
|
+
);
|
|
2119
|
+
if (numstatEntries.length > 0) {
|
|
2120
|
+
footprint = deriveFootprint(numstatEntries, { cap: CHANGED_PATHS_CAP });
|
|
2121
|
+
}
|
|
1978
2122
|
try {
|
|
1979
2123
|
git(worktreePath, [
|
|
1980
2124
|
"-c",
|
|
@@ -2009,7 +2153,7 @@ function createGitService(opts = {}) {
|
|
|
2009
2153
|
headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
2010
2154
|
} else {
|
|
2011
2155
|
git(worktreePath, ["merge", "--abort"]);
|
|
2012
|
-
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles };
|
|
2156
|
+
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles, ...footprint ? { footprint } : {} };
|
|
2013
2157
|
}
|
|
2014
2158
|
} else {
|
|
2015
2159
|
const msg = mergeErr.message;
|
|
@@ -2025,7 +2169,7 @@ function createGitService(opts = {}) {
|
|
|
2025
2169
|
} finally {
|
|
2026
2170
|
cleanup();
|
|
2027
2171
|
}
|
|
2028
|
-
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {} };
|
|
2172
|
+
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {}, ...footprint ? { footprint } : {} };
|
|
2029
2173
|
},
|
|
2030
2174
|
async backupPush(ref, opts2) {
|
|
2031
2175
|
const { worktreePath } = ref;
|
|
@@ -2120,12 +2264,16 @@ import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync3
|
|
|
2120
2264
|
import { join as join6 } from "path";
|
|
2121
2265
|
var MINUTE = 6e4;
|
|
2122
2266
|
var HOUR = 60 * MINUTE;
|
|
2267
|
+
var DAY = 24 * HOUR;
|
|
2123
2268
|
var DEFAULTS = {
|
|
2124
2269
|
// Deliberately non-optional defaults. "No policy configured" used to mean "never collect anything",
|
|
2125
2270
|
// which is precisely how the disk reached 65 GB. Absent config must mean sane collection, not none.
|
|
2126
2271
|
maxRuns: 20,
|
|
2127
2272
|
maxIdleMs: 6 * HOUR,
|
|
2128
|
-
activityGraceMs: 30 * MINUTE
|
|
2273
|
+
activityGraceMs: 30 * MINUTE,
|
|
2274
|
+
// 14 days: long enough that an operator who missed the park log can still recover unpushed work, but
|
|
2275
|
+
// bounded so parked tips cannot pile up for ever. Cited verbatim in docs/logging.md - keep in step.
|
|
2276
|
+
salvageTtlMs: 14 * DAY
|
|
2129
2277
|
};
|
|
2130
2278
|
var noop = { info: () => {
|
|
2131
2279
|
}, warn: () => {
|
|
@@ -2166,6 +2314,38 @@ function registeredWorktrees(mirror) {
|
|
|
2166
2314
|
}
|
|
2167
2315
|
return out2.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
2168
2316
|
}
|
|
2317
|
+
function salvageParkedMs(mirror, ref) {
|
|
2318
|
+
try {
|
|
2319
|
+
return statSync(join6(mirror, ref)).mtimeMs;
|
|
2320
|
+
} catch {
|
|
2321
|
+
}
|
|
2322
|
+
try {
|
|
2323
|
+
const ts = gitOut(mirror, ["log", "-g", "--format=%ct", "-1", ref]).trim();
|
|
2324
|
+
if (ts) return Number(ts) * 1e3;
|
|
2325
|
+
} catch {
|
|
2326
|
+
}
|
|
2327
|
+
return void 0;
|
|
2328
|
+
}
|
|
2329
|
+
function sweepSalvageRefs(mirror, ttlMs, dryRun, now, log) {
|
|
2330
|
+
let refs;
|
|
2331
|
+
try {
|
|
2332
|
+
refs = gitOut(mirror, ["for-each-ref", "--format=%(refname)", "refs/dahrk/salvage/"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2333
|
+
} catch {
|
|
2334
|
+
return 0;
|
|
2335
|
+
}
|
|
2336
|
+
let parked = 0;
|
|
2337
|
+
for (const ref of refs) {
|
|
2338
|
+
const at = salvageParkedMs(mirror, ref);
|
|
2339
|
+
const expired = at !== void 0 && now - at > ttlMs;
|
|
2340
|
+
if (expired && !dryRun) {
|
|
2341
|
+
if (!gitOk(mirror, ["update-ref", "-d", ref])) log.warn(`reaper: could not expire salvage ref ${ref}`);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
if (expired) log.info(`reaper (dry-run): would expire salvage ref ${ref}`);
|
|
2345
|
+
parked++;
|
|
2346
|
+
}
|
|
2347
|
+
return parked;
|
|
2348
|
+
}
|
|
2169
2349
|
function createWorktreeReaper(opts) {
|
|
2170
2350
|
const log = opts.logger ?? noop;
|
|
2171
2351
|
const mirrors = () => {
|
|
@@ -2184,8 +2364,9 @@ function createWorktreeReaper(opts) {
|
|
|
2184
2364
|
const maxRuns = policy.maxRuns ?? DEFAULTS.maxRuns;
|
|
2185
2365
|
const maxIdleMs = policy.maxIdleMs ?? DEFAULTS.maxIdleMs;
|
|
2186
2366
|
const graceMs = policy.activityGraceMs ?? DEFAULTS.activityGraceMs;
|
|
2367
|
+
const salvageTtlMs = policy.salvageTtlMs ?? DEFAULTS.salvageTtlMs;
|
|
2187
2368
|
const dryRun = policy.dryRun ?? false;
|
|
2188
|
-
const report = { scanned: 0, reaped: [], skipped: 0, errors: [] };
|
|
2369
|
+
const report = { scanned: 0, reaped: [], skipped: 0, salvagedRefs: 0, errors: [] };
|
|
2189
2370
|
const now = Date.now();
|
|
2190
2371
|
const registered = /* @__PURE__ */ new Map();
|
|
2191
2372
|
for (const m of mirrors()) {
|
|
@@ -2247,6 +2428,14 @@ function createWorktreeReaper(opts) {
|
|
|
2247
2428
|
if (report.reaped.length) {
|
|
2248
2429
|
log.info(`reaper: reaped ${report.reaped.length} worktree(s), skipped ${report.skipped}`);
|
|
2249
2430
|
}
|
|
2431
|
+
for (const m of registered.keys()) {
|
|
2432
|
+
report.salvagedRefs += sweepSalvageRefs(m, salvageTtlMs, dryRun, now, log);
|
|
2433
|
+
}
|
|
2434
|
+
if (report.salvagedRefs) {
|
|
2435
|
+
log.info(
|
|
2436
|
+
`reaper: ${report.salvagedRefs} salvage ref(s) parked, expire ${Math.round(salvageTtlMs / DAY)}d after parking`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2250
2439
|
return report;
|
|
2251
2440
|
}
|
|
2252
2441
|
};
|
|
@@ -3520,6 +3709,77 @@ async function startMcpGateway(opts) {
|
|
|
3520
3709
|
};
|
|
3521
3710
|
}
|
|
3522
3711
|
|
|
3712
|
+
// ../../packages/edge/src/push-outcome.ts
|
|
3713
|
+
function resolveDeliverOutcome(r, job, pr) {
|
|
3714
|
+
const { jobId, branch, base } = job;
|
|
3715
|
+
if (r.integration === "noop") {
|
|
3716
|
+
return {
|
|
3717
|
+
jobId,
|
|
3718
|
+
status: "ok",
|
|
3719
|
+
branch,
|
|
3720
|
+
headSha: r.headSha,
|
|
3721
|
+
pushed: false,
|
|
3722
|
+
nothingToCommit: true,
|
|
3723
|
+
commitsAhead: r.commitsAhead,
|
|
3724
|
+
summary: `no changes to deliver on ${branch} - work already present on ${base}`
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
if (r.integration === "conflict") {
|
|
3728
|
+
return {
|
|
3729
|
+
jobId,
|
|
3730
|
+
status: "ok",
|
|
3731
|
+
branch,
|
|
3732
|
+
headSha: r.headSha,
|
|
3733
|
+
pushed: false,
|
|
3734
|
+
nothingToCommit: r.nothingToCommit,
|
|
3735
|
+
commitsAhead: r.commitsAhead,
|
|
3736
|
+
integration: "conflict",
|
|
3737
|
+
...r.conflictFiles ? { conflictFiles: r.conflictFiles } : {},
|
|
3738
|
+
...r.footprint ?? {},
|
|
3739
|
+
summary: `base advanced; merge conflict on ${branch} (manual merge needed)`
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
if (r.integration === "diverged") {
|
|
3743
|
+
return {
|
|
3744
|
+
jobId,
|
|
3745
|
+
status: "fail",
|
|
3746
|
+
branch,
|
|
3747
|
+
headSha: r.headSha,
|
|
3748
|
+
pushed: false,
|
|
3749
|
+
nothingToCommit: r.nothingToCommit,
|
|
3750
|
+
commitsAhead: r.commitsAhead,
|
|
3751
|
+
summary: `branch history diverged from ${base}; cannot auto-integrate on ${branch} (the branch likely needs rebuilding from ${base})`
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
return {
|
|
3755
|
+
jobId,
|
|
3756
|
+
status: "ok",
|
|
3757
|
+
branch,
|
|
3758
|
+
headSha: r.headSha,
|
|
3759
|
+
pushed: r.pushed,
|
|
3760
|
+
nothingToCommit: r.nothingToCommit,
|
|
3761
|
+
commitsAhead: r.commitsAhead,
|
|
3762
|
+
...r.integration ? { integration: r.integration } : {},
|
|
3763
|
+
...pr?.prUrl ? { prUrl: pr.prUrl } : {},
|
|
3764
|
+
...pr?.prNumber !== void 0 ? { prNumber: pr.prNumber } : {},
|
|
3765
|
+
...pr?.prError ? { prError: pr.prError } : {},
|
|
3766
|
+
...r.footprint ?? {},
|
|
3767
|
+
summary: r.nothingToCommit ? `no changes to commit; ${r.pushed ? "branch pushed" : "nothing pushed"}` : `committed ${r.headSha.slice(0, 7)} and pushed ${branch}`
|
|
3768
|
+
};
|
|
3769
|
+
}
|
|
3770
|
+
function resolveBackupOutcome(r, job) {
|
|
3771
|
+
return {
|
|
3772
|
+
jobId: job.jobId,
|
|
3773
|
+
status: "ok",
|
|
3774
|
+
branch: job.branch,
|
|
3775
|
+
headSha: r.headSha,
|
|
3776
|
+
pushed: r.pushed,
|
|
3777
|
+
nothingToCommit: r.nothingToCommit,
|
|
3778
|
+
wipRef: r.wipRef,
|
|
3779
|
+
summary: `backup: preserved ${r.headSha.slice(0, 7)} on ${r.wipRef} (no base merge, no PR)`
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3523
3783
|
// ../../packages/edge/src/stage-runner.ts
|
|
3524
3784
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3525
3785
|
var PREVIEW = 500;
|
|
@@ -3865,7 +4125,7 @@ function createStageRunner(deps) {
|
|
|
3865
4125
|
}
|
|
3866
4126
|
sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
|
|
3867
4127
|
};
|
|
3868
|
-
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc) => {
|
|
4128
|
+
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc, failureClass2) => {
|
|
3869
4129
|
active.delete(jobId);
|
|
3870
4130
|
turnQueues.delete(jobId);
|
|
3871
4131
|
await gateway?.stop().catch((e) => log.warn({ err: e, jobId }, "mcp gateway: stop failed"));
|
|
@@ -3902,7 +4162,8 @@ function createStageRunner(deps) {
|
|
|
3902
4162
|
summary: summary2,
|
|
3903
4163
|
...sessionId ? { sessionId } : {},
|
|
3904
4164
|
...costUsd !== void 0 ? { costUsd } : {},
|
|
3905
|
-
...resolved ? { artifact: resolved.artifact } : {}
|
|
4165
|
+
...resolved ? { artifact: resolved.artifact } : {},
|
|
4166
|
+
...failureClass2 ? { failureClass: failureClass2 } : {}
|
|
3906
4167
|
};
|
|
3907
4168
|
};
|
|
3908
4169
|
if (deps.packCache && job.provision && job.provision.length > 0) {
|
|
@@ -4026,6 +4287,14 @@ function createStageRunner(deps) {
|
|
|
4026
4287
|
// never surfaced to the agent's own tool calls. Absent on ambient nodes; inert for the Claude/
|
|
4027
4288
|
// Codex adapters, which use ambient inference.
|
|
4028
4289
|
...job.runtimeEnv ? { runtimeEnv: job.runtimeEnv } : {},
|
|
4290
|
+
// The brokered auth-profile hint (DHK-509/511): WHICH provider each piece of the inference
|
|
4291
|
+
// auth above belongs to, plus the model fallback. The adapter applies nothing for a provider
|
|
4292
|
+
// the hint does not name, so without this passthrough `runtimeEnv` arrives and is ignored -
|
|
4293
|
+
// a managed node then has no inference auth at all and falls through to whatever provider the
|
|
4294
|
+
// runtime defaults to. Read through a narrow cast because the field is not in the published
|
|
4295
|
+
// `@dahrk/contracts` (^0.4.0) yet; this line and `readAuthHint` are the only two seams, and
|
|
4296
|
+
// both drop the cast when contracts ships.
|
|
4297
|
+
...job.runtimeAuth ? { runtimeAuth: job.runtimeAuth } : {},
|
|
4029
4298
|
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
4030
4299
|
// and stamps the rawRef onto the emitted event.
|
|
4031
4300
|
writeRaw: writer.writeRaw,
|
|
@@ -4104,7 +4373,8 @@ function createStageRunner(deps) {
|
|
|
4104
4373
|
}
|
|
4105
4374
|
}
|
|
4106
4375
|
if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
|
|
4107
|
-
|
|
4376
|
+
const failureClass = timedOut || stalled ? "harness" : result.failureClass;
|
|
4377
|
+
return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact, failureClass);
|
|
4108
4378
|
} finally {
|
|
4109
4379
|
inFlight.set(runId, Math.max(0, (inFlight.get(runId) ?? 1) - 1));
|
|
4110
4380
|
}
|
|
@@ -4135,17 +4405,7 @@ function createStageRunner(deps) {
|
|
|
4135
4405
|
branch: job.branch,
|
|
4136
4406
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
4137
4407
|
});
|
|
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;
|
|
4408
|
+
return resolveBackupOutcome(r, { jobId, branch: job.branch });
|
|
4149
4409
|
} catch (e) {
|
|
4150
4410
|
return { jobId, status: "fail", branch: job.branch, summary: `backup push failed: ${e.message}` };
|
|
4151
4411
|
}
|
|
@@ -4170,64 +4430,13 @@ function createStageRunner(deps) {
|
|
|
4170
4430
|
base: job.base,
|
|
4171
4431
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
4172
4432
|
});
|
|
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
4433
|
const pr = job.openPr && r.pushed ? await deps.gitService.openPrAmbient(ref, {
|
|
4212
4434
|
branch: job.branch,
|
|
4213
4435
|
base: job.base,
|
|
4214
4436
|
title: job.openPr.title,
|
|
4215
4437
|
body: job.openPr.body
|
|
4216
4438
|
}) : 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
|
-
};
|
|
4439
|
+
return resolveDeliverOutcome(r, { jobId, branch: job.branch, base: job.base }, pr);
|
|
4231
4440
|
} catch (e) {
|
|
4232
4441
|
return { jobId, status: "fail", summary: `push failed: ${e.message}` };
|
|
4233
4442
|
}
|
|
@@ -4414,10 +4623,10 @@ async function startEdgeNode(opts) {
|
|
|
4414
4623
|
await reconcileInterruptedJobs();
|
|
4415
4624
|
void stageRunner.reapWorktrees().then((r) => {
|
|
4416
4625
|
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
4417
|
-
if (r.reaped.length) {
|
|
4626
|
+
if (r.reaped.length || r.salvagedRefs) {
|
|
4418
4627
|
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})`
|
|
4628
|
+
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped, salvagedRefs: r.salvagedRefs },
|
|
4629
|
+
`EDGE_REAPED:${r.reaped.length} worktrees (scanned ${r.scanned}, skipped ${r.skipped}, salvaged ${r.salvagedRefs})`
|
|
4421
4630
|
);
|
|
4422
4631
|
}
|
|
4423
4632
|
for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
|
|
@@ -5781,7 +5990,8 @@ function driftOf(stored, repo) {
|
|
|
5781
5990
|
const drift = {};
|
|
5782
5991
|
if (typeof stored.defaultBranch === "string" && stored.defaultBranch !== repo.defaultBranch) drift.branch = stored.defaultBranch;
|
|
5783
5992
|
if (typeof stored.name === "string" && stored.name !== repo.name) drift.name = stored.name;
|
|
5784
|
-
|
|
5993
|
+
if (typeof stored.gitUrl === "string" && stored.gitUrl !== repo.gitUrl) drift.gitUrl = stored.gitUrl;
|
|
5994
|
+
return drift.branch || drift.name || drift.gitUrl ? drift : void 0;
|
|
5785
5995
|
}
|
|
5786
5996
|
async function registerRepo(deps, args) {
|
|
5787
5997
|
const url = `${args.base}/config/api/repositories`;
|
|
@@ -7229,7 +7439,7 @@ async function runStatus(inputs, deps) {
|
|
|
7229
7439
|
}
|
|
7230
7440
|
|
|
7231
7441
|
// src/main.ts
|
|
7232
|
-
var CLIENT_VERSION = "0.1.
|
|
7442
|
+
var CLIENT_VERSION = "0.1.23";
|
|
7233
7443
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
7234
7444
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7235
7445
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -7622,6 +7832,7 @@ async function runRepoAdd(flags) {
|
|
|
7622
7832
|
const bits = [];
|
|
7623
7833
|
if (result.drift.branch) bits.push(`branch ${result.drift.branch} (this repo is on ${repo.defaultBranch})`);
|
|
7624
7834
|
if (result.drift.name) bits.push(`name ${result.drift.name}`);
|
|
7835
|
+
if (result.drift.gitUrl) bits.push(`URL ${result.drift.gitUrl} (this repo registers as ${repo.gitUrl})`);
|
|
7625
7836
|
out(hint(`The hub's stored record differs - ${bits.join(", ")} - and was left unchanged.`));
|
|
7626
7837
|
}
|
|
7627
7838
|
return 0;
|