dahrk-node 0.1.25 → 0.1.27

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 +289 -55
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -245,12 +245,12 @@ function documentsBlock(ctx) {
245
245
  const truncated = body.length > cap;
246
246
  const excerpt = truncated ? body.slice(0, cap) : body;
247
247
  budget -= excerpt.length;
248
- const tail2 = truncated ? `
248
+ const tail3 = truncated ? `
249
249
  ...(truncated; full text at ${path})` : "";
250
250
  const title = doc.title.replace(/[<>"]/g, "'");
251
251
  parts.push(
252
252
  `<document title="${title}" file="${path}">
253
- ${neutraliseDelimiters(excerpt)}${tail2}
253
+ ${neutraliseDelimiters(excerpt)}${tail3}
254
254
  </document>`
255
255
  );
256
256
  if (budget <= 0) break;
@@ -303,6 +303,18 @@ ${neutraliseGateFeedbackDelimiters(content)}
303
303
  ${parts.join("\n")}
304
304
  </gate-feedback>`;
305
305
  }
306
+ function checkFailuresBlock(ctx) {
307
+ const failures = ctx.checkFailures;
308
+ if (!failures) return "";
309
+ const { stageId, attempt, verifications } = failures;
310
+ if (verifications.length === 0) return "";
311
+ const safeStage = stageId.replace(/[^A-Za-z0-9._-]/g, "-");
312
+ const rows = verifications.map((v) => ` ${v.name}: ${v.status === "failed" ? "FAILED" : v.status}`).join("\n");
313
+ return `<check-failures stage="${stageId.replace(/[<>"]/g, "'")}" attempt="${attempt}">
314
+ ${rows}
315
+ Full output: .dahrk/scratch/checks/${safeStage}-${attempt}.md
316
+ </check-failures>`;
317
+ }
306
318
  function resolveStagePrompt(ctx) {
307
319
  const instruction = stageInstruction(ctx);
308
320
  const ticket = ctx.issueContext?.trim() ? `<ticket>
@@ -311,14 +323,15 @@ ${ctx.issueContext.trim()}
311
323
  const guidance = guidanceBlock(ctx);
312
324
  const gateFeedback = gateFeedbackBlock(ctx);
313
325
  const docs = documentsBlock(ctx);
314
- const preamble = [ticket, guidance, gateFeedback, docs].filter(Boolean).join("\n\n");
326
+ const checkFailures = checkFailuresBlock(ctx);
327
+ const preamble = [ticket, guidance, gateFeedback, docs, checkFailures].filter(Boolean).join("\n\n");
315
328
  return preamble ? `${preamble}
316
329
 
317
330
  ${instruction}` : instruction;
318
331
  }
319
332
  function hasSystemPrompt(ctx) {
320
333
  return Boolean(
321
- ctx.config.prompt || ctx.config.promptFile || ctx.issueContext?.trim() || ctx.guidance && ctx.guidance.length > 0 || ctx.gateFeedback && ctx.gateFeedback.length > 0 || ctx.attachedDocuments && ctx.attachedDocuments.length > 0
334
+ ctx.config.prompt || ctx.config.promptFile || ctx.issueContext?.trim() || ctx.guidance && ctx.guidance.length > 0 || ctx.gateFeedback && ctx.gateFeedback.length > 0 || Boolean(ctx.checkFailures) || ctx.attachedDocuments && ctx.attachedDocuments.length > 0
322
335
  );
323
336
  }
324
337
  var OPENING_KICKOFF = "Begin now. Using the ticket context and your instructions already provided, ask the human your first question. Do not wait for further input before sending your first message.";
@@ -1349,14 +1362,16 @@ async function defaultCreatePiSession(ctx) {
1349
1362
  applyApiKeyAuth(hint2, ctx.runtimeEnv, authStorage);
1350
1363
  const modelRegistry = ModelRegistry.create(authStorage, join4(configDir, "models.json"));
1351
1364
  let model;
1352
- if (ctx.config.model) {
1353
- const resolved = resolveCliModel({ cliModel: ctx.config.model, modelRegistry });
1354
- if (!resolved?.error) {
1355
- model = pickAuthedModel(resolved?.model, modelRegistry.getAvailable(), hint2?.defaultModel);
1356
- }
1357
- } else if (hint2?.defaultModel) {
1358
- const resolved = resolveCliModel({ cliModel: hint2.defaultModel, modelRegistry });
1359
- if (!resolved?.error) model = pickAuthedModel(resolved?.model, modelRegistry.getAvailable());
1365
+ try {
1366
+ model = selectStageModel({
1367
+ stageModel: ctx.config.model,
1368
+ profileDefaultModel: hint2?.defaultModel,
1369
+ resolve: (id) => resolveCliModel({ cliModel: id, modelRegistry }),
1370
+ available: () => modelRegistry.getAvailable()
1371
+ });
1372
+ } catch (err) {
1373
+ cleanupStageConfigDir(configDir);
1374
+ throw err;
1360
1375
  }
1361
1376
  const stageComplete = defineTool({
1362
1377
  name: PI_STAGE_COMPLETE_TOOL,
@@ -1452,6 +1467,20 @@ function modelFamily(id) {
1452
1467
  const last = id.split(".").pop() ?? id;
1453
1468
  return last.replace(/-v\d+:\d+$/, "").toLowerCase();
1454
1469
  }
1470
+ function selectStageModel(args) {
1471
+ const requested = args.stageModel ?? args.profileDefaultModel;
1472
+ if (!requested) return void 0;
1473
+ const resolved = args.resolve(requested);
1474
+ if (resolved?.error || !resolved?.model) {
1475
+ const available = args.available() ?? [];
1476
+ const sample = available.slice(0, 12).map((m) => `${m.provider}/${m.id}`);
1477
+ const detail = resolved?.error ? resolved.error : "the model id is not recognised";
1478
+ throw new Error(
1479
+ `Pi cannot resolve the model '${requested}': ${detail}. ` + (available.length ? `Models this stage can authenticate to (${available.length}): ${sample.join(", ")}${available.length > sample.length ? ", ..." : ""}.` : "This stage has no authenticated models at all; check the auth profile bound to its node pool.") + " If the id is newer than this node's pinned Pi, upgrade dahrk-node."
1480
+ );
1481
+ }
1482
+ return pickAuthedModel(resolved.model, args.available(), args.stageModel ? args.profileDefaultModel : void 0);
1483
+ }
1455
1484
  function pickAuthedModel(resolved, available, fallbackModel) {
1456
1485
  if (!resolved || !available?.length) return resolved;
1457
1486
  const providers = new Set(available.map((m) => m.provider));
@@ -2617,6 +2646,169 @@ function runRepoSetup(opts) {
2617
2646
  }
2618
2647
  }
2619
2648
 
2649
+ // ../../packages/executor-worktree/src/check-runner.ts
2650
+ import { spawn } from "child_process";
2651
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
2652
+ var OUTPUT_CAP2 = 16384;
2653
+ function tail2(output) {
2654
+ return output.length > OUTPUT_CAP2 ? output.slice(output.length - OUTPUT_CAP2) : output;
2655
+ }
2656
+ async function runOne(check, cwd, startedAt) {
2657
+ const timeoutMs = (check.timeoutSeconds ?? DEFAULT_TIMEOUT_MS2 / 1e3) * 1e3;
2658
+ return await new Promise((resolve3) => {
2659
+ let chunks = "";
2660
+ let timedOut = false;
2661
+ let settled = false;
2662
+ const child = spawn("sh", ["-c", check.command], { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
2663
+ const append = (buf) => {
2664
+ chunks = tail2(chunks + buf.toString("utf8"));
2665
+ };
2666
+ child.stdout?.on("data", append);
2667
+ child.stderr?.on("data", append);
2668
+ const timer = setTimeout(() => {
2669
+ timedOut = true;
2670
+ child.kill("SIGKILL");
2671
+ }, timeoutMs);
2672
+ const settle = (exitCode) => {
2673
+ if (settled) return;
2674
+ settled = true;
2675
+ clearTimeout(timer);
2676
+ resolve3({
2677
+ name: check.name,
2678
+ command: check.command,
2679
+ exitCode,
2680
+ output: chunks,
2681
+ status: exitCode === 0 ? "passed" : "failed",
2682
+ durationMs: Date.now() - startedAt,
2683
+ timedOut
2684
+ });
2685
+ };
2686
+ child.on("close", (code) => settle(code));
2687
+ child.on("error", (err) => {
2688
+ chunks = tail2(`${chunks}
2689
+ check could not start: ${err.message}`);
2690
+ settle(null);
2691
+ });
2692
+ });
2693
+ }
2694
+ function summariseChecks(outcomes) {
2695
+ const failed = outcomes.filter((o) => o.status === "failed");
2696
+ const skipped = outcomes.filter((o) => o.status === "skipped");
2697
+ if (outcomes.length === 0) return "no checks declared";
2698
+ const suffix = skipped.length > 0 ? `; ${skipped.length} not run` : "";
2699
+ if (failed.length === 0) return `all ${outcomes.length} checks passed${suffix}`;
2700
+ return `${failed.length} of ${outcomes.length} checks failed: ${failed.map((o) => o.name).join(", ")}${suffix}`;
2701
+ }
2702
+ function createCheckRunner(checks, onOutcomes) {
2703
+ let cancelled = false;
2704
+ let lastSummary = "";
2705
+ return {
2706
+ // A check stage runs no agent, so claiming an agent runtime here would corrupt the trace record
2707
+ // the optimiser and the observability CLI both read.
2708
+ runtime: "check",
2709
+ async runBatch(ctx, onTrace) {
2710
+ const cwd = ctx.workspace.worktreePath;
2711
+ const outcomes = [];
2712
+ let seq = 0;
2713
+ for (const check of checks) {
2714
+ if (cancelled) {
2715
+ outcomes.push({
2716
+ name: check.name,
2717
+ command: check.command,
2718
+ exitCode: null,
2719
+ output: "",
2720
+ status: "skipped",
2721
+ durationMs: 0,
2722
+ timedOut: false
2723
+ });
2724
+ continue;
2725
+ }
2726
+ onTrace({
2727
+ seq: seq++,
2728
+ runtime: "check",
2729
+ type: "action",
2730
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2731
+ tool: "check",
2732
+ toolUseId: check.name,
2733
+ input: { name: check.name, command: check.command }
2734
+ });
2735
+ const outcome = await runOne(check, cwd, Date.now());
2736
+ outcomes.push(outcome);
2737
+ onTrace({
2738
+ seq: seq++,
2739
+ runtime: "check",
2740
+ type: "observation",
2741
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2742
+ toolUseId: check.name,
2743
+ isError: outcome.status === "failed",
2744
+ output: {
2745
+ name: outcome.name,
2746
+ exitCode: outcome.exitCode,
2747
+ timedOut: outcome.timedOut,
2748
+ durationMs: outcome.durationMs,
2749
+ output: outcome.output
2750
+ }
2751
+ });
2752
+ }
2753
+ lastSummary = summariseChecks(outcomes);
2754
+ onTrace({
2755
+ seq: seq++,
2756
+ runtime: "check",
2757
+ type: "response",
2758
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2759
+ text: lastSummary
2760
+ });
2761
+ onOutcomes?.(outcomes);
2762
+ const verifications = outcomes.map((o) => ({ name: o.name, status: o.status }));
2763
+ const anyFailed = outcomes.some((o) => o.status === "failed");
2764
+ return {
2765
+ status: cancelled ? "timeout" : anyFailed ? "fail" : "ok",
2766
+ verifications
2767
+ };
2768
+ },
2769
+ async runInteractive() {
2770
+ throw new Error("a check stage cannot be interactive");
2771
+ },
2772
+ /** Deterministic, and free: no model is asked to describe a lint run. */
2773
+ async summarise() {
2774
+ return lastSummary || summariseChecks([]);
2775
+ },
2776
+ async cancel() {
2777
+ cancelled = true;
2778
+ }
2779
+ };
2780
+ }
2781
+ var CHECK_NOTE_CAP_BYTES = 64 * 1024;
2782
+ var safeStageSegment = (stageId) => stageId.replace(/[^A-Za-z0-9._-]/g, "-");
2783
+ function renderCheckNote(stageId, attempt, outcomes) {
2784
+ const failed = outcomes.filter((o) => o.status === "failed");
2785
+ const head = `# checks/${stageId} (attempt ${attempt}) - ${failed.length === 0 ? "PASSED" : "FAILED"}
2786
+
2787
+ ${summariseChecks(outcomes)}
2788
+ `;
2789
+ const ordered = [...outcomes].sort((a, b) => Number(b.status === "failed") - Number(a.status === "failed"));
2790
+ let body = "";
2791
+ let budget = CHECK_NOTE_CAP_BYTES - head.length;
2792
+ for (const o of ordered) {
2793
+ const verdict2 = o.status === "skipped" ? "not run (the stage was cancelled before it started)" : o.timedOut ? `TIMED OUT after ${Math.round(o.durationMs / 1e3)}s` : `exit ${o.exitCode}`;
2794
+ const header = `
2795
+ ## ${o.name} - ${o.status} (${verdict2})
2796
+
2797
+ $ ${o.command}
2798
+ `;
2799
+ const output = o.output.trim();
2800
+ const includeOutput = output.length > 0 && (o.status === "failed" || header.length + output.length < budget);
2801
+ const chunk = includeOutput ? `${header}
2802
+ \`\`\`
2803
+ ${output}
2804
+ \`\`\`
2805
+ ` : header;
2806
+ body += chunk;
2807
+ budget -= chunk.length;
2808
+ }
2809
+ return head + body;
2810
+ }
2811
+
2620
2812
  // ../../packages/executor-worktree/src/index.ts
