dahrk-node 0.1.9 → 0.1.11

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.
Files changed (2) hide show
  1. package/dist/main.js +861 -172
  2. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { execFileSync as execFileSync8 } from "child_process";
5
- import { existsSync as existsSync12, realpathSync as realpathSync4 } from "fs";
4
+ import { execFileSync as execFileSync9 } from "child_process";
5
+ import { existsSync as existsSync13, realpathSync as realpathSync5 } from "fs";
6
6
  import { randomUUID as randomUUID3 } from "crypto";
7
- import { homedir as homedir5, platform as osPlatform4 } from "os";
8
- import { basename as basename2, join as join14 } from "path";
7
+ import { homedir as homedir7, platform as osPlatform4 } from "os";
8
+ import { basename as basename2, join as join16 } from "path";
9
9
  import { pathToFileURL } from "url";
10
10
 
11
11
  // ../../packages/edge/src/ws-client.ts
@@ -67,6 +67,8 @@ function createMockRunner(runtime) {
67
67
  }
68
68
 
69
69
  // ../../packages/executor-worktree/src/claude-adapter.ts
70
+ import { homedir, tmpdir } from "os";
71
+ import { join as join2 } from "path";
70
72
  import {
71
73
  query
72
74
  } from "@anthropic-ai/claude-agent-sdk";
@@ -336,7 +338,7 @@ var ManagedMailbox = class {
336
338
  const v = this.q.shift();
337
339
  if (v !== void 0) return Promise.resolve({ value: v, done: false });
338
340
  if (this.done) return Promise.resolve({ value: void 0, done: true });
339
- return new Promise((resolve2) => this.waiters.push(resolve2));
341
+ return new Promise((resolve3) => this.waiters.push(resolve3));
340
342
  }
341
343
  };
342
344
  }
@@ -347,7 +349,7 @@ function interactiveIdleWindows(ctx) {
347
349
  return { firstReplyMs: Math.max(firstReplyMs, idleMs), idleMs };
348
350
  }
