dahrk-node 0.1.25 → 0.1.26

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 +265 -47
  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.";
@@ -2617,6 +2630,169 @@ function runRepoSetup(opts) {
2617
2630
  }
2618
2631
  }
2619
2632
 
2633
+ // ../../packages/executor-worktree/src/check-runner.ts
2634
+ import { spawn } from "child_process";
2635
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
2636
+ var OUTPUT_CAP2 = 16384;
2637
+ function tail2(output) {
2638
+ return output.length > OUTPUT_CAP2 ? output.slice(output.length - OUTPUT_CAP2) : output;
2639
+ }
2640
+ async function runOne(check, cwd, startedAt) {
2641
+ const timeoutMs = (check.timeoutSeconds ?? DEFAULT_TIMEOUT_MS2 / 1e3) * 1e3;
2642
+ return await new Promise((resolve3) => {
2643
+ let chunks = "";
2644
+ let timedOut = false;
2645
+ let settled = false;
2646
+ const child = spawn("sh", ["-c", check.command], { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
2647
+ const append = (buf) => {
2648
+ chunks = tail2(chunks + buf.toString("utf8"));
2649
+ };
2650
+ child.stdout?.on("data", append);
2651
+ child.stderr?.on("data", append);
2652
+ const timer = setTimeout(() => {
2653
+ timedOut = true;
2654
+ child.kill("SIGKILL");
2655
+ }, timeoutMs);
2656
+ const settle = (exitCode) => {
2657
+ if (settled) return;
2658
+ settled = true;
2659
+ clearTimeout(timer);
2660
+ resolve3({
2661
+ name: check.name,
2662
+ command: check.command,
2663
+ exitCode,
2664
+ output: chunks,
2665
+ status: exitCode === 0 ? "passed" : "failed",
2666
+ durationMs: Date.now() - startedAt,
2667
+ timedOut
2668
+ });
2669
+ };
2670
+ child.on("close", (code) => settle(code));
2671
+ child.on("error", (err) => {
2672
+ chunks = tail2(`${chunks}
2673
+ check could not start: ${err.message}`);
2674
+ settle(null);
2675
+ });
2676
+ });
2677
+ }
2678
+ function summariseChecks(outcomes) {
2679
+ const failed = outcomes.filter((o) => o.status === "failed");
2680
+ const skipped = outcomes.filter((o) => o.status === "skipped");
2681
+ if (outcomes.length === 0) return "no checks declared";
2682
+ const suffix = skipped.length > 0 ? `; ${skipped.length} not run` : "";
2683
+ if (failed.length === 0) return `all ${outcomes.length} checks passed${suffix}`;
2684
+ return `${failed.length} of ${outcomes.length} checks failed: ${failed.map((o) => o.name).join(", ")}${suffix}`;
2685
+ }
2686
+ function createCheckRunner(checks, onOutcomes) {
2687
+ let cancelled = false;
2688
+ let lastSummary = "";
2689
+ return {
2690
+ // A check stage runs no agent, so claiming an agent runtime here would corrupt the trace record
2691
+ // the optimiser and the observability CLI both read.
2692
+ runtime: "check",
2693
+ async runBatch(ctx, onTrace) {
2694
+ const cwd = ctx.workspace.worktreePath;
2695
+ const outcomes = [];
2696
+ let seq = 0;
2697
+ for (const check of checks) {
2698
+ if (cancelled) {
2699
+ outcomes.push({
2700
+ name: check.name,
2701
+ command: check.command,
2702
+ exitCode: null,
2703
+ output: "",
2704
+ status: "skipped",
2705
+ durationMs: 0,
2706
+ timedOut: false
2707
+ });
2708
+ continue;
2709
+ }
2710
+ onTrace({
2711
+ seq: seq++,
2712
+ runtime: "check",
2713
+ type: "action",
2714
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2715
+ tool: "check",
2716
+ toolUseId: check.name,
2717
+ input: { name: check.name, command: check.command }
2718
+ });
2719
+ const outcome = await runOne(check, cwd, Date.now());
2720
+ outcomes.push(outcome);
2721
+ onTrace({
2722
+ seq: seq++,
2723
+ runtime: "check",
2724
+ type: "observation",
2725
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2726
+ toolUseId: check.name,
2727
+ isError: outcome.status === "failed",
2728
+ output: {
2729
+ name: outcome.name,
2730
+ exitCode: outcome.exitCode,
2731
+ timedOut: outcome.timedOut,
2732
+ durationMs: outcome.durationMs,
2733
+ output: outcome.output
2734
+ }
2735
+ });
2736
+ }
2737
+ lastSummary = summariseChecks(outcomes);
2738
+ onTrace({
2739
+ seq: seq++,
2740
+ runtime: "check",
2741
+ type: "response",
2742
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2743
+ text: lastSummary
2744
+ });
2745
+ onOutcomes?.(outcomes);
2746
+ const verifications = outcomes.map((o) => ({ name: o.name, status: o.status }));
2747
+ const anyFailed = outcomes.some((o) => o.status === "failed");
2748
+ return {
2749
+ status: cancelled ? "timeout" : anyFailed ? "fail" : "ok",
2750
+ verifications
2751
+ };
2752
+ },
2753
+ async runInteractive() {
2754
+ throw new Error("a check stage cannot be interactive");
2755
+ },
2756
+ /** Deterministic, and free: no model is asked to describe a lint run. */
2757
+ async summarise() {
2758
+ return lastSummary || summariseChecks([]);
2759
+ },
2760
+ async cancel() {
2761
+ cancelled = true;
2762
+ }
2763
+ };
2764
+ }
2765
+ var CHECK_NOTE_CAP_BYTES = 64 * 1024;
2766
+ var safeStageSegment = (stageId) => stageId.replace(/[^A-Za-z0-9._-]/g, "-");
2767
+ function renderCheckNote(stageId, attempt, outcomes) {
2768
+ const failed = outcomes.filter((o) => o.status === "failed");
2769
+ const head = `# checks/${stageId} (attempt ${attempt}) - ${failed.length === 0 ? "PASSED" : "FAILED"}
2770
+
2771
+ ${summariseChecks(outcomes)}
2772
+ `;
2773
+ const ordered = [...outcomes].sort((a, b) => Number(b.status === "failed") - Number(a.status === "failed"));
2774
+ let body = "";
2775
+ let budget = CHECK_NOTE_CAP_BYTES - head.length;
2776
+ for (const o of ordered) {
2777
+ 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}`;
2778
+ const header = `
2779
+ ## ${o.name} - ${o.status} (${verdict2})
2780
+
2781
+ $ ${o.command}
2782
+ `;
2783
+ const output = o.output.trim();
2784
+ const includeOutput = output.length > 0 && (o.status === "failed" || header.length + output.length < budget);
2785
+ const chunk = includeOutput ? `${header}
2786
+ \`\`\`
2787
+ ${output}
2788
+ \`\`\`
2789
+ ` : header;
2790
+ body += chunk;
2791
+ budget -= chunk.length;
2792
+ }
2793
+ return head + body;
2794
+ }
2795
+
2620
2796
  // ../../packages/executor-worktree/src/index.ts
2621
2797
  function makeRunner(runtime) {
2622
2798
  if ((process.env.DAHRK_RUNNER ?? process.env.SKAKEL_RUNNER ?? "real") === "mock") return createMockRunner(runtime);
@@ -3103,7 +3279,7 @@ import { createHash as createHash4 } from "crypto";
3103
3279
  import { mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
3104
3280
  import { tmpdir as tmpdir5 } from "os";
3105
3281
  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";
3282
+ import { attachedDocBasename as attachedDocBasename2, isCheckJob } from "@dahrk/contracts";
3107
3283
 
3108
3284
  // ../../packages/edge/src/builtins.ts
3109
3285
  import { execFileSync as execFileSync5 } from "child_process";
@@ -3124,16 +3300,16 @@ function expandPath(raw, cwd) {
3124
3300
  }
3125
3301
  function realish(p) {
3126
3302
  let head = p;
3127
- const tail2 = [];
3303
+ const tail3 = [];
3128
3304
  while (head !== dirname6(head)) {
3129
3305
  if (existsSync8(head)) {
3130
3306
  try {
3131
- return join13(realpathSync2.native(head), ...tail2.reverse());
3307
+ return join13(realpathSync2.native(head), ...tail3.reverse());
3132
3308
  } catch {
3133
3309
  return p;
3134
3310
  }
3135
3311
  }
3136
- tail2.push(head.slice(dirname6(head).length + 1));
3312
+ tail3.push(head.slice(dirname6(head).length + 1));
3137
3313
  head = dirname6(head);
3138
3314
  }
3139
3315
  return p;
@@ -3920,6 +4096,15 @@ function writeGuidance(ref, guidance) {
3920
4096
  } catch {
3921
4097
  }
3922
4098
  }
4099
+ function writeCheckNote(ref, stageId, attempt, outcomes) {
4100
+ if (outcomes.length === 0) return;
4101
+ try {
4102
+ const dir = join14(ref.scratchPath, "checks");
4103
+ mkdirSync8(dir, { recursive: true });
4104
+ writeFileSync8(join14(dir, `${safeStageSegment(stageId)}-${attempt}.md`), renderCheckNote(stageId, attempt, outcomes));
4105
+ } catch {
4106
+ }
4107
+ }
3923
4108
  var ARTIFACT_CAP_BYTES = 64 * 1024;
3924
4109
  var SCRATCH_OUTPUT_DIR = ".dahrk/scratch/output";
3925
4110
  function capContent(raw) {
@@ -4068,7 +4253,10 @@ function createStageRunner(deps) {
4068
4253
  return reaper.reap(reaperDryRun ? { dryRun: true } : {});
4069
4254
  },
4070
4255
  async runJob(job) {
4071
- const { stageId, jobId, runId, agentConfig } = job;
4256
+ const { stageId, jobId, runId } = job;
4257
+ const isCheck = isCheckJob(job);
4258
+ const agentConfig = job.agentConfig;
4259
+ const traceRuntime = isCheck ? "check" : agentConfig.runtime;
4072
4260
  const attempt = attemptOf(jobId);
4073
4261
  if (deps.tenantId && job.tenantId !== deps.tenantId) {
4074
4262
  return {
@@ -4115,12 +4303,12 @@ function createStageRunner(deps) {
4115
4303
  writeIssueContext(ref, job.issueContext);
4116
4304
  writeGuidance(ref, job.guidance);
4117
4305
  writeAttachedDocuments(ref, job.attachedDocuments);
4118
- if (job.agentConfig.emitArtifact) {
4119
- const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
4120
- const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
4306
+ if (agentConfig?.emitArtifact) {
4307
+ const artifactDir = resolveWorktreeRelativePath(ref, agentConfig.emitArtifact);
4308
+ const slash = agentConfig.emitArtifact.lastIndexOf("/");
4121
4309
  if (artifactDir && slash > 0) {
4122
4310
  try {
4123
- mkdirSync8(join14(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
4311
+ mkdirSync8(join14(ref.worktreePath, agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
4124
4312
  } catch {
4125
4313
  }
4126
4314
  }
@@ -4132,16 +4320,16 @@ function createStageRunner(deps) {
4132
4320
  stageId,
4133
4321
  jobId,
4134
4322
  attempt,
4135
- runtime: agentConfig.runtime,
4136
- model: agentConfig.model,
4323
+ runtime: traceRuntime,
4324
+ model: agentConfig?.model,
4137
4325
  sessionId: job.sessionId,
4138
- configDigest: digest2(agentConfig),
4326
+ configDigest: digest2(isCheck ? job.checks : agentConfig),
4139
4327
  startedAt: nowIso2()
4140
4328
  };
4141
4329
  const writer = createTraceWriter(ref.scratchPath, meta);
4142
4330
  const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
4143
4331
  streamEvent(
4144
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
4332
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "attempt-start" })
4145
4333
  );
4146
4334
  const shipFinalTrace = async (finalMeta) => {
4147
4335
  const sink = deps.trace;
@@ -4178,13 +4366,13 @@ function createStageRunner(deps) {
4178
4366
  }
4179
4367
  sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
4180
4368
  };
4181
- const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc, failureClass2) => {
4369
+ const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc, failureClass2, verifications) => {
4182
4370
  active.delete(jobId);
4183
4371
  turnQueues.delete(jobId);
4184
4372
  await gateway?.stop().catch((e) => log.warn({ err: e, jobId }, "mcp gateway: stop failed"));
4185
4373
  gateway = void 0;
4186
4374
  streamEvent(
4187
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
4375
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "stage-exit", status: status2 })
4188
4376
  );
4189
4377
  const endedAt = nowIso2();
4190
4378
  writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
@@ -4201,12 +4389,12 @@ function createStageRunner(deps) {
4201
4389
  );
4202
4390
  lastUsed.set(runId, Date.now());
4203
4391
  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;
4392
+ const wantsArtifact = agentConfig?.emitArtifact !== void 0 || handedBackDoc !== void 0;
4393
+ const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig?.emitArtifact, handedBackDoc) : void 0;
4206
4394
  if (status2 === "ok" && wantsArtifact) {
4207
4395
  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
4396
  streamEvent(
4209
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "artifact", detail })
4397
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "artifact", detail })
4210
4398
  );
4211
4399
  }
4212
4400
  return {
@@ -4216,7 +4404,12 @@ function createStageRunner(deps) {
4216
4404
  ...sessionId ? { sessionId } : {},
4217
4405
  ...costUsd !== void 0 ? { costUsd } : {},
4218
4406
  ...resolved ? { artifact: resolved.artifact } : {},
4219
- ...failureClass2 ? { failureClass: failureClass2 } : {}
4407
+ ...failureClass2 ? { failureClass: failureClass2 } : {},
4408
+ // DHK-666: `JobResult.verifications` shipped with a contract, a projection fold, a Card
4409
+ // renderer and tests - but this object never forwarded it, which is the entire reason the
4410
+ // field has had no producer. Forwarded for EVERY stage, not just check jobs, so a hook or
4411
+ // an agent-run gate can populate it too.
4412
+ ...verifications?.length ? { verifications } : {}
4220
4413
  };
4221
4414
  };
4222
4415
  if (deps.packCache && job.provision && job.provision.length > 0) {
@@ -4229,30 +4422,30 @@ function createStageRunner(deps) {
4229
4422
  });
4230
4423
  const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
4231
4424
  streamEvent(
4232
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
4425
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "provision", detail })
4233
4426
  );
4234
4427
  const noteText = overlay.warnings.length > 0 ? `${detail}; ${overlay.warnings.join("; ")}` : detail;
4235
4428
  deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: noteText });
4236
4429
  } catch (e) {
4237
4430
  const msg = `component provisioning failed: ${e.message}`;
4238
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: agentConfig.runtime, kind: "provision-failed", message: msg });
4431
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "provision-failed", message: msg });
4239
4432
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
4240
4433
  return finish("fail", `${stageId}: ${msg}`, job.sessionId);
