dahrk-node 0.1.20 → 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/chunk-FYS2JH42.js +31 -0
- package/dist/chunk-G3AU5H5X.js +1568 -0
- package/dist/client-MZ5GEQAD.js +8745 -0
- package/dist/main.js +1311 -841
- package/dist/streamableHttp-S27HYW5J.js +1481 -0
- package/package.json +2 -3
package/dist/main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-FYS2JH42.js";
|
|
2
3
|
|
|
3
4
|
// src/main.ts
|
|
4
5
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5
6
|
import { existsSync as existsSync14, readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
|
|
6
7
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7
8
|
import { homedir as homedir7, platform as osPlatform4 } from "os";
|
|
8
|
-
import { basename as basename2, join as
|
|
9
|
+
import { basename as basename2, join as join19 } from "path";
|
|
9
10
|
import { pathToFileURL } from "url";
|
|
10
11
|
|
|
11
12
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -73,6 +74,13 @@ import {
|
|
|
73
74
|
query
|
|
74
75
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
75
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
|
+
|
|
76
84
|
// ../../packages/executor-worktree/src/claude-mappers.ts
|
|
77
85
|
function mapClaudeMessage(msg) {
|
|
78
86
|
switch (msg.type) {
|
|
@@ -167,27 +175,21 @@ function consumeClaudeMessage(msg, state, suppressStageExit) {
|
|
|
167
175
|
}
|
|
168
176
|
if (msg.type === "result") {
|
|
169
177
|
const status = r.resultStatus === "fail" ? "fail" : "ok";
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
responseText = state.bufferedText;
|
|
173
|
-
events.push({ type: "response", text: state.bufferedText });
|
|
174
|
-
}
|
|
178
|
+
const decision = decideResponse(state.bufferedText ?? "", state.turnEndedOnTool, status);
|
|
179
|
+
if (decision.event) events.push(decision.event);
|
|
175
180
|
for (const e of r.events) {
|
|
176
181
|
if (suppressStageExit && e.type === "state") continue;
|
|
177
182
|
events.push(e);
|
|
178
183
|
}
|
|
179
184
|
state.bufferedText = null;
|
|
180
185
|
state.turnEndedOnTool = false;
|
|
181
|
-
return { events, isResult: true, status, responseText };
|
|
186
|
+
return { events, isResult: true, status, responseText: decision.responseText };
|
|
182
187
|
}
|
|
183
188
|
events.push(...r.events);
|
|
184
189
|
return { events, isResult: false };
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
// ../../packages/executor-worktree/src/
|
|
188
|
-
import { readFileSync } from "fs";
|
|
189
|
-
import { join } from "path";
|
|
190
|
-
import { attachedDocBasename } from "@dahrk/contracts";
|
|
192
|
+
// ../../packages/executor-worktree/src/runtime-session.ts
|
|
191
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.";
|
|
192
194
|
function makeEmit(runtime, onTrace, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
|
|
193
195
|
return (event, rawRef) => {
|
|
@@ -196,6 +198,11 @@ function makeEmit(runtime, onTrace, now = () => (/* @__PURE__ */ new Date()).toI
|
|
|
196
198
|
onTrace(full);
|
|
197
199
|
};
|
|
198
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";
|
|
199
206
|
function stripFrontmatter(text) {
|
|
200
207
|
const lines = text.split("\n");
|
|
201
208
|
if (lines[0]?.trim() !== "---") return text;
|
|
@@ -232,7 +239,7 @@ function documentsBlock(ctx) {
|
|
|
232
239
|
let budget = MAX_INLINE_DOCS_TOTAL_CHARS;
|
|
233
240
|
const parts = [];
|
|
234
241
|
for (const doc of docs) {
|
|
235
|
-
const path = `.
|
|
242
|
+
const path = `.dahrk/scratch/docs/${attachedDocBasename(doc)}.md`;
|
|
236
243
|
const cap = Math.max(0, Math.min(MAX_INLINE_DOC_CHARS, budget));
|
|
237
244
|
const body = doc.content.trim();
|
|
238
245
|
const truncated = body.length > cap;
|
|
@@ -318,6 +325,8 @@ var OPENING_KICKOFF = "Begin now. Using the ticket context and your instructions
|
|
|
318
325
|
function interactiveSeedText(ctx, instructionInSystemPrompt) {
|
|
319
326
|
return instructionInSystemPrompt ? OPENING_KICKOFF : resolveStagePrompt(ctx);
|
|
320
327
|
}
|
|
328
|
+
|
|
329
|
+
// ../../packages/executor-worktree/src/mailbox.ts
|
|
321
330
|
var ManagedMailbox = class {
|
|
322
331
|
q = [];
|
|
323
332
|
waiters = [];
|
|
@@ -343,36 +352,19 @@ var ManagedMailbox = class {
|
|
|
343
352
|
};
|
|
344
353
|
}
|
|
345
354
|
};
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
signal.removeEventListener("abort", onAbort);
|
|
360
|
-
resolve3(r);
|
|
361
|
-
}
|
|
362
|
-
function onAbort() {
|
|
363
|
-
finish({ kind: "cancelled" });
|
|
364
|
-
}
|
|
365
|
-
if (signal.aborted) {
|
|
366
|
-
finish({ kind: "cancelled" });
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
signal.addEventListener("abort", onAbort);
|
|
370
|
-
timer = setTimeout(() => finish({ kind: "idle-timeout" }), idleMs);
|
|
371
|
-
pending.then(
|
|
372
|
-
(res) => finish(res.done ? { kind: "turns-exhausted" } : { kind: "turn", value: res.value }),
|
|
373
|
-
() => finish({ kind: "turns-exhausted" })
|
|
374
|
-
);
|
|
375
|
-
});
|
|
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
|
+
}
|
|
376
368
|
}
|
|
377
369
|
function createElicitTurnRouter(turns, opts) {
|
|
378
370
|
const conversation = new ManagedMailbox();
|
|
@@ -416,6 +408,147 @@ function createElicitTurnRouter(turns, opts) {
|
|
|
416
408
|
return { conversation, ask };
|
|
417
409
|
}
|
|
418
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
|
+
|
|
419
552
|
// ../../packages/executor-worktree/src/stage-complete-tool.ts
|
|
420
553
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
421
554
|
import { z } from "zod";
|
|
@@ -423,6 +556,10 @@ var STAGE_COMPLETE_TOOL_NAME = "mcp__dahrk__dahrk_stage_complete";
|
|
|
423
556
|
function createStageCompleteTool() {
|
|
424
557
|
let captured = null;
|
|
425
558
|
let capturedDoc = null;
|
|
559
|
+
const capture = (args) => {
|
|
560
|
+
captured = args.summary;
|
|
561
|
+
if (args.document !== void 0) capturedDoc = args.document;
|
|
562
|
+
};
|
|
426
563
|
const completeTool = tool(
|
|
427
564
|
"dahrk_stage_complete",
|
|
428
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.",
|
|
@@ -431,8 +568,7 @@ function createStageCompleteTool() {
|
|
|
431
568
|
document: z.string().optional().describe("The full markdown body of the stage's deliverable document, if any.")
|
|
432
569
|
},
|
|
433
570
|
async (args) => {
|
|
434
|
-
|
|
435
|
-
if (args.document !== void 0) capturedDoc = args.document;
|
|
571
|
+
capture(args);
|
|
436
572
|
return { content: [{ type: "text", text: "Stage marked complete." }] };
|
|
437
573
|
}
|
|
438
574
|
);
|
|
@@ -441,7 +577,8 @@ function createStageCompleteTool() {
|
|
|
441
577
|
allowedToolName: STAGE_COMPLETE_TOOL_NAME,
|
|
442
578
|
fired: () => captured !== null,
|
|
443
579
|
summary: () => captured,
|
|
444
|
-
document: () => capturedDoc
|
|
580
|
+
document: () => capturedDoc,
|
|
581
|
+
capture
|
|
445
582
|
};
|
|
446
583
|
}
|
|
447
584
|
|
|
@@ -495,15 +632,15 @@ function createAskUserQuestionTool(deps) {
|
|
|
495
632
|
}
|
|
496
633
|
|
|
497
634
|
// ../../packages/executor-worktree/src/claude-adapter.ts
|
|
498
|
-
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
499
635
|
var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
|
|
500
|
-
var HANDED_BACK_ARTIFACT_PATH = ".
|
|
636
|
+
var HANDED_BACK_ARTIFACT_PATH = ".dahrk/scratch/output/document.md";
|
|
501
637
|
var CLAUDE_CODE_SYSTEM_PROMPT = { type: "preset", preset: "claude_code" };
|
|
502
638
|
var userMsg = (text) => ({
|
|
503
639
|
type: "user",
|
|
504
640
|
parent_tool_use_id: null,
|
|
505
641
|
message: { role: "user", content: text }
|
|
506
642
|
});
|
|
643
|
+
var defaultAsk = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
507
644
|
var sessionIdOf = (msg) => "session_id" in msg && typeof msg.session_id === "string" ? msg.session_id : void 0;
|
|
508
645
|
var policyCanUseTool = async (ctx, toolName, input) => {
|
|
509
646
|
const verdict2 = ctx.authorizeToolUse?.(toolName, input);
|
|
@@ -543,7 +680,25 @@ function runtimeEnvOptions(ctx) {
|
|
|
543
680
|
if (!ctx.runtimeEnv) return {};
|
|
544
681
|
return { env: { ...process.env, ...ctx.runtimeEnv } };
|
|
545
682
|
}
|
|
546
|
-
|
|
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;
|
|
547
702
|
const abortController = new AbortController();
|
|
548
703
|
let cancelled = false;
|
|
549
704
|
let active;
|
|
@@ -608,11 +763,86 @@ function createClaudeRunner() {
|
|
|
608
763
|
}
|
|
609
764
|
active = void 0;
|
|
610
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
|
+
};
|
|
611
842
|
return {
|
|
612
843
|
runtime: "claude-code",
|
|
613
844
|
async runBatch(ctx, onTrace) {
|
|
614
|
-
const
|
|
615
|
-
const prompt = resolveStagePrompt(ctx);
|
|
845
|
+
const hooks = { emit: makeEmit("claude-code", onTrace), ask: defaultAsk };
|
|
616
846
|
const mcpServers = buildBrokeredMcpServers(ctx);
|
|
617
847
|
const options = {
|
|
618
848
|
...baseOptions(ctx),
|
|
@@ -623,171 +853,50 @@ function createClaudeRunner() {
|
|
|
623
853
|
// Headless: allow tools to run without an interactive permission prompt (M6 wires policy).
|
|
624
854
|
canUseTool: async (toolName, input) => policyCanUseTool(ctx, toolName, input)
|
|
625
855
|
};
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
try {
|
|
629
|
-
const q = query({ prompt, options });
|
|
630
|
-
active = q;
|
|
631
|
-
for await (const msg of q) {
|
|
632
|
-
const res = handleMessage(msg, emit, ctx, state, false);
|
|
633
|
-
if (res.isResult && res.status) status = res.status;
|
|
634
|
-
}
|
|
635
|
-
} catch (e) {
|
|
636
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
637
|
-
status = "fail";
|
|
638
|
-
} finally {
|
|
639
|
-
closeActive();
|
|
640
|
-
}
|
|
641
|
-
if (cancelled) status = "fail";
|
|
642
|
-
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 });
|
|
643
858
|
},
|
|
644
859
|
async runInteractive(ctx, turns, onTrace) {
|
|
645
860
|
const emit = makeEmit("claude-code", onTrace);
|
|
646
861
|
const stageTool = createStageCompleteTool();
|
|
647
|
-
const {
|
|
648
|
-
let
|
|
649
|
-
const
|
|
650
|
-
|
|
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
|
-
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
676
|
-
// MCP servers (parity with batch).
|
|
677
|
-
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
678
|
-
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
679
|
-
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
680
|
-
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
681
|
-
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
682
|
-
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
683
|
-
// it does not restrict the other tools canUseTool allows.
|
|
684
|
-
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
685
|
-
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising, stageTool.allowedToolName, ctx, toolName, input),
|
|
686
|
-
maxTurns: MAX_TURNS,
|
|
687
|
-
includePartialMessages: false
|
|
688
|
-
};
|
|
689
|
-
const exit = ctx.config.exit ?? "either";
|
|
690
|
-
const wantsTool = exit === "tool" || exit === "either";
|
|
691
|
-
const mailbox = new ManagedMailbox();
|
|
692
|
-
const q = query({ prompt: mailbox, options });
|
|
693
|
-
active = q;
|
|
694
|
-
const it = q[Symbol.asyncIterator]();
|
|
695
|
-
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
696
|
-
const state = newBufferState();
|
|
697
|
-
const consumeTurn = async () => {
|
|
698
|
-
for (; ; ) {
|
|
699
|
-
const { value: msg, done } = await it.next();
|
|
700
|
-
if (done) return void 0;
|
|
701
|
-
const res = handleMessage(msg, emit, ctx, state, true);
|
|
702
|
-
if (res.isResult) return res.responseText;
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
let exited = null;
|
|
706
|
-
let pending = humanIter.next();
|
|
707
|
-
try {
|
|
708
|
-
mailbox.push(userMsg(interactiveSeedText(ctx, hasSystemPrompt(ctx))));
|
|
709
|
-
await consumeTurn();
|
|
710
|
-
if (stageTool.fired() && wantsTool) exited = "tool";
|
|
711
|
-
while (exited === null) {
|
|
712
|
-
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, abortController.signal);
|
|
713
|
-
awaitingFirstReply = false;
|
|
714
|
-
if (race.kind === "cancelled") {
|
|
715
|
-
exited = "cancelled";
|
|
716
|
-
break;
|
|
717
|
-
}
|
|
718
|
-
if (race.kind === "idle-timeout") {
|
|
719
|
-
exited = "timeout";
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
if (race.kind === "turns-exhausted") {
|
|
723
|
-
exited = "gate";
|
|
724
|
-
break;
|
|
725
|
-
}
|
|
726
|
-
const texts = [race.value.text];
|
|
727
|
-
pending = humanIter.next();
|
|
728
|
-
for (; ; ) {
|
|
729
|
-
const more = await raceNextTurn(pending, COALESCE_MS, abortController.signal);
|
|
730
|
-
if (more.kind === "turn") {
|
|
731
|
-
texts.push(more.value.text);
|
|
732
|
-
pending = humanIter.next();
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (more.kind === "cancelled") exited = "cancelled";
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
if (exited === "cancelled") break;
|
|
739
|
-
mailbox.push(userMsg(texts.join("\n")));
|
|
740
|
-
await consumeTurn();
|
|
741
|
-
if (stageTool.fired() && wantsTool) {
|
|
742
|
-
exited = "tool";
|
|
743
|
-
break;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
} catch (e) {
|
|
747
|
-
if (!cancelled && !exited) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
748
|
-
exited = exited ?? (cancelled ? "cancelled" : "gate");
|
|
749
|
-
}
|
|
750
|
-
let status = "ok";
|
|
751
|
-
let summary = "";
|
|
752
|
-
if (exited === "tool") {
|
|
753
|
-
summary = stageTool.summary() ?? "(stage marked complete)";
|
|
754
|
-
} else if (exited === "gate") {
|
|
755
|
-
summarising = true;
|
|
756
|
-
try {
|
|
757
|
-
mailbox.push(userMsg(SUMMARISE_PROMPT));
|
|
758
|
-
const reply = await consumeTurn();
|
|
759
|
-
summary = (reply ?? "").trim() || "(no summary produced)";
|
|
760
|
-
} catch {
|
|
761
|
-
summary = "(no summary produced)";
|
|
762
|
-
} finally {
|
|
763
|
-
summarising = false;
|
|
764
|
-
}
|
|
765
|
-
} else if (exited === "timeout") {
|
|
766
|
-
status = "timeout";
|
|
767
|
-
summary = "(stage timed out awaiting input)";
|
|
768
|
-
await this.cancel();
|
|
769
|
-
} else {
|
|
770
|
-
status = "fail";
|
|
771
|
-
summary = "(stage cancelled)";
|
|
772
|
-
}
|
|
773
|
-
mailbox.end();
|
|
774
|
-
try {
|
|
775
|
-
for (; ; ) {
|
|
776
|
-
const { done } = await it.next();
|
|
777
|
-
if (done) break;
|
|
778
|
-
}
|
|
779
|
-
} catch {
|
|
780
|
-
}
|
|
781
|
-
closeActive();
|
|
782
|
-
const handedBackDoc = status === "ok" ? stageTool.document() : null;
|
|
783
|
-
const artifact = handedBackDoc !== null ? { path: ctx.config.emitArtifact ?? HANDED_BACK_ARTIFACT_PATH, content: handedBackDoc } : void 0;
|
|
784
|
-
return {
|
|
785
|
-
status,
|
|
786
|
-
summary,
|
|
787
|
-
...sessionId ? { sessionId } : {},
|
|
788
|
-
...costUsd !== void 0 ? { costUsd } : {},
|
|
789
|
-
...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;
|
|
790
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;
|
|
791
900
|
},
|
|
792
901
|
async summarise(ctx) {
|
|
793
902
|
if (!sessionId) return "(no summary: session not established)";
|
|
@@ -802,9 +911,9 @@ function createClaudeRunner() {
|
|
|
802
911
|
};
|
|
803
912
|
try {
|
|
804
913
|
let out2 = "";
|
|
805
|
-
const
|
|
806
|
-
active =
|
|
807
|
-
for await (const msg of
|
|
914
|
+
const session = createSession({ prompt: SUMMARISE_PROMPT, options });
|
|
915
|
+
active = session;
|
|
916
|
+
for await (const msg of session) {
|
|
808
917
|
const found = sessionIdOf(msg);
|
|
809
918
|
if (found) sessionId = found;
|
|
810
919
|
if (msg.type === "result" && msg.subtype === "success") out2 += msg.result;
|
|
@@ -828,237 +937,21 @@ function createClaudeRunner() {
|
|
|
828
937
|
};
|
|
829
938
|
}
|
|
830
939
|
|
|
831
|
-
// ../../packages/executor-worktree/src/
|
|
832
|
-
import {
|
|
940
|
+
// ../../packages/executor-worktree/src/pi-adapter.ts
|
|
941
|
+
import { join as join4 } from "path";
|
|
833
942
|
|
|
834
|
-
// ../../packages/executor-worktree/src/
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return [
|
|
843
|
-
{ type: "action", tool: "command", toolUseId: item.id, input: { command: item.command } },
|
|
844
|
-
{ type: "observation", toolUseId: item.id, output: item.aggregated_output, isError: item.status === "failed" }
|
|
845
|
-
];
|
|
846
|
-
case "mcp_tool_call":
|
|
847
|
-
return [
|
|
848
|
-
{ type: "action", tool: `${item.server}/${item.tool}`, toolUseId: item.id, input: item.arguments },
|
|
849
|
-
{ type: "observation", toolUseId: item.id, output: item.result?.content ?? item.error, isError: Boolean(item.error) }
|
|
850
|
-
];
|
|
851
|
-
case "web_search":
|
|
852
|
-
return [{ type: "action", tool: "web_search", toolUseId: item.id, input: { query: item.query } }];
|
|
853
|
-
case "file_change":
|
|
854
|
-
return [{ type: "action", tool: "apply_patch", toolUseId: item.id, input: item.changes }];
|
|
855
|
-
case "todo_list":
|
|
856
|
-
return [{ type: "thought", text: JSON.stringify(item.items) }];
|
|
857
|
-
case "error":
|
|
858
|
-
return [{ type: "error", kind: "item_error", message: item.message }];
|
|
859
|
-
default:
|
|
860
|
-
return [];
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
function mapCodexEvent(ev) {
|
|
864
|
-
switch (ev.type) {
|
|
865
|
-
case "item.completed":
|
|
866
|
-
return { events: mapItem(ev.item), recognised: true };
|
|
867
|
-
case "turn.completed":
|
|
868
|
-
return {
|
|
869
|
-
events: [
|
|
870
|
-
{ type: "state", event: "stage-exit", status: "ok", usage: mapUsage2(ev.usage) }
|
|
871
|
-
],
|
|
872
|
-
recognised: true
|
|
873
|
-
};
|
|
874
|
-
case "turn.failed":
|
|
875
|
-
return {
|
|
876
|
-
events: [
|
|
877
|
-
{ type: "error", kind: "turn_failed", message: JSON.stringify(ev.error ?? {}) },
|
|
878
|
-
{ type: "state", event: "stage-exit", status: "fail" }
|
|
879
|
-
],
|
|
880
|
-
recognised: true
|
|
881
|
-
};
|
|
882
|
-
case "error":
|
|
883
|
-
return { events: [{ type: "error", kind: "thread_error", message: ev.message }], recognised: true };
|
|
884
|
-
// Lifecycle / interim updates: recognised, captured in raw sidecar, not normalised.
|
|
885
|
-
case "thread.started":
|
|
886
|
-
case "turn.started":
|
|
887
|
-
case "item.started":
|
|
888
|
-
case "item.updated":
|
|
889
|
-
return { events: [], recognised: true };
|
|
890
|
-
default:
|
|
891
|
-
return { events: [], recognised: false };
|
|
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;
|
|
892
951
|
}
|
|
952
|
+
return msg;
|
|
893
953
|
}
|
|
894
954
|
function mapUsage2(u) {
|
|
895
|
-
return {
|
|
896
|
-
input: u?.input_tokens ?? 0,
|
|
897
|
-
output: u?.output_tokens ?? 0,
|
|
898
|
-
cacheRead: u?.cached_input_tokens ?? 0,
|
|
899
|
-
cacheCreate: 0
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ../../packages/executor-worktree/src/codex-adapter.ts
|
|
904
|
-
var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
905
|
-
var CODEX_COST_UNAVAILABLE_NOTE = "codex-adapter: cost reporting unavailable for the Codex runtime (the SDK reports tokens, not price); costUsd left unset, not $0\n";
|
|
906
|
-
function warnCostUnavailable(write = (s) => void process.stderr.write(s)) {
|
|
907
|
-
write(CODEX_COST_UNAVAILABLE_NOTE);
|
|
908
|
-
}
|
|
909
|
-
function runtimeEnvOptions2(ctx) {
|
|
910
|
-
if (!ctx.runtimeEnv) return {};
|
|
911
|
-
const env = {};
|
|
912
|
-
for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
|
|
913
|
-
return { env: { ...env, ...ctx.runtimeEnv } };
|
|
914
|
-
}
|
|
915
|
-
function createCodexRunner() {
|
|
916
|
-
const abortController = new AbortController();
|
|
917
|
-
const signal = abortController.signal;
|
|
918
|
-
let cancelled = false;
|
|
919
|
-
let thread;
|
|
920
|
-
let sessionId;
|
|
921
|
-
const threadOptions = (ctx) => ({
|
|
922
|
-
workingDirectory: ctx.workspace.worktreePath,
|
|
923
|
-
sandboxMode: "workspace-write",
|
|
924
|
-
skipGitRepoCheck: true,
|
|
925
|
-
...ctx.config.model ? { model: ctx.config.model } : {}
|
|
926
|
-
});
|
|
927
|
-
const openThread = (ctx) => {
|
|
928
|
-
const codex = new Codex(runtimeEnvOptions2(ctx));
|
|
929
|
-
const t = ctx.sessionId ? codex.resumeThread(ctx.sessionId, threadOptions(ctx)) : codex.startThread(threadOptions(ctx));
|
|
930
|
-
thread = t;
|
|
931
|
-
return t;
|
|
932
|
-
};
|
|
933
|
-
const pumpTurn = async (events, emit, ctx, suppressStageExit) => {
|
|
934
|
-
let failed = false;
|
|
935
|
-
for await (const ev of events) {
|
|
936
|
-
const rawRef = ctx.writeRaw?.(ev);
|
|
937
|
-
const { events: mapped } = mapCodexEvent(ev);
|
|
938
|
-
for (const e of mapped) {
|
|
939
|
-
if (suppressStageExit && e.type === "state") continue;
|
|
940
|
-
emit(e, rawRef);
|
|
941
|
-
}
|
|
942
|
-
if (ev.type === "thread.started") sessionId = ev.thread_id;
|
|
943
|
-
if (ev.type === "turn.failed") failed = true;
|
|
944
|
-
}
|
|
945
|
-
return failed;
|
|
946
|
-
};
|
|
947
|
-
const captureThreadId = (t) => {
|
|
948
|
-
if (t.id) sessionId = t.id;
|
|
949
|
-
};
|
|
950
|
-
return {
|
|
951
|
-
runtime: "codex",
|
|
952
|
-
async runBatch(ctx, onTrace) {
|
|
953
|
-
if (ctx.config.mcpServers && ctx.config.mcpServers.length > 0) {
|
|
954
|
-
process.stderr.write("codex-adapter: MCP servers not supported on Codex; ignoring\n");
|
|
955
|
-
}
|
|
956
|
-
const emit = makeEmit("codex", onTrace);
|
|
957
|
-
const t = openThread(ctx);
|
|
958
|
-
let status = "ok";
|
|
959
|
-
try {
|
|
960
|
-
const { events } = await t.runStreamed(resolveStagePrompt(ctx), { signal });
|
|
961
|
-
if (await pumpTurn(events, emit, ctx, false)) status = "fail";
|
|
962
|
-
} catch (e) {
|
|
963
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
964
|
-
status = "fail";
|
|
965
|
-
}
|
|
966
|
-
captureThreadId(t);
|
|
967
|
-
if (cancelled) status = "fail";
|
|
968
|
-
warnCostUnavailable();
|
|
969
|
-
return { status, ...sessionId ? { sessionId } : {} };
|
|
970
|
-
},
|
|
971
|
-
async runInteractive(ctx, turns, onTrace) {
|
|
972
|
-
const emit = makeEmit("codex", onTrace);
|
|
973
|
-
const t = openThread(ctx);
|
|
974
|
-
const exit = ctx.config.exit ?? "either";
|
|
975
|
-
if (exit === "tool" || exit === "either") {
|
|
976
|
-
process.stderr.write("codex-adapter: interactive tool-exit not supported in M4; using gate exit\n");
|
|
977
|
-
}
|
|
978
|
-
const humanIter = turns[Symbol.asyncIterator]();
|
|
979
|
-
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
980
|
-
let awaitingFirstReply = true;
|
|
981
|
-
let exited = "gate";
|
|
982
|
-
let pending = humanIter.next();
|
|
983
|
-
try {
|
|
984
|
-
const seed = await t.runStreamed(interactiveSeedText(ctx, false), { signal });
|
|
985
|
-
await pumpTurn(seed.events, emit, ctx, true);
|
|
986
|
-
for (; ; ) {
|
|
987
|
-
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, signal);
|
|
988
|
-
awaitingFirstReply = false;
|
|
989
|
-
if (race.kind === "cancelled") {
|
|
990
|
-
exited = "cancelled";
|
|
991
|
-
break;
|
|
992
|
-
}
|
|
993
|
-
if (race.kind === "idle-timeout") {
|
|
994
|
-
exited = "timeout";
|
|
995
|
-
break;
|
|
996
|
-
}
|
|
997
|
-
if (race.kind === "turns-exhausted") {
|
|
998
|
-
exited = "gate";
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
const texts = [race.value.text];
|
|
1002
|
-
pending = humanIter.next();
|
|
1003
|
-
for (; ; ) {
|
|
1004
|
-
const more = await raceNextTurn(pending, COALESCE_MS2, signal);
|
|
1005
|
-
if (more.kind === "turn") {
|
|
1006
|
-
texts.push(more.value.text);
|
|
1007
|
-
pending = humanIter.next();
|
|
1008
|
-
continue;
|
|
1009
|
-
}
|
|
1010
|
-
if (more.kind === "cancelled") exited = "cancelled";
|
|
1011
|
-
break;
|
|
1012
|
-
}
|
|
1013
|
-
if (exited === "cancelled") break;
|
|
1014
|
-
const { events } = await t.runStreamed(texts.join("\n"), { signal });
|
|
1015
|
-
await pumpTurn(events, emit, ctx, true);
|
|
1016
|
-
}
|
|
1017
|
-
} catch (e) {
|
|
1018
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
1019
|
-
exited = cancelled ? "cancelled" : "gate";
|
|
1020
|
-
}
|
|
1021
|
-
let status = "ok";
|
|
1022
|
-
let summary = "";
|
|
1023
|
-
if (exited === "gate") {
|
|
1024
|
-
try {
|
|
1025
|
-
const turn = await t.run(SUMMARISE_PROMPT, { signal });
|
|
1026
|
-
summary = (turn.finalResponse ?? "").trim() || "(no summary produced)";
|
|
1027
|
-
} catch {
|
|
1028
|
-
summary = "(no summary produced)";
|
|
1029
|
-
}
|
|
1030
|
-
} else if (exited === "timeout") {
|
|
1031
|
-
status = "timeout";
|
|
1032
|
-
summary = "(stage timed out awaiting input)";
|
|
1033
|
-
await this.cancel();
|
|
1034
|
-
} else {
|
|
1035
|
-
status = "fail";
|
|
1036
|
-
summary = "(stage cancelled)";
|
|
1037
|
-
}
|
|
1038
|
-
captureThreadId(t);
|
|
1039
|
-
warnCostUnavailable();
|
|
1040
|
-
return { status, summary, ...sessionId ? { sessionId } : {} };
|
|
1041
|
-
},
|
|
1042
|
-
async summarise(ctx) {
|
|
1043
|
-
if (!thread) return "(no summary: thread not established)";
|
|
1044
|
-
try {
|
|
1045
|
-
const turn = await thread.run(SUMMARISE_PROMPT, { signal });
|
|
1046
|
-
captureThreadId(thread);
|
|
1047
|
-
return (turn.finalResponse ?? "").trim() || "(no summary produced)";
|
|
1048
|
-
} catch (e) {
|
|
1049
|
-
return `(summary unavailable: ${e.message})`;
|
|
1050
|
-
}
|
|
1051
|
-
},
|
|
1052
|
-
async cancel() {
|
|
1053
|
-
if (cancelled) return;
|
|
1054
|
-
cancelled = true;
|
|
1055
|
-
abortController.abort();
|
|
1056
|
-
}
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// ../../packages/executor-worktree/src/pi-mappers.ts
|
|
1061
|
-
function mapUsage3(u) {
|
|
1062
955
|
return {
|
|
1063
956
|
input: u?.input ?? 0,
|
|
1064
957
|
output: u?.output ?? 0,
|
|
@@ -1099,7 +992,7 @@ function mapPiEvent(ev) {
|
|
|
1099
992
|
const kind = ev.type === "agent_end" ? "agent_error" : "turn_error";
|
|
1100
993
|
events.push({ type: "error", kind, message: m?.errorMessage ?? m?.stopReason ?? "failed" });
|
|
1101
994
|
}
|
|
1102
|
-
events.push({ type: "state", event: "stage-exit", status, usage:
|
|
995
|
+
events.push({ type: "state", event: "stage-exit", status, usage: mapUsage2(m?.usage) });
|
|
1103
996
|
return { events, recognised: true };
|
|
1104
997
|
}
|
|
1105
998
|
// Streamed deltas: owned by the buffered state machine, no discrete event here.
|
|
@@ -1154,12 +1047,8 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
1154
1047
|
if (ev.type === "turn_end" || ev.type === "agent_end") {
|
|
1155
1048
|
const r = mapPiEvent(ev);
|
|
1156
1049
|
const status = settleStatus(settleMessage(ev));
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (status === "ok" && text && !state.turnEndedOnTool) {
|
|
1160
|
-
responseText = text;
|
|
1161
|
-
events.push({ type: "response", text });
|
|
1162
|
-
}
|
|
1050
|
+
const decision = decideResponse(state.bufferedText, state.turnEndedOnTool, status);
|
|
1051
|
+
if (decision.event) events.push(decision.event);
|
|
1163
1052
|
if (state.pendingThought) {
|
|
1164
1053
|
events.push({ type: "thought", subtype: "reasoning_text", text: state.pendingThought });
|
|
1165
1054
|
}
|
|
@@ -1170,162 +1059,162 @@ function consumePiEvent(ev, state, suppressStageExit) {
|
|
|
1170
1059
|
state.bufferedText = "";
|
|
1171
1060
|
state.pendingThought = "";
|
|
1172
1061
|
state.turnEndedOnTool = false;
|
|
1173
|
-
return { events, isResult: true, status, responseText };
|
|
1062
|
+
return { events, isResult: true, status, responseText: decision.responseText };
|
|
1174
1063
|
}
|
|
1175
1064
|
return { events, isResult: false };
|
|
1176
1065
|
}
|
|
1177
1066
|
|
|
1067
|
+
// ../../packages/executor-worktree/src/pi-auth.ts
|
|
1068
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
1069
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1070
|
+
import { join as join3 } from "path";
|
|
1071
|
+
function readAuthHint(ctx) {
|
|
1072
|
+
return ctx.runtimeAuth;
|
|
1073
|
+
}
|
|
1074
|
+
function applyApiKeyAuth(hint2, runtimeEnv, authStorage) {
|
|
1075
|
+
const env = runtimeEnv ?? {};
|
|
1076
|
+
for (const p of hint2?.providers ?? []) {
|
|
1077
|
+
if (p.kind !== "api_key") continue;
|
|
1078
|
+
const value = env[p.envVar];
|
|
1079
|
+
if (value) authStorage.setRuntimeApiKey(p.provider, value);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function buildAuthJson(hint2) {
|
|
1083
|
+
const entries = [];
|
|
1084
|
+
for (const p of hint2?.providers ?? []) {
|
|
1085
|
+
if (p.kind !== "oauth") continue;
|
|
1086
|
+
entries.push([p.provider, { type: "oauth", ...p.extra, access: p.access, refresh: p.refresh, expires: p.expires }]);
|
|
1087
|
+
}
|
|
1088
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
1089
|
+
}
|
|
1090
|
+
function buildCustomProviders(hint2) {
|
|
1091
|
+
const providers = {};
|
|
1092
|
+
for (const p of hint2?.providers ?? []) {
|
|
1093
|
+
if (p.kind !== "api_key" || !p.baseUrl) continue;
|
|
1094
|
+
providers[p.provider] = {
|
|
1095
|
+
baseUrl: p.baseUrl,
|
|
1096
|
+
...p.headers ? { headers: p.headers } : {},
|
|
1097
|
+
...p.models ? { models: p.models } : {}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
return Object.keys(providers).length ? { providers } : void 0;
|
|
1101
|
+
}
|
|
1102
|
+
function createStageConfigDir() {
|
|
1103
|
+
return mkdtempSync(join3(tmpdir2(), "dahrk-pi-"));
|
|
1104
|
+
}
|
|
1105
|
+
function cleanupStageConfigDir(dir) {
|
|
1106
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1107
|
+
}
|
|
1108
|
+
function writeStageAuthFile(dir, hint2) {
|
|
1109
|
+
const authJson = buildAuthJson(hint2);
|
|
1110
|
+
if (!authJson) return void 0;
|
|
1111
|
+
const path = join3(dir, "auth.json");
|
|
1112
|
+
writeFileSync(path, JSON.stringify(authJson, null, 2));
|
|
1113
|
+
return path;
|
|
1114
|
+
}
|
|
1115
|
+
function writeStageCustomProviders(dir, hint2) {
|
|
1116
|
+
const models = buildCustomProviders(hint2);
|
|
1117
|
+
if (!models) return void 0;
|
|
1118
|
+
const path = join3(dir, "models.json");
|
|
1119
|
+
writeFileSync(path, JSON.stringify(models, null, 2));
|
|
1120
|
+
return path;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1178
1123
|
// ../../packages/executor-worktree/src/pi-adapter.ts
|
|
1179
|
-
var COALESCE_MS3 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
1180
1124
|
var PI_STAGE_COMPLETE_TOOL = "dahrk_stage_complete";
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1125
|
+
var PI_ASK_USER_QUESTION_TOOL = "ask_user_question";
|
|
1126
|
+
function piToolCallDecision(ctx, toolName, input) {
|
|
1127
|
+
const verdict2 = ctx.authorizeToolUse?.(toolName, input);
|
|
1128
|
+
if (verdict2?.verdict === "deny") {
|
|
1129
|
+
return { block: true, reason: verdict2.reason ?? `tool "${toolName}" denied by policy ${verdict2.policy}` };
|
|
1130
|
+
}
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
function registerToolCallGate(s, ctx) {
|
|
1134
|
+
const policyCtx = ctx;
|
|
1135
|
+
s.setToolCallGate?.((toolName, input) => piToolCallDecision(policyCtx, toolName, input));
|
|
1136
|
+
}
|
|
1137
|
+
function buildBrokeredPiMcpServers(ctx) {
|
|
1138
|
+
const servers = ctx.config.mcpServers;
|
|
1139
|
+
if (!servers || servers.length === 0 || !ctx.mcpProxyBaseUrl) return void 0;
|
|
1140
|
+
const entries = {};
|
|
1141
|
+
for (const s of servers) entries[s.id] = { type: s.type, url: `${ctx.mcpProxyBaseUrl}/${s.id}` };
|
|
1142
|
+
return entries;
|
|
1143
|
+
}
|
|
1144
|
+
function createBrokeredMcpExtension(servers) {
|
|
1199
1145
|
return {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1146
|
+
name: "dahrk-brokered-mcp",
|
|
1147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1148
|
+
factory: async (pi) => {
|
|
1149
|
+
const { Client } = await import("./client-MZ5GEQAD.js");
|
|
1150
|
+
const { StreamableHTTPClientTransport } = await import("./streamableHttp-S27HYW5J.js");
|
|
1151
|
+
for (const [id, server] of Object.entries(servers)) {
|
|
1152
|
+
try {
|
|
1153
|
+
const client = new Client({ name: `dahrk-${id}`, version: "0.1.0" });
|
|
1154
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(server.url)));
|
|
1155
|
+
const { tools } = await client.listTools();
|
|
1156
|
+
for (const tool3 of tools) {
|
|
1157
|
+
pi.registerTool({
|
|
1158
|
+
name: tool3.name,
|
|
1159
|
+
label: tool3.name,
|
|
1160
|
+
description: tool3.description ?? "",
|
|
1161
|
+
// The MCP inputSchema is a JSON-schema object; Pi validates against it at runtime (its
|
|
1162
|
+
// custom tools use the same plain-object shape - see the injected tools above).
|
|
1163
|
+
parameters: tool3.inputSchema,
|
|
1164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1165
|
+
execute: async (_toolCallId, params) => {
|
|
1166
|
+
const result = await client.callTool({ name: tool3.name, arguments: params ?? {} });
|
|
1167
|
+
return { content: result.content, details: {} };
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1219
1173
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
var defaultAsk2 = async () => elicitOutcomeReply({ kind: "noreply" });
|
|
1178
|
+
function makePiRuntimeSession(s, ctx, hooks, interactive) {
|
|
1179
|
+
return {
|
|
1180
|
+
get sessionId() {
|
|
1181
|
+
return s.sessionId;
|
|
1224
1182
|
},
|
|
1225
|
-
async
|
|
1226
|
-
const emit = makeEmit("pi", onTrace);
|
|
1227
|
-
const s = await openSession(ctx);
|
|
1183
|
+
async sendTurn(text) {
|
|
1228
1184
|
const state = newPiBufferState();
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
let toolFired = false;
|
|
1232
|
-
let toolSummary = null;
|
|
1185
|
+
let stageComplete = false;
|
|
1186
|
+
let summary;
|
|
1233
1187
|
let stageCompleteCallId;
|
|
1234
|
-
let
|
|
1188
|
+
let responseText;
|
|
1189
|
+
let status;
|
|
1235
1190
|
const unsub = s.subscribe((ev) => {
|
|
1236
1191
|
const rawRef = ctx.writeRaw?.(ev);
|
|
1237
1192
|
if (ev.type === "tool_execution_start" && ev.toolName === PI_STAGE_COMPLETE_TOOL) {
|
|
1238
|
-
|
|
1193
|
+
stageComplete = true;
|
|
1239
1194
|
stageCompleteCallId = ev.toolCallId;
|
|
1240
1195
|
const args = ev.args;
|
|
1241
|
-
if (args?.summary)
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
if (ev.type === "tool_execution_end" && ev.toolCallId === stageCompleteCallId) return;
|
|
1245
|
-
const r = consumePiEvent(ev, state, true);
|
|
1246
|
-
for (const e of r.events) emit(e, rawRef);
|
|
1247
|
-
if (r.responseText) lastResponseText = r.responseText;
|
|
1248
|
-
});
|
|
1249
|
-
const humanIter = turns[Symbol.asyncIterator]();
|
|
1250
|
-
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
1251
|
-
let awaitingFirstReply = true;
|
|
1252
|
-
let exited = "gate";
|
|
1253
|
-
let pending = humanIter.next();
|
|
1254
|
-
try {
|
|
1255
|
-
await s.prompt(interactiveSeedText(ctx, false));
|
|
1256
|
-
if (toolFired && wantsTool) exited = "tool";
|
|
1257
|
-
for (; ; ) {
|
|
1258
|
-
if (exited === "tool") break;
|
|
1259
|
-
const race = await raceNextTurn(pending, awaitingFirstReply ? firstReplyMs : idleMs, signal);
|
|
1260
|
-
awaitingFirstReply = false;
|
|
1261
|
-
if (race.kind === "cancelled") {
|
|
1262
|
-
exited = "cancelled";
|
|
1263
|
-
break;
|
|
1264
|
-
}
|
|
1265
|
-
if (race.kind === "idle-timeout") {
|
|
1266
|
-
exited = "timeout";
|
|
1267
|
-
break;
|
|
1268
|
-
}
|
|
1269
|
-
if (race.kind === "turns-exhausted") {
|
|
1270
|
-
exited = "gate";
|
|
1271
|
-
break;
|
|
1272
|
-
}
|
|
1273
|
-
const texts = [race.value.text];
|
|
1274
|
-
pending = humanIter.next();
|
|
1275
|
-
for (; ; ) {
|
|
1276
|
-
const more = await raceNextTurn(pending, COALESCE_MS3, signal);
|
|
1277
|
-
if (more.kind === "turn") {
|
|
1278
|
-
texts.push(more.value.text);
|
|
1279
|
-
pending = humanIter.next();
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (more.kind === "cancelled") exited = "cancelled";
|
|
1283
|
-
break;
|
|
1284
|
-
}
|
|
1285
|
-
if (exited === "cancelled") break;
|
|
1286
|
-
await s.prompt(texts.join("\n"));
|
|
1287
|
-
if (toolFired && wantsTool) {
|
|
1288
|
-
exited = "tool";
|
|
1289
|
-
break;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
} catch (e) {
|
|
1293
|
-
if (!cancelled) emit({ type: "error", kind: "runtime_error", message: e.message });
|
|
1294
|
-
exited = cancelled ? "cancelled" : "gate";
|
|
1295
|
-
}
|
|
1296
|
-
let status = "ok";
|
|
1297
|
-
let summary = "";
|
|
1298
|
-
if (exited === "tool") {
|
|
1299
|
-
summary = toolSummary ?? "(stage marked complete)";
|
|
1300
|
-
} else if (exited === "gate") {
|
|
1301
|
-
lastResponseText = void 0;
|
|
1302
|
-
try {
|
|
1303
|
-
await s.prompt(SUMMARISE_PROMPT);
|
|
1304
|
-
summary = (lastResponseText ?? "").trim() || "(no summary produced)";
|
|
1305
|
-
} catch {
|
|
1306
|
-
summary = "(no summary produced)";
|
|
1196
|
+
if (args?.summary) summary = args.summary;
|
|
1197
|
+
return;
|
|
1307
1198
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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();
|
|
1315
1209
|
}
|
|
1316
|
-
unsub();
|
|
1317
|
-
captureSessionId(s);
|
|
1318
|
-
const costUsd = readSessionCost(s);
|
|
1319
1210
|
return {
|
|
1320
|
-
|
|
1321
|
-
summary,
|
|
1322
|
-
...
|
|
1323
|
-
...
|
|
1211
|
+
stageComplete,
|
|
1212
|
+
...summary !== void 0 ? { summary } : {},
|
|
1213
|
+
...responseText !== void 0 ? { responseText } : {},
|
|
1214
|
+
...status !== void 0 ? { status } : {}
|
|
1324
1215
|
};
|
|
1325
1216
|
},
|
|
1326
|
-
async
|
|
1327
|
-
if (!session) return "(no summary: session not established)";
|
|
1328
|
-
const s = session;
|
|
1217
|
+
async summariseTurn() {
|
|
1329
1218
|
if (s.agent) s.agent.state.tools = [];
|
|
1330
1219
|
const state = newPiBufferState();
|
|
1331
1220
|
let out2;
|
|
@@ -1335,7 +1224,6 @@ function createPiRunner(deps = {}) {
|
|
|
1335
1224
|
});
|
|
1336
1225
|
try {
|
|
1337
1226
|
await s.prompt(SUMMARISE_PROMPT);
|
|
1338
|
-
captureSessionId(s);
|
|
1339
1227
|
return (out2 ?? "").trim() || "(no summary produced)";
|
|
1340
1228
|
} catch (e) {
|
|
1341
1229
|
return `(summary unavailable: ${e.message})`;
|
|
@@ -1343,6 +1231,72 @@ function createPiRunner(deps = {}) {
|
|
|
1343
1231
|
unsub();
|
|
1344
1232
|
}
|
|
1345
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
|
+
}
|
|
1243
|
+
function createPiRunner(deps = {}) {
|
|
1244
|
+
const createSession = deps.createSession ?? defaultCreatePiSession;
|
|
1245
|
+
const abortController = new AbortController();
|
|
1246
|
+
const signal = abortController.signal;
|
|
1247
|
+
let cancelled = false;
|
|
1248
|
+
let session;
|
|
1249
|
+
const openSession = async (ctx) => {
|
|
1250
|
+
if (!session) session = await createSession(ctx);
|
|
1251
|
+
return session;
|
|
1252
|
+
};
|
|
1253
|
+
let sessionDisposed = false;
|
|
1254
|
+
const disposeSession = () => {
|
|
1255
|
+
if (sessionDisposed || !session) return;
|
|
1256
|
+
sessionDisposed = true;
|
|
1257
|
+
try {
|
|
1258
|
+
session.dispose();
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
return {
|
|
1263
|
+
runtime: "pi",
|
|
1264
|
+
async runBatch(ctx, onTrace) {
|
|
1265
|
+
const hooks = { emit: makeEmit("pi", onTrace), ask: defaultAsk2 };
|
|
1266
|
+
const s = await openSession(ctx);
|
|
1267
|
+
registerToolCallGate(s, ctx);
|
|
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;
|
|
1272
|
+
},
|
|
1273
|
+
async runInteractive(ctx, turns, onTrace) {
|
|
1274
|
+
const emit = makeEmit("pi", onTrace);
|
|
1275
|
+
const s = await openSession(ctx);
|
|
1276
|
+
registerToolCallGate(s, ctx);
|
|
1277
|
+
const makeSession = (hooks) => {
|
|
1278
|
+
s.setAskUserQuestionHandler?.((questions) => askQuestionsSequentially(questions, (q) => hooks.ask(q)));
|
|
1279
|
+
return makePiRuntimeSession(s, ctx, hooks, true);
|
|
1280
|
+
};
|
|
1281
|
+
const result = await runInteractiveLoop(ctx, turns, emit, makeSession, {
|
|
1282
|
+
signal,
|
|
1283
|
+
cancelled: () => cancelled,
|
|
1284
|
+
cancel: () => this.cancel(),
|
|
1285
|
+
instructionInSystemPrompt: false
|
|
1286
|
+
});
|
|
1287
|
+
disposeSession();
|
|
1288
|
+
return result;
|
|
1289
|
+
},
|
|
1290
|
+
async summarise(ctx) {
|
|
1291
|
+
if (!session) return "(no summary: session not established)";
|
|
1292
|
+
const rt = makePiRuntimeSession(session, ctx, { emit: () => {
|
|
1293
|
+
}, ask: defaultAsk2 }, true);
|
|
1294
|
+
try {
|
|
1295
|
+
return await rt.summariseTurn();
|
|
1296
|
+
} finally {
|
|
1297
|
+
disposeSession();
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1346
1300
|
async cancel() {
|
|
1347
1301
|
if (cancelled) return;
|
|
1348
1302
|
cancelled = true;
|
|
@@ -1351,23 +1305,31 @@ function createPiRunner(deps = {}) {
|
|
|
1351
1305
|
await session?.abort();
|
|
1352
1306
|
} catch {
|
|
1353
1307
|
}
|
|
1354
|
-
|
|
1355
|
-
session?.dispose();
|
|
1356
|
-
} catch {
|
|
1357
|
-
}
|
|
1308
|
+
disposeSession();
|
|
1358
1309
|
}
|
|
1359
1310
|
};
|
|
1360
1311
|
}
|
|
1361
1312
|
async function defaultCreatePiSession(ctx) {
|
|
1362
1313
|
const spec = "@earendil-works/pi-coding-agent";
|
|
1363
1314
|
const mod = await import(spec);
|
|
1364
|
-
const {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1315
|
+
const {
|
|
1316
|
+
AuthStorage,
|
|
1317
|
+
DefaultResourceLoader,
|
|
1318
|
+
ModelRegistry,
|
|
1319
|
+
SessionManager,
|
|
1320
|
+
SettingsManager,
|
|
1321
|
+
createAgentSession,
|
|
1322
|
+
defineTool,
|
|
1323
|
+
getAgentDir,
|
|
1324
|
+
resolveCliModel
|
|
1325
|
+
} = mod;
|
|
1326
|
+
const hint2 = readAuthHint(ctx);
|
|
1327
|
+
const configDir = createStageConfigDir();
|
|
1328
|
+
writeStageAuthFile(configDir, hint2);
|
|
1329
|
+
writeStageCustomProviders(configDir, hint2);
|
|
1330
|
+
const authStorage = AuthStorage.create(join4(configDir, "auth.json"));
|
|
1331
|
+
applyApiKeyAuth(hint2, ctx.runtimeEnv, authStorage);
|
|
1332
|
+
const modelRegistry = ModelRegistry.create(authStorage, join4(configDir, "models.json"));
|
|
1371
1333
|
let model;
|
|
1372
1334
|
if (ctx.config.model) {
|
|
1373
1335
|
const resolved = resolveCliModel({ cliModel: ctx.config.model, modelRegistry });
|
|
@@ -1382,22 +1344,89 @@ async function defaultCreatePiSession(ctx) {
|
|
|
1382
1344
|
parameters: { type: "object", properties: { summary: { type: "string" } }, required: ["summary"] },
|
|
1383
1345
|
execute: async () => ({ content: [{ type: "text", text: "Stage marked complete." }], details: {} })
|
|
1384
1346
|
});
|
|
1347
|
+
let askHandler;
|
|
1348
|
+
const askUserQuestion = defineTool({
|
|
1349
|
+
name: PI_ASK_USER_QUESTION_TOOL,
|
|
1350
|
+
label: "Ask the user a question",
|
|
1351
|
+
description: "Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
|
|
1352
|
+
parameters: {
|
|
1353
|
+
type: "object",
|
|
1354
|
+
properties: {
|
|
1355
|
+
questions: {
|
|
1356
|
+
type: "array",
|
|
1357
|
+
items: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
question: { type: "string" },
|
|
1361
|
+
options: {
|
|
1362
|
+
type: "array",
|
|
1363
|
+
items: {
|
|
1364
|
+
type: "object",
|
|
1365
|
+
properties: { label: { type: "string" }, description: { type: "string" } },
|
|
1366
|
+
required: ["label"]
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
multiSelect: { type: "boolean" }
|
|
1370
|
+
},
|
|
1371
|
+
required: ["question", "options"]
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
required: ["questions"]
|
|
1376
|
+
},
|
|
1377
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1378
|
+
execute: async (_toolCallId, params) => {
|
|
1379
|
+
const text = askHandler ? await askHandler(params.questions) : elicitOutcomeReply({ kind: "noreply" });
|
|
1380
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
let toolCallGate;
|
|
1384
|
+
const toolGateExtension = {
|
|
1385
|
+
name: "dahrk-tool-gate",
|
|
1386
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1387
|
+
factory: (pi) => {
|
|
1388
|
+
pi.on("tool_call", (event) => toolCallGate?.(event.toolName, event.input));
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
const cwd = ctx.workspace.worktreePath;
|
|
1392
|
+
const agentDir = getAgentDir();
|
|
1393
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
1394
|
+
const brokeredMcp = buildBrokeredPiMcpServers(ctx);
|
|
1395
|
+
const extensionFactories = brokeredMcp ? [toolGateExtension, createBrokeredMcpExtension(brokeredMcp)] : [toolGateExtension];
|
|
1396
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
1397
|
+
cwd,
|
|
1398
|
+
agentDir,
|
|
1399
|
+
settingsManager,
|
|
1400
|
+
extensionFactories
|
|
1401
|
+
});
|
|
1402
|
+
await resourceLoader.reload();
|
|
1385
1403
|
const { session } = await createAgentSession({
|
|
1386
1404
|
sessionManager: SessionManager.inMemory(ctx.workspace.worktreePath),
|
|
1387
1405
|
authStorage,
|
|
1388
1406
|
modelRegistry,
|
|
1389
|
-
|
|
1390
|
-
|
|
1407
|
+
settingsManager,
|
|
1408
|
+
resourceLoader,
|
|
1409
|
+
cwd,
|
|
1410
|
+
customTools: [stageComplete, askUserQuestion],
|
|
1391
1411
|
...model ? { model } : {}
|
|
1392
1412
|
});
|
|
1393
|
-
|
|
1413
|
+
const piSession = session;
|
|
1414
|
+
const innerDispose = piSession.dispose.bind(piSession);
|
|
1415
|
+
piSession.dispose = () => {
|
|
1416
|
+
try {
|
|
1417
|
+
innerDispose();
|
|
1418
|
+
} finally {
|
|
1419
|
+
cleanupStageConfigDir(configDir);
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
piSession.setAskUserQuestionHandler = (handler) => {
|
|
1423
|
+
askHandler = handler;
|
|
1424
|
+
};
|
|
1425
|
+
piSession.setToolCallGate = (gate) => {
|
|
1426
|
+
toolCallGate = gate;
|
|
1427
|
+
};
|
|
1428
|
+
return piSession;
|
|
1394
1429
|
}
|
|
1395
|
-
var PROVIDER_BY_ENV = {
|
|
1396
|
-
ANTHROPIC_API_KEY: "anthropic",
|
|
1397
|
-
OPENAI_API_KEY: "openai",
|
|
1398
|
-
GEMINI_API_KEY: "google",
|
|
1399
|
-
GOOGLE_API_KEY: "google"
|
|
1400
|
-
};
|
|
1401
1430
|
function modelFamily(id) {
|
|
1402
1431
|
const last = id.split(".").pop() ?? id;
|
|
1403
1432
|
return last.replace(/-v\d+:\d+$/, "").toLowerCase();
|
|
@@ -1410,15 +1439,294 @@ function pickAuthedModel(resolved, available) {
|
|
|
1410
1439
|
return available.find((m) => modelFamily(m.id) === family) ?? resolved;
|
|
1411
1440
|
}
|
|
1412
1441
|
|
|
1442
|
+
// ../../packages/executor-worktree/src/pi-container.ts
|
|
1443
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
1444
|
+
|
|
1445
|
+
// ../../packages/executor-worktree/src/pi-rpc-client.ts
|
|
1446
|
+
import { StringDecoder } from "string_decoder";
|
|
1447
|
+
function createLineDecoder() {
|
|
1448
|
+
const decoder = new StringDecoder("utf8");
|
|
1449
|
+
let buffer = "";
|
|
1450
|
+
const drain = () => {
|
|
1451
|
+
const lines = [];
|
|
1452
|
+
for (; ; ) {
|
|
1453
|
+
const nl = buffer.indexOf("\n");
|
|
1454
|
+
if (nl === -1) break;
|
|
1455
|
+
let line = buffer.slice(0, nl);
|
|
1456
|
+
buffer = buffer.slice(nl + 1);
|
|
1457
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
1458
|
+
lines.push(line);
|
|
1459
|
+
}
|
|
1460
|
+
return lines;
|
|
1461
|
+
};
|
|
1462
|
+
return {
|
|
1463
|
+
push(chunk) {
|
|
1464
|
+
buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
1465
|
+
return drain();
|
|
1466
|
+
},
|
|
1467
|
+
end() {
|
|
1468
|
+
buffer += decoder.end();
|
|
1469
|
+
const lines = drain();
|
|
1470
|
+
if (buffer.length > 0) {
|
|
1471
|
+
const last = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
1472
|
+
buffer = "";
|
|
1473
|
+
lines.push(last);
|
|
1474
|
+
}
|
|
1475
|
+
return lines;
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function deferred() {
|
|
1480
|
+
let resolve3;
|
|
1481
|
+
let reject;
|
|
1482
|
+
const promise = new Promise((res, rej) => {
|
|
1483
|
+
resolve3 = res;
|
|
1484
|
+
reject = rej;
|
|
1485
|
+
});
|
|
1486
|
+
return { promise, resolve: resolve3, reject };
|
|
1487
|
+
}
|
|
1488
|
+
var isResponse = (msg) => typeof msg === "object" && msg !== null && msg.type === "response";
|
|
1489
|
+
var PiRpcSession = class {
|
|
1490
|
+
#sessionId;
|
|
1491
|
+
#listeners = [];
|
|
1492
|
+
/** Command responses awaited by id (correlated via the optional `id` field). */
|
|
1493
|
+
#pendingResponses = /* @__PURE__ */ new Map();
|
|
1494
|
+
/** The in-flight `prompt()` resolver, settled on the next `agent_end`. */
|
|
1495
|
+
#pendingAgentEnd;
|
|
1496
|
+
#reqCounter = 0;
|
|
1497
|
+
#disposed = false;
|
|
1498
|
+
#child;
|
|
1499
|
+
#kill;
|
|
1500
|
+
constructor(child, options = {}) {
|
|
1501
|
+
this.#child = child;
|
|
1502
|
+
this.#kill = options.kill;
|
|
1503
|
+
const decoder = createLineDecoder();
|
|
1504
|
+
const onData = (chunk) => {
|
|
1505
|
+
for (const line of decoder.push(chunk)) if (line.length > 0) this.#onLine(line);
|
|
1506
|
+
};
|
|
1507
|
+
child.stdout?.on("data", onData);
|
|
1508
|
+
child.stdout?.on("end", () => {
|
|
1509
|
+
for (const line of decoder.end()) if (line.length > 0) this.#onLine(line);
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
get sessionId() {
|
|
1513
|
+
return this.#sessionId;
|
|
1514
|
+
}
|
|
1515
|
+
subscribe(listener) {
|
|
1516
|
+
this.#listeners.push(listener);
|
|
1517
|
+
return () => {
|
|
1518
|
+
this.#listeners = this.#listeners.filter((l) => l !== listener);
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async prompt(text) {
|
|
1522
|
+
if (this.#disposed) throw new Error("pi rpc session disposed");
|
|
1523
|
+
const agentEnd = deferred();
|
|
1524
|
+
this.#pendingAgentEnd = agentEnd;
|
|
1525
|
+
const ack = await this.#send("prompt", { message: text });
|
|
1526
|
+
if (ack.success === false) {
|
|
1527
|
+
this.#pendingAgentEnd = void 0;
|
|
1528
|
+
throw new Error(ack.error ?? "prompt rejected");
|
|
1529
|
+
}
|
|
1530
|
+
await agentEnd.promise;
|
|
1531
|
+
}
|
|
1532
|
+
async abort() {
|
|
1533
|
+
if (this.#disposed) return;
|
|
1534
|
+
await this.#send("abort", {});
|
|
1535
|
+
}
|
|
1536
|
+
/** Best-effort refresh of the resume token from `get_state`; swallows a failed lookup. */
|
|
1537
|
+
async getState() {
|
|
1538
|
+
if (this.#disposed) return;
|
|
1539
|
+
try {
|
|
1540
|
+
const res = await this.#send("get_state", {});
|
|
1541
|
+
const id = res.data?.sessionId;
|
|
1542
|
+
if (typeof id === "string" && id) this.#sessionId = id;
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
dispose() {
|
|
1547
|
+
if (this.#disposed) return;
|
|
1548
|
+
this.#disposed = true;
|
|
1549
|
+
try {
|
|
1550
|
+
this.#child.stdin?.end();
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
const err = new Error("pi rpc session disposed");
|
|
1554
|
+
for (const d of this.#pendingResponses.values()) d.reject(err);
|
|
1555
|
+
this.#pendingResponses.clear();
|
|
1556
|
+
if (this.#pendingAgentEnd) {
|
|
1557
|
+
this.#pendingAgentEnd.reject(err);
|
|
1558
|
+
this.#pendingAgentEnd = void 0;
|
|
1559
|
+
}
|
|
1560
|
+
if (this.#kill) {
|
|
1561
|
+
const kill = this.#kill;
|
|
1562
|
+
this.#kill = void 0;
|
|
1563
|
+
void kill();
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
/** Write a command as one LF-terminated JSON line and await its correlated response. */
|
|
1567
|
+
#send(type, fields) {
|
|
1568
|
+
const id = `req-${++this.#reqCounter}`;
|
|
1569
|
+
const d = deferred();
|
|
1570
|
+
this.#pendingResponses.set(id, d);
|
|
1571
|
+
try {
|
|
1572
|
+
this.#child.stdin?.write(`${JSON.stringify({ id, type, ...fields })}
|
|
1573
|
+
`);
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
this.#pendingResponses.delete(id);
|
|
1576
|
+
d.reject(e);
|
|
1577
|
+
}
|
|
1578
|
+
return d.promise;
|
|
1579
|
+
}
|
|
1580
|
+
#onLine(line) {
|
|
1581
|
+
let msg;
|
|
1582
|
+
try {
|
|
1583
|
+
msg = JSON.parse(line);
|
|
1584
|
+
} catch {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (isResponse(msg)) {
|
|
1588
|
+
const id = msg.id;
|
|
1589
|
+
if (typeof id === "string") {
|
|
1590
|
+
const pending = this.#pendingResponses.get(id);
|
|
1591
|
+
if (pending) {
|
|
1592
|
+
this.#pendingResponses.delete(id);
|
|
1593
|
+
pending.resolve(msg);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const ev = parsePiEvent(msg);
|
|
1599
|
+
if (!ev) return;
|
|
1600
|
+
for (const l of [...this.#listeners]) l(ev);
|
|
1601
|
+
if (ev.type === "agent_end" && this.#pendingAgentEnd) {
|
|
1602
|
+
const d = this.#pendingAgentEnd;
|
|
1603
|
+
this.#pendingAgentEnd = void 0;
|
|
1604
|
+
d.resolve();
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
// ../../packages/executor-worktree/src/pi-container.ts
|
|
1610
|
+
var DEFAULT_IMAGE = process.env.DAHRK_PI_IMAGE ?? process.env.SKAKEL_PI_IMAGE ?? "dahrk/pi:latest";
|
|
1611
|
+
var _seq = 0;
|
|
1612
|
+
function createContainerPiSession(opts = {}) {
|
|
1613
|
+
const {
|
|
1614
|
+
image = DEFAULT_IMAGE,
|
|
1615
|
+
scratchDir: optsScratchDir,
|
|
1616
|
+
spawn: spawnFn = nodeSpawn,
|
|
1617
|
+
onStderr = (line) => void process.stderr.write(`pi-container: ${line}
|
|
1618
|
+
`)
|
|
1619
|
+
} = opts;
|
|
1620
|
+
return async (ctx) => {
|
|
1621
|
+
const containerName = `dahrk-pi-${Date.now()}-${++_seq}`;
|
|
1622
|
+
const resolvedScratchDir = optsScratchDir ?? ctx.workspace.scratchPath;
|
|
1623
|
+
const mountArgs = resolvedScratchDir ? ["-v", `${resolvedScratchDir}:/dahrk/scratch`] : [];
|
|
1624
|
+
const envArgs = [];
|
|
1625
|
+
for (const [k, v] of Object.entries(ctx.runtimeEnv ?? {})) {
|
|
1626
|
+
envArgs.push("-e", `${k}=${v}`);
|
|
1627
|
+
}
|
|
1628
|
+
const child = spawnFn(
|
|
1629
|
+
"docker",
|
|
1630
|
+
["run", "-i", "--rm", "--name", containerName, ...mountArgs, ...envArgs, image, "pi", "--mode", "rpc"],
|
|
1631
|
+
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
1632
|
+
);
|
|
1633
|
+
child.stderr?.setEncoding("utf8");
|
|
1634
|
+
let stderrBuf = "";
|
|
1635
|
+
child.stderr?.on("data", (chunk) => {
|
|
1636
|
+
stderrBuf += chunk;
|
|
1637
|
+
const lines = stderrBuf.split("\n");
|
|
1638
|
+
stderrBuf = lines.pop() ?? "";
|
|
1639
|
+
for (const line of lines) if (line.trim()) onStderr(line);
|
|
1640
|
+
});
|
|
1641
|
+
child.stderr?.on("end", () => {
|
|
1642
|
+
if (stderrBuf.trim()) onStderr(stderrBuf);
|
|
1643
|
+
stderrBuf = "";
|
|
1644
|
+
});
|
|
1645
|
+
const killContainer = () => new Promise((resolve3) => {
|
|
1646
|
+
const killer = spawnFn("docker", ["kill", containerName], { stdio: "ignore" });
|
|
1647
|
+
killer.on("exit", () => resolve3());
|
|
1648
|
+
killer.on("error", () => resolve3());
|
|
1649
|
+
});
|
|
1650
|
+
return new PiRpcSession(child, { kill: killContainer });
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
function createIsolatedPiRunner(opts = {}) {
|
|
1654
|
+
return createPiRunner({ createSession: createContainerPiSession(opts) });
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1413
1657
|
// ../../packages/executor-worktree/src/git-service.ts
|
|
1414
1658
|
import { execFileSync } from "child_process";
|
|
1415
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
1416
|
-
import { homedir as homedir2, tmpdir as
|
|
1417
|
-
import { basename, dirname, isAbsolute, join as
|
|
1659
|
+
import { existsSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1660
|
+
import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
|
|
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
|
|
1418
1718
|
var noopLogger = { info: () => {
|
|
1419
1719
|
}, warn: () => {
|
|
1420
1720
|
} };
|
|
1421
|
-
var SCRATCH_DIR = ".
|
|
1721
|
+
var SCRATCH_DIR = ".dahrk/scratch";
|
|
1722
|
+
var CHANGED_PATHS_CAP = 100;
|
|
1723
|
+
var DEFAULT_MERGE_RESOLVE_RULES = [
|
|
1724
|
+
{ path: "pnpm-lock.yaml", strategy: "theirs" },
|
|
1725
|
+
{ path: "package-lock.json", strategy: "theirs" },
|
|
1726
|
+
{ path: "yarn.lock", strategy: "theirs" },
|
|
1727
|
+
{ path: "CHANGELOG.md", strategy: "union" },
|
|
1728
|
+
{ path: "CHANGELOG.internal.md", strategy: "union" }
|
|
1729
|
+
];
|
|
1422
1730
|
function parseOwnerRepo(gitUrl) {
|
|
1423
1731
|
const m = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(gitUrl.trim());
|
|
1424
1732
|
return m ? `${m[1]}/${m[2]}` : void 0;
|
|
@@ -1428,10 +1736,10 @@ function sanitizeBranchName(name) {
|
|
|
1428
1736
|
return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
|
|
1429
1737
|
}
|
|
1430
1738
|
function resolveWorktreesDir(override) {
|
|
1431
|
-
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ??
|
|
1739
|
+
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join5(homedir2(), ".dahrk", "worktrees");
|
|
1432
1740
|
}
|
|
1433
1741
|
function resolveMirrorsDir(override) {
|
|
1434
|
-
return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ??
|
|
1742
|
+
return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join5(homedir2(), ".dahrk", "mirrors");
|
|
1435
1743
|
}
|
|
1436
1744
|
function createGitService(opts = {}) {
|
|
1437
1745
|
const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
|
|
@@ -1467,15 +1775,17 @@ function createGitService(opts = {}) {
|
|
|
1467
1775
|
return (stderr?.trim() || err?.message || String(e)).split("\n")[0] ?? String(e);
|
|
1468
1776
|
};
|
|
1469
1777
|
const excludeScratchLocally = (worktreePath) => {
|
|
1470
|
-
const
|
|
1778
|
+
const entries = [`${SCRATCH_DIR}/`];
|
|
1471
1779
|
try {
|
|
1472
1780
|
const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
|
|
1473
|
-
const excludePath = isAbsolute(rel) ? rel :
|
|
1781
|
+
const excludePath = isAbsolute(rel) ? rel : join5(worktreePath, rel);
|
|
1474
1782
|
const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
|
|
1475
|
-
|
|
1783
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
1784
|
+
const toAdd = entries.filter((e) => !lines.includes(e));
|
|
1785
|
+
if (toAdd.length === 0) return;
|
|
1476
1786
|
mkdirSync(dirname(excludePath), { recursive: true });
|
|
1477
1787
|
const sep4 = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
1478
|
-
|
|
1788
|
+
writeFileSync2(excludePath, `${existing}${sep4}${toAdd.join("\n")}
|
|
1479
1789
|
`);
|
|
1480
1790
|
} catch (e) {
|
|
1481
1791
|
log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
|
|
@@ -1499,13 +1809,53 @@ function createGitService(opts = {}) {
|
|
|
1499
1809
|
}
|
|
1500
1810
|
return { headSha: git(worktreePath, ["rev-parse", "HEAD"]).trim(), dirty };
|
|
1501
1811
|
};
|
|
1812
|
+
const unmergedPaths = (worktreePath) => git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1813
|
+
const unionMerge = (worktreePath, p) => {
|
|
1814
|
+
const stage = (n) => {
|
|
1815
|
+
try {
|
|
1816
|
+
return git(worktreePath, ["show", `:${n}:${p}`]);
|
|
1817
|
+
} catch {
|
|
1818
|
+
return "";
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const dir = mkdtempSync2(join5(tmpdir3(), "dahrk-union-"));
|
|
1822
|
+
try {
|
|
1823
|
+
const ours = join5(dir, "ours");
|
|
1824
|
+
const base = join5(dir, "base");
|
|
1825
|
+
const theirs = join5(dir, "theirs");
|
|
1826
|
+
writeFileSync2(ours, stage(2));
|
|
1827
|
+
writeFileSync2(base, stage(1));
|
|
1828
|
+
writeFileSync2(theirs, stage(3));
|
|
1829
|
+
const merged = git(worktreePath, ["merge-file", "-p", "--union", ours, base, theirs]);
|
|
1830
|
+
writeFileSync2(join5(worktreePath, p), merged);
|
|
1831
|
+
} finally {
|
|
1832
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
const preResolveConflicts = (worktreePath, rules) => {
|
|
1836
|
+
gitOk2(worktreePath, ["rerere"]);
|
|
1837
|
+
for (const p of unmergedPaths(worktreePath)) {
|
|
1838
|
+
const rule = rules.find((r) => r.path === p || basename(p) === r.path);
|
|
1839
|
+
if (!rule) continue;
|
|
1840
|
+
try {
|
|
1841
|
+
if (rule.strategy === "union") {
|
|
1842
|
+
unionMerge(worktreePath, p);
|
|
1843
|
+
} else {
|
|
1844
|
+
git(worktreePath, ["checkout", `--${rule.strategy}`, "--", p]);
|
|
1845
|
+
}
|
|
1846
|
+
git(worktreePath, ["add", "--", p]);
|
|
1847
|
+
} catch (e) {
|
|
1848
|
+
log.warn(`pre-resolve of ${p} (${rule.strategy}) failed: ${e.message}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1502
1852
|
const setupAuth = (token) => {
|
|
1503
|
-
const dir =
|
|
1504
|
-
const script =
|
|
1505
|
-
|
|
1853
|
+
const dir = mkdtempSync2(join5(tmpdir3(), "dahrk-cred-"));
|
|
1854
|
+
const script = join5(dir, "askpass.sh");
|
|
1855
|
+
writeFileSync2(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
|
|
1506
1856
|
return {
|
|
1507
1857
|
env: { ...process.env, GIT_ASKPASS: script, DAHRK_GIT_TOKEN: token, GIT_TERMINAL_PROMPT: "0" },
|
|
1508
|
-
cleanup: () =>
|
|
1858
|
+
cleanup: () => rmSync2(dir, { recursive: true, force: true })
|
|
1509
1859
|
};
|
|
1510
1860
|
};
|
|
1511
1861
|
const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
@@ -1518,7 +1868,7 @@ function createGitService(opts = {}) {
|
|
|
1518
1868
|
cleanup: () => auth?.cleanup()
|
|
1519
1869
|
};
|
|
1520
1870
|
};
|
|
1521
|
-
const mirrorPathFor = (repoId) =>
|
|
1871
|
+
const mirrorPathFor = (repoId) => join5(mirrorsDir, sanitizeBranchName(repoId));
|
|
1522
1872
|
const listWorktrees = (mirror) => {
|
|
1523
1873
|
let out2;
|
|
1524
1874
|
try {
|
|
@@ -1543,7 +1893,7 @@ function createGitService(opts = {}) {
|
|
|
1543
1893
|
} catch (e) {
|
|
1544
1894
|
log.warn(`git worktree remove failed for ${worktreePath}: ${e.message}`);
|
|
1545
1895
|
}
|
|
1546
|
-
|
|
1896
|
+
rmSync2(worktreePath, { recursive: true, force: true });
|
|
1547
1897
|
gitOk2(mirror, ["worktree", "prune"]);
|
|
1548
1898
|
};
|
|
1549
1899
|
const TRACKING_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
|
|
@@ -1630,18 +1980,19 @@ function createGitService(opts = {}) {
|
|
|
1630
1980
|
baseBranch: spec.baseBranch,
|
|
1631
1981
|
branch: sanitizeBranchName(spec.branch ?? `dahrk/${spec.runId}`),
|
|
1632
1982
|
worktreePath,
|
|
1633
|
-
scratchPath:
|
|
1983
|
+
scratchPath: join5(worktreePath, SCRATCH_DIR)
|
|
1634
1984
|
});
|
|
1635
1985
|
return {
|
|
1636
1986
|
worktreesDir,
|
|
1637
1987
|
async createWorktree(spec) {
|
|
1638
1988
|
const { repoId, gitUrl, baseBranch, runId } = spec;
|
|
1639
1989
|
const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
|
|
1640
|
-
const worktreePath =
|
|
1990
|
+
const worktreePath = join5(worktreesDir, runId);
|
|
1641
1991
|
mkdirSync(worktreesDir, { recursive: true });
|
|
1642
1992
|
if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1643
1993
|
log.info(`reusing existing worktree at ${worktreePath}`);
|
|
1644
|
-
mkdirSync(
|
|
1994
|
+
mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
|
|
1995
|
+
excludeScratchLocally(worktreePath);
|
|
1645
1996
|
return refFor(spec, worktreePath);
|
|
1646
1997
|
}
|
|
1647
1998
|
const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
|
|
@@ -1677,7 +2028,8 @@ function createGitService(opts = {}) {
|
|
|
1677
2028
|
if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
|
|
1678
2029
|
throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
|
|
1679
2030
|
}
|
|
1680
|
-
mkdirSync(
|
|
2031
|
+
mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
|
|
2032
|
+
excludeScratchLocally(worktreePath);
|
|
1681
2033
|
return refFor(spec, worktreePath);
|
|
1682
2034
|
},
|
|
1683
2035
|
async commitAndPush(ref, opts2) {
|
|
@@ -1700,6 +2052,7 @@ function createGitService(opts = {}) {
|
|
|
1700
2052
|
const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
|
|
1701
2053
|
let pushed = false;
|
|
1702
2054
|
let integration;
|
|
2055
|
+
let footprint;
|
|
1703
2056
|
try {
|
|
1704
2057
|
let fetched = false;
|
|
1705
2058
|
if (opts2.base) {
|
|
@@ -1719,12 +2072,22 @@ function createGitService(opts = {}) {
|
|
|
1719
2072
|
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1720
2073
|
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1721
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
|
+
}
|
|
1722
2081
|
try {
|
|
1723
2082
|
git(worktreePath, [
|
|
1724
2083
|
"-c",
|
|
1725
2084
|
`user.name=${authorName}`,
|
|
1726
2085
|
"-c",
|
|
1727
2086
|
`user.email=${authorEmail}`,
|
|
2087
|
+
"-c",
|
|
2088
|
+
"rerere.enabled=true",
|
|
2089
|
+
"-c",
|
|
2090
|
+
"rerere.autoupdate=true",
|
|
1728
2091
|
"merge",
|
|
1729
2092
|
"--no-edit",
|
|
1730
2093
|
"FETCH_HEAD"
|
|
@@ -1734,15 +2097,30 @@ function createGitService(opts = {}) {
|
|
|
1734
2097
|
} catch (mergeErr) {
|
|
1735
2098
|
const inMerge = gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "MERGE_HEAD"]);
|
|
1736
2099
|
if (inMerge) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2100
|
+
preResolveConflicts(worktreePath, opts2.mergeResolve ?? DEFAULT_MERGE_RESOLVE_RULES);
|
|
2101
|
+
const conflictFiles = unmergedPaths(worktreePath);
|
|
2102
|
+
if (conflictFiles.length === 0) {
|
|
2103
|
+
git(worktreePath, [
|
|
2104
|
+
"-c",
|
|
2105
|
+
`user.name=${authorName}`,
|
|
2106
|
+
"-c",
|
|
2107
|
+
`user.email=${authorEmail}`,
|
|
2108
|
+
"commit",
|
|
2109
|
+
"--no-edit"
|
|
2110
|
+
]);
|
|
2111
|
+
integration = "clean";
|
|
2112
|
+
headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
2113
|
+
} else {
|
|
2114
|
+
git(worktreePath, ["merge", "--abort"]);
|
|
2115
|
+
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles, ...footprint ? { footprint } : {} };
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
const msg = mergeErr.message;
|
|
2119
|
+
if (/unrelated histories|refusing to merge/i.test(msg)) {
|
|
2120
|
+
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
|
|
2121
|
+
}
|
|
2122
|
+
throw mergeErr;
|
|
1744
2123
|
}
|
|
1745
|
-
throw mergeErr;
|
|
1746
2124
|
}
|
|
1747
2125
|
}
|
|
1748
2126
|
git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(authEnv));
|
|
@@ -1750,7 +2128,7 @@ function createGitService(opts = {}) {
|
|
|
1750
2128
|
} finally {
|
|
1751
2129
|
cleanup();
|
|
1752
2130
|
}
|
|
1753
|
-
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {} };
|
|
2131
|
+
return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {}, ...footprint ? { footprint } : {} };
|
|
1754
2132
|
},
|
|
1755
2133
|
async backupPush(ref, opts2) {
|
|
1756
2134
|
const { worktreePath } = ref;
|
|
@@ -1805,10 +2183,10 @@ function createGitService(opts = {}) {
|
|
|
1805
2183
|
return void 0;
|
|
1806
2184
|
}
|
|
1807
2185
|
};
|
|
1808
|
-
const tmp =
|
|
1809
|
-
const bodyFile =
|
|
2186
|
+
const tmp = mkdtempSync2(join5(tmpdir3(), "dahrk-pr-"));
|
|
2187
|
+
const bodyFile = join5(tmp, "body.md");
|
|
1810
2188
|
try {
|
|
1811
|
-
|
|
2189
|
+
writeFileSync2(bodyFile, opts2.body ?? "");
|
|
1812
2190
|
try {
|
|
1813
2191
|
gh(worktreePath, [
|
|
1814
2192
|
"pr",
|
|
@@ -1830,7 +2208,7 @@ function createGitService(opts = {}) {
|
|
|
1830
2208
|
}
|
|
1831
2209
|
return readBack() ?? { prError: "gh pr create reported success but no PR was found" };
|
|
1832
2210
|
} finally {
|
|
1833
|
-
|
|
2211
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
1834
2212
|
}
|
|
1835
2213
|
},
|
|
1836
2214
|
async teardownWorktree(ref) {
|
|
@@ -1841,16 +2219,20 @@ function createGitService(opts = {}) {
|
|
|
1841
2219
|
|
|
1842
2220
|
// ../../packages/executor-worktree/src/worktree-reaper.ts
|
|
1843
2221
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1844
|
-
import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as
|
|
1845
|
-
import { join as
|
|
2222
|
+
import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync3, statSync } from "fs";
|
|
2223
|
+
import { join as join6 } from "path";
|
|
1846
2224
|
var MINUTE = 6e4;
|
|
1847
2225
|
var HOUR = 60 * MINUTE;
|
|
2226
|
+
var DAY = 24 * HOUR;
|
|
1848
2227
|
var DEFAULTS = {
|
|
1849
2228
|
// Deliberately non-optional defaults. "No policy configured" used to mean "never collect anything",
|
|
1850
2229
|
// which is precisely how the disk reached 65 GB. Absent config must mean sane collection, not none.
|
|
1851
2230
|
maxRuns: 20,
|
|
1852
2231
|
maxIdleMs: 6 * HOUR,
|
|
1853
|
-
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
|
|
1854
2236
|
};
|
|
1855
2237
|
var noop = { info: () => {
|
|
1856
2238
|
}, warn: () => {
|
|
@@ -1872,7 +2254,7 @@ var canonical = (p) => {
|
|
|
1872
2254
|
}
|
|
1873
2255
|
};
|
|
1874
2256
|
function lastUsedMs(worktreePath) {
|
|
1875
|
-
const candidates = [
|
|
2257
|
+
const candidates = [join6(worktreePath, ".dahrk", "scratch", "state.json"), worktreePath];
|
|
1876
2258
|
let newest = 0;
|
|
1877
2259
|
for (const p of candidates) {
|
|
1878
2260
|
try {
|
|
@@ -1891,11 +2273,43 @@ function registeredWorktrees(mirror) {
|
|
|
1891
2273
|
}
|
|
1892
2274
|
return out2.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
1893
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
|
+
}
|
|
1894
2308
|
function createWorktreeReaper(opts) {
|
|
1895
2309
|
const log = opts.logger ?? noop;
|
|
1896
2310
|
const mirrors = () => {
|
|
1897
2311
|
try {
|
|
1898
|
-
return readdirSync(opts.mirrorsDir).map((d) =>
|
|
2312
|
+
return readdirSync(opts.mirrorsDir).map((d) => join6(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
|
|
1899
2313
|
} catch {
|
|
1900
2314
|
return [];
|
|
1901
2315
|
}
|
|
@@ -1909,8 +2323,9 @@ function createWorktreeReaper(opts) {
|
|
|
1909
2323
|
const maxRuns = policy.maxRuns ?? DEFAULTS.maxRuns;
|
|
1910
2324
|
const maxIdleMs = policy.maxIdleMs ?? DEFAULTS.maxIdleMs;
|
|
1911
2325
|
const graceMs = policy.activityGraceMs ?? DEFAULTS.activityGraceMs;
|
|
2326
|
+
const salvageTtlMs = policy.salvageTtlMs ?? DEFAULTS.salvageTtlMs;
|
|
1912
2327
|
const dryRun = policy.dryRun ?? false;
|
|
1913
|
-
const report = { scanned: 0, reaped: [], skipped: 0, errors: [] };
|
|
2328
|
+
const report = { scanned: 0, reaped: [], skipped: 0, salvagedRefs: 0, errors: [] };
|
|
1914
2329
|
const now = Date.now();
|
|
1915
2330
|
const registered = /* @__PURE__ */ new Map();
|
|
1916
2331
|
for (const m of mirrors()) {
|
|
@@ -1919,7 +2334,7 @@ function createWorktreeReaper(opts) {
|
|
|
1919
2334
|
}
|
|
1920
2335
|
const onDisk = (() => {
|
|
1921
2336
|
try {
|
|
1922
|
-
return readdirSync(opts.worktreesDir).map((d) => canonical(
|
|
2337
|
+
return readdirSync(opts.worktreesDir).map((d) => canonical(join6(opts.worktreesDir, d)));
|
|
1923
2338
|
} catch {
|
|
1924
2339
|
return [];
|
|
1925
2340
|
}
|
|
@@ -1962,7 +2377,7 @@ function createWorktreeReaper(opts) {
|
|
|
1962
2377
|
if (d.mirror) {
|
|
1963
2378
|
gitOk(d.mirror, ["worktree", "remove", "--force", d.path]);
|
|
1964
2379
|
}
|
|
1965
|
-
|
|
2380
|
+
rmSync3(d.path, { recursive: true, force: true });
|
|
1966
2381
|
if (d.mirror) gitOk(d.mirror, ["worktree", "prune"]);
|
|
1967
2382
|
report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
|
|
1968
2383
|
} catch (e) {
|
|
@@ -1972,6 +2387,14 @@ function createWorktreeReaper(opts) {
|
|
|
1972
2387
|
if (report.reaped.length) {
|
|
1973
2388
|
log.info(`reaper: reaped ${report.reaped.length} worktree(s), skipped ${report.skipped}`);
|
|
1974
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
|
+
}
|
|
1975
2398
|
return report;
|
|
1976
2399
|
}
|
|
1977
2400
|
};
|
|
@@ -1979,20 +2402,20 @@ function createWorktreeReaper(opts) {
|
|
|
1979
2402
|
|
|
1980
2403
|
// ../../packages/executor-worktree/src/trace-writer.ts
|
|
1981
2404
|
import { createHash } from "crypto";
|
|
1982
|
-
import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as
|
|
1983
|
-
import { join as
|
|
2405
|
+
import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2406
|
+
import { join as join7 } from "path";
|
|
1984
2407
|
var DEFAULT_SPILL_BYTES = 8192;
|
|
1985
2408
|
function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
1986
2409
|
const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
|
|
1987
|
-
const dir =
|
|
1988
|
-
mkdirSync2(
|
|
1989
|
-
mkdirSync2(
|
|
1990
|
-
const tracePath =
|
|
1991
|
-
const metaPath =
|
|
2410
|
+
const dir = join7(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
|
|
2411
|
+
mkdirSync2(join7(dir, "blobs"), { recursive: true });
|
|
2412
|
+
mkdirSync2(join7(dir, "raw"), { recursive: true });
|
|
2413
|
+
const tracePath = join7(dir, "trace.jsonl");
|
|
2414
|
+
const metaPath = join7(dir, "meta.json");
|
|
1992
2415
|
let current = { ...meta };
|
|
1993
2416
|
let nextSeq = 0;
|
|
1994
2417
|
let rawCount = 0;
|
|
1995
|
-
|
|
2418
|
+
writeFileSync3(metaPath, JSON.stringify(current, null, 2));
|
|
1996
2419
|
const tooBig = (value) => {
|
|
1997
2420
|
const s = typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
1998
2421
|
return s.length > spillBytes;
|
|
@@ -2000,8 +2423,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
2000
2423
|
const spillValue = (value) => {
|
|
2001
2424
|
const data = typeof value === "string" ? value : JSON.stringify(value);
|
|
2002
2425
|
const sha = createHash("sha256").update(data).digest("hex");
|
|
2003
|
-
|
|
2004
|
-
return
|
|
2426
|
+
writeFileSync3(join7(dir, "blobs", sha), data);
|
|
2427
|
+
return join7("blobs", sha);
|
|
2005
2428
|
};
|
|
2006
2429
|
const spill = (event) => {
|
|
2007
2430
|
if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
|
|
@@ -2031,13 +2454,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
2031
2454
|
return written;
|
|
2032
2455
|
},
|
|
2033
2456
|
writeRaw(record) {
|
|
2034
|
-
const rel =
|
|
2035
|
-
|
|
2457
|
+
const rel = join7("raw", `${rawCount++}.json`);
|
|
2458
|
+
writeFileSync3(join7(dir, rel), JSON.stringify(record, null, 2));
|
|
2036
2459
|
return rel;
|
|
2037
2460
|
},
|
|
2038
2461
|
finalise(patch = {}) {
|
|
2039
2462
|
current = { ...current, ...patch };
|
|
2040
|
-
|
|
2463
|
+
writeFileSync3(metaPath, JSON.stringify(current, null, 2));
|
|
2041
2464
|
},
|
|
2042
2465
|
count() {
|
|
2043
2466
|
return nextSeq;
|
|
@@ -2047,13 +2470,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
2047
2470
|
|
|
2048
2471
|
// ../../packages/executor-worktree/src/pack-cache.ts
|
|
2049
2472
|
import { createHash as createHash2 } from "crypto";
|
|
2050
|
-
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as
|
|
2051
|
-
import { dirname as dirname2, join as
|
|
2473
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2474
|
+
import { dirname as dirname2, join as join8, relative, sep } from "path";
|
|
2052
2475
|
function readManifestFiles(dir) {
|
|
2053
2476
|
const out2 = [];
|
|
2054
2477
|
const walk = (cur) => {
|
|
2055
2478
|
for (const entry of readdirSync2(cur, { withFileTypes: true })) {
|
|
2056
|
-
const abs =
|
|
2479
|
+
const abs = join8(cur, entry.name);
|
|
2057
2480
|
if (entry.isDirectory()) walk(abs);
|
|
2058
2481
|
else out2.push(relative(dir, abs).split(sep).join("/"));
|
|
2059
2482
|
}
|
|
@@ -2063,8 +2486,8 @@ function readManifestFiles(dir) {
|
|
|
2063
2486
|
}
|
|
2064
2487
|
|
|
2065
2488
|
// ../../packages/executor-worktree/src/overlay.ts
|
|
2066
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as
|
|
2067
|
-
import { dirname as dirname3, join as
|
|
2489
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2490
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
2068
2491
|
function sameBytes(dest, bytes) {
|
|
2069
2492
|
try {
|
|
2070
2493
|
return readFileSync3(dest).equals(bytes);
|
|
@@ -2076,16 +2499,16 @@ async function overlayComponents(opts) {
|
|
|
2076
2499
|
const { worktreePath, runtime, components, cache } = opts;
|
|
2077
2500
|
const result = { written: [], skippedRepoLocal: [], warnings: [] };
|
|
2078
2501
|
for (const ref of components) {
|
|
2079
|
-
if (runtime
|
|
2502
|
+
if (runtime !== "claude-code") {
|
|
2080
2503
|
result.warnings.push(
|
|
2081
|
-
|
|
2504
|
+
`${runtime} runtime: ${ref.kind} \`${ref.name}@${ref.version}\` not materialised; inline into the prompt or use Claude`
|
|
2082
2505
|
);
|
|
2083
2506
|
continue;
|
|
2084
2507
|
}
|
|
2085
2508
|
const { dir } = await cache.materialise(ref);
|
|
2086
2509
|
for (const relPath of readManifestFiles(dir)) {
|
|
2087
|
-
const src =
|
|
2088
|
-
const dest =
|
|
2510
|
+
const src = join9(dir, relPath);
|
|
2511
|
+
const dest = join9(worktreePath, relPath);
|
|
2089
2512
|
const bytes = readFileSync3(src);
|
|
2090
2513
|
if (existsSync4(dest)) {
|
|
2091
2514
|
if (sameBytes(dest, bytes)) continue;
|
|
@@ -2093,27 +2516,21 @@ async function overlayComponents(opts) {
|
|
|
2093
2516
|
continue;
|
|
2094
2517
|
}
|
|
2095
2518
|
mkdirSync4(dirname3(dest), { recursive: true });
|
|
2096
|
-
|
|
2519
|
+
writeFileSync5(dest, bytes);
|
|
2097
2520
|
result.written.push(relPath);
|
|
2098
2521
|
}
|
|
2099
2522
|
}
|
|
2100
2523
|
return result;
|
|
2101
2524
|
}
|
|
2102
2525
|
|
|
2103
|
-
// ../../packages/executor-worktree/src/pi-container.ts
|
|
2104
|
-
import { spawn as nodeSpawn } from "child_process";
|
|
2105
|
-
|
|
2106
|
-
// ../../packages/executor-worktree/src/pi-rpc-client.ts
|
|
2107
|
-
import { StringDecoder } from "string_decoder";
|
|
2108
|
-
|
|
2109
|
-
// ../../packages/executor-worktree/src/pi-container.ts
|
|
2110
|
-
var DEFAULT_IMAGE = process.env.DAHRK_PI_IMAGE ?? process.env.SKAKEL_PI_IMAGE ?? "dahrk/pi:latest";
|
|
2111
|
-
|
|
2112
2526
|
// ../../packages/executor-worktree/src/index.ts
|
|
2113
2527
|
function makeRunner(runtime) {
|
|
2114
2528
|
if ((process.env.DAHRK_RUNNER ?? process.env.SKAKEL_RUNNER ?? "real") === "mock") return createMockRunner(runtime);
|
|
2115
|
-
if (runtime === "pi") return createPiRunner();
|
|
2116
|
-
return
|
|
2529
|
+
if (runtime === "pi") return piContainerIsolationRequired() ? createIsolatedPiRunner() : createPiRunner();
|
|
2530
|
+
return createClaudeRunner();
|
|
2531
|
+
}
|
|
2532
|
+
function piContainerIsolationRequired() {
|
|
2533
|
+
return (process.env.DAHRK_PI_ISOLATION ?? process.env.SKAKEL_PI_ISOLATION) === "container";
|
|
2117
2534
|
}
|
|
2118
2535
|
|
|
2119
2536
|
// ../../packages/edge/src/health.ts
|
|
@@ -2172,8 +2589,8 @@ function collectHealth(inputs) {
|
|
|
2172
2589
|
}
|
|
2173
2590
|
|
|
2174
2591
|
// ../../packages/edge/src/job-ledger.ts
|
|
2175
|
-
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as
|
|
2176
|
-
import { dirname as dirname4, join as
|
|
2592
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
2593
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2177
2594
|
var FILE_MODE = 384;
|
|
2178
2595
|
var DIR_MODE = 448;
|
|
2179
2596
|
function nullJobLedger() {
|
|
@@ -2202,7 +2619,7 @@ function fileJobLedger(file, warn = console.warn) {
|
|
|
2202
2619
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
2203
2620
|
try {
|
|
2204
2621
|
mkdirSync5(dirname4(file), { recursive: true, mode: DIR_MODE });
|
|
2205
|
-
|
|
2622
|
+
writeFileSync6(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2206
2623
|
`, { mode: FILE_MODE });
|
|
2207
2624
|
chmodSync(tmp, FILE_MODE);
|
|
2208
2625
|
renameSync(tmp, file);
|
|
@@ -2237,7 +2654,7 @@ function announceableJobs(entries) {
|
|
|
2237
2654
|
return out2;
|
|
2238
2655
|
}
|
|
2239
2656
|
function jobLedgerFile(stateDir2) {
|
|
2240
|
-
return
|
|
2657
|
+
return join10(stateDir2, "jobs.json");
|
|
2241
2658
|
}
|
|
2242
2659
|
|
|
2243
2660
|
// ../../packages/edge/src/log-shipper.ts
|
|
@@ -2340,8 +2757,8 @@ function ceilingFromEnv(env) {
|
|
|
2340
2757
|
}
|
|
2341
2758
|
|
|
2342
2759
|
// ../../packages/edge/src/logger.ts
|
|
2343
|
-
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as
|
|
2344
|
-
import { join as
|
|
2760
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync2, writeSync } from "fs";
|
|
2761
|
+
import { join as join11 } from "path";
|
|
2345
2762
|
import pino from "pino";
|
|
2346
2763
|
|
|
2347
2764
|
// ../../packages/edge/src/redact.ts
|
|
@@ -2465,7 +2882,7 @@ var RotatingFile = class {
|
|
|
2465
2882
|
closeSync(this.fd);
|
|
2466
2883
|
this.fd = void 0;
|
|
2467
2884
|
const oldest = `${this.path}.${this.maxFiles}`;
|
|
2468
|
-
if (existsSync6(oldest))
|
|
2885
|
+
if (existsSync6(oldest)) rmSync5(oldest, { force: true });
|
|
2469
2886
|
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2470
2887
|
const from = `${this.path}.${i}`;
|
|
2471
2888
|
if (existsSync6(from)) renameSync2(from, `${this.path}.${i + 1}`);
|
|
@@ -2531,7 +2948,7 @@ function createNodeLogger(opts = {}) {
|
|
|
2531
2948
|
if (opts.dir && fileLevel !== "silent") {
|
|
2532
2949
|
try {
|
|
2533
2950
|
mkdirSync6(opts.dir, { recursive: true, mode: 448 });
|
|
2534
|
-
const file = new RotatingFile(
|
|
2951
|
+
const file = new RotatingFile(join11(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
|
|
2535
2952
|
streams.push({ level: fileLevel, stream: file });
|
|
2536
2953
|
} catch (e) {
|
|
2537
2954
|
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
@@ -2589,9 +3006,9 @@ function denyToolRule(tool3) {
|
|
|
2589
3006
|
// ../../packages/edge/src/stage-runner.ts
|
|
2590
3007
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2591
3008
|
import { createHash as createHash3 } from "crypto";
|
|
2592
|
-
import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as
|
|
2593
|
-
import { tmpdir as
|
|
2594
|
-
import { isAbsolute as isAbsolute3, join as
|
|
3009
|
+
import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
3010
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
3011
|
+
import { isAbsolute as isAbsolute3, join as join13, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
|
|
2595
3012
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
2596
3013
|
|
|
2597
3014
|
// ../../packages/edge/src/builtins.ts
|
|
@@ -2600,8 +3017,8 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
2600
3017
|
// ../../packages/edge/src/fs-roots.ts
|
|
2601
3018
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2602
3019
|
import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
|
|
2603
|
-
import { homedir as homedir3, tmpdir as
|
|
2604
|
-
import { dirname as dirname5, isAbsolute as isAbsolute2, join as
|
|
3020
|
+
import { homedir as homedir3, tmpdir as tmpdir4 } from "os";
|
|
3021
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve, sep as sep2 } from "path";
|
|
2605
3022
|
function isUnder(root, target) {
|
|
2606
3023
|
const rel = relative2(resolve(root), resolve(target));
|
|
2607
3024
|
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
|
|
@@ -2617,7 +3034,7 @@ function realish(p) {
|
|
|
2617
3034
|
while (head !== dirname5(head)) {
|
|
2618
3035
|
if (existsSync7(head)) {
|
|
2619
3036
|
try {
|
|
2620
|
-
return
|
|
3037
|
+
return join12(realpathSync2.native(head), ...tail.reverse());
|
|
2621
3038
|
} catch {
|
|
2622
3039
|
return p;
|
|
2623
3040
|
}
|
|
@@ -2667,7 +3084,7 @@ function computeFsRoots(opts) {
|
|
|
2667
3084
|
// Every git command in the worktree reads and writes here.
|
|
2668
3085
|
...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
|
|
2669
3086
|
// Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
|
|
2670
|
-
...[
|
|
3087
|
+
...[tmpdir4(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
|
|
2671
3088
|
// The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
|
|
2672
3089
|
// it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
|
|
2673
3090
|
// here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
|
|
@@ -2686,26 +3103,26 @@ function computeFsRoots(opts) {
|
|
|
2686
3103
|
"/System",
|
|
2687
3104
|
"/proc",
|
|
2688
3105
|
"/sys",
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3106
|
+
join12(home, ".gitconfig"),
|
|
3107
|
+
join12(home, ".config"),
|
|
3108
|
+
join12(home, ".npmrc"),
|
|
3109
|
+
join12(home, ".cache"),
|
|
3110
|
+
join12(home, "Library", "Caches"),
|
|
3111
|
+
join12(home, "Library", "pnpm"),
|
|
3112
|
+
join12(home, ".local", "share"),
|
|
3113
|
+
join12(home, ".nvm"),
|
|
3114
|
+
join12(home, ".volta"),
|
|
3115
|
+
join12(home, ".asdf"),
|
|
3116
|
+
join12(home, ".cargo"),
|
|
3117
|
+
join12(home, ".rustup"),
|
|
2701
3118
|
...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
|
|
2702
3119
|
].map(realish);
|
|
2703
3120
|
const deny = [
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3121
|
+
join12(home, ".ssh"),
|
|
3122
|
+
join12(home, ".aws"),
|
|
3123
|
+
join12(home, ".gnupg"),
|
|
3124
|
+
join12(home, ".config", "gcloud"),
|
|
3125
|
+
join12(home, "Library", "Keychains"),
|
|
2709
3126
|
"/Volumes",
|
|
2710
3127
|
"/etc/shadow",
|
|
2711
3128
|
"/etc/sudoers"
|
|
@@ -3251,6 +3668,77 @@ async function startMcpGateway(opts) {
|
|
|
3251
3668
|
};
|
|
3252
3669
|
}
|
|
3253
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
|
+
|
|
3254
3742
|
// ../../packages/edge/src/stage-runner.ts
|
|
3255
3743
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3256
3744
|
var PREVIEW = 500;
|
|
@@ -3290,7 +3778,7 @@ var attemptOf = (jobId) => {
|
|
|
3290
3778
|
};
|
|
3291
3779
|
var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
|
|
3292
3780
|
function writeScratchState(ref, job, attempt, status) {
|
|
3293
|
-
const statePath =
|
|
3781
|
+
const statePath = join13(ref.scratchPath, "state.json");
|
|
3294
3782
|
let state;
|
|
3295
3783
|
try {
|
|
3296
3784
|
state = JSON.parse(readFileSync5(statePath, "utf8"));
|
|
@@ -3299,23 +3787,23 @@ function writeScratchState(ref, job, attempt, status) {
|
|
|
3299
3787
|
}
|
|
3300
3788
|
state.stages[job.stageId] = { currentAttempt: attempt, status };
|
|
3301
3789
|
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3302
|
-
|
|
3790
|
+
writeFileSync7(statePath, JSON.stringify(state, null, 2));
|
|
3303
3791
|
}
|
|
3304
3792
|
function writeIssueContext(ref, issueContext) {
|
|
3305
3793
|
if (issueContext === void 0) return;
|
|
3306
3794
|
try {
|
|
3307
3795
|
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3308
|
-
|
|
3796
|
+
writeFileSync7(join13(ref.scratchPath, "issue.md"), issueContext);
|
|
3309
3797
|
} catch {
|
|
3310
3798
|
}
|
|
3311
3799
|
}
|
|
3312
3800
|
function writeAttachedDocuments(ref, docs) {
|
|
3313
3801
|
if (!docs || docs.length === 0) return;
|
|
3314
3802
|
try {
|
|
3315
|
-
const dir =
|
|
3803
|
+
const dir = join13(ref.scratchPath, "docs");
|
|
3316
3804
|
mkdirSync7(dir, { recursive: true });
|
|
3317
3805
|
for (const doc of docs) {
|
|
3318
|
-
|
|
3806
|
+
writeFileSync7(join13(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
|
|
3319
3807
|
}
|
|
3320
3808
|
} catch {
|
|
3321
3809
|
}
|
|
@@ -3334,12 +3822,12 @@ function writeGuidance(ref, guidance) {
|
|
|
3334
3822
|
if (!guidance || guidance.length === 0) return;
|
|
3335
3823
|
try {
|
|
3336
3824
|
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3337
|
-
|
|
3825
|
+
writeFileSync7(join13(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
|
|
3338
3826
|
} catch {
|
|
3339
3827
|
}
|
|
3340
3828
|
}
|
|
3341
3829
|
var ARTIFACT_CAP_BYTES = 64 * 1024;
|
|
3342
|
-
var SCRATCH_OUTPUT_DIR = ".
|
|
3830
|
+
var SCRATCH_OUTPUT_DIR = ".dahrk/scratch/output";
|
|
3343
3831
|
function capContent(raw) {
|
|
3344
3832
|
return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
|
|
3345
3833
|
}
|
|
@@ -3367,14 +3855,14 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
3367
3855
|
}
|
|
3368
3856
|
}
|
|
3369
3857
|
function scanScratchOutput(ref, preferRel) {
|
|
3858
|
+
const preferBase = preferRel?.split("/").pop();
|
|
3370
3859
|
try {
|
|
3371
|
-
const names = readdirSync3(
|
|
3860
|
+
const names = readdirSync3(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
|
|
3372
3861
|
(n) => n.toLowerCase().endsWith(".md")
|
|
3373
3862
|
);
|
|
3374
3863
|
if (names.length === 0) return void 0;
|
|
3375
|
-
const preferBase = preferRel?.split("/").pop();
|
|
3376
3864
|
const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
|
|
3377
|
-
const raw = readFileSync5(
|
|
3865
|
+
const raw = readFileSync5(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
|
|
3378
3866
|
if (raw.trim().length === 0) return void 0;
|
|
3379
3867
|
return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
|
|
3380
3868
|
} catch {
|
|
@@ -3394,7 +3882,7 @@ function scanChangedMarkdown(ref) {
|
|
|
3394
3882
|
);
|
|
3395
3883
|
for (const rel of rels) {
|
|
3396
3884
|
try {
|
|
3397
|
-
const raw = readFileSync5(
|
|
3885
|
+
const raw = readFileSync5(join13(ref.worktreePath, rel), "utf8");
|
|
3398
3886
|
if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
|
|
3399
3887
|
} catch {
|
|
3400
3888
|
}
|
|
@@ -3415,6 +3903,9 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
|
|
|
3415
3903
|
if (changed) return { artifact: changed, source: "changed-file" };
|
|
3416
3904
|
return void 0;
|
|
3417
3905
|
}
|
|
3906
|
+
function runtimeUsesMcpGateway(runtime) {
|
|
3907
|
+
return runtime === "claude-code" || runtime === "pi";
|
|
3908
|
+
}
|
|
3418
3909
|
function createStageRunner(deps) {
|
|
3419
3910
|
const worktrees = /* @__PURE__ */ new Map();
|
|
3420
3911
|
const log = deps.logger ?? createNodeLogger({ level: "silent" });
|
|
@@ -3437,7 +3928,7 @@ function createStageRunner(deps) {
|
|
|
3437
3928
|
if (ref) {
|
|
3438
3929
|
if (scratchOnly.has(runId)) {
|
|
3439
3930
|
try {
|
|
3440
|
-
|
|
3931
|
+
rmSync6(ref.worktreePath, { recursive: true, force: true });
|
|
3441
3932
|
} catch (e) {
|
|
3442
3933
|
log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove scratch dir");
|
|
3443
3934
|
}
|
|
@@ -3517,9 +4008,9 @@ function createStageRunner(deps) {
|
|
|
3517
4008
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
3518
4009
|
});
|
|
3519
4010
|
} else {
|
|
3520
|
-
const base = deps.scratchRoot ??
|
|
3521
|
-
const worktreePath =
|
|
3522
|
-
const scratchPath =
|
|
4011
|
+
const base = deps.scratchRoot ?? join13(tmpdir5(), "dahrk", "scratch");
|
|
4012
|
+
const worktreePath = join13(base, runId);
|
|
4013
|
+
const scratchPath = join13(worktreePath, ".dahrk", "scratch");
|
|
3523
4014
|
mkdirSync7(scratchPath, { recursive: true });
|
|
3524
4015
|
ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
|
|
3525
4016
|
scratchOnly.add(runId);
|
|
@@ -3535,7 +4026,7 @@ function createStageRunner(deps) {
|
|
|
3535
4026
|
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
3536
4027
|
if (artifactDir && slash > 0) {
|
|
3537
4028
|
try {
|
|
3538
|
-
mkdirSync7(
|
|
4029
|
+
mkdirSync7(join13(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
3539
4030
|
} catch {
|
|
3540
4031
|
}
|
|
3541
4032
|
}
|
|
@@ -3563,8 +4054,8 @@ function createStageRunner(deps) {
|
|
|
3563
4054
|
if (!sink) return;
|
|
3564
4055
|
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
3565
4056
|
try {
|
|
3566
|
-
for (const name of readdirSync3(
|
|
3567
|
-
const bytes = readFileSync5(
|
|
4057
|
+
for (const name of readdirSync3(join13(writer.dir, "blobs"))) {
|
|
4058
|
+
const bytes = readFileSync5(join13(writer.dir, "blobs", name));
|
|
3568
4059
|
const { url } = await sink.requestBlobUrl({
|
|
3569
4060
|
...base,
|
|
3570
4061
|
sha256: name,
|
|
@@ -3578,7 +4069,7 @@ function createStageRunner(deps) {
|
|
|
3578
4069
|
}
|
|
3579
4070
|
let archiveKey;
|
|
3580
4071
|
try {
|
|
3581
|
-
const bytes = readFileSync5(
|
|
4072
|
+
const bytes = readFileSync5(join13(writer.dir, "trace.jsonl"));
|
|
3582
4073
|
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
3583
4074
|
const { key, url } = await sink.requestBlobUrl({
|
|
3584
4075
|
...base,
|
|
@@ -3675,6 +4166,7 @@ function createStageRunner(deps) {
|
|
|
3675
4166
|
return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
|
|
3676
4167
|
}
|
|
3677
4168
|
let denied = false;
|
|
4169
|
+
const surfacedDenyReasons = /* @__PURE__ */ new Set();
|
|
3678
4170
|
let escapedUnblocked = false;
|
|
3679
4171
|
const authorisedActions = [];
|
|
3680
4172
|
const runtime = agentConfig.runtime;
|
|
@@ -3693,7 +4185,11 @@ function createStageRunner(deps) {
|
|
|
3693
4185
|
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
3694
4186
|
}
|
|
3695
4187
|
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
3696
|
-
|
|
4188
|
+
const surfaceKey = `${verdict2.policy}\0${reason}`;
|
|
4189
|
+
if (!surfacedDenyReasons.has(surfaceKey)) {
|
|
4190
|
+
surfacedDenyReasons.add(surfaceKey);
|
|
4191
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
4192
|
+
}
|
|
3697
4193
|
};
|
|
3698
4194
|
const authorizeToolUse = (tool3, input) => {
|
|
3699
4195
|
const verdict2 = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
@@ -3704,7 +4200,10 @@ function createStageRunner(deps) {
|
|
|
3704
4200
|
}
|
|
3705
4201
|
return verdict2;
|
|
3706
4202
|
};
|
|
4203
|
+
let bumpStall = () => {
|
|
4204
|
+
};
|
|
3707
4205
|
const onTrace = (event) => {
|
|
4206
|
+
bumpStall();
|
|
3708
4207
|
if (event.type === "action") {
|
|
3709
4208
|
const key = actionKey(event.tool, event.input);
|
|
3710
4209
|
const authorised = authorisedActions.indexOf(key);
|
|
@@ -3727,7 +4226,7 @@ function createStageRunner(deps) {
|
|
|
3727
4226
|
if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
|
|
3728
4227
|
};
|
|
3729
4228
|
const mcpServers = agentConfig.mcpServers;
|
|
3730
|
-
if (mcpServers && mcpServers.length > 0 && runtime
|
|
4229
|
+
if (mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
|
|
3731
4230
|
gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
|
|
3732
4231
|
}
|
|
3733
4232
|
const runner = deps.makeRunner(runtime);
|
|
@@ -3769,6 +4268,20 @@ function createStageRunner(deps) {
|
|
|
3769
4268
|
timedOut = true;
|
|
3770
4269
|
void runner.cancel();
|
|
3771
4270
|
}, killMs) : void 0;
|
|
4271
|
+
let stalled = false;
|
|
4272
|
+
let stallTimer;
|
|
4273
|
+
const stallSource = agentConfig.stallMs ?? Number(process.env.DAHRK_BATCH_STALL_MS ?? process.env.SKAKEL_BATCH_STALL_MS ?? 3e5);
|
|
4274
|
+
const stallMs = interactive ? 0 : Math.max(0, Math.floor(stallSource));
|
|
4275
|
+
if (stallMs > 0) {
|
|
4276
|
+
bumpStall = () => {
|
|
4277
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
4278
|
+
stallTimer = setTimeout(() => {
|
|
4279
|
+
stalled = true;
|
|
4280
|
+
void runner.cancel();
|
|
4281
|
+
}, stallMs);
|
|
4282
|
+
};
|
|
4283
|
+
bumpStall();
|
|
4284
|
+
}
|
|
3772
4285
|
try {
|
|
3773
4286
|
if (interactive) {
|
|
3774
4287
|
const mailbox = new ManagedMailbox();
|
|
@@ -3779,8 +4292,9 @@ function createStageRunner(deps) {
|
|
|
3779
4292
|
}
|
|
3780
4293
|
} finally {
|
|
3781
4294
|
if (killTimer) clearTimeout(killTimer);
|
|
4295
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
3782
4296
|
}
|
|
3783
|
-
let status = timedOut ? "timeout" : result.status;
|
|
4297
|
+
let status = timedOut || stalled ? "timeout" : result.status;
|
|
3784
4298
|
if (status === "ok" && escapedUnblocked) {
|
|
3785
4299
|
status = "fail";
|
|
3786
4300
|
const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
|
|
@@ -3799,6 +4313,9 @@ function createStageRunner(deps) {
|
|
|
3799
4313
|
}
|
|
3800
4314
|
}
|
|
3801
4315
|
let summary = result.summary ?? `${stageId}: ${status}`;
|
|
4316
|
+
if (stalled && !timedOut) {
|
|
4317
|
+
summary = `${stageId}: stalled (no output for ${Math.round(stallMs / 1e3)}s)`;
|
|
4318
|
+
}
|
|
3802
4319
|
if (!interactive && status === "ok") {
|
|
3803
4320
|
try {
|
|
3804
4321
|
summary = await runner.summarise(ctx);
|
|
@@ -3837,17 +4354,7 @@ function createStageRunner(deps) {
|
|
|
3837
4354
|
branch: job.branch,
|
|
3838
4355
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
3839
4356
|
});
|
|
3840
|
-
|
|
3841
|
-
jobId,
|
|
3842
|
-
status: "ok",
|
|
3843
|
-
branch: job.branch,
|
|
3844
|
-
headSha: r.headSha,
|
|
3845
|
-
pushed: r.pushed,
|
|
3846
|
-
nothingToCommit: r.nothingToCommit,
|
|
3847
|
-
wipRef: r.wipRef,
|
|
3848
|
-
summary: `backup: preserved ${r.headSha.slice(0, 7)} on ${r.wipRef} (no base merge, no PR)`
|
|
3849
|
-
};
|
|
3850
|
-
return result;
|
|
4357
|
+
return resolveBackupOutcome(r, { jobId, branch: job.branch });
|
|
3851
4358
|
} catch (e) {
|
|
3852
4359
|
return { jobId, status: "fail", branch: job.branch, summary: `backup push failed: ${e.message}` };
|
|
3853
4360
|
}
|
|
@@ -3872,64 +4379,13 @@ function createStageRunner(deps) {
|
|
|
3872
4379
|
base: job.base,
|
|
3873
4380
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
3874
4381
|
});
|
|
3875
|
-
if (r.integration === "noop") {
|
|
3876
|
-
return {
|
|
3877
|
-
jobId,
|
|
3878
|
-
status: "ok",
|
|
3879
|
-
branch: job.branch,
|
|
3880
|
-
headSha: r.headSha,
|
|
3881
|
-
pushed: false,
|
|
3882
|
-
nothingToCommit: true,
|
|
3883
|
-
commitsAhead: r.commitsAhead,
|
|
3884
|
-
summary: `no changes to deliver on ${job.branch} - work already present on ${job.base}`
|
|
3885
|
-
};
|
|
3886
|
-
}
|
|
3887
|
-
if (r.integration === "conflict") {
|
|
3888
|
-
return {
|
|
3889
|
-
jobId,
|
|
3890
|
-
status: "ok",
|
|
3891
|
-
branch: job.branch,
|
|
3892
|
-
headSha: r.headSha,
|
|
3893
|
-
pushed: false,
|
|
3894
|
-
nothingToCommit: r.nothingToCommit,
|
|
3895
|
-
commitsAhead: r.commitsAhead,
|
|
3896
|
-
integration: "conflict",
|
|
3897
|
-
...r.conflictFiles ? { conflictFiles: r.conflictFiles } : {},
|
|
3898
|
-
summary: `base advanced; merge conflict on ${job.branch} (manual merge needed)`
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
if (r.integration === "diverged") {
|
|
3902
|
-
return {
|
|
3903
|
-
jobId,
|
|
3904
|
-
status: "fail",
|
|
3905
|
-
branch: job.branch,
|
|
3906
|
-
headSha: r.headSha,
|
|
3907
|
-
pushed: false,
|
|
3908
|
-
nothingToCommit: r.nothingToCommit,
|
|
3909
|
-
commitsAhead: r.commitsAhead,
|
|
3910
|
-
summary: `branch history diverged from ${job.base}; cannot auto-integrate on ${job.branch} (the branch likely needs rebuilding from ${job.base})`
|
|
3911
|
-
};
|
|
3912
|
-
}
|
|
3913
4382
|
const pr = job.openPr && r.pushed ? await deps.gitService.openPrAmbient(ref, {
|
|
3914
4383
|
branch: job.branch,
|
|
3915
4384
|
base: job.base,
|
|
3916
4385
|
title: job.openPr.title,
|
|
3917
4386
|
body: job.openPr.body
|
|
3918
4387
|
}) : void 0;
|
|
3919
|
-
return {
|
|
3920
|
-
jobId,
|
|
3921
|
-
status: "ok",
|
|
3922
|
-
branch: job.branch,
|
|
3923
|
-
headSha: r.headSha,
|
|
3924
|
-
pushed: r.pushed,
|
|
3925
|
-
nothingToCommit: r.nothingToCommit,
|
|
3926
|
-
commitsAhead: r.commitsAhead,
|
|
3927
|
-
...r.integration ? { integration: r.integration } : {},
|
|
3928
|
-
...pr?.prUrl ? { prUrl: pr.prUrl } : {},
|
|
3929
|
-
...pr?.prNumber !== void 0 ? { prNumber: pr.prNumber } : {},
|
|
3930
|
-
...pr?.prError ? { prError: pr.prError } : {},
|
|
3931
|
-
summary: r.nothingToCommit ? `no changes to commit; ${r.pushed ? "branch pushed" : "nothing pushed"}` : `committed ${r.headSha.slice(0, 7)} and pushed ${job.branch}`
|
|
3932
|
-
};
|
|
4388
|
+
return resolveDeliverOutcome(r, { jobId, branch: job.branch, base: job.base }, pr);
|
|
3933
4389
|
} catch (e) {
|
|
3934
4390
|
return { jobId, status: "fail", summary: `push failed: ${e.message}` };
|
|
3935
4391
|
}
|
|
@@ -4040,6 +4496,17 @@ async function startEdgeNode(opts) {
|
|
|
4040
4496
|
if (oldest !== void 0) lastResults.delete(oldest);
|
|
4041
4497
|
}
|
|
4042
4498
|
};
|
|
4499
|
+
const ackedCancels = /* @__PURE__ */ new Map();
|
|
4500
|
+
const ackCancel = (jobId) => {
|
|
4501
|
+
const frame = { type: "cancel-ack", jobId };
|
|
4502
|
+
ackedCancels.delete(jobId);
|
|
4503
|
+
ackedCancels.set(jobId, frame);
|
|
4504
|
+
if (ackedCancels.size > MAX_RESEND) {
|
|
4505
|
+
const oldest = ackedCancels.keys().next().value;
|
|
4506
|
+
if (oldest !== void 0) ackedCancels.delete(oldest);
|
|
4507
|
+
}
|
|
4508
|
+
send(frame);
|
|
4509
|
+
};
|
|
4043
4510
|
const pendingBlob = /* @__PURE__ */ new Map();
|
|
4044
4511
|
let blobReqCounter = 0;
|
|
4045
4512
|
const trace = {
|
|
@@ -4105,10 +4572,10 @@ async function startEdgeNode(opts) {
|
|
|
4105
4572
|
await reconcileInterruptedJobs();
|
|
4106
4573
|
void stageRunner.reapWorktrees().then((r) => {
|
|
4107
4574
|
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
4108
|
-
if (r.reaped.length) {
|
|
4575
|
+
if (r.reaped.length || r.salvagedRefs) {
|
|
4109
4576
|
log.info(
|
|
4110
|
-
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped },
|
|
4111
|
-
`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})`
|
|
4112
4579
|
);
|
|
4113
4580
|
}
|
|
4114
4581
|
for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
|
|
@@ -4220,6 +4687,7 @@ async function startEdgeNode(opts) {
|
|
|
4220
4687
|
if (msg.type === "cancel") {
|
|
4221
4688
|
log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
|
|
4222
4689
|
stageRunner.cancel(msg.jobId);
|
|
4690
|
+
ackCancel(msg.jobId);
|
|
4223
4691
|
return;
|
|
4224
4692
|
}
|
|
4225
4693
|
if (msg.type === "turn") {
|
|
@@ -4351,6 +4819,7 @@ async function startEdgeNode(opts) {
|
|
|
4351
4819
|
log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
|
|
4352
4820
|
sendHello();
|
|
4353
4821
|
for (const frame of lastResults.values()) send(frame);
|
|
4822
|
+
for (const frame of ackedCancels.values()) send(frame);
|
|
4354
4823
|
startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
|
|
4355
4824
|
});
|
|
4356
4825
|
sock.on("message", (raw) => void onMessage(raw.toString()));
|
|
@@ -4454,7 +4923,6 @@ async function startEdgeNode(opts) {
|
|
|
4454
4923
|
import { execFile } from "child_process";
|
|
4455
4924
|
var PROBES = [
|
|
4456
4925
|
{ runtime: "claude-code", cmd: "claude" },
|
|
4457
|
-
{ runtime: "codex", cmd: "codex" },
|
|
4458
4926
|
{ runtime: "pi", cmd: "pi" }
|
|
4459
4927
|
];
|
|
4460
4928
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
@@ -5092,8 +5560,8 @@ function usage(bin, command) {
|
|
|
5092
5560
|
}
|
|
5093
5561
|
|
|
5094
5562
|
// src/diagnose.ts
|
|
5095
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as
|
|
5096
|
-
import { join as
|
|
5563
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
5564
|
+
import { join as join14 } from "path";
|
|
5097
5565
|
|
|
5098
5566
|
// src/ui.ts
|
|
5099
5567
|
import { styleText } from "util";
|
|
@@ -5208,7 +5676,7 @@ async function buildBundle(deps) {
|
|
|
5208
5676
|
if (deps.exists(deps.crashDir)) {
|
|
5209
5677
|
for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
5210
5678
|
try {
|
|
5211
|
-
crashes.push(JSON.parse(deps.readFile(
|
|
5679
|
+
crashes.push(JSON.parse(deps.readFile(join14(deps.crashDir, name))));
|
|
5212
5680
|
} catch (e) {
|
|
5213
5681
|
warnings.push(`could not read crash record ${name} (${e.message})`);
|
|
5214
5682
|
}
|
|
@@ -5269,14 +5737,14 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
|
5269
5737
|
listDir: (p) => readdirSync4(p),
|
|
5270
5738
|
exists: (p) => existsSync8(p),
|
|
5271
5739
|
writeFile: (p, content) => {
|
|
5272
|
-
const dir =
|
|
5740
|
+
const dir = join14(p, "..");
|
|
5273
5741
|
if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
|
|
5274
|
-
|
|
5742
|
+
writeFileSync8(p, content, { mode: 384 });
|
|
5275
5743
|
},
|
|
5276
5744
|
out
|
|
5277
5745
|
});
|
|
5278
5746
|
function defaultBundlePath(cwd, now) {
|
|
5279
|
-
return
|
|
5747
|
+
return join14(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
5280
5748
|
}
|
|
5281
5749
|
|
|
5282
5750
|
// src/doctor.ts
|
|
@@ -5471,7 +5939,8 @@ function driftOf(stored, repo) {
|
|
|
5471
5939
|
const drift = {};
|
|
5472
5940
|
if (typeof stored.defaultBranch === "string" && stored.defaultBranch !== repo.defaultBranch) drift.branch = stored.defaultBranch;
|
|
5473
5941
|
if (typeof stored.name === "string" && stored.name !== repo.name) drift.name = stored.name;
|
|
5474
|
-
|
|
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;
|
|
5475
5944
|
}
|
|
5476
5945
|
async function registerRepo(deps, args) {
|
|
5477
5946
|
const url = `${args.base}/config/api/repositories`;
|
|
@@ -5495,7 +5964,7 @@ async function registerRepo(deps, args) {
|
|
|
5495
5964
|
}
|
|
5496
5965
|
|
|
5497
5966
|
// src/lock.ts
|
|
5498
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as
|
|
5967
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
5499
5968
|
import { dirname as dirname6 } from "path";
|
|
5500
5969
|
function parseLock(content) {
|
|
5501
5970
|
if (!content) return void 0;
|
|
@@ -5535,9 +6004,9 @@ var defaultLockDeps = (file) => ({
|
|
|
5535
6004
|
},
|
|
5536
6005
|
writeFile: (path, content) => {
|
|
5537
6006
|
if (!existsSync9(dirname6(path))) mkdirSync9(dirname6(path), { recursive: true, mode: 448 });
|
|
5538
|
-
|
|
6007
|
+
writeFileSync9(path, content);
|
|
5539
6008
|
},
|
|
5540
|
-
removeFile: (path) =>
|
|
6009
|
+
removeFile: (path) => rmSync7(path, { force: true }),
|
|
5541
6010
|
isAlive
|
|
5542
6011
|
});
|
|
5543
6012
|
|
|
@@ -5645,13 +6114,13 @@ var defaultLogsDeps = (files, jsonlFile) => ({
|
|
|
5645
6114
|
});
|
|
5646
6115
|
|
|
5647
6116
|
// src/process-safety.ts
|
|
5648
|
-
import { mkdirSync as mkdirSync10, writeFileSync as
|
|
5649
|
-
import { join as
|
|
6117
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
6118
|
+
import { join as join15 } from "path";
|
|
5650
6119
|
function writeCrashRecord(dir, record) {
|
|
5651
6120
|
try {
|
|
5652
6121
|
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
5653
|
-
const file =
|
|
5654
|
-
|
|
6122
|
+
const file = join15(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
|
|
6123
|
+
writeFileSync10(file, `${JSON.stringify(record, null, 2)}
|
|
5655
6124
|
`, { mode: 384 });
|
|
5656
6125
|
return file;
|
|
5657
6126
|
} catch {
|
|
@@ -5708,7 +6177,7 @@ import { execFileSync as execFileSync6 } from "child_process";
|
|
|
5708
6177
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5709
6178
|
import { accessSync, constants as fsConstants, existsSync as existsSync11, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
|
|
5710
6179
|
import { homedir as homedir4 } from "os";
|
|
5711
|
-
import { join as
|
|
6180
|
+
import { join as join16 } from "path";
|
|
5712
6181
|
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
5713
6182
|
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
5714
6183
|
var PREFLIGHT_STAGES = [
|
|
@@ -5847,7 +6316,7 @@ function commandPresent(cmd) {
|
|
|
5847
6316
|
}
|
|
5848
6317
|
function sshKeyPresent() {
|
|
5849
6318
|
try {
|
|
5850
|
-
const dir =
|
|
6319
|
+
const dir = join16(homedir4(), ".ssh");
|
|
5851
6320
|
if (existsSync11(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
5852
6321
|
} catch {
|
|
5853
6322
|
}
|
|
@@ -5867,12 +6336,12 @@ function writable(dir) {
|
|
|
5867
6336
|
}
|
|
5868
6337
|
}
|
|
5869
6338
|
function worktreeRoot(env) {
|
|
5870
|
-
return env.DAHRK_WORKTREES_DIR ??
|
|
6339
|
+
return env.DAHRK_WORKTREES_DIR ?? join16(env.DAHRK_STATE_DIR ?? join16(homedir4(), ".dahrk"), "worktrees");
|
|
5871
6340
|
}
|
|
5872
6341
|
function nearestExisting(dir) {
|
|
5873
6342
|
let cur = dir;
|
|
5874
6343
|
while (!existsSync11(cur)) {
|
|
5875
|
-
const parent =
|
|
6344
|
+
const parent = join16(cur, "..");
|
|
5876
6345
|
if (parent === cur) break;
|
|
5877
6346
|
cur = parent;
|
|
5878
6347
|
}
|
|
@@ -5934,14 +6403,14 @@ function gatherHostFacts(repoPath) {
|
|
|
5934
6403
|
|
|
5935
6404
|
// src/service.ts
|
|
5936
6405
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
5937
|
-
import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync10, realpathSync as realpathSync3, rmSync as
|
|
6406
|
+
import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync10, realpathSync as realpathSync3, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
5938
6407
|
import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
|
|
5939
|
-
import { join as
|
|
6408
|
+
import { join as join18 } from "path";
|
|
5940
6409
|
|
|
5941
6410
|
// src/state.ts
|
|
5942
|
-
import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as
|
|
6411
|
+
import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
5943
6412
|
import { homedir as homedir5 } from "os";
|
|
5944
|
-
import { join as
|
|
6413
|
+
import { join as join17 } from "path";
|
|
5945
6414
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
5946
6415
|
var isRuntime = (v) => RUNTIMES.includes(v);
|
|
5947
6416
|
var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
|
|
@@ -5949,29 +6418,29 @@ var isDesired = (v) => v === "running" || v === "stopped";
|
|
|
5949
6418
|
var FILE_MODE2 = 384;
|
|
5950
6419
|
var DIR_MODE2 = 448;
|
|
5951
6420
|
function stateDir(env) {
|
|
5952
|
-
return env.DAHRK_STATE_DIR ??
|
|
6421
|
+
return env.DAHRK_STATE_DIR ?? join17(homedir5(), ".dahrk");
|
|
5953
6422
|
}
|
|
5954
6423
|
function legacyStateDir(env) {
|
|
5955
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
6424
|
+
return env.DAHRK_STATE_DIR ? void 0 : join17(homedir5(), ".skakel");
|
|
5956
6425
|
}
|
|
5957
6426
|
function stateFile(env) {
|
|
5958
|
-
return
|
|
6427
|
+
return join17(stateDir(env), "node.json");
|
|
5959
6428
|
}
|
|
5960
6429
|
function logDir(env) {
|
|
5961
|
-
return
|
|
6430
|
+
return join17(stateDir(env), "logs");
|
|
5962
6431
|
}
|
|
5963
6432
|
function logFiles(env) {
|
|
5964
6433
|
const dir = logDir(env);
|
|
5965
|
-
return { out:
|
|
6434
|
+
return { out: join17(dir, "node.out.log"), err: join17(dir, "node.err.log") };
|
|
5966
6435
|
}
|
|
5967
6436
|
function jsonlLogFile(env) {
|
|
5968
|
-
return
|
|
6437
|
+
return join17(logDir(env), "node.jsonl");
|
|
5969
6438
|
}
|
|
5970
6439
|
function crashDir(env) {
|
|
5971
|
-
return
|
|
6440
|
+
return join17(logDir(env), "crashes");
|
|
5972
6441
|
}
|
|
5973
6442
|
function lockFile(env) {
|
|
5974
|
-
return
|
|
6443
|
+
return join17(stateDir(env), "node.pid");
|
|
5975
6444
|
}
|
|
5976
6445
|
function setDesired(env, desired) {
|
|
5977
6446
|
writeState(env, { desired });
|
|
@@ -6001,7 +6470,7 @@ function writeState(env, patch) {
|
|
|
6001
6470
|
try {
|
|
6002
6471
|
mkdirSync11(dir, { recursive: true, mode: DIR_MODE2 });
|
|
6003
6472
|
const next = { ...readState(file), ...patch };
|
|
6004
|
-
|
|
6473
|
+
writeFileSync11(file, `${JSON.stringify(next, null, 2)}
|
|
6005
6474
|
`, { mode: FILE_MODE2 });
|
|
6006
6475
|
chmodSync2(file, FILE_MODE2);
|
|
6007
6476
|
} catch (e) {
|
|
@@ -6082,9 +6551,9 @@ ${envEntries}
|
|
|
6082
6551
|
<key>ThrottleInterval</key>
|
|
6083
6552
|
<integer>10</integer>
|
|
6084
6553
|
<key>StandardOutPath</key>
|
|
6085
|
-
<string>${xmlEscape(
|
|
6554
|
+
<string>${xmlEscape(join18(inputs.logDir, "node.out.log"))}</string>
|
|
6086
6555
|
<key>StandardErrorPath</key>
|
|
6087
|
-
<string>${xmlEscape(
|
|
6556
|
+
<string>${xmlEscape(join18(inputs.logDir, "node.err.log"))}</string>
|
|
6088
6557
|
</dict>
|
|
6089
6558
|
</plist>
|
|
6090
6559
|
`;
|
|
@@ -6107,8 +6576,8 @@ Type=simple
|
|
|
6107
6576
|
ExecStart=${exec}
|
|
6108
6577
|
${envLines}
|
|
6109
6578
|
WorkingDirectory=${inputs.homeDir}
|
|
6110
|
-
StandardOutput=append:${
|
|
6111
|
-
StandardError=append:${
|
|
6579
|
+
StandardOutput=append:${join18(inputs.logDir, "node.out.log")}
|
|
6580
|
+
StandardError=append:${join18(inputs.logDir, "node.err.log")}
|
|
6112
6581
|
Restart=on-failure
|
|
6113
6582
|
RestartSec=3
|
|
6114
6583
|
RestartPreventExitStatus=78
|
|
@@ -6119,7 +6588,7 @@ WantedBy=default.target
|
|
|
6119
6588
|
}
|
|
6120
6589
|
function buildPlan(inputs) {
|
|
6121
6590
|
if (inputs.manager === "launchd") {
|
|
6122
|
-
const filePath2 =
|
|
6591
|
+
const filePath2 = join18(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
6123
6592
|
return {
|
|
6124
6593
|
manager: "launchd",
|
|
6125
6594
|
label: LAUNCHD_LABEL,
|
|
@@ -6136,7 +6605,7 @@ function buildPlan(inputs) {
|
|
|
6136
6605
|
logHint: "dahrk logs -f"
|
|
6137
6606
|
};
|
|
6138
6607
|
}
|
|
6139
|
-
const filePath =
|
|
6608
|
+
const filePath = join18(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
6140
6609
|
const user = userInfo().username;
|
|
6141
6610
|
return {
|
|
6142
6611
|
manager: "systemd",
|
|
@@ -6166,7 +6635,7 @@ function unitIsCurrent(plan, onDisk) {
|
|
|
6166
6635
|
return onDisk === plan.content;
|
|
6167
6636
|
}
|
|
6168
6637
|
function unitPath(manager, homeDir) {
|
|
6169
|
-
return manager === "launchd" ?
|
|
6638
|
+
return manager === "launchd" ? join18(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join18(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
6170
6639
|
}
|
|
6171
6640
|
function statusCommand(manager) {
|
|
6172
6641
|
return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
|
|
@@ -6468,7 +6937,7 @@ var defaultDeps3 = () => ({
|
|
|
6468
6937
|
// no reason for it to be world-readable. `writeFileSync`'s `mode` applies only when it CREATES the file,
|
|
6469
6938
|
// so chmod explicitly too - re-installing over a unit an older client left at 0644 must tighten it.
|
|
6470
6939
|
writeFile: (path, content) => {
|
|
6471
|
-
|
|
6940
|
+
writeFileSync12(path, content, { mode: UNIT_FILE_MODE });
|
|
6472
6941
|
chmodSync3(path, UNIT_FILE_MODE);
|
|
6473
6942
|
},
|
|
6474
6943
|
readFile: (path) => {
|
|
@@ -6478,7 +6947,7 @@ var defaultDeps3 = () => ({
|
|
|
6478
6947
|
return void 0;
|
|
6479
6948
|
}
|
|
6480
6949
|
},
|
|
6481
|
-
removeFile: (path) =>
|
|
6950
|
+
removeFile: (path) => rmSync8(path, { force: true }),
|
|
6482
6951
|
fileExists: (path) => existsSync13(path),
|
|
6483
6952
|
// Captured, never inherited. A supervisor's stderr is ours to relay when it matters, not to leak
|
|
6484
6953
|
// unconditionally: `runCommands` decides who needs to read it.
|
|
@@ -6919,7 +7388,7 @@ async function runStatus(inputs, deps) {
|
|
|
6919
7388
|
}
|
|
6920
7389
|
|
|
6921
7390
|
// src/main.ts
|
|
6922
|
-
var CLIENT_VERSION = "0.1.
|
|
7391
|
+
var CLIENT_VERSION = "0.1.22";
|
|
6923
7392
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
6924
7393
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6925
7394
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -6931,7 +7400,7 @@ function resolveNodeId(env, opts = {}) {
|
|
|
6931
7400
|
if (existing) return existing;
|
|
6932
7401
|
const legacy = legacyStateDir(env);
|
|
6933
7402
|
if (legacy) {
|
|
6934
|
-
const legacyId = readState(
|
|
7403
|
+
const legacyId = readState(join19(legacy, "node.json")).nodeId;
|
|
6935
7404
|
if (legacyId) return legacyId;
|
|
6936
7405
|
}
|
|
6937
7406
|
const nodeId = randomUUID3();
|
|
@@ -7312,6 +7781,7 @@ async function runRepoAdd(flags) {
|
|
|
7312
7781
|
const bits = [];
|
|
7313
7782
|
if (result.drift.branch) bits.push(`branch ${result.drift.branch} (this repo is on ${repo.defaultBranch})`);
|
|
7314
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})`);
|
|
7315
7785
|
out(hint(`The hub's stored record differs - ${bits.join(", ")} - and was left unchanged.`));
|
|
7316
7786
|
}
|
|
7317
7787
|
return 0;
|