2621
2813
  function makeRunner(runtime) {
2622
2814
  if ((process.env.DAHRK_RUNNER ?? process.env.SKAKEL_RUNNER ?? "real") === "mock") return createMockRunner(runtime);
@@ -3103,7 +3295,7 @@ import { createHash as createHash4 } from "crypto";
3103
3295
  import { mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
3104
3296
  import { tmpdir as tmpdir5 } from "os";
3105
3297
  import { isAbsolute as isAbsolute3, join as join14, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
3106
- import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
3298
+ import { attachedDocBasename as attachedDocBasename2, isCheckJob } from "@dahrk/contracts";
3107
3299
 
3108
3300
  // ../../packages/edge/src/builtins.ts
3109
3301
  import { execFileSync as execFileSync5 } from "child_process";
@@ -3124,16 +3316,16 @@ function expandPath(raw, cwd) {
3124
3316
  }
3125
3317
  function realish(p) {
3126
3318
  let head = p;
3127
- const tail2 = [];
3319
+ const tail3 = [];
3128
3320
  while (head !== dirname6(head)) {
3129
3321
  if (existsSync8(head)) {
3130
3322
  try {
3131
- return join13(realpathSync2.native(head), ...tail2.reverse());
3323
+ return join13(realpathSync2.native(head), ...tail3.reverse());
3132
3324
  } catch {
3133
3325
  return p;
3134
3326
  }
3135
3327
  }
3136
- tail2.push(head.slice(dirname6(head).length + 1));
3328
+ tail3.push(head.slice(dirname6(head).length + 1));
3137
3329
  head = dirname6(head);
3138
3330
  }
3139
3331
  return p;
@@ -3920,6 +4112,15 @@ function writeGuidance(ref, guidance) {
3920
4112
  } catch {
3921
4113
  }
3922
4114
  }
4115
+ function writeCheckNote(ref, stageId, attempt, outcomes) {
4116
+ if (outcomes.length === 0) return;
4117
+ try {
4118
+ const dir = join14(ref.scratchPath, "checks");
4119
+ mkdirSync8(dir, { recursive: true });
4120
+ writeFileSync8(join14(dir, `${safeStageSegment(stageId)}-${attempt}.md`), renderCheckNote(stageId, attempt, outcomes));
4121
+ } catch {
4122
+ }
4123
+ }
3923
4124
  var ARTIFACT_CAP_BYTES = 64 * 1024;
3924
4125
  var SCRATCH_OUTPUT_DIR = ".dahrk/scratch/output";
3925
4126
  function capContent(raw) {
@@ -4068,7 +4269,10 @@ function createStageRunner(deps) {
4068
4269
  return reaper.reap(reaperDryRun ? { dryRun: true } : {});
4069
4270
  },
4070
4271
  async runJob(job) {
4071
- const { stageId, jobId, runId, agentConfig } = job;
4272
+ const { stageId, jobId, runId } = job;
4273
+ const isCheck = isCheckJob(job);
4274
+ const agentConfig = job.agentConfig;
4275
+ const traceRuntime = isCheck ? "check" : agentConfig.runtime;
4072
4276
  const attempt = attemptOf(jobId);
4073
4277
  if (deps.tenantId && job.tenantId !== deps.tenantId) {
4074
4278
  return {
@@ -4115,12 +4319,12 @@ function createStageRunner(deps) {
4115
4319
  writeIssueContext(ref, job.issueContext);
4116
4320
  writeGuidance(ref, job.guidance);
4117
4321
  writeAttachedDocuments(ref, job.attachedDocuments);
4118
- if (job.agentConfig.emitArtifact) {
4119
- const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
4120
- const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
4322
+ if (agentConfig?.emitArtifact) {
4323
+ const artifactDir = resolveWorktreeRelativePath(ref, agentConfig.emitArtifact);
4324
+ const slash = agentConfig.emitArtifact.lastIndexOf("/");
4121
4325
  if (artifactDir && slash > 0) {
4122
4326
  try {
4123
- mkdirSync8(join14(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
4327
+ mkdirSync8(join14(ref.worktreePath, agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
4124
4328
  } catch {
4125
4329
  }
4126
4330
  }
@@ -4132,16 +4336,16 @@ function createStageRunner(deps) {
4132
4336
  stageId,
4133
4337
  jobId,
4134
4338
  attempt,
4135
- runtime: agentConfig.runtime,
4136
- model: agentConfig.model,
4339
+ runtime: traceRuntime,
4340
+ model: agentConfig?.model,
4137
4341
  sessionId: job.sessionId,
4138
- configDigest: digest2(agentConfig),
4342
+ configDigest: digest2(isCheck ? job.checks : agentConfig),
4139
4343
  startedAt: nowIso2()
4140
4344
  };
4141
4345
  const writer = createTraceWriter(ref.scratchPath, meta);
4142
4346
  const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
4143
4347
  streamEvent(
4144
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
4348
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "attempt-start" })
4145
4349
  );
4146
4350
  const shipFinalTrace = async (finalMeta) => {
4147
4351
  const sink = deps.trace;
@@ -4178,13 +4382,13 @@ function createStageRunner(deps) {
4178
4382
  }
4179
4383
  sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
4180
4384
  };
4181
- const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc, failureClass2) => {
4385
+ const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc, failureClass2, verifications) => {
4182
4386
  active.delete(jobId);
4183
4387
  turnQueues.delete(jobId);
4184
4388
  await gateway?.stop().catch((e) => log.warn({ err: e, jobId }, "mcp gateway: stop failed"));
4185
4389
  gateway = void 0;
4186
4390
  streamEvent(
4187
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
4391
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "stage-exit", status: status2 })
4188
4392
  );
4189
4393
  const endedAt = nowIso2();
4190
4394
  writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
@@ -4201,12 +4405,12 @@ function createStageRunner(deps) {
4201
4405
  );
4202
4406
  lastUsed.set(runId, Date.now());
4203
4407
  await applyRetention(runId).catch((e) => log.warn({ err: e, runId }, "retention: pass failed"));
4204
- const wantsArtifact = agentConfig.emitArtifact !== void 0 || handedBackDoc !== void 0;
4205
- const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig.emitArtifact, handedBackDoc) : void 0;
4408
+ const wantsArtifact = agentConfig?.emitArtifact !== void 0 || handedBackDoc !== void 0;
4409
+ const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig?.emitArtifact, handedBackDoc) : void 0;
4206
4410
  if (status2 === "ok" && wantsArtifact) {
4207
4411
  const detail = resolved ? `source=${resolved.source} path=${resolved.artifact.path} bytes=${resolved.artifact.content.length}` : "no document resolved (declared path, tool handoff, and scratch/changed-file scans all empty)";
4208
4412
  streamEvent(
4209
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "artifact", detail })
4413
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "artifact", detail })
4210
4414
  );
4211
4415
  }
4212
4416
  return {
@@ -4216,7 +4420,12 @@ function createStageRunner(deps) {
4216
4420
  ...sessionId ? { sessionId } : {},
4217
4421
  ...costUsd !== void 0 ? { costUsd } : {},
4218
4422
  ...resolved ? { artifact: resolved.artifact } : {},
4219
- ...failureClass2 ? { failureClass: failureClass2 } : {}
4423
+ ...failureClass2 ? { failureClass: failureClass2 } : {},
4424
+ // DHK-666: `JobResult.verifications` shipped with a contract, a projection fold, a Card
4425
+ // renderer and tests - but this object never forwarded it, which is the entire reason the
4426
+ // field has had no producer. Forwarded for EVERY stage, not just check jobs, so a hook or
4427
+ // an agent-run gate can populate it too.
4428
+ ...verifications?.length ? { verifications } : {}
4220
4429
  };
4221
4430
  };
4222
4431
  if (deps.packCache && job.provision && job.provision.length > 0) {
@@ -4229,30 +4438,30 @@ function createStageRunner(deps) {
4229
4438
  });
4230
4439
  const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
4231
4440
  streamEvent(
4232
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
4441
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "provision", detail })
4233
4442
  );
4234
4443
  const noteText = overlay.warnings.length > 0 ? `${detail}; ${overlay.warnings.join("; ")}` : detail;
4235
4444
  deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: noteText });