4241
4434
  }
4242
4435
  }
4243
- const setup = job.setup;
4436
+ const setup = job.workspaceRef?.setup;
4244
4437
  if (setup?.command && ref) {
4245
4438
  const outcome = runRepoSetup({ worktreePath: ref.worktreePath, command: setup.command });
4246
4439
  if (outcome.status === "failed") {
4247
4440
  const msg = `repo setup failed (exit ${outcome.exitCode ?? "null"})`;
4248
4441
  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 });
4442
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "setup-failed", message: detail2 });
4250
4443
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: detail2 });
4251
4444
  return finish("fail", `${stageId}: ${msg}`, job.sessionId, void 0, void 0, "harness");
4252
4445
  }
4253
4446
  const detail = outcome.status === "cached" ? "setup: cached (already installed)" : `setup: ran (exit 0, ${outcome.output.length} bytes)`;
4254
4447
  streamEvent(
4255
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "setup", detail })
4448
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "setup", detail })
4256
4449
  );
4257
4450
  if (outcome.status === "ran") {
4258
4451
  deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: outcome.output || detail });
@@ -4275,14 +4468,14 @@ function createStageRunner(deps) {
4275
4468
  const rules = [...jobRules, ...deps.rules];
4276
4469
  const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
4277
4470
  if (entry.verdict === "deny") {
4278
- writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "policy-deny", detail: entry.reason });
4471
+ writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "policy-deny", detail: entry.reason });
4279
4472
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
4280
4473
  }
