dahrk-node 0.1.19 → 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,18 +1713,66 @@ 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" };
1512
1766
  const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
1513
- const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1767
+ const resolveRemoteAuth = (gitUrl, credentialToken) => {
1768
+ const auth = credentialToken ? setupAuth(credentialToken) : void 0;
1769
+ return {
1770
+ remote: credentialToken ? withTokenUser(gitUrl) : gitUrl,
1771
+ authEnv: auth?.env,
1772
+ cleanup: () => auth?.cleanup()
1773
+ };
1774
+ };
1775
+ const mirrorPathFor = (repoId) => join5(mirrorsDir, sanitizeBranchName(repoId));
1514
1776
  const listWorktrees = (mirror) => {
1515
1777
  let out2;
1516
1778
  try {
@@ -1535,7 +1797,7 @@ function createGitService(opts = {}) {
1535
1797
  } catch (e) {
1536
1798
  log.warn(`git worktree remove failed for ${worktreePath}: ${e.message}`);
1537
1799
  }
1538
- rmSync(worktreePath, { recursive: true, force: true });
1800
+ rmSync2(worktreePath, { recursive: true, force: true });
1539
1801
  gitOk2(mirror, ["worktree", "prune"]);
1540
1802
  };
1541
1803
  const TRACKING_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
@@ -1622,18 +1884,19 @@ function createGitService(opts = {}) {
1622
1884
  baseBranch: spec.baseBranch,
1623
1885
  branch: sanitizeBranchName(spec.branch ?? `dahrk/${spec.runId}`),
1624
1886
  worktreePath,
1625
- scratchPath: join3(worktreePath, ".skakel", "scratch")
1887
+ scratchPath: join5(worktreePath, SCRATCH_DIR)
1626
1888
  });
1627
1889
  return {
1628
1890
  worktreesDir,
1629
1891
  async createWorktree(spec) {
1630
1892
  const { repoId, gitUrl, baseBranch, runId } = spec;
1631
1893
  const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
1632
- const worktreePath = join3(worktreesDir, runId);
1894
+ const worktreePath = join5(worktreesDir, runId);
1633
1895
  mkdirSync(worktreesDir, { recursive: true });
1634
1896
  if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
1635
1897
  log.info(`reusing existing worktree at ${worktreePath}`);
1636
- mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1898
+ mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
1899
+ excludeScratchLocally(worktreePath);
1637
1900
  return refFor(spec, worktreePath);
1638
1901
  }
1639
1902
  const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
@@ -1669,7 +1932,8 @@ function createGitService(opts = {}) {
1669
1932
  if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
1670
1933
  throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
1671
1934
  }
1672
- mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1935
+ mkdirSync(join5(worktreePath, SCRATCH_DIR), { recursive: true });
1936
+ excludeScratchLocally(worktreePath);
1673
1937
  return refFor(spec, worktreePath);
1674
1938
  },
1675
1939
  async commitAndPush(ref, opts2) {
@@ -1689,15 +1953,14 @@ function createGitService(opts = {}) {
1689
1953
  } catch {
1690
1954
  }
1691
1955
  }
1692
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1693
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
1956
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1694
1957
  let pushed = false;
1695
1958
  let integration;
1696
1959
  try {
1697
1960
  let fetched = false;
1698
1961
  if (opts2.base) {
1699
1962
  try {
1700
- git(worktreePath, ["fetch", remote, opts2.base], netEnv(auth?.env));
1963
+ git(worktreePath, ["fetch", remote, opts2.base], netEnv(authEnv));
1701
1964
  fetched = true;
1702
1965
  } catch (e) {
1703
1966
  log.warn(`base fetch failed for ${opts2.base}; skipping push-time integration: ${e.message}`);
@@ -1718,30 +1981,49 @@ function createGitService(opts = {}) {
1718
1981
  `user.name=${authorName}`,
1719
1982
  "-c",
1720
1983
  `user.email=${authorEmail}`,
1984
+ "-c",
1985
+ "rerere.enabled=true",
1986
+ "-c",
1987
+ "rerere.autoupdate=true",
1721
1988
  "merge",
1722
1989
  "--no-edit",
1723
1990
  "FETCH_HEAD"
1724
- ], auth?.env);
1991
+ ], authEnv);
1725
1992
  integration = "clean";
1726
1993
  headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
1727
1994
  } catch (mergeErr) {
1728
1995
  const inMerge = gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "MERGE_HEAD"]);
1729
1996
  if (inMerge) {
1730
- const conflictFiles = git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
1731
- git(worktreePath, ["merge", "--abort"]);
1732
- return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "conflict", conflictFiles };
1733
- }
1734
- const msg = mergeErr.message;
1735
- if (/unrelated histories|refusing to merge/i.test(msg)) {
1736
- 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;
1737
2020
  }
1738
- throw mergeErr;
1739
2021
  }
1740
2022
  }
1741
- git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(auth?.env));
2023
+ git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(authEnv));
1742
2024
  pushed = true;
1743
2025
  } finally {
1744
- auth?.cleanup();
2026
+ cleanup();
1745
2027
  }
1746
2028
  return { headSha, pushed, nothingToCommit: !dirty, commitsAhead, ...integration ? { integration } : {} };
1747
2029
  },
@@ -1752,12 +2034,11 @@ function createGitService(opts = {}) {
1752
2034
  }
1753
2035
  const wipRef = sanitizeBranchName(opts2.branch);
1754
2036
  const { headSha, dirty } = commitPending(worktreePath, opts2.message);
1755
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1756
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
2037
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1757
2038
  try {
1758
- git(worktreePath, ["push", "--force", remote, `HEAD:refs/heads/${wipRef}`], netEnv(auth?.env));
2039
+ git(worktreePath, ["push", "--force", remote, `HEAD:refs/heads/${wipRef}`], netEnv(authEnv));
1759
2040
  } finally {
1760
- auth?.cleanup();
2041
+ cleanup();
1761
2042
  }
1762
2043
  return { headSha, pushed: true, nothingToCommit: !dirty, wipRef };
1763
2044
  },
@@ -1772,15 +2053,14 @@ function createGitService(opts = {}) {
1772
2053
  if (!dirty) return { dirty: false, headSha, pushed: false };
1773
2054
  git(worktreePath, ["branch", "--force", wipRef, tailSha]);
1774
2055
  let pushed = false;
1775
- const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
1776
- const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
2056
+ const { remote, authEnv, cleanup } = resolveRemoteAuth(ref.gitUrl, opts2.credentialToken);
1777
2057
  try {
1778
- git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(auth?.env));
2058
+ git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(authEnv));
1779
2059
  pushed = true;
1780
2060
  } catch (e) {
1781
2061
  log.warn(`could not push the preserved tail to ${wipRef}: ${e.message}`);
1782
2062
  } finally {
1783
- auth?.cleanup();
2063
+ cleanup();
1784
2064
  }
1785
2065
  git(worktreePath, ["reset", "--hard", headSha]);
1786
2066
  git(worktreePath, ["clean", "-fd", "--exclude", SCRATCH_DIR]);
@@ -1800,10 +2080,10 @@ function createGitService(opts = {}) {
1800
2080
  return void 0;
1801
2081
  }
1802
2082
  };