349
351
  function raceNextTurn(pending, idleMs, signal) {
350
- return new Promise((resolve2) => {
352
+ return new Promise((resolve3) => {
351
353
  let settled = false;
352
354
  let timer;
353
355
  function finish(r) {
@@ -355,7 +357,7 @@ function raceNextTurn(pending, idleMs, signal) {
355
357
  settled = true;
356
358
  if (timer) clearTimeout(timer);
357
359
  signal.removeEventListener("abort", onAbort);
358
- resolve2(r);
360
+ resolve3(r);
359
361
  }
360
362
  function onAbort() {
361
363
  finish({ kind: "cancelled" });
@@ -391,7 +393,7 @@ function createElicitTurnRouter(turns, opts) {
391
393
  const ask = (firstReply, onRaise) => {
392
394
  if (ref.settle) return Promise.resolve({ kind: "busy" });
393
395
  onRaise();
394
- return new Promise((resolve2) => {
396
+ return new Promise((resolve3) => {
395
397
  let settled = false;
396
398
  const finish = (o) => {
397
399
  if (settled) return;
@@ -399,7 +401,7 @@ function createElicitTurnRouter(turns, opts) {
399
401
  clearTimeout(timer);
400
402
  opts.signal.removeEventListener("abort", onAbort);
401
403
  ref.settle = null;
402
- resolve2(o);
404
+ resolve3(o);
403
405
  };
404
406
  const onAbort = () => finish({ kind: "cancel" });
405
407
  const timer = setTimeout(() => finish({ kind: "noreply" }), firstReply ? opts.firstReplyMs : opts.idleMs);
@@ -518,6 +520,22 @@ function buildBrokeredMcpServers(ctx) {
518
520
  for (const s of servers) entries[s.id] = { type: s.type, url: `${ctx.mcpProxyBaseUrl}/${s.id}` };
519
521
  return entries;
520
522
  }
523
+ function sandboxOptions(ctx) {
524
+ if (process.env.DAHRK_SANDBOX !== "1") return {};
525
+ const home = homedir();
526
+ return {
527
+ sandbox: {
528
+ enabled: true,
529
+ failIfUnavailable: false,
530
+ autoAllowBashIfSandboxed: false,
531
+ allowUnsandboxedCommands: false,
532
+ filesystem: {
533
+ allowWrite: [ctx.workspace.worktreePath, ctx.workspace.scratchPath, tmpdir()],
534
+ denyRead: [join2(home, ".ssh"), join2(home, ".aws"), join2(home, ".gnupg"), "/Volumes"]
535
+ }
536
+ }
537
+ };
538
+ }
521
539
  function createClaudeRunner() {
522
540
  const abortController = new AbortController();
523
541
  let cancelled = false;
@@ -533,7 +551,25 @@ function createClaudeRunner() {
533
551
  // is a Claude Code SETTINGS key (not a top-level Options field), so it rides the inline `settings`
534
552
  // object, which sits at the flag layer and overrides any project/local setting. Mirrors the cyrus
535
553
  // reference (EdgeWorker.ts writes the same key into .claude/settings.local.json).
536
- settings: { includeCoAuthoredBy: false },
554
+ settings: {
555
+ includeCoAuthoredBy: false,
556
+ // DHK-392, defence in depth. The stage runner's `fs_confine` builtin is the real block (it is
557
+ // what `canUseTool` below consults, and unlike these rules it covers Grep and Bash). These deny
558
+ // rules close the same door from inside Claude Code, for the tools its permission system does
559
+ // cover (Read/Glob/NotebookRead via `Read(...)`, Write/Edit via `Edit(...)`): credentials an
560
+ // agent has no business reading, and the mounted volumes whose scan started all this. Deny
561
+ // outranks allow, so a repo's own .claude/settings.json cannot undo them.
562
+ permissions: {
563
+ deny: [
564
+ "Read(//Volumes/**)",
565
+ "Read(~/.ssh/**)",
566
+ "Read(~/.aws/**)",
567
+ "Read(~/.gnupg/**)",
568
+ "Read(~/Library/Keychains/**)"
569
+ ]
570
+ }
571
+ },
572
+ ...sandboxOptions(ctx),
537
573
  // Inherit the REPO's .mcp.json / .claude settings (build spec section 9): do NOT set
538
574
  // strictMcpConfig. Policy enforcement around tools is M6; M4 allows tools to run and the
539
575
  // stage runner intercepts denied actions at the trace level.
@@ -1332,8 +1368,8 @@ var PROVIDER_BY_ENV = {
1332
1368
  // ../../packages/executor-worktree/src/git-service.ts
1333
1369
  import { execFileSync } from "child_process";
1334
1370
  import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1335
- import { homedir, tmpdir } from "os";
1336
- import { basename, dirname, isAbsolute, join as join2 } from "path";
1371
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1372
+ import { basename, dirname, isAbsolute, join as join3 } from "path";
1337
1373
  var noopLogger = { info: () => {
1338
1374
  }, warn: () => {
1339
1375
  } };
@@ -1347,10 +1383,10 @@ function sanitizeBranchName(name) {
1347
1383
  return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
1348
1384
  }
1349
1385
  function resolveWorktreesDir(override) {
1350
- return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
1386
+ return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join3(homedir2(), ".dahrk", "worktrees");
1351
1387
  }
1352
1388
  function resolveMirrorsDir(override) {
1353
- return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
1389
+ return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join3(homedir2(), ".dahrk", "mirrors");
1354
1390
  }
1355
1391
  function createGitService(opts = {}) {
1356
1392
  const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
@@ -1389,12 +1425,12 @@ function createGitService(opts = {}) {
1389
1425
  const entry = `${SCRATCH_DIR}/`;
1390
1426
  try {
1391
1427
  const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
1392
- const excludePath = isAbsolute(rel) ? rel : join2(worktreePath, rel);
1428
+ const excludePath = isAbsolute(rel) ? rel : join3(worktreePath, rel);
1393
1429
  const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
1394
1430
  if (existing.split("\n").some((l) => l.trim() === entry)) return;
1395
1431
  mkdirSync(dirname(excludePath), { recursive: true });
1396
- const sep3 = existing && !existing.endsWith("\n") ? "\n" : "";
1397
- writeFileSync(excludePath, `${existing}${sep3}${entry}
1432
+ const sep4 = existing && !existing.endsWith("\n") ? "\n" : "";
1433
+ writeFileSync(excludePath, `${existing}${sep4}${entry}
1398
1434
  `);
1399
1435
  } catch (e) {
1400
1436
  log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
@@ -1419,8 +1455,8 @@ function createGitService(opts = {}) {
1419
1455
  return { headSha: git(worktreePath, ["rev-parse", "HEAD"]).trim(), dirty };
1420
1456
  };
1421
1457
  const setupAuth = (token) => {
1422
- const dir = mkdtempSync(join2(tmpdir(), "dahrk-cred-"));
1423
- const script = join2(dir, "askpass.sh");
1458
+ const dir = mkdtempSync(join3(tmpdir2(), "dahrk-cred-"));
1459
+ const script = join3(dir, "askpass.sh");
1424
1460
  writeFileSync(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
1425
1461
  return {
1426
1462
  env: { ...process.env, GIT_ASKPASS: script, DAHRK_GIT_TOKEN: token, GIT_TERMINAL_PROMPT: "0" },
@@ -1429,7 +1465,7 @@ function createGitService(opts = {}) {
1429
1465
  };
1430
1466
  const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
1431
1467
  const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
1432
- const mirrorPathFor = (repoId) => join2(mirrorsDir, sanitizeBranchName(repoId));
1468
+ const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1433
1469
  const listWorktrees = (mirror) => {
1434
1470
  let out;
1435
1471
  try {
@@ -1540,18 +1576,18 @@ function createGitService(opts = {}) {
1540
1576
  repo: spec.repo ?? spec.repoId,
1541
1577
  baseBranch: spec.baseBranch,
1542
1578
  worktreePath,
1543
- scratchPath: join2(worktreePath, ".skakel", "scratch")
1579
+ scratchPath: join3(worktreePath, ".skakel", "scratch")
1544
1580
  });
1545
1581
  return {
1546
1582
  worktreesDir,
1547
1583
  async createWorktree(spec) {
1548
1584
  const { repoId, gitUrl, baseBranch, runId } = spec;
1549
1585
  const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
1550
- const worktreePath = join2(worktreesDir, runId);
1586
+ const worktreePath = join3(worktreesDir, runId);
1551
1587
  mkdirSync(worktreesDir, { recursive: true });
1552
1588
  if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
1553
1589
  log.info(`reusing existing worktree at ${worktreePath}`);
1554
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1590
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1555
1591
  return refFor(spec, worktreePath);
1556
1592
  }
1557
1593
  const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
@@ -1587,7 +1623,7 @@ function createGitService(opts = {}) {
1587
1623
  if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
1588
1624
  throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
1589
1625
  }
1590
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1626
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1591
1627
  return refFor(spec, worktreePath);
1592
1628
  },
1593
1629
  async commitAndPush(ref, opts2) {
@@ -1693,8 +1729,8 @@ function createGitService(opts = {}) {
1693
1729
  return void 0;
1694
1730
  }
1695
1731
  };
1696
- const tmp = mkdtempSync(join2(tmpdir(), "dahrk-pr-"));
1697
- const bodyFile = join2(tmp, "body.md");
1732
+ const tmp = mkdtempSync(join3(tmpdir2(), "dahrk-pr-"));
1733
+ const bodyFile = join3(tmp, "body.md");
1698
1734
  try {
1699
1735
  writeFileSync(bodyFile, opts2.body ?? "");
1700
1736
  try {
@@ -1730,7 +1766,7 @@ function createGitService(opts = {}) {
1730
1766
  // ../../packages/executor-worktree/src/worktree-reaper.ts
1731
1767
  import { execFileSync as execFileSync2 } from "child_process";
1732
1768
  import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
1733
- import { join as join3 } from "path";
1769
+ import { join as join4 } from "path";
1734
1770
  var MINUTE = 6e4;
1735
1771
  var HOUR = 60 * MINUTE;
1736
1772
  var DEFAULTS = {
@@ -1760,7 +1796,7 @@ var canonical = (p) => {
1760
1796
  }
1761
1797
  };
1762
1798
  function lastUsedMs(worktreePath) {
1763
- const candidates = [join3(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1799
+ const candidates = [join4(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1764
1800
  let newest = 0;
1765
1801
  for (const p of candidates) {
1766
1802
  try {
@@ -1783,7 +1819,7 @@ function createWorktreeReaper(opts) {
1783
1819
  const log = opts.logger ?? noop;
1784
1820
  const mirrors = () => {
1785
1821
  try {
1786
- return readdirSync(opts.mirrorsDir).map((d) => join3(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1822
+ return readdirSync(opts.mirrorsDir).map((d) => join4(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1787
1823
  } catch {
1788
1824
  return [];
1789
1825
  }
@@ -1807,7 +1843,7 @@ function createWorktreeReaper(opts) {
1807
1843
  }
1808
1844
  const onDisk = (() => {
1809
1845
  try {
1810
- return readdirSync(opts.worktreesDir).map((d) => canonical(join3(opts.worktreesDir, d)));
1846
+ return readdirSync(opts.worktreesDir).map((d) => canonical(join4(opts.worktreesDir, d)));
1811
1847
  } catch {
1812
1848
  return [];
1813
1849
  }
@@ -1868,15 +1904,15 @@ function createWorktreeReaper(opts) {
1868
1904
  // ../../packages/executor-worktree/src/trace-writer.ts
1869
1905
  import { createHash } from "crypto";
1870
1906
  import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1871
- import { join as join4 } from "path";
1907
+ import { join as join5 } from "path";
1872
1908
  var DEFAULT_SPILL_BYTES = 8192;
1873
1909
  function createTraceWriter(scratchPath, meta, opts = {}) {
1874
1910
  const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
1875
- const dir = join4(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1876
- mkdirSync2(join4(dir, "blobs"), { recursive: true });
1877
- mkdirSync2(join4(dir, "raw"), { recursive: true });
1878
- const tracePath = join4(dir, "trace.jsonl");
1879
- const metaPath = join4(dir, "meta.json");
1911
+ const dir = join5(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1912
+ mkdirSync2(join5(dir, "blobs"), { recursive: true });
1913
+ mkdirSync2(join5(dir, "raw"), { recursive: true });
1914
+ const tracePath = join5(dir, "trace.jsonl");
1915
+ const metaPath = join5(dir, "meta.json");
1880
1916
  let current = { ...meta };
1881
1917
  let nextSeq = 0;
1882
1918
  let rawCount = 0;
@@ -1888,8 +1924,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1888
1924
  const spillValue = (value) => {
1889
1925
  const data = typeof value === "string" ? value : JSON.stringify(value);
1890
1926
  const sha = createHash("sha256").update(data).digest("hex");
1891
- writeFileSync2(join4(dir, "blobs", sha), data);
1892
- return join4("blobs", sha);
1927
+ writeFileSync2(join5(dir, "blobs", sha), data);
1928
+ return join5("blobs", sha);
1893
1929
  };
1894
1930
  const spill = (event) => {
1895
1931
  if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
@@ -1919,8 +1955,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1919
1955
  return written;
1920
1956
  },
1921
1957
  writeRaw(record) {
1922
- const rel = join4("raw", `${rawCount++}.json`);
1923
- writeFileSync2(join4(dir, rel), JSON.stringify(record, null, 2));
1958
+ const rel = join5("raw", `${rawCount++}.json`);
1959
+ writeFileSync2(join5(dir, rel), JSON.stringify(record, null, 2));
1924
1960
  return rel;
1925
1961
  },
1926
1962
  finalise(patch = {}) {
@@ -1936,12 +1972,12 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1936
1972
  // ../../packages/executor-worktree/src/pack-cache.ts
1937
1973
  import { createHash as createHash2 } from "crypto";
1938
1974
  import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
1939
- import { dirname as dirname2, join as join5, relative, sep } from "path";
1975
+ import { dirname as dirname2, join as join6, relative, sep } from "path";
1940
1976
  function readManifestFiles(dir) {
1941
1977
  const out = [];
1942
1978
  const walk = (cur) => {
1943
1979
  for (const entry of readdirSync2(cur, { withFileTypes: true })) {
1944
- const abs = join5(cur, entry.name);
1980
+ const abs = join6(cur, entry.name);
1945
1981
  if (entry.isDirectory()) walk(abs);
1946
1982
  else out.push(relative(dir, abs).split(sep).join("/"));
1947
1983
  }
@@ -1952,7 +1988,7 @@ function readManifestFiles(dir) {
1952
1988
 
1953
1989
  // ../../packages/executor-worktree/src/overlay.ts
1954
1990
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1955
- import { dirname as dirname3, join as join6 } from "path";
1991
+ import { dirname as dirname3, join as join7 } from "path";
1956
1992
  function sameBytes(dest, bytes) {
1957
1993
  try {
1958
1994
  return readFileSync3(dest).equals(bytes);
@@ -1972,8 +2008,8 @@ async function overlayComponents(opts) {
1972
2008
  }
1973
2009
  const { dir } = await cache.materialise(ref);
1974
2010
  for (const relPath of readManifestFiles(dir)) {
1975
- const src = join6(dir, relPath);
1976
- const dest = join6(worktreePath, relPath);
2011
+ const src = join7(dir, relPath);
2012
+ const dest = join7(worktreePath, relPath);
1977
2013
  const bytes = readFileSync3(src);
1978
2014
  if (existsSync4(dest)) {
1979
2015
  if (sameBytes(dest, bytes)) continue;
@@ -2004,9 +2040,163 @@ function makeRunner(runtime) {
2004
2040
  return runtime === "codex" ? createCodexRunner() : createClaudeRunner();
2005
2041
  }
2006
2042
 
2043
+ // ../../packages/edge/src/health.ts
2044
+ import { statfsSync } from "fs";
2045
+ var HealthCounters = class {
2046
+ startedAt = Date.now();
2047
+ errors = /* @__PURE__ */ new Map();
2048
+ crashes = 0;
2049
+ /** Connections made this process lifetime. The flapping signal: this climbing while uptime ALSO climbs
2050
+ * means the socket keeps dropping under a process that is otherwise fine. */
2051
+ connectCount = 0;
2052
+ /** Jobs in flight. A node stuck at 1 for an hour is wedged, and nothing else reveals it. */
2053
+ activeJobs = 0;
2054
+ /** Worktrees on disk. Climbing without bound was the shape of the leak DHK-371 fixed. */
2055
+ worktreeCount = 0;
2056
+ /** Bucket a stage failure. The CLASS only - the message would carry their paths. */
2057
+ recordError(kind) {
2058
+ this.errors.set(kind, (this.errors.get(kind) ?? 0) + 1);
2059
+ }
2060
+ recordCrash() {
2061
+ this.crashes++;
2062
+ }
2063
+ uptimeSec() {
2064
+ return Math.round((Date.now() - this.startedAt) / 1e3);
2065
+ }
2066
+ errorCounts() {
2067
+ return Object.fromEntries(this.errors);
2068
+ }
2069
+ crashCount() {
2070
+ return this.crashes;
2071
+ }
2072
+ };
2073
+ function diskFreeBytes(path) {
2074
+ try {
2075
+ const st = statfsSync(path);
2076
+ return Number(st.bfree) * Number(st.bsize);
2077
+ } catch {
2078
+ return void 0;
2079
+ }
2080
+ }
2081
+ function collectHealth(inputs) {
2082
+ const { counters } = inputs;
2083
+ const free = diskFreeBytes(inputs.worktreesDir);
2084
+ const errors = counters.errorCounts();
2085
+ return {
2086
+ uptimeSec: counters.uptimeSec(),
2087
+ clientVersion: inputs.clientVersion,
2088
+ activeJobs: counters.activeJobs,
2089
+ connectCount: counters.connectCount,
2090
+ worktreeCount: counters.worktreeCount,
2091
+ ...free !== void 0 ? { diskFreeBytes: free } : {},
2092
+ runtimes: inputs.runtimes,
2093
+ ...Object.keys(errors).length > 0 ? { errors } : {},
2094
+ ...counters.crashCount() > 0 ? { crashes: counters.crashCount() } : {}
2095
+ };
2096
+ }
2097
+
2098
+ // ../../packages/edge/src/log-shipper.ts
2099
+ import { shouldShip } from "@dahrk/contracts";
2100
+ var SHIP_BUFFER_MAX = 500;
2101
+ var SHIP_BATCH_MAX = 200;
2102
+ var SHIP_FLUSH_MS = 2e3;
2103
+ var LogShipper = class {
2104
+ constructor(opts = {}) {
2105
+ this.opts = opts;
2106
+ this.ceiling = opts.ceiling ?? { health: true, logs: "debug" };
2107
+ this.policy = this.clampToCeiling(opts.initial ?? { health: true, logs: "off" });
2108
+ }
2109
+ opts;
2110
+ buffer = [];
2111
+ policy;
2112
+ ceiling;
2113
+ timer;
2114
+ /** Attached once the socket exists. The shipper is constructed FIRST, because it has to be a pino
2115
+ * stream before the logger is built, which is before the client is started. Until `attach`, records
2116
+ * simply accumulate in the (bounded) ring. */
2117
+ send;
2118
+ /** Records we threw away because the buffer was full. Reported, never hidden. */
2119
+ dropped = 0;
2120
+ /** Give the shipper its transport. Called by the client once the socket exists. */
2121
+ attach(send) {
2122
+ this.send = send;
2123
+ }
2124
+ /** The hub may only ever ask for LESS than the local ceiling allows. */
2125
+ clampToCeiling(p) {
2126
+ const rank = { off: 0, error: 1, warn: 2, info: 3, debug: 4 };
2127
+ const logs = rank[p.logs] > rank[this.ceiling.logs] ? this.ceiling.logs : p.logs;
2128
+ return { health: p.health && this.ceiling.health, logs };
2129
+ }
2130
+ /** Apply a policy from the hub (`welcome`, or a live `policy` frame). */
2131
+ setPolicy(p) {
2132
+ this.policy = this.clampToCeiling(p);
2133
+ if (this.policy.logs === "off") this.buffer = [];
2134
+ }
2135
+ current() {
2136
+ return this.policy;
2137
+ }
2138
+ /** Offer a record. Cheap and synchronous - this sits on the logging hot path. */
2139
+ offer(record) {
2140
+ if (this.policy.logs === "off") return;
2141
+ if (!shouldShip(record.level, this.policy.logs)) return;
2142
+ this.buffer.push(record);
2143
+ if (this.buffer.length > (this.opts.bufferMax ?? SHIP_BUFFER_MAX)) {
2144
+ this.buffer.shift();
2145
+ this.dropped++;
2146
+ }
2147
+ }
2148
+ /** Send one batch if there is anything to send and the socket will take it. */
2149
+ flush() {
2150
+ if (this.buffer.length === 0 || !this.send) return;
2151
+ const batch = this.buffer.slice(0, SHIP_BATCH_MAX);
2152
+ if (!this.send(batch)) return;
2153
+ this.buffer = this.buffer.slice(batch.length);
2154
+ }
2155
+ start() {
2156
+ if (this.timer) return;
2157
+ this.timer = setInterval(() => this.flush(), this.opts.flushMs ?? SHIP_FLUSH_MS);
2158
+ this.timer.unref?.();
2159
+ }
2160
+ stop() {
2161
+ if (this.timer) clearInterval(this.timer);
2162
+ this.timer = void 0;
2163
+ }
2164
+ /** For the health report and for tests. */
2165
+ pending() {
2166
+ return this.buffer.length;
2167
+ }
2168
+ };
2169
+ function shipperStream(shipper) {
2170
+ return {
2171
+ write(chunk) {
2172
+ try {
2173
+ const r = JSON.parse(chunk);
2174
+ if (!r.msg) return;
2175
+ shipper.offer({
2176
+ level: r.level,
2177
+ time: r.time,
2178
+ msg: r.msg,
2179
+ ...r.runId ? { runId: r.runId } : {},
2180
+ ...r.stageId ? { stageId: r.stageId } : {},
2181
+ ...r.jobId ? { jobId: r.jobId } : {},
2182
+ ...r.component ? { component: r.component } : {},
2183
+ ...r.err ? { err: r.err } : {}
2184
+ });
2185
+ } catch {
2186
+ }
2187
+ }
2188
+ };
2189
+ }
2190
+ function ceilingFromEnv(env) {
2191
+ const v = env.DAHRK_TELEMETRY?.toLowerCase();
2192
+ if (v === "off" || v === "0" || v === "false") return { health: false, logs: "off" };
2193
+ if (v === "health") return { health: true, logs: "off" };
2194
+ return { health: true, logs: "debug" };
2195
+ }
2196
+
2007
2197
  // ../../packages/edge/src/logger.ts
2008
2198
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, renameSync, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
2009
- import { join as join7 } from "path";
2199
+ import { join as join8 } from "path";
2010
2200
  import pino from "pino";
2011
2201
 
2012
2202
  // ../../packages/edge/src/redact.ts
@@ -2196,14 +2386,15 @@ function createNodeLogger(opts = {}) {
2196
2386
  if (opts.dir && fileLevel !== "silent") {
2197
2387
  try {
2198
2388
  mkdirSync5(opts.dir, { recursive: true, mode: 448 });
2199
- const file = new RotatingFile(join7(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2389
+ const file = new RotatingFile(join8(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2200
2390
  streams.push({ level: fileLevel, stream: file });
2201
2391
  } catch (e) {
2202
2392
  process.stderr.write(`dahrk: file logging disabled (${e.message})
2203
2393
  `);
2204
2394
  }
2205
2395
  }
2206
- const base = streams.length === 0 ? "silent" : opts.dir ? lowest(level, fileLevel) : level;
2396
+ if (opts.ship) streams.push({ level: "trace", stream: opts.ship });
2397
+ const base = streams.length === 0 ? "silent" : opts.ship ? "trace" : opts.dir ? lowest(level, fileLevel) : level;
2207
2398
  return pino(
2208
2399
  {
2209
2400
  level: base,
@@ -2219,12 +2410,13 @@ function createNodeLogger(opts = {}) {
2219
2410
  pino.multistream(streams)
2220
2411
  );
2221
2412
  }
2222
- function createNodeLoggerFromEnv(env, dir, base) {
2413
+ function createNodeLoggerFromEnv(env, dir, base, ship) {
2223
2414
  const fileLevel = fileLevelFromEnv(env);
2224
2415
  return createNodeLogger({
2225
2416
  level: levelFromEnv(env),
2226
2417
  ...fileLevel === "off" ? {} : { dir, fileLevel },
2227
- ...base ? { base } : {}
2418
+ ...base ? { base } : {},
2419
+ ...ship ? { ship } : {}
2228
2420
  });
2229
2421
  }
2230
2422
 
@@ -2250,15 +2442,377 @@ function denyToolRule(tool3) {
2250
2442
  }
2251
2443
 
2252
2444
  // ../../packages/edge/src/stage-runner.ts
2253
- import { execFileSync as execFileSync4 } from "child_process";
2445
+ import { execFileSync as execFileSync5 } from "child_process";
2254
2446
  import { createHash as createHash3 } from "crypto";
2255
2447
  import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
2256
- import { tmpdir as tmpdir2 } from "os";
2257
- import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
2448
+ import { tmpdir as tmpdir4 } from "os";
2449
+ import { isAbsolute as isAbsolute3, join as join10, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
2258
2450
  import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
2259
2451
 
2260
2452
  // ../../packages/edge/src/builtins.ts
2453
+ import { execFileSync as execFileSync4 } from "child_process";
2454
+
2455
+ // ../../packages/edge/src/fs-roots.ts
2261
2456
  import { execFileSync as execFileSync3 } from "child_process";
2457
+ import { existsSync as existsSync6, realpathSync as realpathSync2 } from "fs";
2458
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2459
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve, sep as sep2 } from "path";
2460
+ function isUnder(root, target) {
2461
+ const rel = relative2(resolve(root), resolve(target));
2462
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
2463
+ }
2464
+ function expandPath(raw, cwd) {
2465
+ const home = homedir3();
2466
+ const expanded = raw.replace(/^~(?=$|\/)/, home).replace(/^\$\{HOME\}(?=$|\/)/, home).replace(/^\$HOME(?=$|\/)/, home);
2467
+ return resolve(cwd, expanded);
2468
+ }
2469
+ function realish(p) {
2470
+ let head = p;
2471
+ const tail = [];
2472
+ while (head !== dirname4(head)) {
2473
+ if (existsSync6(head)) {
2474
+ try {
2475
+ return join9(realpathSync2.native(head), ...tail.reverse());
2476
+ } catch {
2477
+ return p;
2478
+ }
2479
+ }
2480
+ tail.push(head.slice(dirname4(head).length + 1));
2481
+ head = dirname4(head);
2482
+ }
2483
+ return p;
2484
+ }
2485
+ function withinRoots(raw, roots, need) {
2486
+ const literal = expandPath(raw, roots.cwd);
2487
+ const real = realish(literal);
2488
+ const hits = (list2) => list2.some((root) => isUnder(root, literal) || isUnder(root, real));
2489
+ if (hits(roots.deny)) return false;
2490
+ if (hits(roots.rw)) return true;
2491
+ return need === "read" && hits(roots.ro);
2492
+ }
2493
+ function gitCommonDir(worktreePath) {
2494
+ try {
2495
+ const out = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
2496
+ cwd: worktreePath,
2497
+ stdio: ["ignore", "pipe", "ignore"]
2498
+ }).toString().trim();
2499
+ return out || void 0;
2500
+ } catch {
2501
+ return void 0;
2502
+ }
2503
+ }
2504
+ var pnpmStoreCache;
2505
+ function pnpmStore() {
2506
+ if (pnpmStoreCache) return pnpmStoreCache.path;
2507
+ try {
2508
+ const out = execFileSync3("pnpm", ["store", "path"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).toString().trim();
2509
+ pnpmStoreCache = { path: out || void 0 };
2510
+ } catch {
2511
+ pnpmStoreCache = { path: void 0 };
2512
+ }
2513
+ return pnpmStoreCache.path;
2514
+ }
2515
+ var splitRoots = (v) => (v ?? "").split(":").map((s) => s.trim()).filter(Boolean);
2516
+ function computeFsRoots(opts) {
2517
+ const home = homedir3();
2518
+ const wt = realish(resolve(opts.worktreePath));
2519
+ const rw = [
2520
+ wt,
2521
+ ...opts.scratchPath ? [realish(resolve(opts.scratchPath))] : [],
2522
+ // Every git command in the worktree reads and writes here.
2523
+ ...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
2524
+ // Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
2525
+ ...[tmpdir3(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2526
+ // The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
2527
+ // it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
2528
+ // here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
2529
+ ...["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/fd"],
2530
+ ...[pnpmStore()].filter((p) => Boolean(p)).map(realish),
2531
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_ROOTS).map((p) => realish(resolve(p)))
2532
+ ];
2533
+ const ro = [
2534
+ "/usr",
2535
+ "/bin",
2536
+ "/sbin",
2537
+ "/etc",
2538
+ "/opt",
2539
+ "/nix",
2540
+ "/Library",
2541
+ "/System",
2542
+ "/proc",
2543
+ "/sys",
2544
+ join9(home, ".gitconfig"),
2545
+ join9(home, ".config"),
2546
+ join9(home, ".npmrc"),
2547
+ join9(home, ".cache"),
2548
+ join9(home, "Library", "Caches"),
2549
+ join9(home, "Library", "pnpm"),
2550
+ join9(home, ".local", "share"),
2551
+ join9(home, ".nvm"),
2552
+ join9(home, ".volta"),
2553
+ join9(home, ".asdf"),
2554
+ join9(home, ".cargo"),
2555
+ join9(home, ".rustup"),
2556
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
2557
+ ].map(realish);
2558
+ const deny = [
2559
+ join9(home, ".ssh"),
2560
+ join9(home, ".aws"),
2561
+ join9(home, ".gnupg"),
2562
+ join9(home, ".config", "gcloud"),
2563
+ join9(home, "Library", "Keychains"),
2564
+ "/Volumes",
2565
+ "/etc/shadow",
2566
+ "/etc/sudoers"
2567
+ ];
2568
+ return { rw, ro, deny, cwd: wt };
2569
+ }
2570
+
2571
+ // ../../packages/edge/src/shell-scan.ts
2572
+ var PATTERN_FLAGS = /* @__PURE__ */ new Set([
2573
+ "-e",
2574
+ "--regexp",
2575
+ "-path",
2576
+ "-ipath",
2577
+ "-name",
2578
+ "-iname",
2579
+ "-wholename",
2580
+ "--glob",
2581
+ "-g",
2582
+ "--include",
2583
+ "--exclude",
2584
+ "--exclude-dir",
2585
+ "--type",
2586
+ "-m",
2587
+ "--message"
2588
+ ]);
2589
+ var PATTERN_FIRST = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ag", "ack", "sed", "awk", "perl"]);
2590
+ var DIR_FLAGS = /* @__PURE__ */ new Set(["-C", "--git-dir", "--work-tree", "--cwd", "--directory"]);
2591
+ var NO_PATH_ARGV0 = /* @__PURE__ */ new Set(["echo", "printf", ":", "true", "false"]);
2592
+ var WRITE_ALL = /* @__PURE__ */ new Set(["rm", "rmdir", "mkdir", "touch", "chmod", "chown", "truncate", "mkfifo"]);
2593
+ var WRITE_LAST = /* @__PURE__ */ new Set(["cp", "mv", "ln", "tee", "dd", "install", "rsync"]);
2594
+ var SHELL_ARGV0 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "eval", "xargs", "env", "nohup", "time", "sudo"]);
2595
+ var REDIRECTS = /* @__PURE__ */ new Set([">", ">>", "<", "2>", "2>>", "&>", "<<<"]);
2596
+ function tokenise(cmd) {
2597
+ const tokens = [];
2598
+ const subshells = [];
2599
+ let cur = "";
2600
+ let quoted = false;
2601
+ let started = false;
2602
+ let quote = null;
2603
+ const push = () => {
2604
+ if (started) tokens.push({ value: cur, quoted });
2605
+ cur = "";
2606
+ quoted = false;
2607
+ started = false;
2608
+ };
2609
+ for (let i = 0; i < cmd.length; i++) {
2610
+ const c = cmd[i];
2611
+ if (quote) {
2612
+ if (c === quote) {
2613
+ quote = null;
2614
+ } else if (c === "\\" && quote === '"' && i + 1 < cmd.length) {
2615
+ cur += cmd[++i];
2616
+ started = true;
2617
+ } else {
2618
+ cur += c;
2619
+ started = true;
2620
+ }
2621
+ continue;
2622
+ }
2623
+ if (c === '"' || c === "'") {
2624
+ quote = c;
2625
+ quoted = true;
2626
+ started = true;
2627
+ continue;
2628
+ }
2629
+ if (c === "\\" && i + 1 < cmd.length) {
2630
+ cur += cmd[++i];
2631
+ started = true;
2632
+ continue;
2633
+ }
2634
+ if (c === "$" && cmd[i + 1] === "(" || c === "`") {
2635
+ const open = c === "`" ? "`" : "(";
2636
+ const close = c === "`" ? "`" : ")";
2637
+ let depth = 1;
2638
+ let j = i + (c === "`" ? 1 : 2);
2639
+ let body = "";
2640
+ for (; j < cmd.length && depth > 0; j++) {
2641
+ const d = cmd[j];
2642
+ if (d === open && close !== "`") depth++;
2643
+ else if (d === close) {
2644
+ depth--;
2645
+ if (depth === 0) break;
2646
+ }
2647
+ body += d;
2648
+ }
2649
+ if (depth > 0) return null;
2650
+ subshells.push(body);
2651
+ cur += "$SUBSHELL";
2652
+ started = true;
2653
+ i = j;
2654
+ continue;
2655
+ }
2656
+ if (/\s/.test(c)) {
2657
+ push();
2658
+ continue;
2659
+ }
2660
+ const three = cmd.slice(i, i + 3);
2661
+ const two = cmd.slice(i, i + 2);
2662
+ if (three === "<<<") {
2663
+ push();
2664
+ tokens.push({ value: three, quoted: false, operator: three });
2665
+ i += 2;
2666
+ continue;
2667
+ }
2668
+ if (["&&", "||", ">>", "2>", "&>"].includes(two)) {
2669
+ push();
2670
+ tokens.push({ value: two, quoted: false, operator: two });
2671
+ i += 1;
2672
+ continue;
2673
+ }
2674
+ if (c === ";" || c === "|" || c === "&" || c === ">" || c === "<" || c === "\n") {
2675
+ push();
2676
+ tokens.push({ value: c, quoted: false, operator: c });
2677
+ continue;
2678
+ }
2679
+ cur += c;
2680
+ started = true;
2681
+ }
2682
+ if (quote) return null;
2683
+ push();
2684
+ return { tokens, subshells };
2685
+ }
2686
+ function looksLikePath(t) {
2687
+ if (t.value.startsWith("-")) return false;
2688
+ if (/\s/.test(t.value)) return false;
2689
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(t.value)) return false;
2690
+ const s = t.value.replace(/^@/, "");
2691
+ if (s.includes("$SUBSHELL")) return false;
2692
+ if (/^\$(?!\{?HOME\b)/.test(s)) return false;
2693
+ return /^(\/|~(?:$|\/)|\$\{?HOME\}?(?:$|\/)|\.\.(?:$|\/))/.test(s) || s.includes("/../") || s.endsWith("/..");
2694
+ }
2695
+ var bare = (v) => v.replace(/^@/, "");
2696
+ function scanSimple(tokens, roots, cwd) {
2697
+ const scoped = { ...roots, cwd };
2698
+ const check = (raw, need) => withinRoots(bare(raw), scoped, need) ? null : { kind: "escape", path: bare(raw), need };
2699
+ const argv0 = tokens.find((t) => !t.operator && !t.value.includes("="))?.value ?? "";
2700
+ const cmdName = argv0.split("/").pop() ?? argv0;
2701
+ if (SHELL_ARGV0.has(cmdName)) {
2702
+ for (const t of tokens) {
2703
+ if (t.operator || t.value === argv0) continue;
2704
+ if (t.quoted || t.value.includes(" ")) {
2705
+ const inner = scanCommand(t.value, roots, cwd);
2706
+ if (inner.kind !== "ok") return inner;
2707
+ } else if (looksLikePath(t)) {
2708
+ const bad = check(t.value, "read");
2709
+ if (bad) return bad;
2710
+ }
2711
+ }
2712
+ return { kind: "ok" };
2713
+ }
2714
+ if (NO_PATH_ARGV0.has(cmdName)) {
2715
+ return { kind: "ok" };
2716
+ }
2717
+ const writeAll = WRITE_ALL.has(cmdName);
2718
+ const writeLast = WRITE_LAST.has(cmdName);
2719
+ const operands = [];
2720
+ let patternPending = PATTERN_FIRST.has(cmdName);
2721
+ for (let i = 0; i < tokens.length; i++) {
2722
+ const t = tokens[i];
2723
+ if (t.operator) continue;
2724
+ if (t.value === argv0 && i === tokens.indexOf(t)) {
2725
+ if (looksLikePath(t)) {
2726
+ const bad = check(t.value, "read");
2727
+ if (bad) return bad;
2728
+ }
2729
+ continue;
2730
+ }
2731
+ if (DIR_FLAGS.has(t.value)) {
2732
+ const operand = tokens[i + 1];
2733
+ if (operand && !operand.operator) {
2734
+ const bad = check(operand.value, "read");
2735
+ if (bad) return bad;
2736
+ i++;
2737
+ }
2738
+ continue;
2739
+ }
2740
+ if (PATTERN_FLAGS.has(t.value)) {
2741
+ i++;
2742
+ continue;
2743
+ }
2744
+ if (t.value.startsWith("-")) continue;
2745
+ if (patternPending) {
2746
+ patternPending = false;
2747
+ continue;
2748
+ }
2749
+ if (looksLikePath(t)) operands.push({ token: t, index: i });
2750
+ }
2751
+ if (cmdName === "cd") {
2752
+ const target = tokens.find((t) => !t.operator && t.value !== argv0 && !t.value.startsWith("-"));
2753
+ if (!target) return { kind: "ok" };
2754
+ const bad = check(target.value, "read");
2755
+ if (bad) return bad;
2756
+ return { kind: "cd", to: target.value };
2757
+ }
2758
+ for (let n = 0; n < operands.length; n++) {
2759
+ const entry = operands[n];
2760
+ const isLast = n === operands.length - 1;
2761
+ const need = writeAll || writeLast && isLast ? "write" : "read";
2762
+ const bad = check(entry.token.value, need);
2763
+ if (bad) return bad;
2764
+ }
2765
+ return { kind: "ok" };
2766
+ }
2767
+ function scanCommand(cmd, roots, cwd = roots.cwd) {
2768
+ if (!cmd.trim()) return { kind: "ok" };
2769
+ const lexed = tokenise(cmd);
2770
+ if (!lexed) return { kind: "unparseable", reason: "unbalanced quotes or unterminated substitution" };
2771
+ for (const body of lexed.subshells) {
2772
+ const inner = scanCommand(body, roots, cwd);
2773
+ if (inner.kind !== "ok") return inner;
2774
+ }
2775
+ let current = [];
2776
+ let cwdNow = cwd;
2777
+ const segments = [];
2778
+ for (const t of lexed.tokens) {
2779
+ if (t.operator && [";", "|", "&", "&&", "||", "\n"].includes(t.operator)) {
2780
+ segments.push(current);
2781
+ current = [];
2782
+ continue;
2783
+ }
2784
+ current.push(t);
2785
+ }
2786
+ segments.push(current);
2787
+ for (const seg of segments) {
2788
+ if (seg.length === 0) continue;
2789
+ const plain = [];
2790
+ for (let i = 0; i < seg.length; i++) {
2791
+ const t = seg[i];
2792
+ if (t.operator && REDIRECTS.has(t.operator)) {
2793
+ const target = seg[i + 1];
2794
+ if (target && !target.operator) {
2795
+ const need = t.operator === "<" || t.operator === "<<<" ? "read" : "write";
2796
+ if (!withinRoots(bare(target.value), { ...roots, cwd: cwdNow }, need)) {
2797
+ return { kind: "escape", path: bare(target.value), need };
2798
+ }
2799
+ i++;
2800
+ }
2801
+ continue;
2802
+ }
2803
+ plain.push(t);
2804
+ }
2805
+ const out = scanSimple(plain, roots, cwdNow);
2806
+ if (out.kind === "cd") {
2807
+ cwdNow = expandPath(out.to, cwdNow);
2808
+ continue;
2809
+ }
2810
+ if (out.kind !== "ok") return out;
2811
+ }
2812
+ return { kind: "ok" };
2813
+ }
2814
+
2815
+ // ../../packages/edge/src/builtins.ts
2262
2816
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
2263
2817
  "Write",
2264
2818
  "Edit",
@@ -2300,7 +2854,7 @@ function isDangerousRm(cmd) {
2300
2854
  }
2301
2855
  function currentBranch(worktreePath) {
2302
2856
  try {
2303
- return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2857
+ return execFileSync4("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2304
2858
  } catch {
2305
2859
  return "";
2306
2860
  }
@@ -2319,9 +2873,54 @@ function commandOf(input) {
2319
2873
  }
2320
2874
  return "";
2321
2875
  }
2876
+ var PATH_FIELDS = ["file_path", "notebook_path", "path", "filePath", "cwd", "directory"];
2877
+ var PATH_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "apply_patch"]);
2878
+ function pathsIn(input) {
2879
+ if (!input || typeof input !== "object") return [];
2880
+ const o = input;
2881
+ const out = [];
2882
+ for (const f of PATH_FIELDS) {
2883
+ if (typeof o[f] === "string" && o[f]) out.push(o[f]);
2884
+ }
2885
+ if (o.changes && typeof o.changes === "object") {
2886
+ out.push(...Object.keys(o.changes));
2887
+ }
2888
+ return out;
2889
+ }
2890
+ function fsConfineRule(roots) {
2891
+ return {
2892
+ name: "fs_confine",
2893
+ evaluate(event) {
2894
+ if (event.kind !== "action") return null;
2895
+ const deny = (path, need2) => ({
2896
+ verdict: "deny",
2897
+ policy: "fs_confine",
2898
+ reason: `${need2 === "write" ? "write to" : "read of"} "${path}" is outside the run's worktree`
2899
+ });
2900
+ if (SHELL_TOOLS.has(event.tool)) {
2901
+ const result = scanCommand(commandOf(event.input), roots);
2902
+ if (result.kind === "escape") return deny(result.path, result.need);
2903
+ if (result.kind === "unparseable") {
2904
+ return {
2905
+ verdict: "deny",
2906
+ policy: "fs_confine",
2907
+ reason: `shell command could not be parsed for path confinement (${result.reason})`
2908
+ };
2909
+ }
2910
+ return null;
2911
+ }
2912
+ const need = PATH_WRITE_TOOLS.has(event.tool) ? "write" : "read";
2913
+ for (const p of pathsIn(event.input)) {
2914
+ if (!withinRoots(p, roots, need)) return deny(p, need);
2915
+ }
2916
+ return null;
2917
+ }
2918
+ };
2919
+ }
2322
2920
  function buildRules(policies, ctx) {
2323
2921
  const rules = [];
2324
2922
  let stageToolCalls = 0;
2923
+ if (ctx.fsRoots && process.env.DAHRK_FS_CONFINE !== "0") rules.push(fsConfineRule(ctx.fsRoots));
2325
2924
  for (const p of policies) {
2326
2925
  if ("read_only" in p && p.read_only) {
2327
2926
  rules.push({
@@ -2436,22 +3035,23 @@ async function startMcpGateway(opts) {
2436
3035
  if (upstream.body) await pipeline(Readable.fromWeb(upstream.body), res);
2437
3036
  else res.end();
2438
3037
  };
2439
- await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
3038
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
2440
3039
  const { port } = server.address();
2441
3040
  return {
2442
3041
  baseUrl: `http://127.0.0.1:${port}`,
2443
- stop: () => new Promise((resolve2) => server.close(() => resolve2()))
3042
+ stop: () => new Promise((resolve3) => server.close(() => resolve3()))
2444
3043
  };
2445
3044
  }
2446
3045
 
2447
3046
  // ../../packages/edge/src/stage-runner.ts
2448
3047
  var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
2449
3048
  var PREVIEW = 500;
3049
+ var RESULT = 16e3;
2450
3050
  var putBytes = async (url, body, contentType) => {
2451
3051
  await fetch(url, { method: "PUT", headers: { "content-type": contentType }, body: new Uint8Array(body) });
2452
3052
  };
2453
3053
  function previewOf(event) {
2454
- const clip = (v) => (typeof v === "string" ? v : JSON.stringify(v) ?? "").slice(0, PREVIEW);
3054
+ const clip = (v, max = PREVIEW) => (typeof v === "string" ? v : JSON.stringify(v) ?? "").slice(0, max);
2455
3055
  switch (event.type) {
2456
3056
  case "response":
2457
3057
  return event.text !== void 0 ? { text: event.text } : {};
@@ -2460,9 +3060,16 @@ function previewOf(event) {
2460
3060
  case "thought":
2461
3061
  return event.text !== void 0 ? { text: clip(event.text) } : {};
2462
3062
  case "action":
2463
- return { tool: event.tool, ...event.input !== void 0 ? { text: clip(event.input) } : {} };
3063
+ return {
3064
+ tool: event.tool,
3065
+ toolUseId: event.toolUseId,
3066
+ ...event.input !== void 0 ? { text: clip(event.input) } : {}
3067
+ };
2464
3068
  case "observation":
2465
- return event.output !== void 0 ? { text: clip(event.output) } : {};
3069
+ return {
3070
+ toolUseId: event.toolUseId,
3071
+ ...event.output !== void 0 ? { text: clip(event.output, RESULT) } : {}
3072
+ };
2466
3073
  case "error":
2467
3074
  return { text: clip(event.message) };
2468
3075
  default:
@@ -2475,7 +3082,7 @@ var attemptOf = (jobId) => {
2475
3082
  };
2476
3083
  var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
2477
3084
  function writeScratchState(ref, job, attempt, status) {
2478
- const statePath = join8(ref.scratchPath, "state.json");
3085
+ const statePath = join10(ref.scratchPath, "state.json");
2479
3086
  let state;
2480
3087
  try {
2481
3088
  state = JSON.parse(readFileSync4(statePath, "utf8"));
@@ -2490,17 +3097,17 @@ function writeIssueContext(ref, issueContext) {
2490
3097
  if (issueContext === void 0) return;
2491
3098
  try {
2492
3099
  mkdirSync6(ref.scratchPath, { recursive: true });
2493
- writeFileSync5(join8(ref.scratchPath, "issue.md"), issueContext);
3100
+ writeFileSync5(join10(ref.scratchPath, "issue.md"), issueContext);
2494
3101
  } catch {
2495
3102
  }
2496
3103
  }
2497
3104
  function writeAttachedDocuments(ref, docs) {
2498
3105
  if (!docs || docs.length === 0) return;
2499
3106
  try {
2500
- const dir = join8(ref.scratchPath, "docs");
3107
+ const dir = join10(ref.scratchPath, "docs");
2501
3108
  mkdirSync6(dir, { recursive: true });
2502
3109
  for (const doc of docs) {
2503
- writeFileSync5(join8(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3110
+ writeFileSync5(join10(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
2504
3111
  }
2505
3112
  } catch {
2506
3113
  }
@@ -2519,7 +3126,7 @@ function writeGuidance(ref, guidance) {
2519
3126
  if (!guidance || guidance.length === 0) return;
2520
3127
  try {
2521
3128
  mkdirSync6(ref.scratchPath, { recursive: true });
2522
- writeFileSync5(join8(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3129
+ writeFileSync5(join10(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
2523
3130
  } catch {
2524
3131
  }
2525
3132
  }
@@ -2529,11 +3136,11 @@ function capContent(raw) {
2529
3136
  return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
2530
3137
  }
2531
3138
  function resolveWorktreeRelativePath(ref, relPath) {
2532
- if (isAbsolute2(relPath)) return void 0;
2533
- const root = resolve(ref.worktreePath);
2534
- const target = resolve(root, relPath);
2535
- const fromRoot = relative2(root, target);
2536
- if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep2}`) || isAbsolute2(fromRoot)) {
3139
+ if (isAbsolute3(relPath)) return void 0;
3140
+ const root = resolve2(ref.worktreePath);
3141
+ const target = resolve2(root, relPath);
3142
+ const fromRoot = relative3(root, target);
3143
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep3}`) || isAbsolute3(fromRoot)) {
2537
3144
  return void 0;
2538
3145
  }
2539
3146
  return target;
@@ -2553,13 +3160,13 @@ function readEmittedArtifact(ref, relPath) {
2553
3160
  }
2554
3161
  function scanScratchOutput(ref, preferRel) {
2555
3162
  try {
2556
- const names = readdirSync3(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3163
+ const names = readdirSync3(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
2557
3164
  (n) => n.toLowerCase().endsWith(".md")
2558
3165
  );
2559
3166
  if (names.length === 0) return void 0;
2560
3167
  const preferBase = preferRel?.split("/").pop();
2561
3168
  const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
2562
- const raw = readFileSync4(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3169
+ const raw = readFileSync4(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
2563
3170
  if (raw.trim().length === 0) return void 0;
2564
3171
  return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
2565
3172
  } catch {
@@ -2569,7 +3176,7 @@ function scanScratchOutput(ref, preferRel) {
2569
3176
  function scanChangedMarkdown(ref) {
2570
3177
  const git = (args) => {
2571
3178
  try {
2572
- return execFileSync4("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
3179
+ return execFileSync5("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
2573
3180
  } catch {
2574
3181
  return [];
2575
3182
  }
@@ -2579,7 +3186,7 @@ function scanChangedMarkdown(ref) {
2579
3186
  );
2580
3187
  for (const rel of rels) {
2581
3188
  try {
2582
- const raw = readFileSync4(join8(ref.worktreePath, rel), "utf8");
3189
+ const raw = readFileSync4(join10(ref.worktreePath, rel), "utf8");
2583
3190
  if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
2584
3191
  } catch {
2585
3192
  }
@@ -2702,9 +3309,9 @@ function createStageRunner(deps) {
2702
3309
  ...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
2703
3310
  });
2704
3311
  } else {
2705
- const base = deps.scratchRoot ?? join8(tmpdir2(), "dahrk", "scratch");
2706
- const worktreePath = join8(base, runId);
2707
- const scratchPath = join8(worktreePath, ".skakel", "scratch");
3312
+ const base = deps.scratchRoot ?? join10(tmpdir4(), "dahrk", "scratch");
3313
+ const worktreePath = join10(base, runId);
3314
+ const scratchPath = join10(worktreePath, ".skakel", "scratch");
2708
3315
  mkdirSync6(scratchPath, { recursive: true });
2709
3316
  ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
2710
3317
  scratchOnly.add(runId);
@@ -2720,7 +3327,7 @@ function createStageRunner(deps) {
2720
3327
  const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
2721
3328
  if (artifactDir && slash > 0) {
2722
3329
  try {
2723
- mkdirSync6(join8(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
3330
+ mkdirSync6(join10(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
2724
3331
  } catch {
2725
3332
  }
2726
3333
  }
@@ -2748,8 +3355,8 @@ function createStageRunner(deps) {
2748
3355
  if (!sink) return;
2749
3356
  const base = { tenantId: job.tenantId, runId, stageId, attempt };
2750
3357
  try {
2751
- for (const name of readdirSync3(join8(writer.dir, "blobs"))) {
2752
- const bytes = readFileSync4(join8(writer.dir, "blobs", name));
3358
+ for (const name of readdirSync3(join10(writer.dir, "blobs"))) {
3359
+ const bytes = readFileSync4(join10(writer.dir, "blobs", name));
2753
3360
  const { url } = await sink.requestBlobUrl({
2754
3361
  ...base,
2755
3362
  sha256: name,
@@ -2763,7 +3370,7 @@ function createStageRunner(deps) {
2763
3370
  }
2764
3371
  let archiveKey;
2765
3372
  try {
2766
- const bytes = readFileSync4(join8(writer.dir, "trace.jsonl"));
3373
+ const bytes = readFileSync4(join10(writer.dir, "trace.jsonl"));
2767
3374
  const sha = createHash3("sha256").update(bytes).digest("hex");
2768
3375
  const { key, url } = await sink.requestBlobUrl({
2769
3376
  ...base,
@@ -2847,7 +3454,11 @@ function createStageRunner(deps) {
2847
3454
  const jobRules = buildRules(job.policies ?? [], {
2848
3455
  worktreePath: ref.worktreePath,
2849
3456
  repoName: job.workspaceRef?.repo ?? "",
2850
- runToolCalls: counter
3457
+ runToolCalls: counter,
3458
+ // DHK-392: the stage is confined to the run's worktree (plus its scratch dir, the git object
3459
+ // store it depends on, and the toolchain). A node default, not a workflow policy. A run with
3460
+ // no repo still gets a box - just a smaller one, around its scratch dir.
3461
+ fsRoots: computeFsRoots({ worktreePath: ref.worktreePath, scratchPath: ref.scratchPath })
2851
3462
  });
2852
3463
  const rules = [...jobRules, ...deps.rules];
2853
3464
  const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
@@ -2856,6 +3467,7 @@ function createStageRunner(deps) {
2856
3467
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
2857
3468
  }
2858
3469
  let denied = false;
3470
+ let escapedUnblocked = false;
2859
3471
  const authorisedActions = [];
2860
3472
  const runtime = agentConfig.runtime;
2861
3473
  const actionKey = (tool3, input) => {
@@ -2898,6 +3510,7 @@ function createStageRunner(deps) {
2898
3510
  if (verdict.verdict === "deny") {
2899
3511
  streamEvent(writer.append(event));
2900
3512
  recordDeny(verdict, event.toolUseId);
3513
+ if (verdict.policy === "fs_confine" && runtime !== "claude-code") escapedUnblocked = true;
2901
3514
  return;
2902
3515
  }
2903
3516
  }
@@ -2960,10 +3573,16 @@ function createStageRunner(deps) {
2960
3573
  if (killTimer) clearTimeout(killTimer);
2961
3574
  }
2962
3575
  let status = timedOut ? "timeout" : result.status;
3576
+ if (status === "ok" && escapedUnblocked) {
3577
+ status = "fail";
3578
+ const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
3579
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "fs-confine-escape", message: msg });
3580
+ deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
3581
+ }
2963
3582
  if (status === "ok" && job.hooks && job.hooks.length > 0) {
2964
3583
  for (const cmd of job.hooks) {
2965
3584
  try {
2966
- execFileSync4("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
3585
+ execFileSync5("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
2967
3586
  } catch (e) {
2968
3587
  status = "fail";
2969
3588
  writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
@@ -3125,9 +3744,20 @@ function createStageRunner(deps) {
3125
3744
 
3126
3745
  // ../../packages/edge/src/ws-client.ts
3127
3746
  var ENROLMENT_REJECTED_EXIT_CODE = 78;
3747
+ function classifyError(e) {
3748
+ const msg = e instanceof Error ? e.message : String(e);
3749
+ if (/\bgit\b|worktree|clone|fetch|branch|checkout|remote/i.test(msg)) return "git";
3750
+ if (/policy|denied|not permitted|forbidden/i.test(msg)) return "policy";
3751
+ if (/hub|websocket|socket|awakeable/i.test(msg)) return "hub";
3752
+ if (/runtime|claude|codex|\bpi\b|model|sdk|api key/i.test(msg)) return "runtime";
3753
+ return "internal";
3754
+ }
3128
3755
  async function startEdgeNode(opts) {
3129
3756
  const log = opts.logger ?? createNodeLogger({ level: levelFromEnv(process.env) });
3130
3757
  const rules = opts.denyTool ? [denyToolRule(opts.denyTool)] : [];
3758
+ const counters = new HealthCounters();
3759
+ const shipper = opts.shipper;
3760
+ const telemetryCeiling = ceilingFromEnv(process.env);
3131
3761
  const gitLog = log.child({ component: "git" });
3132
3762
  const gitLogger = {
3133
3763
  info: (msg) => gitLog.debug(msg),
@@ -3150,6 +3780,12 @@ async function startEdgeNode(opts) {
3150
3780
  const send = (msg) => {
3151
3781
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
3152
3782
  };
3783
+ shipper?.attach((records) => {
3784
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
3785
+ ws.send(encode({ type: "node-log", records }));
3786
+ return true;
3787
+ });
3788
+ shipper?.start();
3153
3789
  const MISSED_PONGS_BEFORE_DEAD = 3;
3154
3790
  const startHeartbeat = (sock, intervalMs) => {
3155
3791
  if (heartbeat) clearInterval(heartbeat);
@@ -3161,7 +3797,18 @@ async function startEdgeNode(opts) {
3161
3797
  sock.terminate();
3162
3798
  return;
3163
3799
  }
3164
- send({ type: "heartbeat" });
3800
+ const health = shipper?.current().health ?? telemetryCeiling.health;
3801
+ send(
3802
+ health ? {
3803
+ type: "heartbeat",
3804
+ health: collectHealth({
3805
+ counters,
3806
+ clientVersion: opts.clientVersion ?? "0.0.0",
3807
+ runtimes: opts.runtimes,
3808
+ worktreesDir: gitService.worktreesDir
3809
+ })
3810
+ } : { type: "heartbeat" }
3811
+ );
3165
3812
  if (sock.readyState === WebSocket.OPEN) sock.ping();
3166
3813
  }, intervalMs);
3167
3814
  };
@@ -3180,12 +3827,12 @@ async function startEdgeNode(opts) {
3180
3827
  const trace = {
3181
3828
  event: (frame) => send({ type: "trace-event", ...frame }),
3182
3829
  finalised: (frame) => send({ type: "trace-finalised", ...frame }),
3183
- requestBlobUrl: (req) => new Promise((resolve2) => {
3830
+ requestBlobUrl: (req) => new Promise((resolve3) => {
3184
3831
  const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
3185
- pendingBlob.set(reqId, resolve2);
3832
+ pendingBlob.set(reqId, resolve3);
3186
3833
  send({ type: "blob-put-request", reqId, ...req });
3187
3834
  setTimeout(() => {
3188
- if (pendingBlob.delete(reqId)) resolve2({ key: "" });
3835
+ if (pendingBlob.delete(reqId)) resolve3({ key: "" });
3189
3836
  }, 3e4).unref?.();
3190
3837
  })
3191
3838
  };
@@ -3206,6 +3853,7 @@ async function startEdgeNode(opts) {
3206
3853
  const stageRunner = createStageRunner(stageDeps);
3207
3854
  stageRunnerRef = stageRunner;
3208
3855
  void stageRunner.reapWorktrees().then((r) => {
3856
+ counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
3209
3857
  if (r.reaped.length) {
3210
3858
  log.info(
3211
3859
  { reaped: r.reaped, scanned: r.scanned, skipped: r.skipped },
@@ -3224,8 +3872,15 @@ async function startEdgeNode(opts) {
3224
3872
  if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
3225
3873
  startHeartbeat(ws, msg.heartbeatMs);
3226
3874
  }
3875
+ if (msg.telemetry && shipper) shipper.setPolicy(msg.telemetry);
3227
3876
  log.info(
3228
- { name: msg.name, tenantId: msg.tenantId, credentialMode: msg.credentialMode, heartbeatMs: msg.heartbeatMs },
3877
+ {
3878
+ name: msg.name,
3879
+ tenantId: msg.tenantId,
3880
+ credentialMode: msg.credentialMode,
3881
+ heartbeatMs: msg.heartbeatMs,
3882
+ ...shipper ? { telemetry: shipper.current() } : {}
3883
+ },
3229
3884
  `EDGE_WELCOMED:${msg.name} tenant=${msg.tenantId} credentialMode=${msg.credentialMode}`
3230
3885
  );
3231
3886
  try {
@@ -3240,13 +3895,18 @@ async function startEdgeNode(opts) {
3240
3895
  return;
3241
3896
  }
3242
3897
  if (msg.type === "blob-put-url") {
3243
- const resolve2 = pendingBlob.get(msg.reqId);
3244
- if (resolve2) {
3898
+ const resolve3 = pendingBlob.get(msg.reqId);
3899
+ if (resolve3) {
3245
3900
  pendingBlob.delete(msg.reqId);
3246
- resolve2({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3901
+ resolve3({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3247
3902
  }
3248
3903
  return;
3249
3904
  }
3905
+ if (msg.type === "policy") {
3906
+ shipper?.setPolicy(msg.telemetry);
3907
+ log.info({ telemetry: shipper?.current() ?? msg.telemetry }, `EDGE_POLICY:logs=${shipper?.current().logs ?? msg.telemetry.logs}`);
3908
+ return;
3909
+ }
3250
3910
  if (msg.type === "cancel") {
3251
3911
  log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
3252
3912
  stageRunner.cancel(msg.jobId);
@@ -3319,6 +3979,7 @@ async function startEdgeNode(opts) {
3319
3979
  return;
3320
3980
  }
3321
3981
  running.add(job.jobId);
3982
+ counters.activeJobs = running.size;
3322
3983
  const startedAt = Date.now();
3323
3984
  jobLog.info({}, `JOB_STARTED:${job.stageId} ${job.jobId}`);
3324
3985
  try {
@@ -3338,9 +3999,11 @@ async function startEdgeNode(opts) {
3338
3999
  };
3339
4000
  rememberResult(job.jobId, frame);
3340
4001
  send(frame);
4002
+ counters.recordError(classifyError(e));
3341
4003
  jobLog.error({ err: e, durationMs: Date.now() - startedAt }, `JOB_ERROR:${job.stageId} ${e.message}`);
3342
4004
  } finally {
3343
4005
  running.delete(job.jobId);
4006
+ counters.activeJobs = running.size;
3344
4007
  }
3345
4008
  };
3346
4009
  const connect = () => {
@@ -3348,6 +4011,7 @@ async function startEdgeNode(opts) {
3348
4011
  ws = sock;
3349
4012
  sock.on("open", () => {
3350
4013
  connectCount++;
4014
+ counters.connectCount = connectCount;
3351
4015
  log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
3352
4016
  send({
3353
4017
  type: "hello",
@@ -3403,11 +4067,11 @@ async function startEdgeNode(opts) {
3403
4067
  { once: true }
3404
4068
  );
3405
4069
  connect();
3406
- await new Promise((resolve2, reject) => {
4070
+ await new Promise((resolve3, reject) => {
3407
4071
  const t = setInterval(() => {
3408
4072
  if (ws && ws.readyState === WebSocket.OPEN) {
3409
4073
  clearInterval(t);
3410
- resolve2();
4074
+ resolve3();
3411
4075
  }
3412
4076
  }, 50);
3413
4077
  onFatal = (err) => {
@@ -3425,11 +4089,11 @@ var PROBES = [
3425
4089
  { runtime: "pi", cmd: "pi" }
3426
4090
  ];
3427
4091
  function probe(cmd, timeoutMs) {
3428
- return new Promise((resolve2) => {
4092
+ return new Promise((resolve3) => {
3429
4093
  execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
3430
- if (err) return resolve2(void 0);
4094
+ if (err) return resolve3(void 0);
3431
4095
  const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
3432
- resolve2(line ?? "");
4096
+ resolve3(line ?? "");
3433
4097
  });
3434
4098
  });
3435
4099
  }
@@ -3472,7 +4136,7 @@ function probeHub(opts) {
3472
4136
  clientVersion = "0.0.0",
3473
4137
  timeoutMs = 8e3
3474
4138
  } = opts;
3475
- return new Promise((resolve2) => {
4139
+ return new Promise((resolve3) => {
3476
4140
  let settled = false;
3477
4141
  let opened = false;
3478
4142
  let ws;
@@ -3484,7 +4148,7 @@ function probeHub(opts) {
3484
4148
  ws.terminate();
3485
4149
  } catch {
3486
4150
  }
3487
- resolve2(result);
4151
+ resolve3(result);
3488
4152
  };
3489
4153
  const timer = setTimeout(
3490
4154
  () => done({ ok: false, reason: "timeout", detail: `no welcome within ${timeoutMs}ms` }),
@@ -3944,8 +4608,8 @@ function usage(bin, command) {
3944
4608
  }
3945
4609
 
3946
4610
  // src/diagnose.ts
3947
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
3948
- import { join as join9 } from "path";
4611
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
4612
+ import { join as join11 } from "path";
3949
4613
  var BUNDLE_LOG_LINES = 2e3;
3950
4614
  function tailJsonl(raw, n) {
3951
4615
  const records = [];
@@ -3986,7 +4650,7 @@ async function buildBundle(deps) {
3986
4650
  if (deps.exists(deps.crashDir)) {
3987
4651
  for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
3988
4652
  try {
3989
- crashes.push(JSON.parse(deps.readFile(join9(deps.crashDir, name))));
4653
+ crashes.push(JSON.parse(deps.readFile(join11(deps.crashDir, name))));
3990
4654
  } catch (e) {
3991
4655
  warnings.push(`could not read crash record ${name} (${e.message})`);
3992
4656
  }
@@ -4045,17 +4709,17 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
4045
4709
  ...doctor ? { doctor } : {},
4046
4710
  readFile: (p) => readFileSync5(p, "utf8"),
4047
4711
  listDir: (p) => readdirSync4(p),
4048
- exists: (p) => existsSync6(p),
4712
+ exists: (p) => existsSync7(p),
4049
4713
  writeFile: (p, content) => {
4050
- const dir = join9(p, "..");
4051
- if (!existsSync6(dir)) mkdirSync7(dir, { recursive: true });
4714
+ const dir = join11(p, "..");
4715
+ if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true });
4052
4716
  writeFileSync6(p, content, { mode: 384 });
4053
4717
  },
4054
4718
  out: (line) => void process.stdout.write(`${line}
4055
4719
  `)
4056
4720
  });
4057
4721
  function defaultBundlePath(cwd, now) {
4058
- return join9(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4722
+ return join11(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4059
4723
  }
4060
4724
 
4061
4725
  // src/doctor.ts
@@ -4190,8 +4854,8 @@ async function runDoctor(inputs, deps = {}) {
4190
4854
  }
4191
4855
 
4192
4856
  // src/lock.ts
4193
- import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4194
- import { dirname as dirname4 } from "path";
4857
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4858
+ import { dirname as dirname5 } from "path";
4195
4859
  function parseLock(content) {
4196
4860
  if (!content) return void 0;
4197
4861
  const pid = Number(content.trim());
@@ -4204,7 +4868,11 @@ function acquireLock(deps) {
4204
4868
  }
4205
4869
  deps.writeFile(deps.file, `${deps.pid}
4206
4870
  `);
4207
- return { ok: true, release: () => deps.removeFile(deps.file) };
4871
+ return { ok: true, release: () => releaseLock(deps) };
4872
+ }
4873
+ function releaseLock(deps) {
4874
+ if (parseLock(deps.readFile(deps.file)) !== deps.pid) return;
4875
+ deps.removeFile(deps.file);
4208
4876
  }
4209
4877
  function isAlive(pid) {
4210
4878
  try {
@@ -4225,7 +4893,7 @@ var defaultLockDeps = (file) => ({
4225
4893
  }
4226
4894
  },
4227
4895
  writeFile: (path, content) => {
4228
- if (!existsSync7(dirname4(path))) mkdirSync8(dirname4(path), { recursive: true, mode: 448 });
4896
+ if (!existsSync8(dirname5(path))) mkdirSync8(dirname5(path), { recursive: true, mode: 448 });
4229
4897
  writeFileSync7(path, content);
4230
4898
  },
4231
4899
  removeFile: (path) => rmSync6(path, { force: true }),
@@ -4234,7 +4902,7 @@ var defaultLockDeps = (file) => ({
4234
4902
 
4235
4903
  // src/logs.ts
4236
4904
  import { spawn } from "child_process";
4237
- import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4905
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4238
4906
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
4239
4907
  function logsCommand(inputs) {
4240
4908
  return [
@@ -4315,7 +4983,7 @@ async function runStructuredLogs(inputs, deps) {
4315
4983
  }
4316
4984
  function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4317
4985
  try {
4318
- if (!existsSync8(file) || statSync4(file).size <= maxBytes) return;
4986
+ if (!existsSync9(file) || statSync4(file).size <= maxBytes) return;
4319
4987
  copyFileSync(file, `${file}.1`);
4320
4988
  truncateSync(file, 0);
4321
4989
  } catch {
@@ -4324,13 +4992,13 @@ function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4324
4992
  var defaultLogsDeps = (files, jsonlFile) => ({
4325
4993
  files,
4326
4994
  jsonlFile,
4327
- fileExists: (path) => existsSync8(path),
4995
+ fileExists: (path) => existsSync9(path),
4328
4996
  readFile: (path) => readFileSync7(path, "utf8"),
4329
- run: (argv) => new Promise((resolve2) => {
4997
+ run: (argv) => new Promise((resolve3) => {
4330
4998
  const [cmd, ...args] = argv;
4331
4999
  const child = spawn(cmd, args, { stdio: "inherit" });
4332
- child.on("error", () => resolve2(1));
4333
- child.on("close", (code) => resolve2(code ?? 0));
5000
+ child.on("error", () => resolve3(1));
5001
+ child.on("close", (code) => resolve3(code ?? 0));
4334
5002
  }),
4335
5003
  out: (line) => void process.stdout.write(`${line}
4336
5004
  `)
@@ -4338,11 +5006,11 @@ var defaultLogsDeps = (files, jsonlFile) => ({
4338
5006
 
4339
5007
  // src/process-safety.ts
4340
5008
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
4341
- import { join as join10 } from "path";
5009
+ import { join as join12 } from "path";
4342
5010
  function writeCrashRecord(dir, record) {
4343
5011
  try {
4344
5012
  mkdirSync9(dir, { recursive: true, mode: 448 });
4345
- const file = join10(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5013
+ const file = join12(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
4346
5014
  writeFileSync8(file, `${JSON.stringify(record, null, 2)}
4347
5015
  `, { mode: 384 });
4348
5016
  return file;
@@ -4396,11 +5064,11 @@ function installProcessSafetyNet(opts) {
4396
5064
  }
4397
5065
 
4398
5066
  // src/preflight.ts
4399
- import { execFileSync as execFileSync5 } from "child_process";
5067
+ import { execFileSync as execFileSync6 } from "child_process";
4400
5068
  import { randomUUID as randomUUID2 } from "crypto";
4401
- import { accessSync, constants as fsConstants, existsSync as existsSync9, readdirSync as readdirSync5, statfsSync } from "fs";
4402
- import { homedir as homedir2 } from "os";
4403
- import { join as join11 } from "path";
5069
+ import { accessSync, constants as fsConstants, existsSync as existsSync10, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
5070
+ import { homedir as homedir4 } from "os";
5071
+ import { join as join13 } from "path";
4404
5072
  var REPORT_BASE_URL = "https://app.dahrk.ai/r";
4405
5073
  var LOW_DISK_BYTES = 512 * 1024 * 1024;
4406
5074
  var PREFLIGHT_STAGES = [
@@ -4529,7 +5197,7 @@ var defaultDeps2 = () => ({
4529
5197
  });
4530
5198
  function commandPresent(cmd) {
4531
5199
  try {
4532
- execFileSync5("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
5200
+ execFileSync6("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
4533
5201
  return true;
4534
5202
  } catch {
4535
5203
  return false;
@@ -4537,12 +5205,12 @@ function commandPresent(cmd) {
4537
5205
  }
4538
5206
  function sshKeyPresent() {
4539
5207
  try {
4540
- const dir = join11(homedir2(), ".ssh");
4541
- if (existsSync9(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
5208
+ const dir = join13(homedir4(), ".ssh");
5209
+ if (existsSync10(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
4542
5210
  } catch {
4543
5211
  }
4544
5212
  try {
4545
- execFileSync5("ssh-add", ["-l"], { stdio: "ignore" });
5213
+ execFileSync6("ssh-add", ["-l"], { stdio: "ignore" });
4546
5214
  return true;
4547
5215
  } catch {
4548
5216
  return false;
@@ -4557,12 +5225,12 @@ function writable(dir) {
4557
5225
  }
4558
5226
  }
4559
5227
  function worktreeRoot(env) {
4560
- return env.DAHRK_WORKTREES_DIR ?? join11(env.DAHRK_STATE_DIR ?? join11(homedir2(), ".dahrk"), "worktrees");
5228
+ return env.DAHRK_WORKTREES_DIR ?? join13(env.DAHRK_STATE_DIR ?? join13(homedir4(), ".dahrk"), "worktrees");
4561
5229
  }
4562
5230
  function nearestExisting(dir) {
4563
5231
  let cur = dir;
4564
- while (!existsSync9(cur)) {
4565
- const parent = join11(cur, "..");
5232
+ while (!existsSync10(cur)) {
5233
+ const parent = join13(cur, "..");
4566
5234
  if (parent === cur) break;
4567
5235
  cur = parent;
4568
5236
  }
@@ -4570,14 +5238,14 @@ function nearestExisting(dir) {
4570
5238
  }
4571
5239
  function freeDiskBytes(dir) {
4572
5240
  try {
4573
- const s = statfsSync(nearestExisting(dir));
5241
+ const s = statfsSync2(nearestExisting(dir));
4574
5242
  return s.bavail * s.bsize;
4575
5243
  } catch {
4576
5244
  return void 0;
4577
5245
  }
4578
5246
  }
4579
5247
  function probeRepo(repoPath) {
4580
- const git = (args) => execFileSync5("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5248
+ const git = (args) => execFileSync6("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4581
5249
  try {
4582
5250
  if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
4583
5251
  return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
@@ -4612,49 +5280,49 @@ function gatherHostFacts(repoPath) {
4612
5280
  }
4613
5281
 
4614
5282
  // src/service.ts
4615
- import { execFileSync as execFileSync6 } from "child_process";
4616
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
4617
- import { homedir as homedir4, platform as osPlatform3, userInfo } from "os";
4618
- import { join as join13 } from "path";
5283
+ import { execFileSync as execFileSync7 } from "child_process";
5284
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
5285
+ import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
5286
+ import { join as join15 } from "path";
4619
5287
 
4620
5288
  // src/state.ts
4621
- import { chmodSync, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
4622
- import { homedir as homedir3 } from "os";
4623
- import { join as join12 } from "path";
5289
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
5290
+ import { homedir as homedir5 } from "os";
5291
+ import { join as join14 } from "path";
4624
5292
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
4625
5293
  var isDesired = (v) => v === "running" || v === "stopped";
4626
5294
  var FILE_MODE = 384;
4627
5295
  var DIR_MODE = 448;
4628
5296
  function stateDir(env) {
4629
- return env.DAHRK_STATE_DIR ?? join12(homedir3(), ".dahrk");
5297
+ return env.DAHRK_STATE_DIR ?? join14(homedir5(), ".dahrk");
4630
5298
  }
4631
5299
  function legacyStateDir(env) {
4632
- return env.DAHRK_STATE_DIR ? void 0 : join12(homedir3(), ".skakel");
5300
+ return env.DAHRK_STATE_DIR ? void 0 : join14(homedir5(), ".skakel");
4633
5301
  }
4634
5302
  function stateFile(env) {
4635
- return join12(stateDir(env), "node.json");
5303
+ return join14(stateDir(env), "node.json");
4636
5304
  }
4637
5305
  function logDir(env) {
4638
- return join12(stateDir(env), "logs");
5306
+ return join14(stateDir(env), "logs");
4639
5307
  }
4640
5308
  function logFiles(env) {
4641
5309
  const dir = logDir(env);
4642
- return { out: join12(dir, "node.out.log"), err: join12(dir, "node.err.log") };
5310
+ return { out: join14(dir, "node.out.log"), err: join14(dir, "node.err.log") };
4643
5311
  }
4644
5312
  function jsonlLogFile(env) {
4645
- return join12(logDir(env), "node.jsonl");
5313
+ return join14(logDir(env), "node.jsonl");
4646
5314
  }
4647
5315
  function crashDir(env) {
4648
- return join12(logDir(env), "crashes");
5316
+ return join14(logDir(env), "crashes");
4649
5317
  }
4650
5318
  function lockFile(env) {
4651
- return join12(stateDir(env), "node.pid");
5319
+ return join14(stateDir(env), "node.pid");
4652
5320
  }
4653
5321
  function setDesired(env, desired) {
4654
5322
  writeState(env, { desired });
4655
5323
  }
4656
5324
  function readState(file) {
4657
- if (!existsSync10(file)) return {};
5325
+ if (!existsSync11(file)) return {};
4658
5326
  try {
4659
5327
  const parsed = JSON.parse(readFileSync8(file, "utf8"));
4660
5328
  const state = {};
@@ -4755,9 +5423,9 @@ ${envEntries}
4755
5423
  <key>ThrottleInterval</key>
4756
5424
  <integer>10</integer>
4757
5425
  <key>StandardOutPath</key>
4758
- <string>${xmlEscape(join13(inputs.logDir, "node.out.log"))}</string>
5426
+ <string>${xmlEscape(join15(inputs.logDir, "node.out.log"))}</string>
4759
5427
  <key>StandardErrorPath</key>
4760
- <string>${xmlEscape(join13(inputs.logDir, "node.err.log"))}</string>
5428
+ <string>${xmlEscape(join15(inputs.logDir, "node.err.log"))}</string>
4761
5429
  </dict>
4762
5430
  </plist>
4763
5431
  `;
@@ -4780,8 +5448,8 @@ Type=simple
4780
5448
  ExecStart=${exec}
4781
5449
  ${envLines}
4782
5450
  WorkingDirectory=${inputs.homeDir}
4783
- StandardOutput=append:${join13(inputs.logDir, "node.out.log")}
4784
- StandardError=append:${join13(inputs.logDir, "node.err.log")}
5451
+ StandardOutput=append:${join15(inputs.logDir, "node.out.log")}
5452
+ StandardError=append:${join15(inputs.logDir, "node.err.log")}
4785
5453
  Restart=on-failure
4786
5454
  RestartSec=3
4787
5455
  RestartPreventExitStatus=78
@@ -4792,7 +5460,7 @@ WantedBy=default.target
4792
5460
  }
4793
5461
  function buildPlan(inputs) {
4794
5462
  if (inputs.manager === "launchd") {
4795
- const filePath2 = join13(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5463
+ const filePath2 = join15(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
4796
5464
  return {
4797
5465
  manager: "launchd",
4798
5466
  label: LAUNCHD_LABEL,
@@ -4809,7 +5477,7 @@ function buildPlan(inputs) {
4809
5477
  logHint: "dahrk logs -f"
4810
5478
  };
4811
5479
  }
4812
- const filePath = join13(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5480
+ const filePath = join15(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
4813
5481
  const user = userInfo().username;
4814
5482
  return {
4815
5483
  manager: "systemd",
@@ -4839,7 +5507,7 @@ function unitIsCurrent(plan, onDisk) {
4839
5507
  return onDisk === plan.content;
4840
5508
  }
4841
5509
  function unitPath(manager, homeDir) {
4842
- return manager === "launchd" ? join13(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join13(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5510
+ return manager === "launchd" ? join15(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join15(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
4843
5511
  }
4844
5512
  function statusCommand(manager) {
4845
5513
  return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
@@ -4975,6 +5643,12 @@ async function runNodeStart(inputs, deps = {}) {
4975
5643
  d.out(" stop: dahrk stop");
4976
5644
  return { kind: "running", code: 0 };
4977
5645
  }
5646
+ var STOP_FOREIGN_NODE = 3;
5647
+ function foreignNodePid(lock, servicePid, isAlive2) {
5648
+ const held = parseLock(lock);
5649
+ if (held === void 0 || held === servicePid) return void 0;
5650
+ return isAlive2(held) ? held : void 0;
5651
+ }
4978
5652
  async function runNodeStop(deps = {}) {
4979
5653
  const d = { ...defaultDeps3(), ...deps };
4980
5654
  const manager = detectManager(d.platform);
@@ -4990,11 +5664,22 @@ async function runNodeStop(deps = {}) {
4990
5664
  if (!d.fileExists(plan.filePath)) {
4991
5665
  d.out("No service installed, so there is nothing to stop.");
4992
5666
  d.out("If you are running a node in a terminal (`dahrk start --foreground`), stop it with Ctrl-C.");
4993
- return 0;
5667
+ return reportForeignNode(d, void 0) ?? 0;
4994
5668
  }
5669
+ const servicePid = parseServiceStatus(manager, true, d.capture(statusCommand(manager))).pid;
4995
5670
  runCommands(plan.stopCommands, d);
4996
5671
  d.out("Node stopped. It will stay stopped across reboots until you run `dahrk start`.");
4997
- return 0;
5672
+ return reportForeignNode(d, servicePid) ?? 0;
5673
+ }
5674
+ function reportForeignNode(d, servicePid) {
5675
+ const pid = foreignNodePid(d.readFile(d.lockFile), servicePid, d.isAlive);
5676
+ if (pid === void 0) return void 0;
5677
+ d.out("");
5678
+ d.out(`WARNING: a node is STILL RUNNING on this host (pid ${pid}). \`dahrk stop\` cannot stop it.`);
5679
+ d.out("Something else is supervising it: pm2, a container, or `dahrk start --foreground` in a terminal.");
5680
+ d.out("It is not a stray copy - it holds this node's identity, so it is still connected to the hub and");
5681
+ d.out("still taking Jobs. Stop it where it was started (e.g. `pm2 delete dahrk-node`), or kill the pid.");
5682
+ return STOP_FOREIGN_NODE;
4998
5683
  }
4999
5684
  async function runServiceUninstall(deps = {}) {
5000
5685
  const d = { ...defaultDeps3(), ...deps };
@@ -5031,7 +5716,7 @@ function dirOf(path) {
5031
5716
  function resolveScriptPath() {
5032
5717
  const argv1 = process.argv[1] ?? "";
5033
5718
  try {
5034
- return realpathSync2(argv1);
5719
+ return realpathSync3(argv1);
5035
5720
  } catch {
5036
5721
  return argv1;
5037
5722
  }
@@ -5045,19 +5730,21 @@ function stableNodeBin(execPath, realpath) {
5045
5730
  }
5046
5731
  var realpathOrUndefined = (p) => {
5047
5732
  try {
5048
- return realpathSync2(p);
5733
+ return realpathSync3(p);
5049
5734
  } catch {
5050
5735
  return void 0;
5051
5736
  }
5052
5737
  };
5053
5738
  var defaultDeps3 = () => ({
5054
5739
  platform: osPlatform3(),
5055
- homeDir: homedir4(),
5740
+ homeDir: homedir6(),
5056
5741
  // Not `process.execPath` raw: that is the versioned Homebrew Cellar path, which the next
5057
5742
  // `brew upgrade node` deletes out from under the unit. See `stableNodeBin`.
5058
5743
  nodeBin: stableNodeBin(process.execPath, realpathOrUndefined),
5059
5744
  scriptPath: resolveScriptPath(),
5060
5745
  logDir: logDir(process.env),
5746
+ lockFile: lockFile(process.env),
5747
+ isAlive,
5061
5748
  // Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
5062
5749
  // npm-global bins) that a supervisor's minimal PATH would otherwise hide.
5063
5750
  pathEnv: process.env.PATH,
@@ -5077,11 +5764,11 @@ var defaultDeps3 = () => ({
5077
5764
  }
5078
5765
  },
5079
5766
  removeFile: (path) => rmSync7(path, { force: true }),
5080
- fileExists: (path) => existsSync11(path),
5767
+ fileExists: (path) => existsSync12(path),
5081
5768
  run: (argv) => {
5082
5769
  const [cmd, ...args] = argv;
5083
5770
  try {
5084
- execFileSync6(cmd, args, { stdio: "inherit" });
5771
+ execFileSync7(cmd, args, { stdio: "inherit" });
5085
5772
  return 0;
5086
5773
  } catch (e) {
5087
5774
  const status = e.status;
@@ -5091,7 +5778,7 @@ var defaultDeps3 = () => ({
5091
5778
  capture: (argv) => {
5092
5779
  const [cmd, ...args] = argv;
5093
5780
  try {
5094
- const stdout = execFileSync6(cmd, args, {
5781
+ const stdout = execFileSync7(cmd, args, {
5095
5782
  encoding: "utf8",
5096
5783
  stdio: ["ignore", "pipe", "ignore"]
5097
5784
  });
@@ -5106,8 +5793,8 @@ var defaultDeps3 = () => ({
5106
5793
  });
5107
5794
 
5108
5795
  // src/update.ts
5109
- import { execFileSync as execFileSync7 } from "child_process";
5110
- import { realpathSync as realpathSync3 } from "fs";
5796
+ import { execFileSync as execFileSync8 } from "child_process";
5797
+ import { realpathSync as realpathSync4 } from "fs";
5111
5798
  var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
5112
5799
  var CHANNEL_COMMANDS = {
5113
5800
  npm: "npm install -g dahrk-node@latest",
@@ -5128,7 +5815,7 @@ function detectChannel(binPath) {
5128
5815
  if (!binPath) return "unknown";
5129
5816
  let resolved = binPath;
5130
5817
  try {
5131
- resolved = realpathSync3(binPath);
5818
+ resolved = realpathSync4(binPath);
5132
5819
  } catch {
5133
5820
  }
5134
5821
  if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
@@ -5216,7 +5903,7 @@ async function fetchLatestVersion(signal) {
5216
5903
  function spawnUpgrade(argv) {
5217
5904
  const [cmd, ...args] = argv;
5218
5905
  try {
5219
- execFileSync7(cmd, args, { stdio: "inherit" });
5906
+ execFileSync8(cmd, args, { stdio: "inherit" });
5220
5907
  return 0;
5221
5908
  } catch (e) {
5222
5909
  const status = e.status;
@@ -5358,7 +6045,7 @@ async function runStatus(inputs, deps) {
5358
6045
  }
5359
6046
 
5360
6047
  // src/main.ts
5361
- var CLIENT_VERSION = "0.1.9";
6048
+ var CLIENT_VERSION = "0.1.11";
5362
6049
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
5363
6050
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
5364
6051
  var RUNTIMES = ["claude-code", "codex", "pi"];
@@ -5370,7 +6057,7 @@ function resolveNodeId(env, opts = {}) {
5370
6057
  if (existing) return existing;
5371
6058
  const legacy = legacyStateDir(env);
5372
6059
  if (legacy) {
5373
- const legacyId = readState(join14(legacy, "node.json")).nodeId;
6060
+ const legacyId = readState(join16(legacy, "node.json")).nodeId;
5374
6061
  if (legacyId) return legacyId;
5375
6062
  }
5376
6063
  const nodeId = randomUUID3();
@@ -5472,7 +6159,8 @@ async function startForeground(env, flags) {
5472
6159
  rotateIfLarge(files.out);
5473
6160
  rotateIfLarge(files.err);
5474
6161
  const nodeId = resolveNodeId(env, { ephemeral: flags.ephemeral });
5475
- const logger = createNodeLoggerFromEnv(env, logDir(env), { nodeId, clientVersion: CLIENT_VERSION });
6162
+ const shipper = new LogShipper({ ceiling: ceilingFromEnv(env) });
6163
+ const logger = createNodeLoggerFromEnv(env, logDir(env), { nodeId, clientVersion: CLIENT_VERSION }, shipperStream(shipper));
5476
6164
  installProcessSafetyNet({
5477
6165
  logger,
5478
6166
  crashDir: crashDir(env),
@@ -5494,6 +6182,7 @@ async function startForeground(env, flags) {
5494
6182
  await startEdgeNode({
5495
6183
  ...buildEdgeOptions(env, resolved),
5496
6184
  logger,
6185
+ shipper,
5497
6186
  ...persist ? {
5498
6187
  onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
5499
6188
  } : {}
@@ -5502,7 +6191,7 @@ async function startForeground(env, flags) {
5502
6191
  }
5503
6192
  async function stop(env) {
5504
6193
  const code = await runNodeStop();
5505
- if (code === 0) setDesired(env, "stopped");
6194
+ if (code === 0 || code === STOP_FOREIGN_NODE) setDesired(env, "stopped");
5506
6195
  return code;
5507
6196
  }
5508
6197
  function updateCheckDeps(env) {
@@ -5549,15 +6238,15 @@ function scheduleUpdateChecks(env) {
5549
6238
  function statusDeps(env) {
5550
6239
  return {
5551
6240
  platform: osPlatform4(),
5552
- homeDir: homedir5(),
6241
+ homeDir: homedir7(),
5553
6242
  env,
5554
6243
  binPath: process.argv[1],
5555
6244
  detectRuntimes: () => resolveRuntimes(env),
5556
- fileExists: (path) => existsSync12(path),
6245
+ fileExists: (path) => existsSync13(path),
5557
6246
  capture: (argv) => {
5558
6247
  const [cmd, ...args] = argv;
5559
6248
  try {
5560
- const stdout = execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
6249
+ const stdout = execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
5561
6250
  return { code: 0, stdout };
5562
6251
  } catch (e) {
5563
6252
  const status = e.status;
@@ -5698,7 +6387,7 @@ var invokedAsEntrypoint = (() => {
5698
6387
  const argv1 = process.argv[1];
5699
6388
  if (!argv1) return false;
5700
6389
  try {
5701
- return pathToFileURL(realpathSync4(argv1)).href === import.meta.url;
6390
+ return pathToFileURL(realpathSync5(argv1)).href === import.meta.url;
5702
6391
  } catch {
5703
6392
  return false;
5704
6393
  }