4281
4474
  let denied = false;
4282
4475
  const surfacedDenyReasons = /* @__PURE__ */ new Set();
4283
4476
  let escapedUnblocked = false;
4284
4477
  const authorisedActions = [];
4285
- const runtime = agentConfig.runtime;
4478
+ const runtime = agentConfig?.runtime;
4286
4479
  const actionKey = (tool3, input) => {
4287
4480
  try {
4288
4481
  return `${tool3}\0${JSON.stringify(input)}`;
@@ -4295,9 +4488,9 @@ function createStageRunner(deps) {
4295
4488
  denied = true;
4296
4489
  const reason = policyReason(verdict2);
4297
4490
  if (toolUseId) {
4298
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
4491
+ streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime: traceRuntime, toolUseId, isError: true, output: { error: reason } }));
4299
4492
  }
4300
- streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
4493
+ streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: traceRuntime, event: "policy-deny", detail: reason }));
4301
4494
  const surfaceKey = `${verdict2.policy}\0${reason}`;
4302
4495
  if (!surfacedDenyReasons.has(surfaceKey)) {
4303
4496
  surfacedDenyReasons.add(surfaceKey);
@@ -4338,19 +4531,28 @@ function createStageRunner(deps) {
4338
4531
  streamEvent(writer.append(event));
4339
4532
  if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
4340
4533
  };
4341
- const mcpServers = agentConfig.mcpServers;
4342
- if (mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
4534
+ const mcpServers = agentConfig?.mcpServers;
4535
+ if (runtime && mcpServers && mcpServers.length > 0 && runtimeUsesMcpGateway(runtime)) {
4343
4536
  gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
4344
4537
  }
4345
- const runner = deps.makeRunner(runtime);
4538
+ let checkOutcomes = [];
4539
+ const runner = isCheck ? (deps.makeCheckRunner ?? createCheckRunner)(job.checks, (o) => {
4540
+ checkOutcomes = o;
4541
+ }) : deps.makeRunner(runtime);
4346
4542
  active.set(jobId, runner);
4347
4543
  const ctx = {
4348
- config: agentConfig,
4544
+ // A check runner reads only `workspace.worktreePath`; the empty config keeps the shared
4545
+ // RunnerContext shape without inventing a runtime the stage does not have.
4546
+ config: agentConfig ?? { runtime: "claude-code" },
4349
4547
  workspace: ref,
4350
4548
  sessionId: job.sessionId,
4351
4549
  ...job.issueContext !== void 0 ? { issueContext: job.issueContext } : {},
4352
4550
  ...job.guidance !== void 0 ? { guidance: job.guidance } : {},
4353
4551
  ...job.gateFeedback !== void 0 ? { gateFeedback: job.gateFeedback } : {},
4552
+ // Why this stage is running again, when it is. Without this passthrough the failing checks
4553
+ // reach the node and stop dead here, and the looped-back agent gets the stage's static prompt
4554
+ // with no hint of what it is meant to fix.
4555
+ ...job.checkFailures !== void 0 ? { checkFailures: job.checkFailures } : {},
4354
4556
  ...job.attachedDocuments !== void 0 ? { attachedDocuments: job.attachedDocuments } : {},
4355
4557
  ...gateway ? { mcpProxyBaseUrl: gateway.baseUrl } : {},
4356
4558
  // brokered inference env for a managed node (no operator login). The runtime adapter
@@ -4380,7 +4582,7 @@ function createStageRunner(deps) {
4380
4582
  })
4381
4583
  } : {}
4382
4584
  };