4236
4445
  } catch (e) {
4237
4446
  const msg = `component provisioning failed: ${e.message}`;
4238
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: agentConfig.runtime, kind: "provision-failed", message: msg });
4447
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "provision-failed", message: msg });
4239
4448
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
4240
4449
  return finish("fail", `${stageId}: ${msg}`, job.sessionId);
4241
4450
  }
4242
4451
  }
4243
- const setup = job.setup;
4452
+ const setup = job.workspaceRef?.setup;
4244
4453
  if (setup?.command && ref) {
4245
4454
  const outcome = runRepoSetup({ worktreePath: ref.worktreePath, command: setup.command });
4246
4455
  if (outcome.status === "failed") {
4247
4456
  const msg = `repo setup failed (exit ${outcome.exitCode ?? "null"})`;
4248
4457
  const detail2 = outcome.output ? `${msg}: ${outcome.output}` : msg;
4249
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: agentConfig.runtime, kind: "setup-failed", message: detail2 });
4458
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "setup-failed", message: detail2 });
4250
4459
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: detail2 });
4251
4460
  return finish("fail", `${stageId}: ${msg}`, job.sessionId, void 0, void 0, "harness");
4252
4461
  }
4253
4462
  const detail = outcome.status === "cached" ? "setup: cached (already installed)" : `setup: ran (exit 0, ${outcome.output.length} bytes)`;
