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.
- package/dist/main.js +265 -47
- 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
|
|
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)}${
|
|
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
|
|
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
|
|
3303
|
+
const tail3 = [];
|
|
3128
3304
|
while (head !== dirname6(head)) {
|
|
3129
3305
|
if (existsSync8(head)) {
|
|
3130
3306
|
try {
|
|
3131
|
-
return join13(realpathSync2.native(head), ...
|
|
3307
|
+
return join13(realpathSync2.native(head), ...tail3.reverse());
|
|
3132
3308
|
} catch {
|
|
3133
3309
|
return p;
|
|
3134
3310
|
}
|
|
3135
3311
|
}
|
|
3136
|
-
|
|
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
|
|
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 (
|
|
4119
|
-
const artifactDir = resolveWorktreeRelativePath(ref,
|
|
4120
|
-
const slash =
|
|
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,
|
|
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:
|
|
4136
|
-
model: agentConfig
|
|
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:
|
|
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:
|
|
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
|
|
4205
|
-
const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
+
"@dahrk/contracts": "^0.8.2",
|
|
38
38
|
"pino": "^10.3.1",
|
|
39
39
|
"ws": "^8.18.0",
|
|
40
40
|
"zod": "^3.23.8"
|