4383
- const interactive = agentConfig.interaction === "interactive";
4585
+ const interactive = agentConfig?.interaction === "interactive";
4384
4586
  let result;
4385
4587
  let timedOut = false;
4386
4588
  const killMs = Math.max(0, Math.floor((job.timeout ?? 0) * 1e3));
@@ -4391,7 +4593,7 @@ function createStageRunner(deps) {
4391
4593
  let stalled = false;
4392
4594
  let stallTimer;
4393
4595
  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));
4596
+ const stallMs = interactive || isCheck ? 0 : Math.max(0, Math.floor(stallSource));
4395
4597
  if (stallMs > 0) {
4396
4598
  bumpStall = () => {
4397
4599
  if (stallTimer) clearTimeout(stallTimer);
@@ -4418,7 +4620,7 @@ function createStageRunner(deps) {
4418
4620
  if (status === "ok" && escapedUnblocked) {
4419
4621
  status = "fail";
4420
4622
  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 });
4623
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "fs-confine-escape", message: msg });
4422
4624
  deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
4423
4625
  }
4424
4626
  if (status === "ok" && job.hooks && job.hooks.length > 0) {
@@ -4427,7 +4629,7 @@ function createStageRunner(deps) {
4427
4629
  execFileSync6("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
4428
4630
  } catch (e) {
4429
4631
  status = "fail";
4430
- writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
4632
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: traceRuntime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
4431
4633
  break;
4432
4634
  }
4433
4635
  }
@@ -4444,7 +4646,16 @@ function createStageRunner(deps) {
4444
4646
  }
4445
4647
  if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
4446
4648
  const failureClass = timedOut || stalled ? "harness" : result.failureClass;
4447
- return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact, failureClass);
4649
+ if (isCheck && ref) writeCheckNote(ref, stageId, attempt, checkOutcomes);
4650
+ return finish(
4651
+ status,
4652
+ summary,
4653
+ result.sessionId ?? job.sessionId,
4654
+ result.costUsd,
4655
+ result.artifact,
4656
+ failureClass,
4657
+ result.verifications
4658
+ );
4448
4659
  } finally {
4449
4660
  inFlight.set(runId, Math.max(0, (inFlight.get(runId) ?? 1) - 1));
4450
4661
  }