4254
4463
  streamEvent(
4255
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "setup", detail })
4464
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "setup", detail })
4256
4465
  );
4257
4466
  if (outcome.status === "ran") {
4258
4467
  deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: outcome.output || detail });
@@ -4275,14 +4484,14 @@ function createStageRunner(deps) {
4275
4484
  const rules = [...jobRules, ...deps.rules];
4276
4485
  const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
4277
4486
  if (entry.verdict === "deny") {
4278
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "policy-deny", detail: entry.reason });
4487
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "policy-deny", detail: entry.reason });
4279
4488
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
4280
4489
  }
4281
4490
  let denied = false;
4282
4491
  const surfacedDenyReasons = /* @__PURE__ */ new Set();
4283
4492
  let escapedUnblocked = false;
4284
4493
  const authorisedActions = [];
4285
- const runtime = agentConfig.runtime;
4494
+ const runtime = agentConfig?.runtime;
4286
4495
  const actionKey = (tool3, input) => {
4287
4496
  try {
4288
4497
  return `${tool3}\0${JSON.stringify(input)}`;
@@ -4295,9 +4504,9 @@ function createStageRunner(deps) {
4295
4504
  denied = true;
4296
4505
  const reason = policyReason(verdict2);
4297
4506
  if (toolUseId) {
4298
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
4507
+ streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime: traceRuntime, toolUseId, isError: true, output: { error: reason } }));
4299
4508
  }