1803
- const tmp = mkdtempSync(join3(tmpdir2(), "dahrk-pr-"));
1804
- const bodyFile = join3(tmp, "body.md");
2083
+ const tmp = mkdtempSync2(join5(tmpdir3(), "dahrk-pr-"));
2084
+ const bodyFile = join5(tmp, "body.md");
1805
2085
  try {
1806
- writeFileSync(bodyFile, opts2.body ?? "");
2086
+ writeFileSync2(bodyFile, opts2.body ?? "");
1807
2087
  try {
1808
2088
  gh(worktreePath, [
1809
2089
  "pr",
@@ -1825,7 +2105,7 @@ function createGitService(opts = {}) {
1825
2105
  }
1826
2106
  return readBack() ?? { prError: "gh pr create reported success but no PR was found" };
1827
2107
  } finally {
1828
- rmSync(tmp, { recursive: true, force: true });
2108
+ rmSync2(tmp, { recursive: true, force: true });
1829
2109
  }
1830
2110
  },
1831
2111
  async teardownWorktree(ref) {
@@ -1836,8 +2116,8 @@ function createGitService(opts = {}) {
1836
2116
 
1837
2117
  // ../../packages/executor-worktree/src/worktree-reaper.ts
1838
2118
  import { execFileSync as execFileSync2 } from "child_process";
1839
- import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
1840
- 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";
1841
2121
  var MINUTE = 6e4;
1842
2122
  var HOUR = 60 * MINUTE;
1843
2123
  var DEFAULTS = {
@@ -1867,7 +2147,7 @@ var canonical = (p) => {
1867
2147
  }
1868
2148
  };
1869
2149
  function lastUsedMs(worktreePath) {
1870
- const candidates = [join4(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
2150
+ const candidates = [join6(worktreePath, ".dahrk", "scratch", "state.json"), worktreePath];
1871
2151
  let newest = 0;
1872
2152
  for (const p of candidates) {
1873
2153
  try {
@@ -1890,7 +2170,7 @@ function createWorktreeReaper(opts) {
1890
2170
  const log = opts.logger ?? noop;
1891
2171
  const mirrors = () => {
1892
2172
  try {
1893
- 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"]));
1894
2174
  } catch {
1895
2175
  return [];
1896
2176
  }
@@ -1914,7 +2194,7 @@ function createWorktreeReaper(opts) {
1914
2194
  }
1915
2195
  const onDisk = (() => {
1916
2196
  try {
1917
- return readdirSync(opts.worktreesDir).map((d) => canonical(join4(opts.worktreesDir, d)));
2197
+ return readdirSync(opts.worktreesDir).map((d) => canonical(join6(opts.worktreesDir, d)));
1918
2198
  } catch {
1919
2199
  return [];
1920
2200
  }
@@ -1957,7 +2237,7 @@ function createWorktreeReaper(opts) {
1957
2237
  if (d.mirror) {
1958
2238
  gitOk(d.mirror, ["worktree", "remove", "--force", d.path]);
1959
2239
  }
1960
- rmSync2(d.path, { recursive: true, force: true });
2240
+ rmSync3(d.path, { recursive: true, force: true });
1961
2241
  if (d.mirror) gitOk(d.mirror, ["worktree", "prune"]);
1962
2242
  report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
1963
2243
  } catch (e) {
@@ -1974,20 +2254,20 @@ function createWorktreeReaper(opts) {
1974
2254
 
1975
2255
  // ../../packages/executor-worktree/src/trace-writer.ts
1976
2256
  import { createHash } from "crypto";
1977
- import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1978
- import { join as join5 } from "path";
2257
+ import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2258
+ import { join as join7 } from "path";
1979
2259
  var DEFAULT_SPILL_BYTES = 8192;
1980
2260
  function createTraceWriter(scratchPath, meta, opts = {}) {
1981
2261
  const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
1982
- const dir = join5(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1983
- mkdirSync2(join5(dir, "blobs"), { recursive: true });
1984
- mkdirSync2(join5(dir, "raw"), { recursive: true });
1985
- const tracePath = join5(dir, "trace.jsonl");
1986
- 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");
1987
2267
  let current = { ...meta };
1988
2268
  let nextSeq = 0;
1989
2269
  let rawCount = 0;
1990
- writeFileSync2(metaPath, JSON.stringify(current, null, 2));
2270
+ writeFileSync3(metaPath, JSON.stringify(current, null, 2));
1991
2271
  const tooBig = (value) => {
1992
2272
  const s = typeof value === "string" ? value : JSON.stringify(value ?? "");
1993
2273
  return s.length > spillBytes;
@@ -1995,8 +2275,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1995
2275
  const spillValue = (value) => {
1996
2276
  const data = typeof value === "string" ? value : JSON.stringify(value);
1997
2277
  const sha = createHash("sha256").update(data).digest("hex");
1998
- writeFileSync2(join5(dir, "blobs", sha), data);
1999
- return join5("blobs", sha);
2278
+ writeFileSync3(join7(dir, "blobs", sha), data);
2279
+ return join7("blobs", sha);
2000
2280
  };
2001
2281
  const spill = (event) => {
2002
2282
  if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
@@ -2026,13 +2306,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
2026
2306
  return written;
2027
2307
  },
2028
2308
  writeRaw(record) {
2029
- const rel = join5("raw", `${rawCount++}.json`);
2030
- 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));
2031
2311
  return rel;
2032
2312
  },
2033
2313
  finalise(patch = {}) {
2034
2314
  current = { ...current, ...patch };
2035
- writeFileSync2(metaPath, JSON.stringify(current, null, 2));
2315
+ writeFileSync3(metaPath, JSON.stringify(current, null, 2));
2036
2316
  },
2037
2317
  count() {
2038
2318
  return nextSeq;
@@ -2042,13 +2322,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
2042
2322
 
2043
2323
  // ../../packages/executor-worktree/src/pack-cache.ts
2044
2324
  import { createHash as createHash2 } from "crypto";
2045
- import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
2046
- 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";
2047
2327
  function readManifestFiles(dir) {
2048
2328
  const out2 = [];
2049
2329
  const walk = (cur) => {
2050
2330
  for (const entry of readdirSync2(cur, { withFileTypes: true })) {
2051
- const abs = join6(cur, entry.name);
2331
+ const abs = join8(cur, entry.name);
2052
2332
  if (entry.isDirectory()) walk(abs);
2053
2333
  else out2.push(relative(dir, abs).split(sep).join("/"));
2054
2334
  }
@@ -2058,8 +2338,8 @@ function readManifestFiles(dir) {
2058
2338
  }
2059
2339
 
2060
2340
  // ../../packages/executor-worktree/src/overlay.ts
2061
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
2062
- 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";
2063
2343
  function sameBytes(dest, bytes) {
2064
2344
  try {
2065
2345
  return readFileSync3(dest).equals(bytes);
@@ -2071,16 +2351,16 @@ async function overlayComponents(opts) {
2071
2351
  const { worktreePath, runtime, components, cache } = opts;
2072
2352
  const result = { written: [], skippedRepoLocal: [], warnings: [] };
2073
2353
  for (const ref of components) {
2074
- if (runtime === "codex") {
2354
+ if (runtime !== "claude-code") {
2075
2355
  result.warnings.push(
2076
- `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`
2077
2357
  );
2078
2358
  continue;
2079
2359
  }
2080
2360
  const { dir } = await cache.materialise(ref);
2081
2361
  for (const relPath of readManifestFiles(dir)) {
2082
- const src = join7(dir, relPath);
2083
- const dest = join7(worktreePath, relPath);
2362
+ const src = join9(dir, relPath);
2363
+ const dest = join9(worktreePath, relPath);
2084
2364
  const bytes = readFileSync3(src);
2085
2365
  if (existsSync4(dest)) {
2086
2366
  if (sameBytes(dest, bytes)) continue;
@@ -2088,27 +2368,21 @@ async function overlayComponents(opts) {
2088
2368
  continue;
2089
2369
  }
2090
2370
  mkdirSync4(dirname3(dest), { recursive: true });
2091
- writeFileSync4(dest, bytes);
2371
+ writeFileSync5(dest, bytes);
2092
2372
  result.written.push(relPath);
2093
2373
  }
2094
2374
  }
2095
2375
  return result;
2096
2376
  }
2097
2377
 
2098
- // ../../packages/executor-worktree/src/pi-container.ts
2099
- import { spawn as nodeSpawn } from "child_process";
2100
-
2101
- // ../../packages/executor-worktree/src/pi-rpc-client.ts
2102
- import { StringDecoder } from "string_decoder";
2103
-
2104
- // ../../packages/executor-worktree/src/pi-container.ts
2105
- var DEFAULT_IMAGE = process.env.DAHRK_PI_IMAGE ?? process.env.SKAKEL_PI_IMAGE ?? "dahrk/pi:latest";
2106
-
2107
2378
  // ../../packages/executor-worktree/src/index.ts
2108
2379
  function makeRunner(runtime) {
2109
2380
  if ((process.env.DAHRK_RUNNER ?? process.env.SKAKEL_RUNNER ?? "real") === "mock") return createMockRunner(runtime);
2110
- if (runtime === "pi") return createPiRunner();
2111
- 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";
2112
2386
  }
2113
2387
 
2114
2388
  // ../../packages/edge/src/health.ts
@@ -2167,8 +2441,8 @@ function collectHealth(inputs) {
2167
2441
  }
2168
2442
 
2169
2443
  // ../../packages/edge/src/job-ledger.ts
2170
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
2171
- 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";
2172
2446
  var FILE_MODE = 384;
2173
2447
  var DIR_MODE = 448;
2174
2448
  function nullJobLedger() {
@@ -2197,7 +2471,7 @@ function fileJobLedger(file, warn = console.warn) {
2197
2471
  const tmp = `${file}.${process.pid}.tmp`;
2198
2472
  try {
2199
2473
  mkdirSync5(dirname4(file), { recursive: true, mode: DIR_MODE });
2200
- writeFileSync5(tmp, `${JSON.stringify(entries, null, 2)}
2474
+ writeFileSync6(tmp, `${JSON.stringify(entries, null, 2)}
2201
2475
  `, { mode: FILE_MODE });
2202
2476
  chmodSync(tmp, FILE_MODE);
2203
2477
  renameSync(tmp, file);
@@ -2232,7 +2506,7 @@ function announceableJobs(entries) {
2232
2506
  return out2;
2233
2507
  }
2234
2508
  function jobLedgerFile(stateDir2) {
2235
- return join8(stateDir2, "jobs.json");
2509
+ return join10(stateDir2, "jobs.json");
2236
2510
  }
2237
2511
 
2238
2512
  // ../../packages/edge/src/log-shipper.ts
@@ -2335,8 +2609,8 @@ function ceilingFromEnv(env) {
2335
2609
  }
2336
2610
 
2337
2611
  // ../../packages/edge/src/logger.ts
2338
- import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
2339
- 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";
2340
2614
  import pino from "pino";
2341
2615
 
2342
2616
  // ../../packages/edge/src/redact.ts
@@ -2460,7 +2734,7 @@ var RotatingFile = class {
2460
2734
  closeSync(this.fd);
2461
2735
  this.fd = void 0;
2462
2736
  const oldest = `${this.path}.${this.maxFiles}`;
2463
- if (existsSync6(oldest)) rmSync4(oldest, { force: true });
2737
+ if (existsSync6(oldest)) rmSync5(oldest, { force: true });
2464
2738
  for (let i = this.maxFiles - 1; i >= 1; i--) {
2465
2739
  const from = `${this.path}.${i}`;
2466
2740
  if (existsSync6(from)) renameSync2(from, `${this.path}.${i + 1}`);
@@ -2526,7 +2800,7 @@ function createNodeLogger(opts = {}) {
2526
2800
  if (opts.dir && fileLevel !== "silent") {
2527
2801
  try {
2528
2802
  mkdirSync6(opts.dir, { recursive: true, mode: 448 });
2529
- 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);
2530
2804
  streams.push({ level: fileLevel, stream: file });
2531
2805
  } catch (e) {
2532
2806
  process.stderr.write(`dahrk: file logging disabled (${e.message})
@@ -2584,9 +2858,9 @@ function denyToolRule(tool3) {
2584
2858
  // ../../packages/edge/src/stage-runner.ts
2585
2859
  import { execFileSync as execFileSync5 } from "child_process";
2586
2860
  import { createHash as createHash3 } from "crypto";
2587
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
2588
- import { tmpdir as tmpdir4 } from "os";
2589
- 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";
2590
2864
  import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
2591
2865
 
2592
2866
  // ../../packages/edge/src/builtins.ts
@@ -2595,8 +2869,8 @@ import { execFileSync as execFileSync4 } from "child_process";
2595
2869
  // ../../packages/edge/src/fs-roots.ts
2596
2870
  import { execFileSync as execFileSync3 } from "child_process";
2597
2871
  import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
2598
- import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2599
- 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";
2600
2874
  function isUnder(root, target) {
2601
2875
  const rel = relative2(resolve(root), resolve(target));
2602
2876
  return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
@@ -2612,7 +2886,7 @@ function realish(p) {
2612
2886
  while (head !== dirname5(head)) {
2613
2887
  if (existsSync7(head)) {
2614
2888
  try {
2615
- return join10(realpathSync2.native(head), ...tail.reverse());
2889
+ return join12(realpathSync2.native(head), ...tail.reverse());
2616
2890
  } catch {
2617
2891
  return p;
2618
2892
  }
@@ -2662,7 +2936,7 @@ function computeFsRoots(opts) {
2662
2936
  // Every git command in the worktree reads and writes here.
2663
2937
  ...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
2664
2938
  // Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
2665
- ...[tmpdir3(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2939
+ ...[tmpdir4(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2666
2940
  // The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
2667
2941
  // it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
2668
2942
  // here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
@@ -2681,26 +2955,26 @@ function computeFsRoots(opts) {
2681
2955
  "/System",
2682
2956
  "/proc",
2683
2957
  "/sys",
2684
- join10(home, ".gitconfig"),
2685
- join10(home, ".config"),
2686
- join10(home, ".npmrc"),
2687
- join10(home, ".cache"),
2688
- join10(home, "Library", "Caches"),
2689
- join10(home, "Library", "pnpm"),
2690
- join10(home, ".local", "share"),
2691
- join10(home, ".nvm"),
2692
- join10(home, ".volta"),
2693
- join10(home, ".asdf"),
2694
- join10(home, ".cargo"),
2695
- 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"),
2696
2970
  ...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
2697
2971
  ].map(realish);
2698
2972
  const deny = [
2699
- join10(home, ".ssh"),
2700
- join10(home, ".aws"),
2701
- join10(home, ".gnupg"),
2702
- join10(home, ".config", "gcloud"),
2703
- 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"),
2704
2978
  "/Volumes",
2705
2979
  "/etc/shadow",
2706
2980
  "/etc/sudoers"
@@ -3285,7 +3559,7 @@ var attemptOf = (jobId) => {
3285
3559
  };
3286
3560
  var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
3287
3561
  function writeScratchState(ref, job, attempt, status) {
3288
- const statePath = join11(ref.scratchPath, "state.json");
3562
+ const statePath = join13(ref.scratchPath, "state.json");
3289
3563
  let state;
3290
3564
  try {
3291
3565
  state = JSON.parse(readFileSync5(statePath, "utf8"));
@@ -3294,23 +3568,23 @@ function writeScratchState(ref, job, attempt, status) {
3294
3568
  }
3295
3569
  state.stages[job.stageId] = { currentAttempt: attempt, status };
3296
3570
  mkdirSync7(ref.scratchPath, { recursive: true });
3297
- writeFileSync6(statePath, JSON.stringify(state, null, 2));
3571
+ writeFileSync7(statePath, JSON.stringify(state, null, 2));
3298
3572
  }
3299
3573
  function writeIssueContext(ref, issueContext) {
3300
3574
  if (issueContext === void 0) return;
3301
3575
  try {
3302
3576
  mkdirSync7(ref.scratchPath, { recursive: true });
3303
- writeFileSync6(join11(ref.scratchPath, "issue.md"), issueContext);
3577
+ writeFileSync7(join13(ref.scratchPath, "issue.md"), issueContext);
3304
3578
  } catch {
3305
3579
  }
3306
3580
  }
3307
3581
  function writeAttachedDocuments(ref, docs) {
3308
3582
  if (!docs || docs.length === 0) return;
3309
3583
  try {
3310
- const dir = join11(ref.scratchPath, "docs");
3584
+ const dir = join13(ref.scratchPath, "docs");
3311
3585
  mkdirSync7(dir, { recursive: true });
3312
3586
  for (const doc of docs) {
3313
- writeFileSync6(join11(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3587
+ writeFileSync7(join13(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3314
3588
  }
3315
3589
  } catch {
3316
3590
  }
@@ -3329,12 +3603,12 @@ function writeGuidance(ref, guidance) {
3329
3603
  if (!guidance || guidance.length === 0) return;
3330
3604
  try {
3331
3605
  mkdirSync7(ref.scratchPath, { recursive: true });
3332
- writeFileSync6(join11(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3606
+ writeFileSync7(join13(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3333
3607
  } catch {
3334
3608
  }
3335
3609
  }
3336
3610
  var ARTIFACT_CAP_BYTES = 64 * 1024;
3337
- var SCRATCH_OUTPUT_DIR = ".skakel/scratch/output";
3611
+ var SCRATCH_OUTPUT_DIR = ".dahrk/scratch/output";
3338
3612
  function capContent(raw) {
3339
3613
  return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
3340
3614
  }
@@ -3362,14 +3636,14 @@ function readEmittedArtifact(ref, relPath) {
3362
3636
  }
3363
3637
  }
3364
3638
  function scanScratchOutput(ref, preferRel) {
3639
+ const preferBase = preferRel?.split("/").pop();
3365
3640
  try {
3366
- const names = readdirSync3(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3641
+ const names = readdirSync3(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3367
3642
  (n) => n.toLowerCase().endsWith(".md")
3368
3643
  );
3369
3644
  if (names.length === 0) return void 0;
3370
- const preferBase = preferRel?.split("/").pop();
3371
3645
  const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
3372
- const raw = readFileSync5(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3646
+ const raw = readFileSync5(join13(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3373
3647
  if (raw.trim().length === 0) return void 0;
3374
3648
  return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
3375
3649
  } catch {
@@ -3389,7 +3663,7 @@ function scanChangedMarkdown(ref) {
3389
3663
  );
3390
3664
  for (const rel of rels) {
3391
3665
  try {
3392
- const raw = readFileSync5(join11(ref.worktreePath, rel), "utf8");
3666
+ const raw = readFileSync5(join13(ref.worktreePath, rel), "utf8");
3393
3667
  if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
3394
3668
  } catch {
3395
3669
  }
@@ -3410,6 +3684,9 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
3410
3684
  if (changed) return { artifact: changed, source: "changed-file" };
3411
3685
  return void 0;
3412
3686
  }
3687
+ function runtimeUsesMcpGateway(runtime) {
3688
+ return runtime === "claude-code" || runtime === "pi";
3689
+ }
3413
3690
  function createStageRunner(deps) {
3414
3691
  const worktrees = /* @__PURE__ */ new Map();
3415
3692
  const log = deps.logger ?? createNodeLogger({ level: "silent" });
@@ -3432,7 +3709,7 @@ function createStageRunner(deps) {
3432
3709
  if (ref) {
3433
3710
  if (scratchOnly.has(runId)) {
3434
3711
  try {
3435
- rmSync5(ref.worktreePath, { recursive: true, force: true });
3712
+ rmSync6(ref.worktreePath, { recursive: true, force: true });
3436
3713
  } catch (e) {
3437
3714
  log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove scratch dir");
3438
3715
  }
@@ -3512,9 +3789,9 @@ function createStageRunner(deps) {
3512
3789
  ...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
3513
3790
  });
3514
3791
  } else {
3515
- const base = deps.scratchRoot ?? join11(tmpdir4(), "dahrk", "scratch");
3516
- const worktreePath = join11(base, runId);
3517
- 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");
3518
3795
  mkdirSync7(scratchPath, { recursive: true });
3519
3796
  ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
3520
3797
  scratchOnly.add(runId);
@@ -3530,7 +3807,7 @@ function createStageRunner(deps) {
3530
3807
  const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
3531
3808
  if (artifactDir && slash > 0) {
3532
3809
  try {
3533
- 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 });
3534
3811
  } catch {
3535
3812
  }
3536
3813
  }
@@ -3558,8 +3835,8 @@ function createStageRunner(deps) {
3558
3835
  if (!sink) return;
3559
3836
  const base = { tenantId: job.tenantId, runId, stageId, attempt };
3560
3837
  try {
3561
- for (const name of readdirSync3(join11(writer.dir, "blobs"))) {
3562
- 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));
3563
3840
  const { url } = await sink.requestBlobUrl({
3564
3841
  ...base,
3565
3842
  sha256: name,
@@ -3573,7 +3850,7 @@ function createStageRunner(deps) {
3573
3850
  }
3574
3851
  let archiveKey;
3575
3852
  try {
3576
- const bytes = readFileSync5(join11(writer.dir, "trace.jsonl"));
3853
+ const bytes = readFileSync5(join13(writer.dir, "trace.jsonl"));
3577
3854
  const sha = createHash3("sha256").update(bytes).digest("hex");
3578
3855
  const { key, url } = await sink.requestBlobUrl({
3579
3856
  ...base,
@@ -3670,6 +3947,7 @@ function createStageRunner(deps) {
3670
3947
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
3671
3948
  }
3672
3949
  let denied = false;
3950
+ const surfacedDenyReasons = /* @__PURE__ */ new Set();
3673
3951
  let escapedUnblocked = false;
3674
3952
  const authorisedActions = [];
3675
3953
  const runtime = agentConfig.runtime;
@@ -3688,7 +3966,11 @@ function createStageRunner(deps) {
3688
3966
  streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
3689
3967
  }
3690
3968
  streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
3691
- 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
+ }
3692
3974
  };
3693
3975
  const authorizeToolUse = (tool3, input) => {
3694
3976
  const verdict2 = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
@@ -3699,7 +3981,10 @@ function createStageRunner(deps) {
3699
3981
  }
3700
3982
  return verdict2;
3701
3983
  };
3984
+ let bumpStall = () => {
3985
+ };
3702
3986
  const onTrace = (event) => {
3987
+ bumpStall();
3703
3988
  if (event.type === "action") {
3704
3989
  const key = actionKey(event.tool, event.input);
3705
3990
  const authorised = authorisedActions.indexOf(key);
@@ -3722,7 +4007,7 @@ function createStageRunner(deps) {
3722
4007
  if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
3723
4008
  };
3724
4009
  const mcpServers = agentConfig.mcpServers;
3725
- if (mcpServers && mcpServers.length > 0 && runtime === "claude-code") {
4010
+ if (mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
3726
4011
  gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
3727
4012
  }
3728
4013
  const runner = deps.makeRunner(runtime);
@@ -3764,6 +4049,20 @@ function createStageRunner(deps) {
3764
4049
  timedOut = true;
3765
4050
  void runner.cancel();
3766
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
+ }
3767
4066
  try {
3768
4067
  if (interactive) {
3769
4068
  const mailbox = new ManagedMailbox();
@@ -3774,8 +4073,9 @@ function createStageRunner(deps) {
3774
4073
  }
3775
4074
  } finally {
3776
4075
  if (killTimer) clearTimeout(killTimer);
4076
+ if (stallTimer) clearTimeout(stallTimer);
3777
4077
  }
3778
- let status = timedOut ? "timeout" : result.status;
4078
+ let status = timedOut || stalled ? "timeout" : result.status;
3779
4079
  if (status === "ok" && escapedUnblocked) {
3780
4080
  status = "fail";
3781
4081
  const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
@@ -3794,6 +4094,9 @@ function createStageRunner(deps) {
3794
4094
  }
3795
4095
  }
3796
4096
  let summary = result.summary ?? `${stageId}: ${status}`;
4097
+ if (stalled && !timedOut) {
4098
+ summary = `${stageId}: stalled (no output for ${Math.round(stallMs / 1e3)}s)`;
4099
+ }
3797
4100
  if (!interactive && status === "ok") {
3798
4101
  try {
3799
4102
  summary = await runner.summarise(ctx);
@@ -4035,6 +4338,17 @@ async function startEdgeNode(opts) {
4035
4338
  if (oldest !== void 0) lastResults.delete(oldest);
4036
4339
  }
4037
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
+ };
4038
4352
  const pendingBlob = /* @__PURE__ */ new Map();
4039
4353
  let blobReqCounter = 0;
4040
4354
  const trace = {
@@ -4215,6 +4529,7 @@ async function startEdgeNode(opts) {
4215
4529
  if (msg.type === "cancel") {
4216
4530
  log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
4217
4531
  stageRunner.cancel(msg.jobId);
4532
+ ackCancel(msg.jobId);
4218
4533
  return;
4219
4534
  }
4220
4535
  if (msg.type === "turn") {
@@ -4346,6 +4661,7 @@ async function startEdgeNode(opts) {
4346
4661
  log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
4347
4662
  sendHello();
4348
4663
  for (const frame of lastResults.values()) send(frame);
4664
+ for (const frame of ackedCancels.values()) send(frame);
4349
4665
  startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
4350
4666
  });
4351
4667
  sock.on("message", (raw) => void onMessage(raw.toString()));
@@ -4449,7 +4765,6 @@ async function startEdgeNode(opts) {
4449
4765
  import { execFile } from "child_process";
4450
4766
  var PROBES = [
4451
4767
  { runtime: "claude-code", cmd: "claude" },
4452
- { runtime: "codex", cmd: "codex" },
4453
4768
  { runtime: "pi", cmd: "pi" }
4454
4769
  ];
4455
4770
  var DEFAULT_TIMEOUT_MS = 5e3;
@@ -4596,6 +4911,7 @@ var COMMANDS = /* @__PURE__ */ new Set([
4596
4911
  "logs",
4597
4912
  "diagnose",
4598
4913
  "run",
4914
+ "repo",
4599
4915
  "service",
4600
4916
  "doctor",
4601
4917
  "status",
@@ -4619,6 +4935,7 @@ function parseCli(argv) {
4619
4935
  flagArgs = rest;
4620
4936
  }
4621
4937
  if (command === "run") return parseRun(flagArgs);
4938
+ if (command === "repo") return parseRepo(flagArgs);
4622
4939
  if (command === "service") return parseService(flagArgs);
4623
4940
  if (command === "update") return parseUpdate(flagArgs);
4624
4941
  if (command === "logs") return parseLogs(flagArgs);
@@ -4633,6 +4950,8 @@ function parseCli(argv) {
4633
4950
  "hub-url": { type: "string" },
4634
4951
  ephemeral: { type: "boolean", default: false },
4635
4952
  foreground: { type: "boolean", default: false },
4953
+ // `start`: enrol but do not install the service (the operator supervises the node themselves).
4954
+ "no-service": { type: "boolean", default: false },
4636
4955
  // `stop` / `restart`: take the node down even though it has work in flight.
4637
4956
  force: { type: "boolean", default: false },
4638
4957
  // `status`: machine-readable output.
@@ -4654,7 +4973,8 @@ function parseCli(argv) {
4654
4973
  ephemeral,
4655
4974
  // An ephemeral node mints a throwaway id and persists nothing, so there is nothing coherent to hand to
4656
4975
  // a supervisor that restarts it on boot. It is a foreground node by definition.
4657
- foreground: (values.foreground ?? false) || ephemeral
4976
+ foreground: (values.foreground ?? false) || ephemeral,
4977
+ ...values["no-service"] ? { noService: true } : {}
4658
4978
  };
4659
4979
  if (command === "stop") return { kind: "stop", force };
4660
4980
  if (command === "restart") return { kind: "restart", flags, force };
@@ -4786,6 +5106,43 @@ function parseService(flagArgs) {
4786
5106
  };
4787
5107
  return { kind: "service", flags };
4788
5108
  }
5109
+ var REPO_ACTIONS = /* @__PURE__ */ new Set(["add"]);
5110
+ var isRepoAction = (s) => REPO_ACTIONS.has(s);
5111
+ function parseRepo(flagArgs) {
5112
+ let values;
5113
+ let positionals;
5114
+ try {
5115
+ ({ values, positionals } = parseArgs({
5116
+ args: flagArgs,
5117
+ options: {
5118
+ name: { type: "string" },
5119
+ "hub-url": { type: "string" },
5120
+ token: { type: "string" },
5121
+ help: { type: "boolean", default: false }
5122
+ },
5123
+ allowPositionals: true
5124
+ }));
5125
+ } catch (e) {
5126
+ return { kind: "error", message: e.message };
5127
+ }
5128
+ if (values.help) return { kind: "help", command: "repo" };
5129
+ if (positionals.length === 0) {
5130
+ return { kind: "error", message: "repo: missing action (`dahrk repo add`)" };
5131
+ }
5132
+ if (positionals.length > 1) {
5133
+ return { kind: "error", message: `repo: unexpected argument "${positionals[1]}" (one action at a time)` };
5134
+ }
5135
+ const action = positionals[0];
5136
+ if (!isRepoAction(action)) {
5137
+ return { kind: "error", message: `repo: unknown action "${action}" (expected add)` };
5138
+ }
5139
+ const flags = {
5140
+ ...values.name ? { name: values.name } : {},
5141
+ ...values["hub-url"] ? { hubUrl: values["hub-url"] } : {},
5142
+ ...values.token ? { token: values.token } : {}
5143
+ };
5144
+ return { kind: "repo", flags };
5145
+ }
4789
5146
  function parseUpdate(flagArgs) {
4790
5147
  let values;
4791
5148
  try {
@@ -4830,6 +5187,8 @@ function usage(bin, command) {
4830
5187
  " setting DAHRK_FOREGROUND=1, which is easier to set in a Dockerfile or pm2 config.",
4831
5188
  " --ephemeral Do not persist (or read) node id and token; mint a throwaway id (CI / one-shot).",
4832
5189
  " Implies --foreground: a node with no persistent identity has nothing to daemonise.",
5190
+ " --no-service Enrol (cache the token) but do not install the always-on service, for a node you",
5191
+ " supervise yourself. Run it later with `dahrk start --foreground` (or your own supervisor).",
4833
5192
  "",
4834
5193
  "Only one node may run at a time on a host - they would share this machine's node id and race each",
4835
5194
  "other for Jobs - so a second `start` refuses rather than dialling the hub twice."
@@ -4930,6 +5289,30 @@ function usage(bin, command) {
4930
5289
  " --token <token> Enrolment token to verify against the hub (or set DAHRK_ENROL_TOKEN)."
4931
5290
  ].join("\n");
4932
5291
  }
5292
+ if (command === "repo") {
5293
+ return [
5294
+ `Usage: ${bin} repo add [options]`,
5295
+ "",
5296
+ "Register the current git repository with the hub, so it can run workflows. Run it from inside the",
5297
+ "repo - `cd your-repo && dahrk repo add` - and the node reads the `origin` remote and the current",
5298
+ "branch itself: no form, no pasted git URL, because the node already sits next to the code.",
5299
+ "",
5300
+ "The git URL is registered in the form the host can authenticate. An HTTPS origin is kept as-is; an",
5301
+ "SSH origin is kept when this host has an SSH key, and otherwise normalised to HTTPS with a warning.",
5302
+ "Re-running on an already-registered repo is a no-op, not an error or a duplicate.",
5303
+ "",
5304
+ "This uses the node's existing enrolment - run `dahrk start --token <token>` first if it is not yet",
5305
+ "enrolled. It dials the hub itself, so the daemon need not be running.",
5306
+ "",
5307
+ "Actions:",
5308
+ " add Register the repository in the current directory.",
5309
+ "",
5310
+ "Options:",
5311
+ " --name <name> Display name for the repo (default: the slug from the origin URL).",
5312
+ " --hub-url <url> Hub URL to register with (or set DAHRK_HUB_URL).",
5313
+ " --token <token> Enrolment token to authenticate with (default: the cached one)."
5314
+ ].join("\n");
5315
+ }
4933
5316
  if (command === "service") {
4934
5317
  return [
4935
5318
  `Usage: ${bin} service install|uninstall [options]`,
@@ -5006,6 +5389,7 @@ function usage(bin, command) {
5006
5389
  " diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
5007
5390
  " status Is it up, is it enrolled, what is it working on? (local, dials nothing)",
5008
5391
  " run Run a workflow locally (engine-backed), e.g. `run preflight`.",
5392
+ " repo Register the current repository with the hub (`repo add`).",
5009
5393
  " doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
5010
5394
  " service Install/uninstall the always-on service by hand (`start` does this for you).",
5011
5395
  " update Update the client to the latest release (or print how for your channel).",
@@ -5018,8 +5402,8 @@ function usage(bin, command) {
5018
5402
  }
5019
5403
 
5020
5404
  // src/diagnose.ts
5021
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
5022
- 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";
5023
5407
 
5024
5408
  // src/ui.ts
5025
5409
  import { styleText } from "util";
@@ -5134,7 +5518,7 @@ async function buildBundle(deps) {
5134
5518
  if (deps.exists(deps.crashDir)) {
5135
5519
  for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
5136
5520
  try {
5137
- crashes.push(JSON.parse(deps.readFile(join12(deps.crashDir, name))));
5521
+ crashes.push(JSON.parse(deps.readFile(join14(deps.crashDir, name))));
5138
5522
  } catch (e) {
5139
5523
  warnings.push(`could not read crash record ${name} (${e.message})`);
5140
5524
  }
@@ -5195,14 +5579,14 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
5195
5579
  listDir: (p) => readdirSync4(p),
5196
5580
  exists: (p) => existsSync8(p),
5197
5581
  writeFile: (p, content) => {
5198
- const dir = join12(p, "..");
5582
+ const dir = join14(p, "..");
5199
5583
  if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
5200
- writeFileSync7(p, content, { mode: 384 });
5584
+ writeFileSync8(p, content, { mode: 384 });
5201
5585
  },
5202
5586
  out
5203
5587
  });
5204
5588
  function defaultBundlePath(cwd, now) {
5205
- return join12(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
5589
+ return join14(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
5206
5590
  }
5207
5591
 
5208
5592
  // src/doctor.ts
@@ -5335,8 +5719,93 @@ async function runDoctor(inputs, deps = {}) {
5335
5719
  return checks.some((c) => c.status === "fail") ? 1 : 0;
5336
5720
  }
5337
5721
 
5722
+ // src/repo-add.ts
5723
+ import { createHash as createHash4 } from "crypto";
5724
+ var stripRepoSuffix = (s) => s.replace(/\.git$/i, "").replace(/\/+$/, "");
5725
+ function splitOwnerRepo(path) {
5726
+ const parts = stripRepoSuffix(path).split("/").filter(Boolean);
5727
+ if (parts.length < 2) return void 0;
5728
+ const repo = parts[parts.length - 1];
5729
+ const owner = parts.slice(0, -1).join("/");
5730
+ return { owner, repo };
5731
+ }
5732
+ function parseGitRemote(url) {
5733
+ const trimmed = url.trim();
5734
+ if (!trimmed) return void 0;
5735
+ const urlMatch = /^(?:ssh|https?):\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+)$/i.exec(trimmed);
5736
+ if (urlMatch) {
5737
+ const parts = splitOwnerRepo(urlMatch[2]);
5738
+ return parts ? { host: urlMatch[1], ...parts } : void 0;
5739
+ }
5740
+ const scpMatch = /^(?:[^@\s]+@)?([^/:\s]+):(.+)$/.exec(trimmed);
5741
+ if (scpMatch) {
5742
+ const parts = splitOwnerRepo(scpMatch[2]);
5743
+ return parts ? { host: scpMatch[1], ...parts } : void 0;
5744
+ }
5745
+ return void 0;
5746
+ }
5747
+ var httpsFormOf = (r) => `https://${r.host}/${r.owner}/${r.repo}.git`;
5748
+ var isHttpsRemote = (url) => /^https?:\/\//i.test(url.trim());
5749
+ function chooseGitUrl(input) {
5750
+ const originUrl = input.originUrl.trim();
5751
+ if (isHttpsRemote(originUrl)) return { gitUrl: originUrl, converted: false };
5752
+ if (input.sshKeyPresent) return { gitUrl: originUrl, converted: false };
5753
+ const parsed = parseGitRemote(originUrl);
5754
+ if (!parsed) return { gitUrl: originUrl, converted: false };
5755
+ return { gitUrl: httpsFormOf(parsed), converted: true };
5756
+ }
5757
+ var deriveRepoName = (remote) => remote.repo;
5758
+ function deriveRepoId(gitUrl) {
5759
+ const parsed = parseGitRemote(gitUrl);
5760
+ const canonical2 = parsed ? `${parsed.host}/${parsed.owner}/${parsed.repo}`.toLowerCase() : gitUrl.trim().toLowerCase();
5761
+ const hash = createHash4("sha256").update(canonical2).digest("hex").slice(0, 12);
5762
+ const slug = (parsed ? parsed.repo : "repo").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
5763
+ return `${slug}-${hash}`;
5764
+ }
5765
+ function hubHttpBase(url) {
5766
+ const trimmed = url.trim().replace(/\/+$/, "");
5767
+ if (/^wss:\/\//i.test(trimmed)) return trimmed.replace(/^wss:\/\//i, "https://");
5768
+ if (/^ws:\/\//i.test(trimmed)) return trimmed.replace(/^ws:\/\//i, "http://");
5769
+ return trimmed;
5770
+ }
5771
+ async function readJson(res) {
5772
+ try {
5773
+ const body = await res.json();
5774
+ return body && typeof body === "object" ? body : void 0;
5775
+ } catch {
5776
+ return void 0;
5777
+ }
5778
+ }
5779
+ function driftOf(stored, repo) {
5780
+ if (!stored) return void 0;
5781
+ const drift = {};
5782
+ if (typeof stored.defaultBranch === "string" && stored.defaultBranch !== repo.defaultBranch) drift.branch = stored.defaultBranch;
5783
+ if (typeof stored.name === "string" && stored.name !== repo.name) drift.name = stored.name;
5784
+ return drift.branch || drift.name ? drift : void 0;
5785
+ }
5786
+ async function registerRepo(deps, args) {
5787
+ const url = `${args.base}/config/api/repositories`;
5788
+ let res;
5789
+ try {
5790
+ res = await deps.fetch(url, {
5791
+ method: "POST",
5792
+ headers: { "content-type": "application/json", authorization: `Bearer ${args.token}` },
5793
+ body: JSON.stringify(args.repo)
5794
+ });
5795
+ } catch (e) {
5796
+ return { kind: "error", message: `could not reach the hub at ${args.base}: ${e.message}` };
5797
+ }
5798
+ const stored = await readJson(res);
5799
+ if (res.status === 200 || res.status === 409) {
5800
+ const drift = driftOf(stored, args.repo);
5801
+ return { kind: "already", ...drift ? { drift } : {} };
5802
+ }
5803
+ if (res.ok) return { kind: "registered" };
5804
+ return { kind: "error", message: `the hub rejected the repo (HTTP ${res.status})` };
5805
+ }
5806
+
5338
5807
  // src/lock.ts
5339
- 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";
5340
5809
  import { dirname as dirname6 } from "path";
5341
5810
  function parseLock(content) {
5342
5811
  if (!content) return void 0;
@@ -5376,9 +5845,9 @@ var defaultLockDeps = (file) => ({
5376
5845
  },
5377
5846
  writeFile: (path, content) => {
5378
5847
  if (!existsSync9(dirname6(path))) mkdirSync9(dirname6(path), { recursive: true, mode: 448 });
5379
- writeFileSync8(path, content);
5848
+ writeFileSync9(path, content);
5380
5849
  },
5381
- removeFile: (path) => rmSync6(path, { force: true }),
5850
+ removeFile: (path) => rmSync7(path, { force: true }),
5382
5851
  isAlive
5383
5852
  });
5384
5853
 
@@ -5486,13 +5955,13 @@ var defaultLogsDeps = (files, jsonlFile) => ({
5486
5955
  });
5487
5956
 
5488
5957
  // src/process-safety.ts
5489
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
5490
- import { join as join13 } from "path";
5958
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
5959
+ import { join as join15 } from "path";
5491
5960
  function writeCrashRecord(dir, record) {
5492
5961
  try {
5493
5962
  mkdirSync10(dir, { recursive: true, mode: 448 });
5494
- const file = join13(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5495
- 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)}
5496
5965
  `, { mode: 384 });
5497
5966
  return file;
5498
5967
  } catch {
@@ -5549,7 +6018,7 @@ import { execFileSync as execFileSync6 } from "child_process";
5549
6018
  import { randomUUID as randomUUID2 } from "crypto";
5550
6019
  import { accessSync, constants as fsConstants, existsSync as existsSync11, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
5551
6020
  import { homedir as homedir4 } from "os";
5552
- import { join as join14 } from "path";
6021
+ import { join as join16 } from "path";
5553
6022
  var REPORT_BASE_URL = "https://app.dahrk.ai/r";
5554
6023
  var LOW_DISK_BYTES = 512 * 1024 * 1024;
5555
6024
  var PREFLIGHT_STAGES = [
@@ -5688,7 +6157,7 @@ function commandPresent(cmd) {
5688
6157
  }
5689
6158
  function sshKeyPresent() {
5690
6159
  try {
5691
- const dir = join14(homedir4(), ".ssh");
6160
+ const dir = join16(homedir4(), ".ssh");
5692
6161
  if (existsSync11(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
5693
6162
  } catch {
5694
6163
  }
@@ -5708,12 +6177,12 @@ function writable(dir) {
5708
6177
  }
5709
6178
  }
5710
6179
  function worktreeRoot(env) {
5711
- 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");
5712
6181
  }
5713
6182
  function nearestExisting(dir) {
5714
6183
  let cur = dir;
5715
6184
  while (!existsSync11(cur)) {
5716
- const parent = join14(cur, "..");
6185
+ const parent = join16(cur, "..");
5717
6186
  if (parent === cur) break;
5718
6187
  cur = parent;
5719
6188
  }
@@ -5744,7 +6213,18 @@ function probeRepo(repoPath) {
5744
6213
  baseBranch = git(["rev-parse", "--abbrev-ref", "HEAD"]) || void 0;
5745
6214
  } catch {
5746
6215
  }
5747
- return { path: repoPath, isGitRepo: true, headResolves, ...baseBranch ? { baseBranch } : {} };
6216
+ let remoteUrl;
6217
+ try {
6218
+ remoteUrl = git(["remote", "get-url", "origin"]) || void 0;
6219
+ } catch {
6220
+ }
6221
+ return {
6222
+ path: repoPath,
6223
+ isGitRepo: true,
6224
+ headResolves,
6225
+ ...baseBranch ? { baseBranch } : {},
6226
+ ...remoteUrl ? { remoteUrl } : {}
6227
+ };
5748
6228
  }
5749
6229
  function gatherHostFacts(repoPath) {
5750
6230
  const root = worktreeRoot(process.env);
@@ -5764,14 +6244,14 @@ function gatherHostFacts(repoPath) {
5764
6244
 
5765
6245
  // src/service.ts
5766
6246
  import { execFileSync as execFileSync7 } from "child_process";
5767
- 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";
5768
6248
  import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
5769
- import { join as join16 } from "path";
6249
+ import { join as join18 } from "path";
5770
6250
 
5771
6251
  // src/state.ts
5772
- 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";
5773
6253
  import { homedir as homedir5 } from "os";
5774
- import { join as join15 } from "path";
6254
+ import { join as join17 } from "path";
5775
6255
  var RUNTIMES = ["claude-code", "codex", "pi"];
5776
6256
  var isRuntime = (v) => RUNTIMES.includes(v);
5777
6257
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
@@ -5779,29 +6259,29 @@ var isDesired = (v) => v === "running" || v === "stopped";
5779
6259
  var FILE_MODE2 = 384;
5780
6260
  var DIR_MODE2 = 448;
5781
6261
  function stateDir(env) {
5782
- return env.DAHRK_STATE_DIR ?? join15(homedir5(), ".dahrk");
6262
+ return env.DAHRK_STATE_DIR ?? join17(homedir5(), ".dahrk");
5783
6263
  }
5784
6264
  function legacyStateDir(env) {
5785
- return env.DAHRK_STATE_DIR ? void 0 : join15(homedir5(), ".skakel");
6265
+ return env.DAHRK_STATE_DIR ? void 0 : join17(homedir5(), ".skakel");
5786
6266
  }
5787
6267
  function stateFile(env) {
5788
- return join15(stateDir(env), "node.json");
6268
+ return join17(stateDir(env), "node.json");
5789
6269
  }
5790
6270
  function logDir(env) {
5791
- return join15(stateDir(env), "logs");
6271
+ return join17(stateDir(env), "logs");
5792
6272
  }
5793
6273
  function logFiles(env) {
5794
6274
  const dir = logDir(env);
5795
- 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") };
5796
6276
  }
5797
6277
  function jsonlLogFile(env) {
5798
- return join15(logDir(env), "node.jsonl");
6278
+ return join17(logDir(env), "node.jsonl");
5799
6279
  }
5800
6280
  function crashDir(env) {
5801
- return join15(logDir(env), "crashes");
6281
+ return join17(logDir(env), "crashes");
5802
6282
  }
5803
6283
  function lockFile(env) {
5804
- return join15(stateDir(env), "node.pid");
6284
+ return join17(stateDir(env), "node.pid");
5805
6285
  }
5806
6286
  function setDesired(env, desired) {
5807
6287
  writeState(env, { desired });
@@ -5831,7 +6311,7 @@ function writeState(env, patch) {
5831
6311
  try {
5832
6312
  mkdirSync11(dir, { recursive: true, mode: DIR_MODE2 });
5833
6313
  const next = { ...readState(file), ...patch };
5834
- writeFileSync10(file, `${JSON.stringify(next, null, 2)}
6314
+ writeFileSync11(file, `${JSON.stringify(next, null, 2)}
5835
6315
  `, { mode: FILE_MODE2 });
5836
6316
  chmodSync2(file, FILE_MODE2);
5837
6317
  } catch (e) {
@@ -5912,9 +6392,9 @@ ${envEntries}
5912
6392
  <key>ThrottleInterval</key>
5913
6393
  <integer>10</integer>
5914
6394
  <key>StandardOutPath</key>
5915
- <string>${xmlEscape(join16(inputs.logDir, "node.out.log"))}</string>
6395
+ <string>${xmlEscape(join18(inputs.logDir, "node.out.log"))}</string>
5916
6396
  <key>StandardErrorPath</key>
5917
- <string>${xmlEscape(join16(inputs.logDir, "node.err.log"))}</string>
6397
+ <string>${xmlEscape(join18(inputs.logDir, "node.err.log"))}</string>
5918
6398
  </dict>
5919
6399
  </plist>
5920
6400
  `;
@@ -5937,8 +6417,8 @@ Type=simple
5937
6417
  ExecStart=${exec}
5938
6418
  ${envLines}
5939
6419
  WorkingDirectory=${inputs.homeDir}
5940
- StandardOutput=append:${join16(inputs.logDir, "node.out.log")}
5941
- 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")}
5942
6422
  Restart=on-failure
5943
6423
  RestartSec=3
5944
6424
  RestartPreventExitStatus=78
@@ -5949,7 +6429,7 @@ WantedBy=default.target
5949
6429
  }
5950
6430
  function buildPlan(inputs) {
5951
6431
  if (inputs.manager === "launchd") {
5952
- const filePath2 = join16(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
6432
+ const filePath2 = join18(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5953
6433
  return {
5954
6434
  manager: "launchd",
5955
6435
  label: LAUNCHD_LABEL,
@@ -5966,7 +6446,7 @@ function buildPlan(inputs) {
5966
6446
  logHint: "dahrk logs -f"
5967
6447
  };
5968
6448
  }
5969
- const filePath = join16(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
6449
+ const filePath = join18(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5970
6450
  const user = userInfo().username;
5971
6451
  return {
5972
6452
  manager: "systemd",
@@ -5996,7 +6476,7 @@ function unitIsCurrent(plan, onDisk) {
5996
6476
  return onDisk === plan.content;
5997
6477
  }
5998
6478
  function unitPath(manager, homeDir) {
5999
- 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);
6000
6480
  }
6001
6481
  function statusCommand(manager) {
6002
6482
  return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
@@ -6298,7 +6778,7 @@ var defaultDeps3 = () => ({
6298
6778
  // no reason for it to be world-readable. `writeFileSync`'s `mode` applies only when it CREATES the file,
6299
6779
  // so chmod explicitly too - re-installing over a unit an older client left at 0644 must tighten it.
6300
6780
  writeFile: (path, content) => {
6301
- writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
6781
+ writeFileSync12(path, content, { mode: UNIT_FILE_MODE });
6302
6782
  chmodSync3(path, UNIT_FILE_MODE);
6303
6783
  },
6304
6784
  readFile: (path) => {
@@ -6308,7 +6788,7 @@ var defaultDeps3 = () => ({
6308
6788
  return void 0;
6309
6789
  }
6310
6790
  },
6311
- removeFile: (path) => rmSync7(path, { force: true }),
6791
+ removeFile: (path) => rmSync8(path, { force: true }),
6312
6792
  fileExists: (path) => existsSync13(path),
6313
6793
  // Captured, never inherited. A supervisor's stderr is ours to relay when it matters, not to leak
6314
6794
  // unconditionally: `runCommands` decides who needs to read it.
@@ -6749,7 +7229,7 @@ async function runStatus(inputs, deps) {
6749
7229
  }
6750
7230
 
6751
7231
  // src/main.ts
6752
- var CLIENT_VERSION = "0.1.19";
7232
+ var CLIENT_VERSION = "0.1.21";
6753
7233
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
6754
7234
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6755
7235
  var RUNTIMES2 = ["claude-code", "codex", "pi"];
@@ -6761,7 +7241,7 @@ function resolveNodeId(env, opts = {}) {
6761
7241
  if (existing) return existing;
6762
7242
  const legacy = legacyStateDir(env);
6763
7243
  if (legacy) {
6764
- const legacyId = readState(join17(legacy, "node.json")).nodeId;
7244
+ const legacyId = readState(join19(legacy, "node.json")).nodeId;
6765
7245
  if (legacyId) return legacyId;
6766
7246
  }
6767
7247
  const nodeId = randomUUID3();
@@ -6836,6 +7316,12 @@ async function start(flags) {
6836
7316
  if (wantsForeground(env, flags)) return startForeground(env, flags);
6837
7317
  const enrolled = await enrolToDisk(env);
6838
7318
  if (enrolled !== 0) return enrolled;
7319
+ if (flags.noService) {
7320
+ out("");
7321
+ out(verdict("ok", "Enrolled. Not installing a service (--no-service)."));
7322
+ out(hint("Run the node with `dahrk start --foreground` (or your own supervisor), or `dahrk start` to install the service."));
7323
+ return 0;
7324
+ }
6839
7325
  await offerUpdate(env);
6840
7326
  const outcome = await runNodeStart(serviceInputs(env));
6841
7327
  if (outcome.kind === "error") return outcome.code;
@@ -7081,6 +7567,69 @@ async function runWorkflow(flags) {
7081
7567
  clientVersion: CLIENT_VERSION
7082
7568
  });
7083
7569
  }
7570
+ async function runRepoAdd(flags) {
7571
+ const env = applyEnvAliases(process.env);
7572
+ if (flags.hubUrl) env.DAHRK_HUB_URL = flags.hubUrl;
7573
+ if (flags.token) env.DAHRK_ENROL_TOKEN = flags.token;
7574
+ const cwd = process.cwd();
7575
+ const probe2 = probeRepo(cwd);
7576
+ if (!probe2.isGitRepo) {
7577
+ out("");
7578
+ out(verdict("fail", `${cwd} is not a git repository.`));
7579
+ out(hint("Run `dahrk repo add` from inside the repository you want to register."));
7580
+ return 1;
7581
+ }
7582
+ if (!probe2.remoteUrl) {
7583
+ out("");
7584
+ out(verdict("fail", "This repository has no `origin` remote, so there is nothing to register."));
7585
+ out(hint("Add one with `git remote add origin <url>`, then run `dahrk repo add` again."));
7586
+ return 1;
7587
+ }
7588
+ const token = resolveEnrolToken(env);
7589
+ if (!token) {
7590
+ out("");
7591
+ out(verdict("fail", "This node is not enrolled, so it cannot register a repository."));
7592
+ out(hint("Enrol it first with `dahrk start --token <token>`."));
7593
+ return 1;
7594
+ }
7595
+ const { gitUrl, converted } = chooseGitUrl({ originUrl: probe2.remoteUrl, sshKeyPresent: sshKeyPresent() });
7596
+ if (converted) {
7597
+ out("");
7598
+ out(verdict("warn", `No SSH key found on this host, so the SSH remote was registered as HTTPS: ${gitUrl}`));
7599
+ out(hint("Add an SSH key (or set up brokered credentials) if this repo must be cloned over SSH - see DHK-252."));
7600
+ }
7601
+ const parsed = parseGitRemote(gitUrl);
7602
+ const name = flags.name ?? (parsed ? deriveRepoName(parsed) : "repo");
7603
+ const repo = {
7604
+ id: deriveRepoId(gitUrl),
7605
+ name,
7606
+ gitUrl,
7607
+ // HEAD's branch is the run's base. An empty repo (no commits) has none yet; default to `main`.
7608
+ defaultBranch: probe2.baseBranch ?? "main"
7609
+ };
7610
+ const base = hubHttpBase(env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL);
7611
+ const result = await registerRepo({ fetch }, { base, token, repo });
7612
+ out("");
7613
+ switch (result.kind) {
7614
+ case "registered":
7615
+ out(verdict("ok", `Registered ${name} with the hub.`));
7616
+ out(kv("URL", gitUrl));
7617
+ out(kv("Branch", repo.defaultBranch));
7618
+ return 0;
7619
+ case "already":
7620
+ out(verdict("ok", `${name} is already registered - nothing to do.`));
7621
+ if (result.drift) {
7622
+ const bits = [];
7623
+ if (result.drift.branch) bits.push(`branch ${result.drift.branch} (this repo is on ${repo.defaultBranch})`);
7624
+ if (result.drift.name) bits.push(`name ${result.drift.name}`);
7625
+ out(hint(`The hub's stored record differs - ${bits.join(", ")} - and was left unchanged.`));
7626
+ }
7627
+ return 0;
7628
+ case "error":
7629
+ out(verdict("fail", `Could not register the repository: ${result.message}`));
7630
+ return 1;
7631
+ }
7632
+ }
7084
7633
  async function main() {
7085
7634
  const invoked = basename2(process.argv[1] ?? "");
7086
7635
  const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
@@ -7127,6 +7676,9 @@ async function main() {
7127
7676
  case "run":
7128
7677
  process.exitCode = await runWorkflow(parsed.flags);
7129
7678
  break;
7679
+ case "repo":
7680
+ process.exitCode = await runRepoAdd(parsed.flags);
7681
+ break;
7130
7682
  case "service": {
7131
7683
  if (parsed.flags.action === "uninstall") {
7132
7684
  process.exitCode = await runServiceUninstall();