@@ -4531,6 +4742,7 @@ function createStageRunner(deps) {
4531
4742
  var ENROLMENT_REJECTED_EXIT_CODE = 78;
4532
4743
  var RECONNECT_BASE_MS = 500;
4533
4744
  var RECONNECT_MAX_MS = 3e4;
4745
+ var NODE_CAPABILITIES = ["check"];
4534
4746
  var PARK_POLL_MS = 6e4;
4535
4747
  function classifyError(e) {
4536
4748
  const msg = e instanceof Error ? e.message : String(e);
@@ -4714,6 +4926,12 @@ async function startEdgeNode(opts) {
4714
4926
  os: osPlatform(),
4715
4927
  arch: osArch(),
4716
4928
  clientVersion: opts.clientVersion ?? "0.0.0",
4929
+ // Non-runtime job kinds this build can execute. The hub REFUSES to place a check job on a node
4930
+ // that does not advertise `check`, and that refusal is a safety gate rather than an optimisation:
4931
+ // `makeRunner()` defaults to Claude rather than erroring on an unknown job shape, so an old client
4932
+ // handed a check job would boot an unbounded write-access agent with the fallback instruction
4933
+ // "Begin the stage.", report ok, and let the run reach deliver green having run no checks.
4934
+ capabilities: NODE_CAPABILITIES,
4717
4935
  // Advertise the resolved worktree base so the hub records each run's real worktree location in
4718
4936
  // the projection instead of an advisory placeholder. Single-sourced from the git service so it
4719
4937
  // always matches where worktrees actually land.
@@ -5046,7 +5264,7 @@ var PROBES = [
5046
5264
  { runtime: "claude-code", cmd: "claude" },
5047
5265
  { runtime: "pi", cmd: "pi" }
5048
5266
  ];
5049
- var DEFAULT_TIMEOUT_MS2 = 5e3;
5267
+ var DEFAULT_TIMEOUT_MS3 = 5e3;
5050
5268
  var DEFAULT_ATTEMPTS = 2;
5051
5269
  function probeOnce(cmd, timeoutMs) {
5052
5270
  return new Promise((resolve3) => {
@@ -5068,14 +5286,14 @@ async function probe(cmd, timeoutMs, attempts) {
5068
5286
  }
5069
5287
  return void 0;
5070
5288
  }
5071
- async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS2, attempts = DEFAULT_ATTEMPTS) {
5289
+ async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS3, attempts = DEFAULT_ATTEMPTS) {
5072
5290
  const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs, attempts)));
5073
5291
  return PROBES.map((p, i) => {
5074
5292
  const version = versions[i];
5075
5293
  return version === void 0 ? { runtime: p.runtime, cmd: p.cmd, installed: false } : { runtime: p.runtime, cmd: p.cmd, installed: true, version };
5076
5294
  });
5077
5295
  }
5078
- async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS2, attempts = DEFAULT_ATTEMPTS) {
5296
+ async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS3, attempts = DEFAULT_ATTEMPTS) {
5079
5297
  const statuses = await probeRuntimeStatuses(timeoutMs, attempts);
5080
5298
  return statuses.filter((s) => s.installed).map((s) => s.runtime);
5081
5299
  }