4300
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
4509
+ streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "policy-deny", detail: reason }));
4301
4510
  const surfaceKey = `${verdict2.policy}\0${reason}`;
4302
4511
  if (!surfacedDenyReasons.has(surfaceKey)) {
4303
4512
  surfacedDenyReasons.add(surfaceKey);
@@ -4338,19 +4547,28 @@ function createStageRunner(deps) {
4338
4547
  streamEvent(writer.append(event));
4339
4548
  if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
4340
4549
  };
4341
- const mcpServers = agentConfig.mcpServers;
4342
- if (mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
4550
+ const mcpServers = agentConfig?.mcpServers;
4551
+ if (runtime && mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
4343
4552
  gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
4344
4553
  }
4345
- const runner = deps.makeRunner(runtime);
4554
+ let checkOutcomes = [];
4555
+ const runner = isCheck ? (deps.makeCheckRunner ?? createCheckRunner)(job.checks, (o) => {
4556
+ checkOutcomes = o;
4557
+ }) : deps.makeRunner(runtime);
4346
4558
  active.set(jobId, runner);
4347
4559
  const ctx = {
4348
- config: agentConfig,
4560
+ // A check runner reads only `workspace.worktreePath`; the empty config keeps the shared
4561
+ // RunnerContext shape without inventing a runtime the stage does not have.
4562
+ config: agentConfig ?? { runtime: "claude-code" },
4349
4563
  workspace: ref,
4350
4564
  sessionId: job.sessionId,
4351
4565
  ...job.issueContext !== void 0 ? { issueContext: job.issueContext } : {},
4352
4566
  ...job.guidance !== void 0 ? { guidance: job.guidance } : {},
4353
4567
  ...job.gateFeedback !== void 0 ? { gateFeedback: job.gateFeedback } : {},
4568
+ // Why this stage is running again, when it is. Without this passthrough the failing checks
4569
+ // reach the node and stop dead here, and the looped-back agent gets the stage's static prompt
4570
+ // with no hint of what it is meant to fix.
4571
+ ...job.checkFailures !== void 0 ? { checkFailures: job.checkFailures } : {},
4354
4572
  ...job.attachedDocuments !== void 0 ? { attachedDocuments: job.attachedDocuments } : {},
4355
4573
  ...gateway ? { mcpProxyBaseUrl: gateway.baseUrl } : {},
4356
4574
  // brokered inference env for a managed node (no operator login). The runtime adapter
@@ -4380,7 +4598,7 @@ function createStageRunner(deps) {
4380
4598
  })
4381
4599
  } : {}
4382
4600
  };
