dahrk-node 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js 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 join17 } from "path";
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
@@ -232,7 +233,7 @@ function documentsBlock(ctx) {
232
233
  let budget = MAX_INLINE_DOCS_TOTAL_CHARS;
233
234
  const parts = [];
234
235
  for (const doc of docs) {
235
- const path = `.skakel/scratch/docs/${attachedDocBasename(doc)}.md`;
236
+ const path = `.dahrk/scratch/docs/${attachedDocBasename(doc)}.md`;
236
237
  const cap = Math.max(0, Math.min(MAX_INLINE_DOC_CHARS, budget));
237
238
  const body = doc.content.trim();
238
239
  const truncated = body.length > cap;
@@ -497,7 +498,7 @@ function createAskUserQuestionTool(deps) {
497
498
  // ../../packages/executor-worktree/src/claude-adapter.ts
498
499
  var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
499
500
  var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
500
- var HANDED_BACK_ARTIFACT_PATH = ".skakel/scratch/output/document.md";
501
+ var HANDED_BACK_ARTIFACT_PATH = ".dahrk/scratch/output/document.md";
501
502
  var CLAUDE_CODE_SYSTEM_PROMPT = { type: "preset", preset: "claude_code" };
502
503
  var userMsg = (text) => ({
503
504
  type: "user",
@@ -828,237 +829,11 @@ function createClaudeRunner() {
828
829
  };
829
830
  }
830
831
 
831
- // ../../packages/executor-worktree/src/codex-adapter.ts
832
- import { Codex } from "@openai/codex-sdk";
833
-
834
- // ../../packages/executor-worktree/src/codex-mappers.ts
835
- function mapItem(item) {
836
- switch (item.type) {
837
- case "reasoning":
838
- return [{ type: "thought", subtype: "reasoning_text", text: item.text }];
839
- case "agent_message":
840
- return [{ type: "response", text: item.text }];
841
- case "command_execution":
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 };
892
- }
893
- }
894
- 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
- }
832
+ // ../../packages/executor-worktree/src/pi-adapter.ts
833
+ import { join as join4 } from "path";
1059
834
 
1060
835
  // ../../packages/executor-worktree/src/pi-mappers.ts
1061
- function mapUsage3(u) {
836
+ function mapUsage2(u) {
1062
837
  return {
1063
838
  input: u?.input ?? 0,
1064
839
  output: u?.output ?? 0,
@@ -1099,7 +874,7 @@ function mapPiEvent(ev) {
1099
874
  const kind = ev.type === "agent_end" ? "agent_error" : "turn_error";
1100
875
  events.push({ type: "error", kind, message: m?.errorMessage ?? m?.stopReason ?? "failed" });
1101
876
  }
1102
- events.push({ type: "state", event: "stage-exit", status, usage: mapUsage3(m?.usage) });
877
+ events.push({ type: "state", event: "stage-exit", status, usage: mapUsage2(m?.usage) });
1103
878
  return { events, recognised: true };
1104
879
  }
1105
880
  // Streamed deltas: owned by the buffered state machine, no discrete event here.
@@ -1175,9 +950,117 @@ function consumePiEvent(ev, state, suppressStageExit) {
1175
950
  return { events, isResult: false };
1176
951
  }
1177
952
 
953
+ // ../../packages/executor-worktree/src/pi-auth.ts
954
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
955
+ import { tmpdir as tmpdir2 } from "os";
956
+ import { join as join3 } from "path";
957
+ function readAuthHint(ctx) {
958
+ return ctx.runtimeAuth;
959
+ }
960
+ function applyApiKeyAuth(hint2, runtimeEnv, authStorage) {
961
+ const env = runtimeEnv ?? {};
962
+ for (const p of hint2?.providers ?? []) {
963
+ if (p.kind !== "api_key") continue;
964
+ const value = env[p.envVar];
965
+ if (value) authStorage.setRuntimeApiKey(p.provider, value);
966
+ }
967
+ }
968
+ function buildAuthJson(hint2) {
969
+ const entries = [];
970
+ for (const p of hint2?.providers ?? []) {
971
+ if (p.kind !== "oauth") continue;
972
+ entries.push([p.provider, { type: "oauth", ...p.extra, access: p.access, refresh: p.refresh, expires: p.expires }]);
973
+ }
974
+ return entries.length ? Object.fromEntries(entries) : void 0;
975
+ }
976
+ function buildCustomProviders(hint2) {
977
+ const providers = {};
978
+ for (const p of hint2?.providers ?? []) {
979
+ if (p.kind !== "api_key" || !p.baseUrl) continue;
980
+ providers[p.provider] = {
981
+ baseUrl: p.baseUrl,
982
+ ...p.headers ? { headers: p.headers } : {},
983
+ ...p.models ? { models: p.models } : {}
984
+ };
985
+ }
986
+ return Object.keys(providers).length ? { providers } : void 0;
987
+ }
988
+ function createStageConfigDir() {
989
+ return mkdtempSync(join3(tmpdir2(), "dahrk-pi-"));
990
+ }
991
+ function cleanupStageConfigDir(dir) {
992
+ rmSync(dir, { recursive: true, force: true });
993
+ }
994
+ function writeStageAuthFile(dir, hint2) {
995
+ const authJson = buildAuthJson(hint2);
996
+ if (!authJson) return void 0;
997
+ const path = join3(dir, "auth.json");
998
+ writeFileSync(path, JSON.stringify(authJson, null, 2));
999
+ return path;
1000
+ }
1001
+ function writeStageCustomProviders(dir, hint2) {
1002
+ const models = buildCustomProviders(hint2);
1003
+ if (!models) return void 0;
1004
+ const path = join3(dir, "models.json");
1005
+ writeFileSync(path, JSON.stringify(models, null, 2));
1006
+ return path;
1007
+ }
1008
+
1178
1009
  // ../../packages/executor-worktree/src/pi-adapter.ts
1179
- var COALESCE_MS3 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
1010
+ var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
1180
1011
  var PI_STAGE_COMPLETE_TOOL = "dahrk_stage_complete";
1012
+ var PI_ASK_USER_QUESTION_TOOL = "ask_user_question";
1013
+ function piToolCallDecision(ctx, toolName, input) {
1014
+ const verdict2 = ctx.authorizeToolUse?.(toolName, input);
1015
+ if (verdict2?.verdict === "deny") {
1016
+ return { block: true, reason: verdict2.reason ?? `tool "${toolName}" denied by policy ${verdict2.policy}` };
1017
+ }
1018
+ return void 0;
1019
+ }
1020
+ function registerToolCallGate(s, ctx) {
1021
+ const policyCtx = ctx;
1022
+ s.setToolCallGate?.((toolName, input) => piToolCallDecision(policyCtx, toolName, input));
1023
+ }
1024
+ function buildBrokeredPiMcpServers(ctx) {
1025
+ const servers = ctx.config.mcpServers;
1026
+ if (!servers || servers.length === 0 || !ctx.mcpProxyBaseUrl) return void 0;
1027
+ const entries = {};
1028
+ for (const s of servers) entries[s.id] = { type: s.type, url: `${ctx.mcpProxyBaseUrl}/${s.id}` };
1029
+ return entries;
1030
+ }
1031
+ function createBrokeredMcpExtension(servers) {
1032
+ return {
1033
+ name: "dahrk-brokered-mcp",
1034
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1035
+ factory: async (pi) => {
1036
+ const { Client } = await import("./client-MZ5GEQAD.js");
1037
+ const { StreamableHTTPClientTransport } = await import("./streamableHttp-S27HYW5J.js");
1038
+ for (const [id, server] of Object.entries(servers)) {
1039
+ try {
1040
+ const client = new Client({ name: `dahrk-${id}`, version: "0.1.0" });
1041
+ await client.connect(new StreamableHTTPClientTransport(new URL(server.url)));
1042
+ const { tools } = await client.listTools();
1043
+ for (const tool3 of tools) {
1044
+ pi.registerTool({
1045
+ name: tool3.name,
1046
+ label: tool3.name,
1047
+ description: tool3.description ?? "",
1048
+ // The MCP inputSchema is a JSON-schema object; Pi validates against it at runtime (its
1049
+ // custom tools use the same plain-object shape - see the injected tools above).
1050
+ parameters: tool3.inputSchema,
1051
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1052
+ execute: async (_toolCallId, params) => {
1053
+ const result = await client.callTool({ name: tool3.name, arguments: params ?? {} });
1054
+ return { content: result.content, details: {} };
1055
+ }
1056
+ });
1057
+ }
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ }
1062
+ };
1063
+ }
1181
1064
  function createPiRunner(deps = {}) {
1182
1065
  const createSession = deps.createSession ?? defaultCreatePiSession;
1183
1066
  const abortController = new AbortController();
@@ -1192,6 +1075,15 @@ function createPiRunner(deps = {}) {
1192
1075
  const captureSessionId = (s) => {
1193
1076
  if (s.sessionId) sessionId = s.sessionId;
1194
1077
  };
1078
+ let sessionDisposed = false;
1079
+ const disposeSession = () => {
1080
+ if (sessionDisposed || !session) return;
1081
+ sessionDisposed = true;
1082
+ try {
1083
+ session.dispose();
1084
+ } catch {
1085
+ }
1086
+ };
1195
1087
  const readSessionCost = (s) => {
1196
1088
  const cost = s.getSessionStats?.()?.cost;
1197
1089
  return typeof cost === "number" ? cost : void 0;
@@ -1201,6 +1093,7 @@ function createPiRunner(deps = {}) {
1201
1093
  async runBatch(ctx, onTrace) {
1202
1094
  const emit = makeEmit("pi", onTrace);
1203
1095
  const s = await openSession(ctx);
1096
+ registerToolCallGate(s, ctx);
1204
1097
  const state = newPiBufferState();
1205
1098
  let status = "ok";
1206
1099
  const unsub = s.subscribe((ev) => {
@@ -1220,11 +1113,13 @@ function createPiRunner(deps = {}) {
1220
1113
  captureSessionId(s);
1221
1114
  if (cancelled) status = "fail";
1222
1115
  const costUsd = readSessionCost(s);
1116
+ if (status !== "ok") disposeSession();
1223
1117
  return { status, ...sessionId ? { sessionId } : {}, ...costUsd !== void 0 ? { costUsd } : {} };
1224
1118
  },
1225
1119
  async runInteractive(ctx, turns, onTrace) {
1226
1120
  const emit = makeEmit("pi", onTrace);
1227
1121
  const s = await openSession(ctx);
1122
+ registerToolCallGate(s, ctx);
1228
1123
  const state = newPiBufferState();
1229
1124
  const exit = ctx.config.exit ?? "either";
1230
1125
  const wantsTool = exit === "tool" || exit === "either";
@@ -1246,9 +1141,28 @@ function createPiRunner(deps = {}) {
1246
1141
  for (const e of r.events) emit(e, rawRef);
1247
1142
  if (r.responseText) lastResponseText = r.responseText;
1248
1143
  });
1249
- const humanIter = turns[Symbol.asyncIterator]();
1250
1144
  const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
1145
+ const router = createElicitTurnRouter(turns, { signal, firstReplyMs, idleMs });
1146
+ const humanIter = router.conversation[Symbol.asyncIterator]();
1251
1147
  let awaitingFirstReply = true;
1148
+ const elicitCtx = ctx;
1149
+ const ask = async (question) => {
1150
+ const outcome = await router.ask(awaitingFirstReply, () => {
1151
+ emit({ type: "elicitation", prompt: question.prompt, signal: "select", options: question.options });
1152
+ elicitCtx.emitElicit?.(question);
1153
+ });
1154
+ switch (outcome.kind) {
1155
+ case "reply":
1156
+ return `The user selected: ${outcome.text}`;
1157
+ case "busy":
1158
+ return "Only one question can be asked at a time; wait for the current one to be answered, then ask again.";
1159
+ case "noreply":
1160
+ return "No response from the user; proceed with your best judgement.";
1161
+ case "cancel":
1162
+ return "The question was cancelled.";
1163
+ }
1164
+ };
1165
+ s.setAskUserQuestionHandler?.((questions) => askQuestionsSequentially(questions, ask));
1252
1166
  let exited = "gate";
1253
1167
  let pending = humanIter.next();
1254
1168
  try {
@@ -1273,7 +1187,7 @@ function createPiRunner(deps = {}) {
1273
1187
  const texts = [race.value.text];
1274
1188
  pending = humanIter.next();
1275
1189
  for (; ; ) {
1276
- const more = await raceNextTurn(pending, COALESCE_MS3, signal);
1190
+ const more = await raceNextTurn(pending, COALESCE_MS2, signal);
1277
1191
  if (more.kind === "turn") {
1278
1192
  texts.push(more.value.text);
1279
1193
  pending = humanIter.next();
@@ -1316,6 +1230,7 @@ function createPiRunner(deps = {}) {
1316
1230
  unsub();
1317
1231
  captureSessionId(s);
1318
1232
  const costUsd = readSessionCost(s);
1233
+ disposeSession();
1319
1234
  return {
1320
1235
  status,
1321
1236
  summary,
@@ -1341,6 +1256,7 @@ function createPiRunner(deps = {}) {
1341
1256
  return `(summary unavailable: ${e.message})`;
1342
1257
  } finally {
1343
1258
  unsub();
1259
+ disposeSession();
1344
1260
  }
1345
1261
  },
1346
1262
  async cancel() {
@@ -1351,23 +1267,31 @@ function createPiRunner(deps = {}) {
1351
1267
  await session?.abort();
1352
1268
  } catch {
1353
1269
  }
1354
- try {
1355
- session?.dispose();
1356
- } catch {
1357
- }
1270
+ disposeSession();
1358
1271
  }
1359
1272
  };
1360
1273
  }
1361
1274
  async function defaultCreatePiSession(ctx) {
1362
1275
  const spec = "@earendil-works/pi-coding-agent";
1363
1276
  const mod = await import(spec);
1364
- const { AuthStorage, ModelRegistry, SessionManager, createAgentSession, defineTool, resolveCliModel } = mod;
1365
- const authStorage = AuthStorage.create();
1366
- for (const [key, value] of Object.entries(ctx.runtimeEnv ?? {})) {
1367
- const provider = PROVIDER_BY_ENV[key];
1368
- if (provider) authStorage.setRuntimeApiKey(provider, value);
1369
- }
1370
- const modelRegistry = ModelRegistry.create(authStorage);
1277
+ const {
1278
+ AuthStorage,
1279
+ DefaultResourceLoader,
1280
+ ModelRegistry,
1281
+ SessionManager,
1282
+ SettingsManager,
1283
+ createAgentSession,
1284
+ defineTool,
1285
+ getAgentDir,
1286
+ resolveCliModel
1287
+ } = mod;
1288
+ const hint2 = readAuthHint(ctx);
1289
+ const configDir = createStageConfigDir();
1290
+ writeStageAuthFile(configDir, hint2);
1291
+ writeStageCustomProviders(configDir, hint2);
1292
+ const authStorage = AuthStorage.create(join4(configDir, "auth.json"));
1293
+ applyApiKeyAuth(hint2, ctx.runtimeEnv, authStorage);
1294
+ const modelRegistry = ModelRegistry.create(authStorage, join4(configDir, "models.json"));
1371
1295
  let model;
1372
1296
  if (ctx.config.model) {
1373
1297
  const resolved = resolveCliModel({ cliModel: ctx.config.model, modelRegistry });
@@ -1382,22 +1306,89 @@ async function defaultCreatePiSession(ctx) {
1382
1306
  parameters: { type: "object", properties: { summary: { type: "string" } }, required: ["summary"] },
1383
1307
  execute: async () => ({ content: [{ type: "text", text: "Stage marked complete." }], details: {} })
1384
1308
  });
1309
+ let askHandler;
1310
+ const askUserQuestion = defineTool({
1311
+ name: PI_ASK_USER_QUESTION_TOOL,
1312
+ label: "Ask the user a question",
1313
+ 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.",
1314
+ parameters: {
1315
+ type: "object",
1316
+ properties: {
1317
+ questions: {
1318
+ type: "array",
1319
+ items: {
1320
+ type: "object",
1321
+ properties: {
1322
+ question: { type: "string" },
1323
+ options: {
1324
+ type: "array",
1325
+ items: {
1326
+ type: "object",
1327
+ properties: { label: { type: "string" }, description: { type: "string" } },
1328
+ required: ["label"]
1329
+ }
1330
+ },
1331
+ multiSelect: { type: "boolean" }
1332
+ },
1333
+ required: ["question", "options"]
1334
+ }
1335
+ }
1336
+ },
1337
+ required: ["questions"]
1338
+ },
1339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1340
+ execute: async (_toolCallId, params) => {
1341
+ const text = askHandler ? await askHandler(params.questions) : "No response from the user; proceed with your best judgement.";
1342
+ return { content: [{ type: "text", text }], details: {} };
1343
+ }
1344
+ });
1345
+ let toolCallGate;
1346
+ const toolGateExtension = {
1347
+ name: "dahrk-tool-gate",
1348
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1349
+ factory: (pi) => {
1350
+ pi.on("tool_call", (event) => toolCallGate?.(event.toolName, event.input));
1351
+ }
1352
+ };
1353
+ const cwd = ctx.workspace.worktreePath;
1354
+ const agentDir = getAgentDir();
1355
+ const settingsManager = SettingsManager.create(cwd, agentDir);
1356
+ const brokeredMcp = buildBrokeredPiMcpServers(ctx);
1357
+ const extensionFactories = brokeredMcp ? [toolGateExtension, createBrokeredMcpExtension(brokeredMcp)] : [toolGateExtension];
1358
+ const resourceLoader = new DefaultResourceLoader({
1359
+ cwd,
1360
+ agentDir,
1361
+ settingsManager,
1362
+ extensionFactories
1363
+ });
1364
+ await resourceLoader.reload();
1385
1365
  const { session } = await createAgentSession({
1386
1366
  sessionManager: SessionManager.inMemory(ctx.workspace.worktreePath),
1387
1367
  authStorage,
1388
1368
  modelRegistry,
1389
- cwd: ctx.workspace.worktreePath,
1390
- customTools: [stageComplete],
1369
+ settingsManager,
1370
+ resourceLoader,
1371
+ cwd,
1372
+ customTools: [stageComplete, askUserQuestion],
1391
1373
  ...model ? { model } : {}
1392
1374
  });
1393
- return session;
1375
+ const piSession = session;
1376
+ const innerDispose = piSession.dispose.bind(piSession);
1377
+ piSession.dispose = () => {
1378
+ try {
1379
+ innerDispose();
1380
+ } finally {
1381
+ cleanupStageConfigDir(configDir);
1382
+ }
1383
+ };
1384
+ piSession.setAskUserQuestionHandler = (handler) => {
1385
+ askHandler = handler;
1386
+ };
1387
+ piSession.setToolCallGate = (gate) => {
1388
+ toolCallGate = gate;
1389
+ };
1390
+ return piSession;
1394
1391
  }
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
1392
  function modelFamily(id) {
1402
1393
  const last = id.split(".").pop() ?? id;
1403
1394
  return last.replace(/-v\d+:\d+$/, "").toLowerCase();
@@ -1410,15 +1401,236 @@ function pickAuthedModel(resolved, available) {
1410
1401
  return available.find((m) => modelFamily(m.id) === family) ?? resolved;
1411
1402
  }
1412
1403
 
1404
+ // ../../packages/executor-worktree/src/pi-container.ts
1405
+ import { spawn as nodeSpawn } from "child_process";
1406
+
1407
+ // ../../packages/executor-worktree/src/pi-rpc-client.ts
1408
+ import { StringDecoder } from "string_decoder";
1409
+ function createLineDecoder() {
1410
+ const decoder = new StringDecoder("utf8");
1411
+ let buffer = "";
1412
+ const drain = () => {
1413
+ const lines = [];
1414
+ for (; ; ) {
1415
+ const nl = buffer.indexOf("\n");
1416
+ if (nl === -1) break;
1417
+ let line = buffer.slice(0, nl);
1418
+ buffer = buffer.slice(nl + 1);
1419
+ if (line.endsWith("\r")) line = line.slice(0, -1);
1420
+ lines.push(line);
1421
+ }
1422
+ return lines;
1423
+ };
1424
+ return {
1425
+ push(chunk) {
1426
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
1427
+ return drain();
1428
+ },
1429
+ end() {
1430
+ buffer += decoder.end();
1431
+ const lines = drain();
1432
+ if (buffer.length > 0) {
1433
+ const last = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
1434
+ buffer = "";
1435
+ lines.push(last);
1436
+ }
1437
+ return lines;
1438
+ }
1439
+ };
1440
+ }
1441
+ function deferred() {
1442
+ let resolve3;
1443
+ let reject;
1444
+ const promise = new Promise((res, rej) => {
1445
+ resolve3 = res;
1446
+ reject = rej;
1447
+ });
1448
+ return { promise, resolve: resolve3, reject };
1449
+ }
1450
+ var isResponse = (msg) => typeof msg === "object" && msg !== null && msg.type === "response";
1451
+ var PiRpcSession = class {
1452
+ #sessionId;
1453
+ #listeners = [];
1454
+ /** Command responses awaited by id (correlated via the optional `id` field). */
1455
+ #pendingResponses = /* @__PURE__ */ new Map();
1456
+ /** The in-flight `prompt()` resolver, settled on the next `agent_end`. */
1457
+ #pendingAgentEnd;
1458
+ #reqCounter = 0;
1459
+ #disposed = false;
1460
+ #child;
1461
+ #kill;
1462
+ constructor(child, options = {}) {
1463
+ this.#child = child;
1464
+ this.#kill = options.kill;
1465
+ const decoder = createLineDecoder();
1466
+ const onData = (chunk) => {
1467
+ for (const line of decoder.push(chunk)) if (line.length > 0) this.#onLine(line);
1468
+ };
1469
+ child.stdout?.on("data", onData);
1470
+ child.stdout?.on("end", () => {
1471
+ for (const line of decoder.end()) if (line.length > 0) this.#onLine(line);
1472
+ });
1473
+ }
1474
+ get sessionId() {
1475
+ return this.#sessionId;
1476
+ }
1477
+ subscribe(listener) {
1478
+ this.#listeners.push(listener);
1479
+ return () => {
1480
+ this.#listeners = this.#listeners.filter((l) => l !== listener);
1481
+ };
1482
+ }
1483
+ async prompt(text) {
1484
+ if (this.#disposed) throw new Error("pi rpc session disposed");
1485
+ const agentEnd = deferred();
1486
+ this.#pendingAgentEnd = agentEnd;
1487
+ const ack = await this.#send("prompt", { message: text });
1488
+ if (ack.success === false) {
1489
+ this.#pendingAgentEnd = void 0;
1490
+ throw new Error(ack.error ?? "prompt rejected");
1491
+ }
1492
+ await agentEnd.promise;
1493
+ }
1494
+ async abort() {
1495
+ if (this.#disposed) return;
1496
+ await this.#send("abort", {});
1497
+ }
1498
+ /** Best-effort refresh of the resume token from `get_state`; swallows a failed lookup. */
1499
+ async getState() {
1500
+ if (this.#disposed) return;
1501
+ try {
1502
+ const res = await this.#send("get_state", {});
1503
+ const id = res.data?.sessionId;
1504
+ if (typeof id === "string" && id) this.#sessionId = id;
1505
+ } catch {
1506
+ }
1507
+ }
1508
+ dispose() {
1509
+ if (this.#disposed) return;
1510
+ this.#disposed = true;
1511
+ try {
1512
+ this.#child.stdin?.end();
1513
+ } catch {
1514
+ }
1515
+ const err = new Error("pi rpc session disposed");
1516
+ for (const d of this.#pendingResponses.values()) d.reject(err);
1517
+ this.#pendingResponses.clear();
1518
+ if (this.#pendingAgentEnd) {
1519
+ this.#pendingAgentEnd.reject(err);
1520
+ this.#pendingAgentEnd = void 0;
1521
+ }
1522
+ if (this.#kill) {
1523
+ const kill = this.#kill;
1524
+ this.#kill = void 0;
1525
+ void kill();
1526
+ }
1527
+ }
1528
+ /** Write a command as one LF-terminated JSON line and await its correlated response. */
1529
+ #send(type, fields) {
1530
+ const id = `req-${++this.#reqCounter}`;
1531
+ const d = deferred();
1532
+ this.#pendingResponses.set(id, d);
1533
+ try {
1534
+ this.#child.stdin?.write(`${JSON.stringify({ id, type, ...fields })}
1535
+ `);
1536
+ } catch (e) {
1537
+ this.#pendingResponses.delete(id);
1538
+ d.reject(e);
1539
+ }
1540
+ return d.promise;
1541
+ }
1542
+ #onLine(line) {
1543
+ let msg;
1544
+ try {
1545
+ msg = JSON.parse(line);
1546
+ } catch {
1547
+ return;
1548
+ }
1549
+ if (isResponse(msg)) {
1550
+ const id = msg.id;
1551
+ if (typeof id === "string") {
1552
+ const pending = this.#pendingResponses.get(id);
1553
+ if (pending) {
1554
+ this.#pendingResponses.delete(id);
1555
+ pending.resolve(msg);
1556
+ }
1557
+ }
1558
+ return;
1559
+ }
1560
+ const ev = msg;
1561
+ for (const l of [...this.#listeners]) l(ev);
1562
+ if (ev.type === "agent_end" && this.#pendingAgentEnd) {
1563
+ const d = this.#pendingAgentEnd;
1564
+ this.#pendingAgentEnd = void 0;
1565
+ d.resolve();
1566
+ }
1567
+ }
1568
+ };
1569
+
1570
+ // ../../packages/executor-worktree/src/pi-container.ts
1571
+ var DEFAULT_IMAGE = process.env.DAHRK_PI_IMAGE ?? process.env.SKAKEL_PI_IMAGE ?? "dahrk/pi:latest";
1572
+ var _seq = 0;
1573
+ function createContainerPiSession(opts = {}) {
1574
+ const {
1575
+ image = DEFAULT_IMAGE,
1576
+ scratchDir: optsScratchDir,
1577
+ spawn: spawnFn = nodeSpawn,
1578
+ onStderr = (line) => void process.stderr.write(`pi-container: ${line}
1579
+ `)
1580
+ } = opts;
1581
+ return async (ctx) => {
1582
+ const containerName = `dahrk-pi-${Date.now()}-${++_seq}`;
1583
+ const resolvedScratchDir = optsScratchDir ?? ctx.workspace.scratchPath;
1584
+ const mountArgs = resolvedScratchDir ? ["-v", `${resolvedScratchDir}:/dahrk/scratch`] : [];
1585
+ const envArgs = [];
1586
+ for (const [k, v] of Object.entries(ctx.runtimeEnv ?? {})) {
1587
+ envArgs.push("-e", `${k}=${v}`);
1588
+ }
1589
+ const child = spawnFn(
1590
+ "docker",
1591
+ ["run", "-i", "--rm", "--name", containerName, ...mountArgs, ...envArgs, image, "pi", "--mode", "rpc"],
1592
+ { stdio: ["pipe", "pipe", "pipe"] }
1593
+ );
1594
+ child.stderr?.setEncoding("utf8");
1595
+ let stderrBuf = "";
1596
+ child.stderr?.on("data", (chunk) => {
1597
+ stderrBuf += chunk;
1598
+ const lines = stderrBuf.split("\n");
1599
+ stderrBuf = lines.pop() ?? "";
1600
+ for (const line of lines) if (line.trim()) onStderr(line);
1601
+ });
1602
+ child.stderr?.on("end", () => {
1603
+ if (stderrBuf.trim()) onStderr(stderrBuf);
1604
+ stderrBuf = "";
1605
+ });
1606
+ const killContainer = () => new Promise((resolve3) => {
1607
+ const killer = spawnFn("docker", ["kill", containerName], { stdio: "ignore" });
1608
+ killer.on("exit", () => resolve3());
1609
+ killer.on("error", () => resolve3());
1610
+ });
1611
+ return new PiRpcSession(child, { kill: killContainer });
1612
+ };
1613
+ }
1614
+ function createIsolatedPiRunner(opts = {}) {
1615
+ return createPiRunner({ createSession: createContainerPiSession(opts) });
1616
+ }
1617
+
1413
1618
  // ../../packages/executor-worktree/src/git-service.ts
1414
1619
  import { execFileSync } from "child_process";
1415
- import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1416
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1417
- import { basename, dirname, isAbsolute, join as join3 } from "path";
1620
+ import { existsSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1621
+ import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
1622
+ import { basename, dirname, isAbsolute, join as join5 } from "path";
1418
1623
  var noopLogger = { info: () => {
1419
1624
  }, warn: () => {
1420
1625
  } };
1421
- var SCRATCH_DIR = ".skakel/scratch";
1626
+ var SCRATCH_DIR = ".dahrk/scratch";
1627
+ var DEFAULT_MERGE_RESOLVE_RULES = [
1628
+ { path: "pnpm-lock.yaml", strategy: "theirs" },
1629
+ { path: "package-lock.json", strategy: "theirs" },
1630
+ { path: "yarn.lock", strategy: "theirs" },
1631
+ { path: "CHANGELOG.md", strategy: "union" },
1632
+ { path: "CHANGELOG.internal.md", strategy: "union" }
1633
+ ];
1422
1634
  function parseOwnerRepo(gitUrl) {
1423
1635
  const m = /[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/.exec(gitUrl.trim());
1424
1636
  return m ? `${m[1]}/${m[2]}` : void 0;
@@ -1428,10 +1640,10 @@ function sanitizeBranchName(name) {
1428
1640
  return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
1429
1641
  }
1430
1642
  function resolveWorktreesDir(override) {
1431
- return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join3(homedir2(), ".dahrk", "worktrees");
1643
+ return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join5(homedir2(), ".dahrk", "worktrees");
1432
1644
  }
1433
1645
  function resolveMirrorsDir(override) {
1434
- return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join3(homedir2(), ".dahrk", "mirrors");
1646
+ return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join5(homedir2(), ".dahrk", "mirrors");
1435
1647
  }
1436
1648
  function createGitService(opts = {}) {
1437
1649
  const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
@@ -1467,15 +1679,17 @@ function createGitService(opts = {}) {
1467
1679
  return (stderr?.trim() || err?.message || String(e)).split("\n")[0] ?? String(e);
1468
1680
  };
1469
1681
  const excludeScratchLocally = (worktreePath) => {
1470
- const entry = `${SCRATCH_DIR}/`;
1682
+ const entries = [`${SCRATCH_DIR}/`];
1471
1683
  try {
1472
1684
  const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
1473
- const excludePath = isAbsolute(rel) ? rel : join3(worktreePath, rel);
1685
+ const excludePath = isAbsolute(rel) ? rel : join5(worktreePath, rel);
1474
1686
  const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
1475
- if (existing.split("\n").some((l) => l.trim() === entry)) return;
1687
+ const lines = existing.split("\n").map((l) => l.trim());
1688
+ const toAdd = entries.filter((e) => !lines.includes(e));
1689
+ if (toAdd.length === 0) return;
1476
1690
  mkdirSync(dirname(excludePath), { recursive: true });
1477
1691
  const sep4 = existing && !existing.endsWith("\n") ? "\n" : "";
1478
- writeFileSync(excludePath, `${existing}${sep4}${entry}
1692
+ writeFileSync2(excludePath, `${existing}${sep4}${toAdd.join("\n")}
1479
1693
  `);
1480
1694
  } catch (e) {
1481
1695
  log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
@@ -1499,13 +1713,53 @@ function createGitService(opts = {}) {
1499
1713
  }
1500
1714
  return { headSha: git(worktreePath, ["rev-parse", "HEAD"]).trim(), dirty };
1501
1715
  };
1716
+ const unmergedPaths = (worktreePath) => git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
1717
+ const unionMerge = (worktreePath, p) => {
1718
+ const stage = (n) => {
1719
+ try {
1720
+ return git(worktreePath, ["show", `:${n}:${p}`]);
1721
+ } catch {
1722
+ return "";
1723
+ }
1724
+ };
1725
+ const dir = mkdtempSync2(join5(tmpdir3(), "dahrk-union-"));
1726
+ try {
1727
+ const ours = join5(dir, "ours");
1728
+ const base = join5(dir, "base");
1729
+ const theirs = join5(dir, "theirs");
1730
+ writeFileSync2(ours, stage(2));
1731
+ writeFileSync2(base, stage(1));
1732
+ writeFileSync2(theirs, stage(3));
1733
+ const merged = git(worktreePath, ["merge-file", "-p", "--union", ours, base, theirs]);
1734
+ writeFileSync2(join5(worktreePath, p), merged);
1735
+ } finally {
1736
+ rmSync2(dir, { recursive: true, force: true });
1737
+ }
1738
+ };
1739
+ const preResolveConflicts = (worktreePath, rules) => {
1740
+ gitOk2(worktreePath, ["rerere"]);
1741
+ for (const p of unmergedPaths(worktreePath)) {
1742
+ const rule = rules.find((r) => r.path === p || basename(p) === r.path);
1743
+ if (!rule) continue;
1744
+ try {
1745
+ if (rule.strategy === "union") {
1746
+ unionMerge(worktreePath, p);
1747
+ } else {
1748
+ git(worktreePath, ["checkout", `--${rule.strategy}`, "--", p]);
1749
+ }
1750
+ git(worktreePath, ["add", "--", p]);
1751
+ } catch (e) {
1752
+ log.warn(`pre-resolve of ${p} (${rule.strategy}) failed: ${e.message}`);
1753
+ }
1754
+ }
1755
+ };
1502
1756
  const setupAuth = (token) => {
1503
- const dir = mkdtempSync(join3(tmpdir2(), "dahrk-cred-"));
1504
- const script = join3(dir, "askpass.sh");
1505
- writeFileSync(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
1757
+ const dir = mkdtempSync2(join5(tmpdir3(), "dahrk-cred-"));
1758
+ const script = join5(dir, "askpass.sh");
1759
+ writeFileSync2(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
1506
1760
  return {
1507
1761
  env: { ...process.env, GIT_ASKPASS: script, DAHRK_GIT_TOKEN: token, GIT_TERMINAL_PROMPT: "0" },
1508
- cleanup: () => rmSync(dir, { recursive: true, force: true })
1762
+ cleanup: () => rmSync2(dir, { recursive: true, force: true })
1509
1763
  };
1510
1764
  };
1511
1765
  const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
@@ -1518,7 +1772,7 @@ function createGitService(opts = {}) {
1518
1772
  cleanup: () => auth?.cleanup()
1519
1773
  };
1520
1774
  };
1521
- const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1775
+ const mirrorPathFor = (repoId) => join5(mirrorsDir, sanitizeBranchName(repoId));
1522
1776
  const listWorktrees = (mirror) => {
1523
1777
  let out2;
1524
1778
  try {
@@ -1543,7 +1797,7 @@ function createGitService(opts = {}) {
1543
1797
  } catch (e) {
1544
1798
  log.warn(`git worktree remove failed for ${worktreePath}: ${e.message}`);
1545
1799
  }
1546
- rmSync(worktreePath, { recursive: true, force: true });
1800
+ rmSync2(worktreePath, { recursive: true, force: true });
1547
1801
  gitOk2(mirror, ["worktree", "prune"]);
1548
1802
  };
1549
1803
  const TRACKING_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
@@ -1630,18 +1884,19 @@ function createGitService(opts = {}) {
1630
1884
  baseBranch: spec.baseBranch,
1631
1885
  branch: sanitizeBranchName(spec.branch ?? `dahrk/${spec.runId}`),
1632
1886
  worktreePath,
1633
- scratchPath: join3(worktreePath, ".skakel", "scratch")
1887
+ scratchPath: join5(worktreePath, SCRATCH_DIR)
1634
1888
  });
1635
1889
  return {
1636
1890
  worktreesDir,
1637
1891
  async createWorktree(spec) {
1638
1892
  const { repoId, gitUrl, baseBranch, runId } = spec;
1639
1893
  const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
1640
- const worktreePath = join3(worktreesDir, runId);
1894
+ const worktreePath = join5(worktreesDir, runId);
1641
1895
  mkdirSync(worktreesDir, { recursive: true });
1642
1896
  if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
1643
1897
  log.info(`reusing existing worktree at ${worktreePath}`);
1644
- mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1898
+ mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
1899
+ excludeScratchLocally(worktreePath);
1645
1900
  return refFor(spec, worktreePath);
1646
1901
  }
1647
1902
  const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
@@ -1677,7 +1932,8 @@ function createGitService(opts = {}) {
1677
1932
  if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
1678
1933
  throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
1679
1934
  }
1680
- mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1935
+ mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
1936
+ excludeScratchLocally(worktreePath);
1681
1937
  return refFor(spec, worktreePath);
1682
1938
  },
1683
1939
  async commitAndPush(ref, opts2) {
@@ -1725,6 +1981,10 @@ function createGitService(opts = {}) {
1725
1981
  `user.name=${authorName}`,
1726
1982
  "-c",
1727
1983
  `user.email=${authorEmail}`,
1984
+ "-c",
1985
+ "rerere.enabled=true",
1986
+ "-c",
1987
+ "rerere.autoupdate=true",
1728
1988
  "merge",
1729
1989
  "--no-edit",
1730
1990
  "FETCH_HEAD"
@@ -1734,15 +1994,30 @@ function createGitService(opts = {}) {
1734
1994
  } catch (mergeErr) {
1735
1995
  const inMerge = gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "MERGE_HEAD"]);
1736
1996
  if (inMerge) {
1737
- const conflictFiles = git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
1738
- git(worktreePath, ["merge", "--abort"]);
1739
- return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles };
1740
- }
1741
- const msg = mergeErr.message;
1742
- if (/unrelated histories|refusing to merge/i.test(msg)) {
1743
- return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
1997
+ preResolveConflicts(worktreePath, opts2.mergeResolve ?? DEFAULT_MERGE_RESOLVE_RULES);
1998
+ const conflictFiles = unmergedPaths(worktreePath);
1999
+ if (conflictFiles.length === 0) {
2000
+ git(worktreePath, [
2001
+ "-c",
2002
+ `user.name=${authorName}`,
2003
+ "-c",
2004
+ `user.email=${authorEmail}`,
2005
+ "commit",
2006
+ "--no-edit"
2007
+ ]);
2008
+ integration = "clean";
2009
+ headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
2010
+ } else {
2011
+ git(worktreePath, ["merge", "--abort"]);
2012
+ return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles };
2013
+ }
2014
+ } else {
2015
+ const msg = mergeErr.message;
2016
+ if (/unrelated histories|refusing to merge/i.test(msg)) {
2017
+ return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
2018
+ }
2019
+ throw mergeErr;
1744
2020
  }
1745
- throw mergeErr;
1746
2021
  }
1747
2022
  }
1748
2023
  git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(authEnv));
@@ -1805,10 +2080,10 @@ function createGitService(opts = {}) {
1805
2080
  return void 0;
1806
2081
  }
1807
2082
  };
1808
- const tmp = mkdtempSync(join3(tmpdir2(), "dahrk-pr-"));
1809
- const bodyFile = join3(tmp, "body.md");
2083
+ const tmp = mkdtempSync2(join5(tmpdir3(), "dahrk-pr-"));
2084
+ const bodyFile = join5(tmp, "body.md");
1810
2085
  try {
1811
- writeFileSync(bodyFile, opts2.body ?? "");
2086
+ writeFileSync2(bodyFile, opts2.body ?? "");
1812
2087
  try {
1813
2088
  gh(worktreePath, [
1814
2089
  "pr",
@@ -1830,7 +2105,7 @@ function createGitService(opts = {}) {
1830
2105
  }
1831
2106
  return readBack() ?? { prError: "gh pr create reported success but no PR was found" };
1832
2107
  } finally {
1833
- rmSync(tmp, { recursive: true, force: true });
2108
+ rmSync2(tmp, { recursive: true, force: true });
1834
2109
  }
1835
2110
  },
1836
2111
  async teardownWorktree(ref) {
@@ -1841,8 +2116,8 @@ function createGitService(opts = {}) {
1841
2116
 
1842
2117
  // ../../packages/executor-worktree/src/worktree-reaper.ts
1843
2118
  import { execFileSync as execFileSync2 } from "child_process";
1844
- import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
1845
- import { join as join4 } from "path";
2119
+ import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync3, statSync } from "fs";
2120
+ import { join as join6 } from "path";
1846
2121
  var MINUTE = 6e4;
1847
2122
  var HOUR = 60 * MINUTE;
1848
2123
  var DEFAULTS = {
@@ -1872,7 +2147,7 @@ var canonical = (p) => {
1872
2147
  }
1873
2148
  };
1874
2149
  function lastUsedMs(worktreePath) {
1875
- const candidates = [join4(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
2150
+ const candidates = [join6(worktreePath, ".dahrk", "scratch", "state.json"), worktreePath];
1876
2151
  let newest = 0;
1877
2152
  for (const p of candidates) {
1878
2153
  try {
@@ -1895,7 +2170,7 @@ function createWorktreeReaper(opts) {
1895
2170
  const log = opts.logger ?? noop;
1896
2171
  const mirrors = () => {
1897
2172
  try {
1898
- return readdirSync(opts.mirrorsDir).map((d) => join4(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
2173
+ return readdirSync(opts.mirrorsDir).map((d) => join6(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1899
2174
  } catch {
1900
2175
  return [];
1901
2176
  }
@@ -1919,7 +2194,7 @@ function createWorktreeReaper(opts) {
1919
2194
  }
1920
2195
  const onDisk = (() => {
1921
2196
  try {
1922
- return readdirSync(opts.worktreesDir).map((d) => canonical(join4(opts.worktreesDir, d)));
2197
+ return readdirSync(opts.worktreesDir).map((d) => canonical(join6(opts.worktreesDir, d)));
1923
2198
  } catch {
1924
2199
  return [];
1925
2200
  }
@@ -1962,7 +2237,7 @@ function createWorktreeReaper(opts) {
1962
2237
  if (d.mirror) {
1963
2238
  gitOk(d.mirror, ["worktree", "remove", "--force", d.path]);
1964
2239
  }
1965
- rmSync2(d.path, { recursive: true, force: true });
2240
+ rmSync3(d.path, { recursive: true, force: true });
1966
2241
  if (d.mirror) gitOk(d.mirror, ["worktree", "prune"]);
1967
2242
  report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
1968
2243
  } catch (e) {
@@ -1979,20 +2254,20 @@ function createWorktreeReaper(opts) {
1979
2254
 
1980
2255
  // ../../packages/executor-worktree/src/trace-writer.ts
1981
2256
  import { createHash } from "crypto";
1982
- import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1983
- import { join as join5 } from "path";
2257
+ import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2258
+ import { join as join7 } from "path";
1984
2259
  var DEFAULT_SPILL_BYTES = 8192;
1985
2260
  function createTraceWriter(scratchPath, meta, opts = {}) {
1986
2261
  const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
1987
- const dir = join5(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1988
- mkdirSync2(join5(dir, "blobs"), { recursive: true });
1989
- mkdirSync2(join5(dir, "raw"), { recursive: true });
1990
- const tracePath = join5(dir, "trace.jsonl");
1991
- const metaPath = join5(dir, "meta.json");
2262
+ const dir = join7(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
2263
+ mkdirSync2(join7(dir, "blobs"), { recursive: true });
2264
+ mkdirSync2(join7(dir, "raw"), { recursive: true });
2265
+ const tracePath = join7(dir, "trace.jsonl");
2266
+ const metaPath = join7(dir, "meta.json");
1992
2267
  let current = { ...meta };
1993
2268
  let nextSeq = 0;
1994
2269
  let rawCount = 0;
1995
- writeFileSync2(metaPath, JSON.stringify(current, null, 2));
2270
+ writeFileSync3(metaPath, JSON.stringify(current, null, 2));
1996
2271
  const tooBig = (value) => {
1997
2272
  const s = typeof value === "string" ? value : JSON.stringify(value ?? "");
1998
2273
  return s.length > spillBytes;
@@ -2000,8 +2275,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
2000
2275
  const spillValue = (value) => {
2001
2276
  const data = typeof value === "string" ? value : JSON.stringify(value);
2002
2277
  const sha = createHash("sha256").update(data).digest("hex");
2003
- writeFileSync2(join5(dir, "blobs", sha), data);
2004
- return join5("blobs", sha);
2278
+ writeFileSync3(join7(dir, "blobs", sha), data);
2279
+ return join7("blobs", sha);
2005
2280
  };
2006
2281
  const spill = (event) => {
2007
2282
  if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
@@ -2031,13 +2306,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
2031
2306
  return written;
2032
2307
  },
2033
2308
  writeRaw(record) {
2034
- const rel = join5("raw", `${rawCount++}.json`);
2035
- writeFileSync2(join5(dir, rel), JSON.stringify(record, null, 2));
2309
+ const rel = join7("raw", `${rawCount++}.json`);
2310
+ writeFileSync3(join7(dir, rel), JSON.stringify(record, null, 2));
2036
2311
  return rel;
2037
2312
  },
2038
2313
  finalise(patch = {}) {
2039
2314
  current = { ...current, ...patch };
2040
- writeFileSync2(metaPath, JSON.stringify(current, null, 2));
2315
+ writeFileSync3(metaPath, JSON.stringify(current, null, 2));
2041
2316
  },
2042
2317
  count() {
2043
2318
  return nextSeq;
@@ -2047,13 +2322,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
2047
2322
 
2048
2323
  // ../../packages/executor-worktree/src/pack-cache.ts
2049
2324
  import { createHash as createHash2 } from "crypto";
2050
- import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2051
- import { dirname as dirname2, join as join6, relative, sep } from "path";
2325
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync3, readdirSync as readdirSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
2326
+ import { dirname as dirname2, join as join8, relative, sep } from "path";
2052
2327
  function readManifestFiles(dir) {
2053
2328
  const out2 = [];
2054
2329
  const walk = (cur) => {
2055
2330
  for (const entry of readdirSync2(cur, { withFileTypes: true })) {
2056
- const abs = join6(cur, entry.name);
2331
+ const abs = join8(cur, entry.name);
2057
2332
  if (entry.isDirectory()) walk(abs);
2058
2333
  else out2.push(relative(dir, abs).split(sep).join("/"));
2059
2334
  }
@@ -2063,8 +2338,8 @@ function readManifestFiles(dir) {
2063
2338
  }
2064
2339
 
2065
2340
  // ../../packages/executor-worktree/src/overlay.ts
2066
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
2067
- import { dirname as dirname3, join as join7 } from "path";
2341
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
2342
+ import { dirname as dirname3, join as join9 } from "path";
2068
2343
  function sameBytes(dest, bytes) {
2069
2344
  try {
2070
2345
  return readFileSync3(dest).equals(bytes);
@@ -2076,16 +2351,16 @@ async function overlayComponents(opts) {
2076
2351
  const { worktreePath, runtime, components, cache } = opts;
2077
2352
  const result = { written: [], skippedRepoLocal: [], warnings: [] };
2078
2353
  for (const ref of components) {
2079
- if (runtime === "codex") {
2354
+ if (runtime !== "claude-code") {
2080
2355
  result.warnings.push(
2081
- `codex runtime: ${ref.kind} \`${ref.name}@${ref.version}\` not materialised; inline into the prompt or use Claude`
2356
+ `${runtime} runtime: ${ref.kind} \`${ref.name}@${ref.version}\` not materialised; inline into the prompt or use Claude`
2082
2357
  );
2083
2358
  continue;
2084
2359
  }
2085
2360
  const { dir } = await cache.materialise(ref);
2086
2361
  for (const relPath of readManifestFiles(dir)) {
2087
- const src = join7(dir, relPath);
2088
- const dest = join7(worktreePath, relPath);
2362
+ const src = join9(dir, relPath);
2363
+ const dest = join9(worktreePath, relPath);
2089
2364
  const bytes = readFileSync3(src);
2090
2365
  if (existsSync4(dest)) {
2091
2366
  if (sameBytes(dest, bytes)) continue;
@@ -2093,27 +2368,21 @@ async function overlayComponents(opts) {
2093
2368
  continue;
2094
2369
  }
2095
2370
  mkdirSync4(dirname3(dest), { recursive: true });
2096
- writeFileSync4(dest, bytes);
2371
+ writeFileSync5(dest, bytes);
2097
2372
  result.written.push(relPath);
2098
2373
  }
2099
2374
  }
2100
2375
  return result;
2101
2376
  }
2102
2377
 
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
2378
  // ../../packages/executor-worktree/src/index.ts
2113
2379
  function makeRunner(runtime) {
2114
2380
  if ((process.env.DAHRK_RUNNER ?? process.env.SKAKEL_RUNNER ?? "real") === "mock") return createMockRunner(runtime);
2115
- if (runtime === "pi") return createPiRunner();
2116
- return runtime === "codex" ? createCodexRunner() : createClaudeRunner();
2381
+ if (runtime === "pi") return piContainerIsolationRequired() ? createIsolatedPiRunner() : createPiRunner();
2382
+ return createClaudeRunner();
2383
+ }
2384
+ function piContainerIsolationRequired() {
2385
+ return (process.env.DAHRK_PI_ISOLATION ?? process.env.SKAKEL_PI_ISOLATION) === "container";
2117
2386
  }
2118
2387
 
2119
2388
  // ../../packages/edge/src/health.ts
@@ -2172,8 +2441,8 @@ function collectHealth(inputs) {
2172
2441
  }
2173
2442
 
2174
2443
  // ../../packages/edge/src/job-ledger.ts
2175
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
2176
- import { dirname as dirname4, join as join8 } from "path";
2444
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync6 } from "fs";
2445
+ import { dirname as dirname4, join as join10 } from "path";
2177
2446
  var FILE_MODE = 384;
2178
2447
  var DIR_MODE = 448;
2179
2448
  function nullJobLedger() {
@@ -2202,7 +2471,7 @@ function fileJobLedger(file, warn = console.warn) {
2202
2471
  const tmp = `${file}.${process.pid}.tmp`;
2203
2472
  try {
2204
2473
  mkdirSync5(dirname4(file), { recursive: true, mode: DIR_MODE });
2205
- writeFileSync5(tmp, `${JSON.stringify(entries, null, 2)}
2474
+ writeFileSync6(tmp, `${JSON.stringify(entries, null, 2)}
2206
2475
  `, { mode: FILE_MODE });
2207
2476
  chmodSync(tmp, FILE_MODE);
2208
2477
  renameSync(tmp, file);
@@ -2237,7 +2506,7 @@ function announceableJobs(entries) {
2237
2506
  return out2;
2238
2507
  }
2239
2508
  function jobLedgerFile(stateDir2) {
2240
- return join8(stateDir2, "jobs.json");
2509
+ return join10(stateDir2, "jobs.json");
2241
2510
  }
2242
2511
 
2243
2512
  // ../../packages/edge/src/log-shipper.ts
@@ -2340,8 +2609,8 @@ function ceilingFromEnv(env) {
2340
2609
  }
2341
2610
 
2342
2611
  // ../../packages/edge/src/logger.ts
2343
- import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
2344
- import { join as join9 } from "path";
2612
+ import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync2, writeSync } from "fs";
2613
+ import { join as join11 } from "path";
2345
2614
  import pino from "pino";
2346
2615
 
2347
2616
  // ../../packages/edge/src/redact.ts
@@ -2465,7 +2734,7 @@ var RotatingFile = class {
2465
2734
  closeSync(this.fd);
2466
2735
  this.fd = void 0;
2467
2736
  const oldest = `${this.path}.${this.maxFiles}`;
2468
- if (existsSync6(oldest)) rmSync4(oldest, { force: true });
2737
+ if (existsSync6(oldest)) rmSync5(oldest, { force: true });
2469
2738
  for (let i = this.maxFiles - 1; i >= 1; i--) {
2470
2739
  const from = `${this.path}.${i}`;
2471
2740
  if (existsSync6(from)) renameSync2(from, `${this.path}.${i + 1}`);
@@ -2531,7 +2800,7 @@ function createNodeLogger(opts = {}) {
2531
2800
  if (opts.dir && fileLevel !== "silent") {
2532
2801
  try {
2533
2802
  mkdirSync6(opts.dir, { recursive: true, mode: 448 });
2534
- const file = new RotatingFile(join9(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2803
+ const file = new RotatingFile(join11(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2535
2804
  streams.push({ level: fileLevel, stream: file });
2536
2805
  } catch (e) {
2537
2806
  process.stderr.write(`dahrk: file logging disabled (${e.message})
@@ -2589,9 +2858,9 @@ function denyToolRule(tool3) {
2589
2858
  // ../../packages/edge/src/stage-runner.ts
2590
2859
  import { execFileSync as execFileSync5 } from "child_process";
2591
2860
  import { createHash as createHash3 } from "crypto";
2592
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
2593
- import { tmpdir as tmpdir4 } from "os";
2594
- import { isAbsolute as isAbsolute3, join as join11, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
2861
+ import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
2862
+ import { tmpdir as tmpdir5 } from "os";
2863
+ import { isAbsolute as isAbsolute3, join as join13, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
2595
2864
  import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
2596
2865
 
2597
2866
  // ../../packages/edge/src/builtins.ts
@@ -2600,8 +2869,8 @@ import { execFileSync as execFileSync4 } from "child_process";
2600
2869
  // ../../packages/edge/src/fs-roots.ts
2601
2870
  import { execFileSync as execFileSync3 } from "child_process";
2602
2871
  import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
2603
- import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2604
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve, sep as sep2 } from "path";
2872
+ import { homedir as homedir3, tmpdir as tmpdir4 } from "os";
2873
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve, sep as sep2 } from "path";
2605
2874
  function isUnder(root, target) {
2606
2875
  const rel = relative2(resolve(root), resolve(target));
2607
2876
  return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
@@ -2617,7 +2886,7 @@ function realish(p) {
2617
2886
  while (head !== dirname5(head)) {
2618
2887
  if (existsSync7(head)) {
2619
2888
  try {
2620
- return join10(realpathSync2.native(head), ...tail.reverse());
2889
+ return join12(realpathSync2.native(head), ...tail.reverse());
2621
2890
  } catch {
2622
2891
  return p;
2623
2892
  }
@@ -2667,7 +2936,7 @@ function computeFsRoots(opts) {
2667
2936
  // Every git command in the worktree reads and writes here.
2668
2937
  ...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
2669
2938
  // Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
2670
- ...[tmpdir3(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2939
+ ...[tmpdir4(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2671
2940
  // The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
2672
2941
  // it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
2673
2942
  // here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
@@ -2686,26 +2955,26 @@ function computeFsRoots(opts) {
2686
2955
  "/System",
2687
2956
  "/proc",
2688
2957
  "/sys",
2689
- join10(home, ".gitconfig"),
2690
- join10(home, ".config"),
2691
- join10(home, ".npmrc"),
2692
- join10(home, ".cache"),
2693
- join10(home, "Library", "Caches"),
2694
- join10(home, "Library", "pnpm"),
2695
- join10(home, ".local", "share"),
2696
- join10(home, ".nvm"),
2697
- join10(home, ".volta"),
2698
- join10(home, ".asdf"),
2699
- join10(home, ".cargo"),
2700
- join10(home, ".rustup"),
2958
+ join12(home, ".gitconfig"),
2959
+ join12(home, ".config"),
2960
+ join12(home, ".npmrc"),
2961
+ join12(home, ".cache"),
2962
+ join12(home, "Library", "Caches"),
2963
+ join12(home, "Library", "pnpm"),
2964
+ join12(home, ".local", "share"),
2965
+ join12(home, ".nvm"),
2966
+ join12(home, ".volta"),
2967
+ join12(home, ".asdf"),
2968
+ join12(home, ".cargo"),
2969
+ join12(home, ".rustup"),
2701
2970
  ...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
2702
2971
  ].map(realish);
2703
2972
  const deny = [
2704
- join10(home, ".ssh"),
2705
- join10(home, ".aws"),
2706
- join10(home, ".gnupg"),
2707
- join10(home, ".config", "gcloud"),
2708
- join10(home, "Library", "Keychains"),
2973
+ join12(home, ".ssh"),
2974
+ join12(home, ".aws"),
2975
+ join12(home, ".gnupg"),
2976
+ join12(home, ".config", "gcloud"),
2977
+ join12(home, "Library", "Keychains"),
2709
2978
  "/Volumes",
2710
2979
  "/etc/shadow",
2711
2980
  "/etc/sudoers"
@@ -3290,7 +3559,7 @@ var attemptOf = (jobId) => {
3290
3559
  };
3291
3560
  var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
3292
3561
  function writeScratchState(ref, job, attempt, status) {
3293
- const statePath = join11(ref.scratchPath, "state.json");
3562
+ const statePath = join13(ref.scratchPath, "state.json");
3294
3563
  let state;
3295
3564
  try {
3296
3565
  state = JSON.parse(readFileSync5(statePath, "utf8"));
@@ -3299,23 +3568,23 @@ function writeScratchState(ref, job, attempt, status) {
3299
3568
  }
3300
3569
  state.stages[job.stageId] = { currentAttempt: attempt, status };
3301
3570
  mkdirSync7(ref.scratchPath, { recursive: true });
3302
- writeFileSync6(statePath, JSON.stringify(state, null, 2));
3571
+ writeFileSync7(statePath, JSON.stringify(state, null, 2));
3303
3572
  }
3304
3573
  function writeIssueContext(ref, issueContext) {
3305
3574
  if (issueContext === void 0) return;
3306
3575
  try {
3307
3576
  mkdirSync7(ref.scratchPath, { recursive: true });
3308
- writeFileSync6(join11(ref.scratchPath, "issue.md"), issueContext);
3577
+ writeFileSync7(join13(ref.scratchPath, "issue.md"), issueContext);
3309
3578
  } catch {
3310
3579
  }
3311
3580
  }
3312
3581
  function writeAttachedDocuments(ref, docs) {
3313
3582
  if (!docs || docs.length === 0) return;
3314
3583
  try {
3315
- const dir = join11(ref.scratchPath, "docs");
3584
+ const dir = join13(ref.scratchPath, "docs");
3316
3585
  mkdirSync7(dir, { recursive: true });
3317
3586
  for (const doc of docs) {
3318
- writeFileSync6(join11(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3587
+ writeFileSync7(join13(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3319
3588
  }
3320
3589
  } catch {
3321
3590
  }
@@ -3334,12 +3603,12 @@ function writeGuidance(ref, guidance) {
3334
3603
  if (!guidance || guidance.length === 0) return;
3335
3604
  try {
3336
3605
  mkdirSync7(ref.scratchPath, { recursive: true });
3337
- writeFileSync6(join11(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3606
+ writeFileSync7(join13(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3338
3607
  } catch {
3339
3608
  }
3340
3609
  }
3341
3610
  var ARTIFACT_CAP_BYTES = 64 * 1024;
3342
- var SCRATCH_OUTPUT_DIR = ".skakel/scratch/output";
3611
+ var SCRATCH_OUTPUT_DIR = ".dahrk/scratch/output";
3343
3612
  function capContent(raw) {
3344
3613
  return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
3345
3614
  }
@@ -3367,14 +3636,14 @@ function readEmittedArtifact(ref, relPath) {
3367
3636
  }
3368
3637
  }
3369
3638
  function scanScratchOutput(ref, preferRel) {
3639
+ const preferBase = preferRel?.split("/").pop();
3370
3640
  try {
3371
- const names = readdirSync3(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3641
+ const names = readdirSync3(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3372
3642
  (n) => n.toLowerCase().endsWith(".md")
3373
3643
  );
3374
3644
  if (names.length === 0) return void 0;
3375
- const preferBase = preferRel?.split("/").pop();
3376
3645
  const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
3377
- const raw = readFileSync5(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3646
+ const raw = readFileSync5(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3378
3647
  if (raw.trim().length === 0) return void 0;
3379
3648
  return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
3380
3649
  } catch {
@@ -3394,7 +3663,7 @@ function scanChangedMarkdown(ref) {
3394
3663
  );
3395
3664
  for (const rel of rels) {
3396
3665
  try {
3397
- const raw = readFileSync5(join11(ref.worktreePath, rel), "utf8");
3666
+ const raw = readFileSync5(join13(ref.worktreePath, rel), "utf8");
3398
3667
  if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
3399
3668
  } catch {
3400
3669
  }
@@ -3415,6 +3684,9 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
3415
3684
  if (changed) return { artifact: changed, source: "changed-file" };
3416
3685
  return void 0;
3417
3686
  }
3687
+ function runtimeUsesMcpGateway(runtime) {
3688
+ return runtime === "claude-code" || runtime === "pi";
3689
+ }
3418
3690
  function createStageRunner(deps) {
3419
3691
  const worktrees = /* @__PURE__ */ new Map();
3420
3692
  const log = deps.logger ?? createNodeLogger({ level: "silent" });
@@ -3437,7 +3709,7 @@ function createStageRunner(deps) {
3437
3709
  if (ref) {
3438
3710
  if (scratchOnly.has(runId)) {
3439
3711
  try {
3440
- rmSync5(ref.worktreePath, { recursive: true, force: true });
3712
+ rmSync6(ref.worktreePath, { recursive: true, force: true });
3441
3713
  } catch (e) {
3442
3714
  log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove scratch dir");
3443
3715
  }
@@ -3517,9 +3789,9 @@ function createStageRunner(deps) {
3517
3789
  ...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
3518
3790
  });
3519
3791
  } else {
3520
- const base = deps.scratchRoot ?? join11(tmpdir4(), "dahrk", "scratch");
3521
- const worktreePath = join11(base, runId);
3522
- const scratchPath = join11(worktreePath, ".skakel", "scratch");
3792
+ const base = deps.scratchRoot ?? join13(tmpdir5(), "dahrk", "scratch");
3793
+ const worktreePath = join13(base, runId);
3794
+ const scratchPath = join13(worktreePath, ".dahrk", "scratch");
3523
3795
  mkdirSync7(scratchPath, { recursive: true });
3524
3796
  ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
3525
3797
  scratchOnly.add(runId);
@@ -3535,7 +3807,7 @@ function createStageRunner(deps) {
3535
3807
  const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
3536
3808
  if (artifactDir && slash > 0) {
3537
3809
  try {
3538
- mkdirSync7(join11(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
3810
+ mkdirSync7(join13(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
3539
3811
  } catch {
3540
3812
  }
3541
3813
  }
@@ -3563,8 +3835,8 @@ function createStageRunner(deps) {
3563
3835
  if (!sink) return;
3564
3836
  const base = { tenantId: job.tenantId, runId, stageId, attempt };
3565
3837
  try {
3566
- for (const name of readdirSync3(join11(writer.dir, "blobs"))) {
3567
- const bytes = readFileSync5(join11(writer.dir, "blobs", name));
3838
+ for (const name of readdirSync3(join13(writer.dir, "blobs"))) {
3839
+ const bytes = readFileSync5(join13(writer.dir, "blobs", name));
3568
3840
  const { url } = await sink.requestBlobUrl({
3569
3841
  ...base,
3570
3842
  sha256: name,
@@ -3578,7 +3850,7 @@ function createStageRunner(deps) {
3578
3850
  }
3579
3851
  let archiveKey;
3580
3852
  try {
3581
- const bytes = readFileSync5(join11(writer.dir, "trace.jsonl"));
3853
+ const bytes = readFileSync5(join13(writer.dir, "trace.jsonl"));
3582
3854
  const sha = createHash3("sha256").update(bytes).digest("hex");
3583
3855
  const { key, url } = await sink.requestBlobUrl({
3584
3856
  ...base,
@@ -3675,6 +3947,7 @@ function createStageRunner(deps) {
3675
3947
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
3676
3948
  }
3677
3949
  let denied = false;
3950
+ const surfacedDenyReasons = /* @__PURE__ */ new Set();
3678
3951
  let escapedUnblocked = false;
3679
3952
  const authorisedActions = [];
3680
3953
  const runtime = agentConfig.runtime;
@@ -3693,7 +3966,11 @@ function createStageRunner(deps) {
3693
3966
  streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
3694
3967
  }
3695
3968
  streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
3696
- deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
3969
+ const surfaceKey = `${verdict2.policy}\0${reason}`;
3970
+ if (!surfacedDenyReasons.has(surfaceKey)) {
3971
+ surfacedDenyReasons.add(surfaceKey);
3972
+ deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
3973
+ }
3697
3974
  };
3698
3975
  const authorizeToolUse = (tool3, input) => {
3699
3976
  const verdict2 = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
@@ -3704,7 +3981,10 @@ function createStageRunner(deps) {
3704
3981
  }
3705
3982
  return verdict2;
3706
3983
  };
3984
+ let bumpStall = () => {
3985
+ };
3707
3986
  const onTrace = (event) => {
3987
+ bumpStall();
3708
3988
  if (event.type === "action") {
3709
3989
  const key = actionKey(event.tool, event.input);
3710
3990
  const authorised = authorisedActions.indexOf(key);
@@ -3727,7 +4007,7 @@ function createStageRunner(deps) {
3727
4007
  if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
3728
4008
  };
3729
4009
  const mcpServers = agentConfig.mcpServers;
3730
- if (mcpServers && mcpServers.length > 0 && runtime === "claude-code") {
4010
+ if (mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
3731
4011
  gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
3732
4012
  }
3733
4013
  const runner = deps.makeRunner(runtime);
@@ -3769,6 +4049,20 @@ function createStageRunner(deps) {
3769
4049
  timedOut = true;
3770
4050
  void runner.cancel();
3771
4051
  }, killMs) : void 0;
4052
+ let stalled = false;
4053
+ let stallTimer;
4054
+ const stallSource = agentConfig.stallMs ?? Number(process.env.DAHRK_BATCH_STALL_MS ?? process.env.SKAKEL_BATCH_STALL_MS ?? 3e5);
4055
+ const stallMs = interactive ? 0 : Math.max(0, Math.floor(stallSource));
4056
+ if (stallMs > 0) {
4057
+ bumpStall = () => {
4058
+ if (stallTimer) clearTimeout(stallTimer);
4059
+ stallTimer = setTimeout(() => {
4060
+ stalled = true;
4061
+ void runner.cancel();
4062
+ }, stallMs);
4063
+ };
4064
+ bumpStall();
4065
+ }
3772
4066
  try {
3773
4067
  if (interactive) {
3774
4068
  const mailbox = new ManagedMailbox();
@@ -3779,8 +4073,9 @@ function createStageRunner(deps) {
3779
4073
  }
3780
4074
  } finally {
3781
4075
  if (killTimer) clearTimeout(killTimer);
4076
+ if (stallTimer) clearTimeout(stallTimer);
3782
4077
  }
3783
- let status = timedOut ? "timeout" : result.status;
4078
+ let status = timedOut || stalled ? "timeout" : result.status;
3784
4079
  if (status === "ok" && escapedUnblocked) {
3785
4080
  status = "fail";
3786
4081
  const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
@@ -3799,6 +4094,9 @@ function createStageRunner(deps) {
3799
4094
  }
3800
4095
  }
3801
4096
  let summary = result.summary ?? `${stageId}: ${status}`;
4097
+ if (stalled && !timedOut) {
4098
+ summary = `${stageId}: stalled (no output for ${Math.round(stallMs / 1e3)}s)`;
4099
+ }
3802
4100
  if (!interactive && status === "ok") {
3803
4101
  try {
3804
4102
  summary = await runner.summarise(ctx);
@@ -4040,6 +4338,17 @@ async function startEdgeNode(opts) {
4040
4338
  if (oldest !== void 0) lastResults.delete(oldest);
4041
4339
  }
4042
4340
  };
4341
+ const ackedCancels = /* @__PURE__ */ new Map();
4342
+ const ackCancel = (jobId) => {
4343
+ const frame = { type: "cancel-ack", jobId };
4344
+ ackedCancels.delete(jobId);
4345
+ ackedCancels.set(jobId, frame);
4346
+ if (ackedCancels.size > MAX_RESEND) {
4347
+ const oldest = ackedCancels.keys().next().value;
4348
+ if (oldest !== void 0) ackedCancels.delete(oldest);
4349
+ }
4350
+ send(frame);
4351
+ };
4043
4352
  const pendingBlob = /* @__PURE__ */ new Map();
4044
4353
  let blobReqCounter = 0;
4045
4354
  const trace = {
@@ -4220,6 +4529,7 @@ async function startEdgeNode(opts) {
4220
4529
  if (msg.type === "cancel") {
4221
4530
  log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
4222
4531
  stageRunner.cancel(msg.jobId);
4532
+ ackCancel(msg.jobId);
4223
4533
  return;
4224
4534
  }
4225
4535
  if (msg.type === "turn") {
@@ -4351,6 +4661,7 @@ async function startEdgeNode(opts) {
4351
4661
  log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
4352
4662
  sendHello();
4353
4663
  for (const frame of lastResults.values()) send(frame);
4664
+ for (const frame of ackedCancels.values()) send(frame);
4354
4665
  startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
4355
4666
  });
4356
4667
  sock.on("message", (raw) => void onMessage(raw.toString()));
@@ -4454,7 +4765,6 @@ async function startEdgeNode(opts) {
4454
4765
  import { execFile } from "child_process";
4455
4766
  var PROBES = [
4456
4767
  { runtime: "claude-code", cmd: "claude" },
4457
- { runtime: "codex", cmd: "codex" },
4458
4768
  { runtime: "pi", cmd: "pi" }
4459
4769
  ];
4460
4770
  var DEFAULT_TIMEOUT_MS = 5e3;
@@ -5092,8 +5402,8 @@ function usage(bin, command) {
5092
5402
  }
5093
5403
 
5094
5404
  // src/diagnose.ts
5095
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
5096
- import { join as join12 } from "path";
5405
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
5406
+ import { join as join14 } from "path";
5097
5407
 
5098
5408
  // src/ui.ts
5099
5409
  import { styleText } from "util";
@@ -5208,7 +5518,7 @@ async function buildBundle(deps) {
5208
5518
  if (deps.exists(deps.crashDir)) {
5209
5519
  for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
5210
5520
  try {
5211
- crashes.push(JSON.parse(deps.readFile(join12(deps.crashDir, name))));
5521
+ crashes.push(JSON.parse(deps.readFile(join14(deps.crashDir, name))));
5212
5522
  } catch (e) {
5213
5523
  warnings.push(`could not read crash record ${name} (${e.message})`);
5214
5524
  }
@@ -5269,14 +5579,14 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
5269
5579
  listDir: (p) => readdirSync4(p),
5270
5580
  exists: (p) => existsSync8(p),
5271
5581
  writeFile: (p, content) => {
5272
- const dir = join12(p, "..");
5582
+ const dir = join14(p, "..");
5273
5583
  if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
5274
- writeFileSync7(p, content, { mode: 384 });
5584
+ writeFileSync8(p, content, { mode: 384 });
5275
5585
  },
5276
5586
  out
5277
5587
  });
5278
5588
  function defaultBundlePath(cwd, now) {
5279
- return join12(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
5589
+ return join14(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
5280
5590
  }
5281
5591
 
5282
5592
  // src/doctor.ts
@@ -5495,7 +5805,7 @@ async function registerRepo(deps, args) {
5495
5805
  }
5496
5806
 
5497
5807
  // src/lock.ts
5498
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
5808
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
5499
5809
  import { dirname as dirname6 } from "path";
5500
5810
  function parseLock(content) {
5501
5811
  if (!content) return void 0;
@@ -5535,9 +5845,9 @@ var defaultLockDeps = (file) => ({
5535
5845
  },
5536
5846
  writeFile: (path, content) => {
5537
5847
  if (!existsSync9(dirname6(path))) mkdirSync9(dirname6(path), { recursive: true, mode: 448 });
5538
- writeFileSync8(path, content);
5848
+ writeFileSync9(path, content);
5539
5849
  },
5540
- removeFile: (path) => rmSync6(path, { force: true }),
5850
+ removeFile: (path) => rmSync7(path, { force: true }),
5541
5851
  isAlive
5542
5852
  });
5543
5853
 
@@ -5645,13 +5955,13 @@ var defaultLogsDeps = (files, jsonlFile) => ({
5645
5955
  });
5646
5956
 
5647
5957
  // src/process-safety.ts
5648
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
5649
- import { join as join13 } from "path";
5958
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
5959
+ import { join as join15 } from "path";
5650
5960
  function writeCrashRecord(dir, record) {
5651
5961
  try {
5652
5962
  mkdirSync10(dir, { recursive: true, mode: 448 });
5653
- const file = join13(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5654
- writeFileSync9(file, `${JSON.stringify(record, null, 2)}
5963
+ const file = join15(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5964
+ writeFileSync10(file, `${JSON.stringify(record, null, 2)}
5655
5965
  `, { mode: 384 });
5656
5966
  return file;
5657
5967
  } catch {
@@ -5708,7 +6018,7 @@ import { execFileSync as execFileSync6 } from "child_process";
5708
6018
  import { randomUUID as randomUUID2 } from "crypto";
5709
6019
  import { accessSync, constants as fsConstants, existsSync as existsSync11, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
5710
6020
  import { homedir as homedir4 } from "os";
5711
- import { join as join14 } from "path";
6021
+ import { join as join16 } from "path";
5712
6022
  var REPORT_BASE_URL = "https://app.dahrk.ai/r";
5713
6023
  var LOW_DISK_BYTES = 512 * 1024 * 1024;
5714
6024
  var PREFLIGHT_STAGES = [
@@ -5847,7 +6157,7 @@ function commandPresent(cmd) {
5847
6157
  }
5848
6158
  function sshKeyPresent() {
5849
6159
  try {
5850
- const dir = join14(homedir4(), ".ssh");
6160
+ const dir = join16(homedir4(), ".ssh");
5851
6161
  if (existsSync11(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
5852
6162
  } catch {
5853
6163
  }
@@ -5867,12 +6177,12 @@ function writable(dir) {
5867
6177
  }
5868
6178
  }
5869
6179
  function worktreeRoot(env) {
5870
- return env.DAHRK_WORKTREES_DIR ?? join14(env.DAHRK_STATE_DIR ?? join14(homedir4(), ".dahrk"), "worktrees");
6180
+ return env.DAHRK_WORKTREES_DIR ?? join16(env.DAHRK_STATE_DIR ?? join16(homedir4(), ".dahrk"), "worktrees");
5871
6181
  }
5872
6182
  function nearestExisting(dir) {
5873
6183
  let cur = dir;
5874
6184
  while (!existsSync11(cur)) {
5875
- const parent = join14(cur, "..");
6185
+ const parent = join16(cur, "..");
5876
6186
  if (parent === cur) break;
5877
6187
  cur = parent;
5878
6188
  }
@@ -5934,14 +6244,14 @@ function gatherHostFacts(repoPath) {
5934
6244
 
5935
6245
  // src/service.ts
5936
6246
  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 rmSync7, writeFileSync as writeFileSync11 } from "fs";
6247
+ 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
6248
  import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
5939
- import { join as join16 } from "path";
6249
+ import { join as join18 } from "path";
5940
6250
 
5941
6251
  // src/state.ts
5942
- import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
6252
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync11 } from "fs";
5943
6253
  import { homedir as homedir5 } from "os";
5944
- import { join as join15 } from "path";
6254
+ import { join as join17 } from "path";
5945
6255
  var RUNTIMES = ["claude-code", "codex", "pi"];
5946
6256
  var isRuntime = (v) => RUNTIMES.includes(v);
5947
6257
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
@@ -5949,29 +6259,29 @@ var isDesired = (v) => v === "running" || v === "stopped";
5949
6259
  var FILE_MODE2 = 384;
5950
6260
  var DIR_MODE2 = 448;
5951
6261
  function stateDir(env) {
5952
- return env.DAHRK_STATE_DIR ?? join15(homedir5(), ".dahrk");
6262
+ return env.DAHRK_STATE_DIR ?? join17(homedir5(), ".dahrk");
5953
6263
  }
5954
6264
  function legacyStateDir(env) {
5955
- return env.DAHRK_STATE_DIR ? void 0 : join15(homedir5(), ".skakel");
6265
+ return env.DAHRK_STATE_DIR ? void 0 : join17(homedir5(), ".skakel");
5956
6266
  }
5957
6267
  function stateFile(env) {
5958
- return join15(stateDir(env), "node.json");
6268
+ return join17(stateDir(env), "node.json");
5959
6269
  }
5960
6270
  function logDir(env) {
5961
- return join15(stateDir(env), "logs");
6271
+ return join17(stateDir(env), "logs");
5962
6272
  }
5963
6273
  function logFiles(env) {
5964
6274
  const dir = logDir(env);
5965
- return { out: join15(dir, "node.out.log"), err: join15(dir, "node.err.log") };
6275
+ return { out: join17(dir, "node.out.log"), err: join17(dir, "node.err.log") };
5966
6276
  }
5967
6277
  function jsonlLogFile(env) {
5968
- return join15(logDir(env), "node.jsonl");
6278
+ return join17(logDir(env), "node.jsonl");
5969
6279
  }
5970
6280
  function crashDir(env) {
5971
- return join15(logDir(env), "crashes");
6281
+ return join17(logDir(env), "crashes");
5972
6282
  }
5973
6283
  function lockFile(env) {
5974
- return join15(stateDir(env), "node.pid");
6284
+ return join17(stateDir(env), "node.pid");
5975
6285
  }
5976
6286
  function setDesired(env, desired) {
5977
6287
  writeState(env, { desired });
@@ -6001,7 +6311,7 @@ function writeState(env, patch) {
6001
6311
  try {
6002
6312
  mkdirSync11(dir, { recursive: true, mode: DIR_MODE2 });
6003
6313
  const next = { ...readState(file), ...patch };
6004
- writeFileSync10(file, `${JSON.stringify(next, null, 2)}
6314
+ writeFileSync11(file, `${JSON.stringify(next, null, 2)}
6005
6315
  `, { mode: FILE_MODE2 });
6006
6316
  chmodSync2(file, FILE_MODE2);
6007
6317
  } catch (e) {
@@ -6082,9 +6392,9 @@ ${envEntries}
6082
6392
  <key>ThrottleInterval</key>
6083
6393
  <integer>10</integer>
6084
6394
  <key>StandardOutPath</key>
6085
- <string>${xmlEscape(join16(inputs.logDir, "node.out.log"))}</string>
6395
+ <string>${xmlEscape(join18(inputs.logDir, "node.out.log"))}</string>
6086
6396
  <key>StandardErrorPath</key>
6087
- <string>${xmlEscape(join16(inputs.logDir, "node.err.log"))}</string>
6397
+ <string>${xmlEscape(join18(inputs.logDir, "node.err.log"))}</string>
6088
6398
  </dict>
6089
6399
  </plist>
6090
6400
  `;
@@ -6107,8 +6417,8 @@ Type=simple
6107
6417
  ExecStart=${exec}
6108
6418
  ${envLines}
6109
6419
  WorkingDirectory=${inputs.homeDir}
6110
- StandardOutput=append:${join16(inputs.logDir, "node.out.log")}
6111
- StandardError=append:${join16(inputs.logDir, "node.err.log")}
6420
+ StandardOutput=append:${join18(inputs.logDir, "node.out.log")}
6421
+ StandardError=append:${join18(inputs.logDir, "node.err.log")}
6112
6422
  Restart=on-failure
6113
6423
  RestartSec=3
6114
6424
  RestartPreventExitStatus=78
@@ -6119,7 +6429,7 @@ WantedBy=default.target
6119
6429
  }
6120
6430
  function buildPlan(inputs) {
6121
6431
  if (inputs.manager === "launchd") {
6122
- const filePath2 = join16(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6432
+ const filePath2 = join18(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6123
6433
  return {
6124
6434
  manager: "launchd",
6125
6435
  label: LAUNCHD_LABEL,
@@ -6136,7 +6446,7 @@ function buildPlan(inputs) {
6136
6446
  logHint: "dahrk logs -f"
6137
6447
  };
6138
6448
  }
6139
- const filePath = join16(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
6449
+ const filePath = join18(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
6140
6450
  const user = userInfo().username;
6141
6451
  return {
6142
6452
  manager: "systemd",
@@ -6166,7 +6476,7 @@ function unitIsCurrent(plan, onDisk) {
6166
6476
  return onDisk === plan.content;
6167
6477
  }
6168
6478
  function unitPath(manager, homeDir) {
6169
- return manager === "launchd" ? join16(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join16(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
6479
+ return manager === "launchd" ? join18(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join18(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
6170
6480
  }
6171
6481
  function statusCommand(manager) {
6172
6482
  return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
@@ -6468,7 +6778,7 @@ var defaultDeps3 = () => ({
6468
6778
  // no reason for it to be world-readable. `writeFileSync`'s `mode` applies only when it CREATES the file,
6469
6779
  // so chmod explicitly too - re-installing over a unit an older client left at 0644 must tighten it.
6470
6780
  writeFile: (path, content) => {
6471
- writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
6781
+ writeFileSync12(path, content, { mode: UNIT_FILE_MODE });
6472
6782
  chmodSync3(path, UNIT_FILE_MODE);
6473
6783
  },
6474
6784
  readFile: (path) => {
@@ -6478,7 +6788,7 @@ var defaultDeps3 = () => ({
6478
6788
  return void 0;
6479
6789
  }
6480
6790
  },
6481
- removeFile: (path) => rmSync7(path, { force: true }),
6791
+ removeFile: (path) => rmSync8(path, { force: true }),
6482
6792
  fileExists: (path) => existsSync13(path),
6483
6793
  // Captured, never inherited. A supervisor's stderr is ours to relay when it matters, not to leak
6484
6794
  // unconditionally: `runCommands` decides who needs to read it.
@@ -6919,7 +7229,7 @@ async function runStatus(inputs, deps) {
6919
7229
  }
6920
7230
 
6921
7231
  // src/main.ts
6922
- var CLIENT_VERSION = "0.1.20";
7232
+ var CLIENT_VERSION = "0.1.21";
6923
7233
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
6924
7234
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6925
7235
  var RUNTIMES2 = ["claude-code", "codex", "pi"];
@@ -6931,7 +7241,7 @@ function resolveNodeId(env, opts = {}) {
6931
7241
  if (existing) return existing;
6932
7242
  const legacy = legacyStateDir(env);
6933
7243
  if (legacy) {
6934
- const legacyId = readState(join17(legacy, "node.json")).nodeId;
7244
+ const legacyId = readState(join19(legacy, "node.json")).nodeId;
6935
7245
  if (legacyId) return legacyId;
6936
7246
  }
6937
7247
  const nodeId = randomUUID3();