@@ -6132,7 +6350,7 @@ var defaultLockDeps = (file) => ({
6132
6350
  });
6133
6351
 
6134
6352
  // src/logs.ts
6135
- import { spawn } from "child_process";
6353
+ import { spawn as spawn2 } from "child_process";
6136
6354
  import { copyFileSync, existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync4, truncateSync } from "fs";
6137
6355
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
6138
6356
  function logsCommand(inputs) {
@@ -6227,7 +6445,7 @@ var defaultLogsDeps = (files, jsonlFile) => ({
6227
6445
  readFile: (path) => readFileSync9(path, "utf8"),
6228
6446
  run: (argv) => new Promise((resolve3) => {
6229
6447
  const [cmd, ...args] = argv;
6230
- const child = spawn(cmd, args, { stdio: "inherit" });
6448
+ const child = spawn2(cmd, args, { stdio: "inherit" });
6231
6449
  child.on("error", () => resolve3(1));
6232
6450
  child.on("close", (code) => resolve3(code ?? 0));
6233
6451
  }),
@@ -7509,7 +7727,7 @@ async function runStatus(inputs, deps) {
7509
7727
  }
7510
7728
 
7511
7729
  // src/main.ts
7512
- var CLIENT_VERSION = "0.1.25";
7730
+ var CLIENT_VERSION = "0.1.26";
7513
7731
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
7514
7732
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
7515
7733
  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.26",
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"