4383
- const interactive = agentConfig.interaction === "interactive";
4601
+ const interactive = agentConfig?.interaction === "interactive";
4384
4602
  let result;
4385
4603
  let timedOut = false;
4386
4604
  const killMs = Math.max(0, Math.floor((job.timeout ?? 0) * 1e3));
@@ -4391,7 +4609,7 @@ function createStageRunner(deps) {
4391
4609
  let stalled = false;
4392
4610
  let stallTimer;
4393
4611
  const stallSource = agentConfig.stallMs ?? Number(process.env.DAHRK_BATCH_STALL_MS ?? process.env.SKAKEL_BATCH_STALL_MS ?? 3e5);
4394
- const stallMs = interactive ? 0 : Math.max(0, Math.floor(stallSource));
4612
+ const stallMs = interactive || isCheck ? 0 : Math.max(0, Math.floor(stallSource));
4395
4613
  if (stallMs > 0) {
4396
4614
  bumpStall = () => {
4397
4615
  if (stallTimer) clearTimeout(stallTimer);
@@ -4418,7 +4636,7 @@ function createStageRunner(deps) {
4418
4636
  if (status === "ok" && escapedUnblocked) {
4419
4637
  status = "fail";
4420
4638
  const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
4421
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "fs-confine-escape", message: msg });
4639
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "fs-confine-escape", message: msg });
4422
4640
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
4423
4641
  }
