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/chunk-FYS2JH42.js +31 -0
- package/dist/chunk-G3AU5H5X.js +1568 -0
- package/dist/client-MZ5GEQAD.js +8745 -0
- package/dist/main.js +992 -440
- package/dist/streamableHttp-S27HYW5J.js +1481 -0
- package/package.json +2 -3
package/dist/main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-FYS2JH42.js";
|
|
2
3
|
|
|
3
4
|
// src/main.ts
|
|
4
5
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5
6
|
import { existsSync as existsSync14, readFileSync as readFileSync11, realpathSync as realpathSync5 } from "fs";
|
|
6
7
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7
8
|
import { homedir as homedir7, platform as osPlatform4 } from "os";
|
|
8
|
-
import { basename as basename2, join as
|
|
9
|
+
import { basename as basename2, join as join19 } from "path";
|
|
9
10
|
import { pathToFileURL } from "url";
|
|
10
11
|
|
|
11
12
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -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 = `.
|
|
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 = ".
|
|
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/
|
|
832
|
-
import {
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
1369
|
+
settingsManager,
|
|
1370
|
+
resourceLoader,
|
|
1371
|
+
cwd,
|
|
1372
|
+
customTools: [stageComplete, askUserQuestion],
|
|
1391
1373
|
...model ? { model } : {}
|
|
1392
1374
|
});
|
|
1393
|
-
|
|
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
|
|
1417
|
-
import { basename, dirname, isAbsolute, join as
|
|
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 = ".
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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 :
|
|
1685
|
+
const excludePath = isAbsolute(rel) ? rel : join5(worktreePath, rel);
|
|
1474
1686
|
const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
|
|
1475
|
-
|
|
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
|
-
|
|
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 =
|
|
1504
|
-
const script =
|
|
1505
|
-
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
],
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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(
|
|
2023
|
+
git(worktreePath, ["push", remote, `HEAD:refs/heads/${branch}`], netEnv(authEnv));
|
|
1742
2024
|
pushed = true;
|
|
1743
2025
|
} finally {
|
|
1744
|
-
|
|
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
|
|
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(
|
|
2039
|
+
git(worktreePath, ["push", "--force", remote, `HEAD:refs/heads/${wipRef}`], netEnv(authEnv));
|
|
1759
2040
|
} finally {
|
|
1760
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1804
|
-
const bodyFile =
|
|
2083
|
+
const tmp = mkdtempSync2(join5(tmpdir3(), "dahrk-pr-"));
|
|
2084
|
+
const bodyFile = join5(tmp, "body.md");
|
|
1805
2085
|
try {
|
|
1806
|
-
|
|
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
|
-
|
|
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
|
|
1840
|
-
import { join as
|
|
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 = [
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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
|
|
1978
|
-
import { join as
|
|
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 =
|
|
1983
|
-
mkdirSync2(
|
|
1984
|
-
mkdirSync2(
|
|
1985
|
-
const tracePath =
|
|
1986
|
-
const metaPath =
|
|
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
|
-
|
|
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
|
-
|
|
1999
|
-
return
|
|
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 =
|
|
2030
|
-
|
|
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
|
-
|
|
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
|
|
2046
|
-
import { dirname as dirname2, join as
|
|
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 =
|
|
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
|
|
2062
|
-
import { dirname as dirname3, join as
|
|
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
|
|
2354
|
+
if (runtime !== "claude-code") {
|
|
2075
2355
|
result.warnings.push(
|
|
2076
|
-
|
|
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 =
|
|
2083
|
-
const dest =
|
|
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
|
-
|
|
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
|
|
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
|
|
2171
|
-
import { dirname as dirname4, join as
|
|
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
|
-
|
|
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
|
|
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
|
|
2339
|
-
import { join as
|
|
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))
|
|
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(
|
|
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
|
|
2588
|
-
import { tmpdir as
|
|
2589
|
-
import { isAbsolute as isAbsolute3, join as
|
|
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
|
|
2599
|
-
import { dirname as dirname5, isAbsolute as isAbsolute2, join as
|
|
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
|
|
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
|
-
...[
|
|
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
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
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
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3584
|
+
const dir = join13(ref.scratchPath, "docs");
|
|
3311
3585
|
mkdirSync7(dir, { recursive: true });
|
|
3312
3586
|
for (const doc of docs) {
|
|
3313
|
-
|
|
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
|
-
|
|
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 = ".
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
3516
|
-
const worktreePath =
|
|
3517
|
-
const scratchPath =
|
|
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(
|
|
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(
|
|
3562
|
-
const bytes = readFileSync5(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
5022
|
-
import { join as
|
|
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(
|
|
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 =
|
|
5582
|
+
const dir = join14(p, "..");
|
|
5199
5583
|
if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
|
|
5200
|
-
|
|
5584
|
+
writeFileSync8(p, content, { mode: 384 });
|
|
5201
5585
|
},
|
|
5202
5586
|
out
|
|
5203
5587
|
});
|
|
5204
5588
|
function defaultBundlePath(cwd, now) {
|
|
5205
|
-
return
|
|
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
|
|
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
|
-
|
|
5848
|
+
writeFileSync9(path, content);
|
|
5380
5849
|
},
|
|
5381
|
-
removeFile: (path) =>
|
|
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
|
|
5490
|
-
import { join as
|
|
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 =
|
|
5495
|
-
|
|
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
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
6262
|
+
return env.DAHRK_STATE_DIR ?? join17(homedir5(), ".dahrk");
|
|
5783
6263
|
}
|
|
5784
6264
|
function legacyStateDir(env) {
|
|
5785
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
6265
|
+
return env.DAHRK_STATE_DIR ? void 0 : join17(homedir5(), ".skakel");
|
|
5786
6266
|
}
|
|
5787
6267
|
function stateFile(env) {
|
|
5788
|
-
return
|
|
6268
|
+
return join17(stateDir(env), "node.json");
|
|
5789
6269
|
}
|
|
5790
6270
|
function logDir(env) {
|
|
5791
|
-
return
|
|
6271
|
+
return join17(stateDir(env), "logs");
|
|
5792
6272
|
}
|
|
5793
6273
|
function logFiles(env) {
|
|
5794
6274
|
const dir = logDir(env);
|
|
5795
|
-
return { out:
|
|
6275
|
+
return { out: join17(dir, "node.out.log"), err: join17(dir, "node.err.log") };
|
|
5796
6276
|
}
|
|
5797
6277
|
function jsonlLogFile(env) {
|
|
5798
|
-
return
|
|
6278
|
+
return join17(logDir(env), "node.jsonl");
|
|
5799
6279
|
}
|
|
5800
6280
|
function crashDir(env) {
|
|
5801
|
-
return
|
|
6281
|
+
return join17(logDir(env), "crashes");
|
|
5802
6282
|
}
|
|
5803
6283
|
function lockFile(env) {
|
|
5804
|
-
return
|
|
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
|
-
|
|
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(
|
|
6395
|
+
<string>${xmlEscape(join18(inputs.logDir, "node.out.log"))}</string>
|
|
5916
6396
|
<key>StandardErrorPath</key>
|
|
5917
|
-
<string>${xmlEscape(
|
|
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:${
|
|
5941
|
-
StandardError=append:${
|
|
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 =
|
|
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 =
|
|
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" ?
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
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(
|
|
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();
|