4424
4642
  if (status === "ok" && job.hooks && job.hooks.length > 0) {
@@ -4427,7 +4645,7 @@ function createStageRunner(deps) {
4427
4645
  execFileSync6("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
4428
4646
  } catch (e) {
4429
4647
  status = "fail";
4430
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
4648
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
4431
4649
  break;
4432
4650
  }
4433
4651
  }
@@ -4444,7 +4662,16 @@ function createStageRunner(deps) {
4444
4662
  }
4445
4663
  if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
4446
4664
  const failureClass = timedOut || stalled ? "harness" : result.failureClass;
4447
- return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact, failureClass);
4665
+ if (isCheck && ref) writeCheckNote(ref, stageId, attempt, checkOutcomes);
4666
+ return finish(
4667
+ status,
4668
+ summary,
4669
+ result.sessionId ?? job.sessionId,
4670
+ result.costUsd,
4671
+ result.artifact,
4672
+ failureClass,
4673
+ result.verifications
4674
+ );
4448
4675
  } finally {
4449
4676
  inFlight.set(runId, Math.max(0, (inFlight.get(runId) ?? 1) - 1));
4450
4677
  }
@@ -4531,6 +4758,7 @@ function createStageRunner(deps) {
4531
4758
  var ENROLMENT_REJECTED_EXIT_CODE = 78;
4532
4759
  var RECONNECT_BASE_MS = 500;
4533
4760
  var RECONNECT_MAX_MS = 3e4;
4761
+ var NODE_CAPABILITIES = ["check"];
4534
4762
  var PARK_POLL_MS = 6e4;
4535
4763
  function classifyError(e) {
4536
4764
  const msg = e instanceof Error ? e.message : String(e);
@@ -4714,6 +4942,12 @@ async function startEdgeNode(opts) {
4714
4942
  os: osPlatform(),
4715
4943
  arch: osArch(),
4716
4944
  clientVersion: opts.clientVersion ?? "0.0.0",
4945
+ // Non-runtime job kinds this build can execute. The hub REFUSES to place a check job on a node
4946
+ // that does not advertise `check`, and that refusal is a safety gate rather than an optimisation:
4947
+ // `makeRunner()` defaults to Claude rather than erroring on an unknown job shape, so an old client
4948
+ // handed a check job would boot an unbounded write-access agent with the fallback instruction
4949
+ // "Begin the stage.", report ok, and let the run reach deliver green having run no checks.
4950
+ capabilities: NODE_CAPABILITIES,
4717
4951
  // Advertise the resolved worktree base so the hub records each run's real worktree location in
4718
4952
  // the projection instead of an advisory placeholder. Single-sourced from the git service so it
4719
4953
  // always matches where worktrees actually land.
@@ -5046,7 +5280,7 @@ var PROBES = [
5046
5280
  { runtime: "claude-code", cmd: "claude" },
5047
5281
  { runtime: "pi", cmd: "pi" }
5048
5282
  ];
5049
- var DEFAULT_TIMEOUT_MS2 = 5e3;
5283
+ var DEFAULT_TIMEOUT_MS3 = 5e3;
5050
5284
  var DEFAULT_ATTEMPTS = 2;
5051
5285
  function probeOnce(cmd, timeoutMs) {
5052
5286
  return new Promise((resolve3) => {
@@ -5068,14 +5302,14 @@ async function probe(cmd, timeoutMs, attempts) {
5068
5302
  }
5069
5303
  return void 0;
5070
5304
  }
5071
- async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS2, attempts = DEFAULT_ATTEMPTS) {
5305
+ async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS3, attempts = DEFAULT_ATTEMPTS) {
5072
5306
  const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs, attempts)));
5073
5307
  return PROBES.map((p, i) => {
5074
5308
  const version = versions[i];
5075
5309
  return version === void 0 ? { runtime: p.runtime, cmd: p.cmd, installed: false } : { runtime: p.runtime, cmd: p.cmd, installed: true, version };
5076
5310
  });
5077
5311
  }
5078
- async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS2, attempts = DEFAULT_ATTEMPTS) {
5312
+ async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS3, attempts = DEFAULT_ATTEMPTS) {
5079
5313
  const statuses = await probeRuntimeStatuses(timeoutMs, attempts);
5080
5314
  return statuses.filter((s) => s.installed).map((s) => s.runtime);
5081
5315
  }
@@ -6132,7 +6366,7 @@ var defaultLockDeps = (file) => ({
6132
6366
  });
6133
6367
 
6134
6368
  // src/logs.ts
6135
- import { spawn } from "child_process";
6369
+ import { spawn as spawn2 } from "child_process";
6136
6370
  import { copyFileSync, existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync4, truncateSync } from "fs";
6137
6371
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
6138
6372
  function logsCommand(inputs) {
@@ -6227,7 +6461,7 @@ var defaultLogsDeps = (files, jsonlFile) => ({
6227
6461
  readFile: (path) => readFileSync9(path, "utf8"),
6228
6462
  run: (argv) => new Promise((resolve3) => {
6229
6463
  const [cmd, ...args] = argv;
6230
- const child = spawn(cmd, args, { stdio: "inherit" });
6464
+ const child = spawn2(cmd, args, { stdio: "inherit" });
6231
6465
  child.on("error", () => resolve3(1));
6232
6466
  child.on("close", (code) => resolve3(code ?? 0));
6233
6467
  }),
@@ -7509,7 +7743,7 @@ async function runStatus(inputs, deps) {
7509
7743
  }
7510
7744
 
7511
7745
  // src/main.ts
7512
- var CLIENT_VERSION = "0.1.25";
7746
+ var CLIENT_VERSION = "0.1.27";
7513
7747
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
7514
7748
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
7515
7749
  var RUNTIMES2 = ["claude-code", "codex", "pi"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dahrk-node",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "exports": "./dist/main.js",
35
35
  "dependencies": {
36
36
  "@anthropic-ai/claude-agent-sdk": "0.3.183",
37
- "@dahrk/contracts": "^0.7.0",
37
+ "@dahrk/contracts": "^0.8.2",
38
38
  "pino": "^10.3.1",
39
39
  "ws": "^8.18.0",
40
40
  "zod": "^3.23.8"