dahrk-node 0.1.12 → 0.1.14
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 +321 -139
- package/package.json +4 -4
package/dist/main.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
4
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync14, realpathSync as realpathSync5 } from "fs";
|
|
6
6
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7
7
|
import { homedir as homedir7, platform as osPlatform4 } from "os";
|
|
8
|
-
import { basename as basename2, join as
|
|
8
|
+
import { basename as basename2, join as join17 } from "path";
|
|
9
9
|
import { pathToFileURL } from "url";
|
|
10
10
|
|
|
11
11
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -470,13 +470,13 @@ function toElicitQuestion(q) {
|
|
|
470
470
|
${lines.join("\n")}` : q.question;
|
|
471
471
|
return { prompt, options, ...q.multiSelect ? { multiSelect: true } : {} };
|
|
472
472
|
}
|
|
473
|
-
function
|
|
474
|
-
const
|
|
475
|
-
const q
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
(
|
|
479
|
-
return {
|
|
473
|
+
async function askQuestionsSequentially(questions, ask) {
|
|
474
|
+
const answers = [];
|
|
475
|
+
for (const q of questions) {
|
|
476
|
+
answers.push(await ask(toElicitQuestion(q)));
|
|
477
|
+
}
|
|
478
|
+
if (answers.length === 1) return answers[0];
|
|
479
|
+
return answers.map((a, i) => `Q${i + 1} (${questions[i].question}): ${a}`).join("\n");
|
|
480
480
|
}
|
|
481
481
|
function createAskUserQuestionTool(deps) {
|
|
482
482
|
const askTool = tool2(
|
|
@@ -484,7 +484,7 @@ function createAskUserQuestionTool(deps) {
|
|
|
484
484
|
"Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
|
|
485
485
|
{ questions: z2.array(questionSchema).min(1) },
|
|
486
486
|
async (args) => {
|
|
487
|
-
const text = await
|
|
487
|
+
const text = await askQuestionsSequentially(args.questions, deps.ask);
|
|
488
488
|
return { content: [{ type: "text", text }] };
|
|
489
489
|
}
|
|
490
490
|
);
|
|
@@ -1590,6 +1590,7 @@ function createGitService(opts = {}) {
|
|
|
1590
1590
|
gitUrl: spec.gitUrl,
|
|
1591
1591
|
repo: spec.repo ?? spec.repoId,
|
|
1592
1592
|
baseBranch: spec.baseBranch,
|
|
1593
|
+
branch: sanitizeBranchName(spec.branch ?? `dahrk/${spec.runId}`),
|
|
1593
1594
|
worktreePath,
|
|
1594
1595
|
scratchPath: join3(worktreePath, ".skakel", "scratch")
|
|
1595
1596
|
});
|
|
@@ -1730,6 +1731,31 @@ function createGitService(opts = {}) {
|
|
|
1730
1731
|
}
|
|
1731
1732
|
return { headSha, pushed: true, nothingToCommit: !dirty, wipRef };
|
|
1732
1733
|
},
|
|
1734
|
+
async reconcileInterrupted(ref, opts2) {
|
|
1735
|
+
const { worktreePath } = ref;
|
|
1736
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1737
|
+
throw new Error(`worktree missing for reconcile: ${worktreePath}`);
|
|
1738
|
+
}
|
|
1739
|
+
const headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
1740
|
+
const wipRef = sanitizeBranchName(opts2.branch);
|
|
1741
|
+
const { headSha: tailSha, dirty } = commitPending(worktreePath, opts2.message);
|
|
1742
|
+
if (!dirty) return { dirty: false, headSha, pushed: false };
|
|
1743
|
+
git(worktreePath, ["branch", "--force", wipRef, tailSha]);
|
|
1744
|
+
let pushed = false;
|
|
1745
|
+
const auth = opts2.credentialToken ? setupAuth(opts2.credentialToken) : void 0;
|
|
1746
|
+
const remote = opts2.credentialToken ? withTokenUser(ref.gitUrl) : ref.gitUrl;
|
|
1747
|
+
try {
|
|
1748
|
+
git(worktreePath, ["push", "--force", remote, `${tailSha}:refs/heads/${wipRef}`], netEnv(auth?.env));
|
|
1749
|
+
pushed = true;
|
|
1750
|
+
} catch (e) {
|
|
1751
|
+
log.warn(`could not push the preserved tail to ${wipRef}: ${e.message}`);
|
|
1752
|
+
} finally {
|
|
1753
|
+
auth?.cleanup();
|
|
1754
|
+
}
|
|
1755
|
+
git(worktreePath, ["reset", "--hard", headSha]);
|
|
1756
|
+
git(worktreePath, ["clean", "-fd", "--exclude", SCRATCH_DIR]);
|
|
1757
|
+
return { dirty: true, headSha, tailSha, wipRef, pushed };
|
|
1758
|
+
},
|
|
1733
1759
|
async openPrAmbient(ref, opts2) {
|
|
1734
1760
|
const ownerRepo = parseOwnerRepo(ref.gitUrl);
|
|
1735
1761
|
if (!ownerRepo) return { prError: `cannot derive owner/repo from ${ref.gitUrl}` };
|
|
@@ -2110,6 +2136,75 @@ function collectHealth(inputs) {
|
|
|
2110
2136
|
};
|
|
2111
2137
|
}
|
|
2112
2138
|
|
|
2139
|
+
// ../../packages/edge/src/job-ledger.ts
|
|
2140
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2141
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
2142
|
+
var FILE_MODE = 384;
|
|
2143
|
+
var DIR_MODE = 448;
|
|
2144
|
+
function nullJobLedger() {
|
|
2145
|
+
return { all: () => [], stale: () => [], upsert: () => {
|
|
2146
|
+
}, remove: () => {
|
|
2147
|
+
}, clear: () => {
|
|
2148
|
+
} };
|
|
2149
|
+
}
|
|
2150
|
+
var isEntry = (v) => {
|
|
2151
|
+
if (typeof v !== "object" || v === null) return false;
|
|
2152
|
+
const e = v;
|
|
2153
|
+
return typeof e["jobId"] === "string" && !!e["jobId"] && typeof e["runId"] === "string" && (e["kind"] === "stage" || e["kind"] === "push") && typeof e["startedAt"] === "number" && typeof e["nodePid"] === "number";
|
|
2154
|
+
};
|
|
2155
|
+
function fileJobLedger(file, warn = console.warn) {
|
|
2156
|
+
const read = () => {
|
|
2157
|
+
if (!existsSync5(file)) return [];
|
|
2158
|
+
try {
|
|
2159
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
2160
|
+
if (!Array.isArray(parsed)) return [];
|
|
2161
|
+
return parsed.filter(isEntry);
|
|
2162
|
+
} catch {
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const write = (entries) => {
|
|
2167
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2168
|
+
try {
|
|
2169
|
+
mkdirSync5(dirname4(file), { recursive: true, mode: DIR_MODE });
|
|
2170
|
+
writeFileSync5(tmp, `${JSON.stringify(entries, null, 2)}
|
|
2171
|
+
`, { mode: FILE_MODE });
|
|
2172
|
+
chmodSync(tmp, FILE_MODE);
|
|
2173
|
+
renameSync(tmp, file);
|
|
2174
|
+
} catch (e) {
|
|
2175
|
+
warn(`could not persist the job ledger to ${file}: ${e.message}`);
|
|
2176
|
+
try {
|
|
2177
|
+
if (existsSync5(tmp)) unlinkSync(tmp);
|
|
2178
|
+
} catch {
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
return {
|
|
2183
|
+
all: read,
|
|
2184
|
+
stale: (currentPid) => read().filter((e) => e.nodePid !== currentPid),
|
|
2185
|
+
upsert(entry) {
|
|
2186
|
+
write([...read().filter((e) => e.jobId !== entry.jobId), entry]);
|
|
2187
|
+
},
|
|
2188
|
+
remove(jobId) {
|
|
2189
|
+
const next = read().filter((e) => e.jobId !== jobId);
|
|
2190
|
+
write(next);
|
|
2191
|
+
},
|
|
2192
|
+
clear() {
|
|
2193
|
+
write([]);
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
function announceableJobs(entries) {
|
|
2198
|
+
const out = [];
|
|
2199
|
+
for (const e of entries) {
|
|
2200
|
+
if (e.payloadVersion) out.push({ jobId: e.jobId, payloadVersion: e.payloadVersion });
|
|
2201
|
+
}
|
|
2202
|
+
return out;
|
|
2203
|
+
}
|
|
2204
|
+
function jobLedgerFile(stateDir2) {
|
|
2205
|
+
return join8(stateDir2, "jobs.json");
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2113
2208
|
// ../../packages/edge/src/log-shipper.ts
|
|
2114
2209
|
import { shouldShip } from "@dahrk/contracts";
|
|
2115
2210
|
var SHIP_BUFFER_MAX = 500;
|
|
@@ -2210,8 +2305,8 @@ function ceilingFromEnv(env) {
|
|
|
2210
2305
|
}
|
|
2211
2306
|
|
|
2212
2307
|
// ../../packages/edge/src/logger.ts
|
|
2213
|
-
import { closeSync, existsSync as
|
|
2214
|
-
import { join as
|
|
2308
|
+
import { closeSync, existsSync as existsSync6, mkdirSync as mkdirSync6, openSync, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
|
|
2309
|
+
import { join as join9 } from "path";
|
|
2215
2310
|
import pino from "pino";
|
|
2216
2311
|
|
|
2217
2312
|
// ../../packages/edge/src/redact.ts
|
|
@@ -2307,7 +2402,7 @@ var RotatingFile = class {
|
|
|
2307
2402
|
open() {
|
|
2308
2403
|
try {
|
|
2309
2404
|
this.fd = openSync(this.path, "a");
|
|
2310
|
-
this.size =
|
|
2405
|
+
this.size = existsSync6(this.path) ? statSync2(this.path).size : 0;
|
|
2311
2406
|
} catch (e) {
|
|
2312
2407
|
this.disable(e);
|
|
2313
2408
|
}
|
|
@@ -2335,12 +2430,12 @@ var RotatingFile = class {
|
|
|
2335
2430
|
closeSync(this.fd);
|
|
2336
2431
|
this.fd = void 0;
|
|
2337
2432
|
const oldest = `${this.path}.${this.maxFiles}`;
|
|
2338
|
-
if (
|
|
2433
|
+
if (existsSync6(oldest)) rmSync4(oldest, { force: true });
|
|
2339
2434
|
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2340
2435
|
const from = `${this.path}.${i}`;
|
|
2341
|
-
if (
|
|
2436
|
+
if (existsSync6(from)) renameSync2(from, `${this.path}.${i + 1}`);
|
|
2342
2437
|
}
|
|
2343
|
-
if (
|
|
2438
|
+
if (existsSync6(this.path)) renameSync2(this.path, `${this.path}.1`);
|
|
2344
2439
|
this.open();
|
|
2345
2440
|
} catch (e) {
|
|
2346
2441
|
this.disable(e);
|
|
@@ -2400,8 +2495,8 @@ function createNodeLogger(opts = {}) {
|
|
|
2400
2495
|
if (level !== "silent") streams.push({ level, stream: humanSink });
|
|
2401
2496
|
if (opts.dir && fileLevel !== "silent") {
|
|
2402
2497
|
try {
|
|
2403
|
-
|
|
2404
|
-
const file = new RotatingFile(
|
|
2498
|
+
mkdirSync6(opts.dir, { recursive: true, mode: 448 });
|
|
2499
|
+
const file = new RotatingFile(join9(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
|
|
2405
2500
|
streams.push({ level: fileLevel, stream: file });
|
|
2406
2501
|
} catch (e) {
|
|
2407
2502
|
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
@@ -2459,9 +2554,9 @@ function denyToolRule(tool3) {
|
|
|
2459
2554
|
// ../../packages/edge/src/stage-runner.ts
|
|
2460
2555
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2461
2556
|
import { createHash as createHash3 } from "crypto";
|
|
2462
|
-
import { mkdirSync as
|
|
2557
|
+
import { mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2463
2558
|
import { tmpdir as tmpdir4 } from "os";
|
|
2464
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2559
|
+
import { isAbsolute as isAbsolute3, join as join11, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
|
|
2465
2560
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
2466
2561
|
|
|
2467
2562
|
// ../../packages/edge/src/builtins.ts
|
|
@@ -2469,9 +2564,9 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
2469
2564
|
|
|
2470
2565
|
// ../../packages/edge/src/fs-roots.ts
|
|
2471
2566
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
2472
|
-
import { existsSync as
|
|
2567
|
+
import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
|
|
2473
2568
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
|
|
2474
|
-
import { dirname as
|
|
2569
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve, sep as sep2 } from "path";
|
|
2475
2570
|
function isUnder(root, target) {
|
|
2476
2571
|
const rel = relative2(resolve(root), resolve(target));
|
|
2477
2572
|
return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
|
|
@@ -2484,16 +2579,16 @@ function expandPath(raw, cwd) {
|
|
|
2484
2579
|
function realish(p) {
|
|
2485
2580
|
let head = p;
|
|
2486
2581
|
const tail = [];
|
|
2487
|
-
while (head !==
|
|
2488
|
-
if (
|
|
2582
|
+
while (head !== dirname5(head)) {
|
|
2583
|
+
if (existsSync7(head)) {
|
|
2489
2584
|
try {
|
|
2490
|
-
return
|
|
2585
|
+
return join10(realpathSync2.native(head), ...tail.reverse());
|
|
2491
2586
|
} catch {
|
|
2492
2587
|
return p;
|
|
2493
2588
|
}
|
|
2494
2589
|
}
|
|
2495
|
-
tail.push(head.slice(
|
|
2496
|
-
head =
|
|
2590
|
+
tail.push(head.slice(dirname5(head).length + 1));
|
|
2591
|
+
head = dirname5(head);
|
|
2497
2592
|
}
|
|
2498
2593
|
return p;
|
|
2499
2594
|
}
|
|
@@ -2556,26 +2651,26 @@ function computeFsRoots(opts) {
|
|
|
2556
2651
|
"/System",
|
|
2557
2652
|
"/proc",
|
|
2558
2653
|
"/sys",
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2654
|
+
join10(home, ".gitconfig"),
|
|
2655
|
+
join10(home, ".config"),
|
|
2656
|
+
join10(home, ".npmrc"),
|
|
2657
|
+
join10(home, ".cache"),
|
|
2658
|
+
join10(home, "Library", "Caches"),
|
|
2659
|
+
join10(home, "Library", "pnpm"),
|
|
2660
|
+
join10(home, ".local", "share"),
|
|
2661
|
+
join10(home, ".nvm"),
|
|
2662
|
+
join10(home, ".volta"),
|
|
2663
|
+
join10(home, ".asdf"),
|
|
2664
|
+
join10(home, ".cargo"),
|
|
2665
|
+
join10(home, ".rustup"),
|
|
2571
2666
|
...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
|
|
2572
2667
|
].map(realish);
|
|
2573
2668
|
const deny = [
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2669
|
+
join10(home, ".ssh"),
|
|
2670
|
+
join10(home, ".aws"),
|
|
2671
|
+
join10(home, ".gnupg"),
|
|
2672
|
+
join10(home, ".config", "gcloud"),
|
|
2673
|
+
join10(home, "Library", "Keychains"),
|
|
2579
2674
|
"/Volumes",
|
|
2580
2675
|
"/etc/shadow",
|
|
2581
2676
|
"/etc/sudoers"
|
|
@@ -3097,32 +3192,32 @@ var attemptOf = (jobId) => {
|
|
|
3097
3192
|
};
|
|
3098
3193
|
var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
|
|
3099
3194
|
function writeScratchState(ref, job, attempt, status) {
|
|
3100
|
-
const statePath =
|
|
3195
|
+
const statePath = join11(ref.scratchPath, "state.json");
|
|
3101
3196
|
let state;
|
|
3102
3197
|
try {
|
|
3103
|
-
state = JSON.parse(
|
|
3198
|
+
state = JSON.parse(readFileSync5(statePath, "utf8"));
|
|
3104
3199
|
} catch {
|
|
3105
3200
|
state = { runId: job.runId, tenantId: job.tenantId, stages: {} };
|
|
3106
3201
|
}
|
|
3107
3202
|
state.stages[job.stageId] = { currentAttempt: attempt, status };
|
|
3108
|
-
|
|
3109
|
-
|
|
3203
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3204
|
+
writeFileSync6(statePath, JSON.stringify(state, null, 2));
|
|
3110
3205
|
}
|
|
3111
3206
|
function writeIssueContext(ref, issueContext) {
|
|
3112
3207
|
if (issueContext === void 0) return;
|
|
3113
3208
|
try {
|
|
3114
|
-
|
|
3115
|
-
|
|
3209
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3210
|
+
writeFileSync6(join11(ref.scratchPath, "issue.md"), issueContext);
|
|
3116
3211
|
} catch {
|
|
3117
3212
|
}
|
|
3118
3213
|
}
|
|
3119
3214
|
function writeAttachedDocuments(ref, docs) {
|
|
3120
3215
|
if (!docs || docs.length === 0) return;
|
|
3121
3216
|
try {
|
|
3122
|
-
const dir =
|
|
3123
|
-
|
|
3217
|
+
const dir = join11(ref.scratchPath, "docs");
|
|
3218
|
+
mkdirSync7(dir, { recursive: true });
|
|
3124
3219
|
for (const doc of docs) {
|
|
3125
|
-
|
|
3220
|
+
writeFileSync6(join11(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
|
|
3126
3221
|
}
|
|
3127
3222
|
} catch {
|
|
3128
3223
|
}
|
|
@@ -3140,8 +3235,8 @@ ${lines.join("\n")}
|
|
|
3140
3235
|
function writeGuidance(ref, guidance) {
|
|
3141
3236
|
if (!guidance || guidance.length === 0) return;
|
|
3142
3237
|
try {
|
|
3143
|
-
|
|
3144
|
-
|
|
3238
|
+
mkdirSync7(ref.scratchPath, { recursive: true });
|
|
3239
|
+
writeFileSync6(join11(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
|
|
3145
3240
|
} catch {
|
|
3146
3241
|
}
|
|
3147
3242
|
}
|
|
@@ -3167,7 +3262,7 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
3167
3262
|
const path = resolveWorktreeRelativePath(ref, relPath);
|
|
3168
3263
|
if (!path) return void 0;
|
|
3169
3264
|
try {
|
|
3170
|
-
const raw =
|
|
3265
|
+
const raw = readFileSync5(path, "utf8");
|
|
3171
3266
|
return { path: relPath, content: capContent(raw) };
|
|
3172
3267
|
} catch {
|
|
3173
3268
|
return void 0;
|
|
@@ -3175,13 +3270,13 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
3175
3270
|
}
|
|
3176
3271
|
function scanScratchOutput(ref, preferRel) {
|
|
3177
3272
|
try {
|
|
3178
|
-
const names = readdirSync3(
|
|
3273
|
+
const names = readdirSync3(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
|
|
3179
3274
|
(n) => n.toLowerCase().endsWith(".md")
|
|
3180
3275
|
);
|
|
3181
3276
|
if (names.length === 0) return void 0;
|
|
3182
3277
|
const preferBase = preferRel?.split("/").pop();
|
|
3183
3278
|
const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
|
|
3184
|
-
const raw =
|
|
3279
|
+
const raw = readFileSync5(join11(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
|
|
3185
3280
|
if (raw.trim().length === 0) return void 0;
|
|
3186
3281
|
return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
|
|
3187
3282
|
} catch {
|
|
@@ -3201,7 +3296,7 @@ function scanChangedMarkdown(ref) {
|
|
|
3201
3296
|
);
|
|
3202
3297
|
for (const rel of rels) {
|
|
3203
3298
|
try {
|
|
3204
|
-
const raw =
|
|
3299
|
+
const raw = readFileSync5(join11(ref.worktreePath, rel), "utf8");
|
|
3205
3300
|
if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
|
|
3206
3301
|
} catch {
|
|
3207
3302
|
}
|
|
@@ -3324,10 +3419,10 @@ function createStageRunner(deps) {
|
|
|
3324
3419
|
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
3325
3420
|
});
|
|
3326
3421
|
} else {
|
|
3327
|
-
const base = deps.scratchRoot ??
|
|
3328
|
-
const worktreePath =
|
|
3329
|
-
const scratchPath =
|
|
3330
|
-
|
|
3422
|
+
const base = deps.scratchRoot ?? join11(tmpdir4(), "dahrk", "scratch");
|
|
3423
|
+
const worktreePath = join11(base, runId);
|
|
3424
|
+
const scratchPath = join11(worktreePath, ".skakel", "scratch");
|
|
3425
|
+
mkdirSync7(scratchPath, { recursive: true });
|
|
3331
3426
|
ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
|
|
3332
3427
|
scratchOnly.add(runId);
|
|
3333
3428
|
}
|
|
@@ -3342,7 +3437,7 @@ function createStageRunner(deps) {
|
|
|
3342
3437
|
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
3343
3438
|
if (artifactDir && slash > 0) {
|
|
3344
3439
|
try {
|
|
3345
|
-
|
|
3440
|
+
mkdirSync7(join11(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
3346
3441
|
} catch {
|
|
3347
3442
|
}
|
|
3348
3443
|
}
|
|
@@ -3370,8 +3465,8 @@ function createStageRunner(deps) {
|
|
|
3370
3465
|
if (!sink) return;
|
|
3371
3466
|
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
3372
3467
|
try {
|
|
3373
|
-
for (const name of readdirSync3(
|
|
3374
|
-
const bytes =
|
|
3468
|
+
for (const name of readdirSync3(join11(writer.dir, "blobs"))) {
|
|
3469
|
+
const bytes = readFileSync5(join11(writer.dir, "blobs", name));
|
|
3375
3470
|
const { url } = await sink.requestBlobUrl({
|
|
3376
3471
|
...base,
|
|
3377
3472
|
sha256: name,
|
|
@@ -3385,7 +3480,7 @@ function createStageRunner(deps) {
|
|
|
3385
3480
|
}
|
|
3386
3481
|
let archiveKey;
|
|
3387
3482
|
try {
|
|
3388
|
-
const bytes =
|
|
3483
|
+
const bytes = readFileSync5(join11(writer.dir, "trace.jsonl"));
|
|
3389
3484
|
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
3390
3485
|
const { key, url } = await sink.requestBlobUrl({
|
|
3391
3486
|
...base,
|
|
@@ -3771,6 +3866,7 @@ async function startEdgeNode(opts) {
|
|
|
3771
3866
|
const log = opts.logger ?? createNodeLogger({ level: levelFromEnv(process.env) });
|
|
3772
3867
|
const rules = opts.denyTool ? [denyToolRule(opts.denyTool)] : [];
|
|
3773
3868
|
const counters = new HealthCounters();
|
|
3869
|
+
const ledger = opts.jobLedger ?? nullJobLedger();
|
|
3774
3870
|
const shipper = opts.shipper;
|
|
3775
3871
|
const telemetryCeiling = ceilingFromEnv(process.env);
|
|
3776
3872
|
let currentRuntimes = opts.runtimes;
|
|
@@ -3869,6 +3965,39 @@ async function startEdgeNode(opts) {
|
|
|
3869
3965
|
};
|
|
3870
3966
|
const stageRunner = createStageRunner(stageDeps);
|
|
3871
3967
|
stageRunnerRef = stageRunner;
|
|
3968
|
+
const reconcileInterruptedJobs = async () => {
|
|
3969
|
+
const stale = ledger.stale(process.pid);
|
|
3970
|
+
if (!stale.length) return;
|
|
3971
|
+
log.warn({ count: stale.length }, `EDGE_INTERRUPTED:${stale.length} jobs died with the previous process`);
|
|
3972
|
+
for (const entry of stale) {
|
|
3973
|
+
const entryLog = log.child({ runId: entry.runId, jobId: entry.jobId, ...entry.stageId ? { stageId: entry.stageId } : {} });
|
|
3974
|
+
if (!entry.worktreePath || !entry.gitUrl) {
|
|
3975
|
+
entryLog.warn({}, `EDGE_INTERRUPTED_ABANDONED:${entry.jobId} no worktree recorded`);
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
try {
|
|
3979
|
+
const r = await gitService.reconcileInterrupted(
|
|
3980
|
+
{ worktreePath: entry.worktreePath, gitUrl: entry.gitUrl },
|
|
3981
|
+
{
|
|
3982
|
+
message: `wip: work in progress when the node was interrupted (run ${entry.runId})`,
|
|
3983
|
+
branch: `dahrk/wip/${entry.runId}`
|
|
3984
|
+
}
|
|
3985
|
+
);
|
|
3986
|
+
if (r.dirty) {
|
|
3987
|
+
entryLog.warn(
|
|
3988
|
+
{ wipRef: r.wipRef, tailSha: r.tailSha, headSha: r.headSha, pushed: r.pushed },
|
|
3989
|
+
`EDGE_INTERRUPTED_RESET:${entry.jobId} preserved the uncommitted tail on ${r.wipRef}${r.pushed ? "" : " (locally only)"} and reset to ${r.headSha.slice(0, 8)}`
|
|
3990
|
+
);
|
|
3991
|
+
} else {
|
|
3992
|
+
entryLog.info({ headSha: r.headSha }, `EDGE_INTERRUPTED_CLEAN:${entry.jobId} worktree was already clean`);
|
|
3993
|
+
}
|
|
3994
|
+
} catch (e) {
|
|
3995
|
+
entryLog.warn({ err: e }, `EDGE_INTERRUPTED_ERROR:${entry.jobId} ${e.message}`);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
ledger.clear();
|
|
3999
|
+
};
|
|
4000
|
+
await reconcileInterruptedJobs();
|
|
3872
4001
|
void stageRunner.reapWorktrees().then((r) => {
|
|
3873
4002
|
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
3874
4003
|
if (r.reaped.length) {
|
|
@@ -3895,7 +4024,22 @@ async function startEdgeNode(opts) {
|
|
|
3895
4024
|
// Advertise the resolved worktree base so the hub records each run's real worktree location in
|
|
3896
4025
|
// the projection instead of an advisory placeholder. Single-sourced from the git service so it
|
|
3897
4026
|
// always matches where worktrees actually land.
|
|
3898
|
-
worktreesDir: gitService.worktreesDir
|
|
4027
|
+
worktreesDir: gitService.worktreesDir,
|
|
4028
|
+
// What we are running RIGHT NOW (DHK-416), which is what lets a new hub build ADOPT an in-flight
|
|
4029
|
+
// stage rather than duplicate it (DHK-415). Without this the hub cannot tell "this node is midway
|
|
4030
|
+
// through the stage you are about to re-dispatch" from "this node is idle", so a hub roll or a
|
|
4031
|
+
// reconnect re-ran the stage from scratch.
|
|
4032
|
+
//
|
|
4033
|
+
// Always sent, even empty. Per the wire contract an ABSENT list means "unknown" (a node too old to
|
|
4034
|
+
// answer, for which the hub must keep its old re-dispatch behaviour), while an EMPTY list means "I
|
|
4035
|
+
// have nothing in flight" - a positive statement the hub can act on. We always know, so we always
|
|
4036
|
+
// say, and the two must not be conflated.
|
|
4037
|
+
//
|
|
4038
|
+
// Only genuinely-running jobs are in `running`: boot reconciliation has already dropped the entries
|
|
4039
|
+
// whose runner died with the previous process, so we never claim to be running something we are not.
|
|
4040
|
+
// `announceableJobs` drops what we cannot version-stamp - announcing such a job would make the hub
|
|
4041
|
+
// KILL it, not adopt it. See there.
|
|
4042
|
+
inFlightJobs: announceableJobs(running.values())
|
|
3899
4043
|
});
|
|
3900
4044
|
};
|
|
3901
4045
|
let reprobeTimer;
|
|
@@ -3915,7 +4059,15 @@ async function startEdgeNode(opts) {
|
|
|
3915
4059
|
}, opts.runtimeRecheckMs ?? 6e4);
|
|
3916
4060
|
reprobeTimer.unref?.();
|
|
3917
4061
|
}
|
|
3918
|
-
const running = /* @__PURE__ */ new
|
|
4062
|
+
const running = /* @__PURE__ */ new Map();
|
|
4063
|
+
const trackJob = (entry) => {
|
|
4064
|
+
running.set(entry.jobId, entry);
|
|
4065
|
+
ledger.upsert(entry);
|
|
4066
|
+
};
|
|
4067
|
+
const untrackJob = (jobId) => {
|
|
4068
|
+
running.delete(jobId);
|
|
4069
|
+
ledger.remove(jobId);
|
|
4070
|
+
};
|
|
3919
4071
|
const onMessage = async (raw) => {
|
|
3920
4072
|
const msg = decode(raw);
|
|
3921
4073
|
if (msg.type === "welcome") {
|
|
@@ -3984,8 +4136,20 @@ async function startEdgeNode(opts) {
|
|
|
3984
4136
|
pushLog.info({}, `PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
3985
4137
|
return;
|
|
3986
4138
|
}
|
|
3987
|
-
running.add(job2.jobId);
|
|
3988
4139
|
const pushStartedAt = Date.now();
|
|
4140
|
+
trackJob({
|
|
4141
|
+
jobId: job2.jobId,
|
|
4142
|
+
runId: job2.runId,
|
|
4143
|
+
kind: "push",
|
|
4144
|
+
// No `payloadVersion`: `PushJob` does not carry one (DHK-415 added the field to `JobRequest`
|
|
4145
|
+
// only). It is ledgered anyway - boot reconciliation still has to clean its worktree - but it is
|
|
4146
|
+
// deliberately never announced. See `sendHello`.
|
|
4147
|
+
...job2.workspaceRef?.worktreePath ? { worktreePath: job2.workspaceRef.worktreePath } : {},
|
|
4148
|
+
...job2.branch ? { branch: job2.branch } : {},
|
|
4149
|
+
...job2.workspaceRef?.gitUrl ? { gitUrl: job2.workspaceRef.gitUrl } : {},
|
|
4150
|
+
startedAt: pushStartedAt,
|
|
4151
|
+
nodePid: process.pid
|
|
4152
|
+
});
|
|
3989
4153
|
pushLog.info({}, `PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
3990
4154
|
try {
|
|
3991
4155
|
const result = await stageRunner.runPush(job2);
|
|
@@ -4006,7 +4170,7 @@ async function startEdgeNode(opts) {
|
|
|
4006
4170
|
send(frame);
|
|
4007
4171
|
pushLog.error({ err: e, durationMs: Date.now() - pushStartedAt }, `PUSH_ERROR:${job2.runId} ${e.message}`);
|
|
4008
4172
|
} finally {
|
|
4009
|
-
|
|
4173
|
+
untrackJob(job2.jobId);
|
|
4010
4174
|
}
|
|
4011
4175
|
return;
|
|
4012
4176
|
}
|
|
@@ -4030,9 +4194,23 @@ async function startEdgeNode(opts) {
|
|
|
4030
4194
|
jobLog.info({}, `JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
4031
4195
|
return;
|
|
4032
4196
|
}
|
|
4033
|
-
running.add(job.jobId);
|
|
4034
|
-
counters.activeJobs = running.size;
|
|
4035
4197
|
const startedAt = Date.now();
|
|
4198
|
+
trackJob({
|
|
4199
|
+
jobId: job.jobId,
|
|
4200
|
+
runId: job.runId,
|
|
4201
|
+
kind: "stage",
|
|
4202
|
+
stageId: job.stageId,
|
|
4203
|
+
// The hub stamped this on dispatch. Persisting it is what lets a reconnecting node prove, after a
|
|
4204
|
+
// restart, that the job it is announcing was dispatched under a contract the adopting build can
|
|
4205
|
+
// still read. Announced without it, the hub's gate version-rejects the job rather than adopting it.
|
|
4206
|
+
...job.payloadVersion ? { payloadVersion: job.payloadVersion } : {},
|
|
4207
|
+
...job.workspaceRef?.worktreePath ? { worktreePath: job.workspaceRef.worktreePath } : {},
|
|
4208
|
+
...job.workspaceRef?.branch ? { branch: job.workspaceRef.branch } : {},
|
|
4209
|
+
...job.workspaceRef?.gitUrl ? { gitUrl: job.workspaceRef.gitUrl } : {},
|
|
4210
|
+
startedAt,
|
|
4211
|
+
nodePid: process.pid
|
|
4212
|
+
});
|
|
4213
|
+
counters.activeJobs = running.size;
|
|
4036
4214
|
jobLog.info({}, `JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
4037
4215
|
try {
|
|
4038
4216
|
const result = await stageRunner.runJob(job);
|
|
@@ -4054,7 +4232,7 @@ async function startEdgeNode(opts) {
|
|
|
4054
4232
|
counters.recordError(classifyError(e));
|
|
4055
4233
|
jobLog.error({ err: e, durationMs: Date.now() - startedAt }, `JOB_ERROR:${job.stageId} ${e.message}`);
|
|
4056
4234
|
} finally {
|
|
4057
|
-
|
|
4235
|
+
untrackJob(job.jobId);
|
|
4058
4236
|
counters.activeJobs = running.size;
|
|
4059
4237
|
}
|
|
4060
4238
|
};
|
|
@@ -4660,8 +4838,8 @@ function usage(bin, command) {
|
|
|
4660
4838
|
}
|
|
4661
4839
|
|
|
4662
4840
|
// src/diagnose.ts
|
|
4663
|
-
import { existsSync as
|
|
4664
|
-
import { join as
|
|
4841
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
4842
|
+
import { join as join12 } from "path";
|
|
4665
4843
|
var BUNDLE_LOG_LINES = 2e3;
|
|
4666
4844
|
function tailJsonl(raw, n) {
|
|
4667
4845
|
const records = [];
|
|
@@ -4702,7 +4880,7 @@ async function buildBundle(deps) {
|
|
|
4702
4880
|
if (deps.exists(deps.crashDir)) {
|
|
4703
4881
|
for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
4704
4882
|
try {
|
|
4705
|
-
crashes.push(JSON.parse(deps.readFile(
|
|
4883
|
+
crashes.push(JSON.parse(deps.readFile(join12(deps.crashDir, name))));
|
|
4706
4884
|
} catch (e) {
|
|
4707
4885
|
warnings.push(`could not read crash record ${name} (${e.message})`);
|
|
4708
4886
|
}
|
|
@@ -4759,19 +4937,19 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
|
4759
4937
|
...paths,
|
|
4760
4938
|
clientVersion,
|
|
4761
4939
|
...doctor ? { doctor } : {},
|
|
4762
|
-
readFile: (p) =>
|
|
4940
|
+
readFile: (p) => readFileSync6(p, "utf8"),
|
|
4763
4941
|
listDir: (p) => readdirSync4(p),
|
|
4764
|
-
exists: (p) =>
|
|
4942
|
+
exists: (p) => existsSync8(p),
|
|
4765
4943
|
writeFile: (p, content) => {
|
|
4766
|
-
const dir =
|
|
4767
|
-
if (!
|
|
4768
|
-
|
|
4944
|
+
const dir = join12(p, "..");
|
|
4945
|
+
if (!existsSync8(dir)) mkdirSync8(dir, { recursive: true });
|
|
4946
|
+
writeFileSync7(p, content, { mode: 384 });
|
|
4769
4947
|
},
|
|
4770
4948
|
out: (line) => void process.stdout.write(`${line}
|
|
4771
4949
|
`)
|
|
4772
4950
|
});
|
|
4773
4951
|
function defaultBundlePath(cwd, now) {
|
|
4774
|
-
return
|
|
4952
|
+
return join12(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
4775
4953
|
}
|
|
4776
4954
|
|
|
4777
4955
|
// src/doctor.ts
|
|
@@ -4906,8 +5084,8 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
4906
5084
|
}
|
|
4907
5085
|
|
|
4908
5086
|
// src/lock.ts
|
|
4909
|
-
import { existsSync as
|
|
4910
|
-
import { dirname as
|
|
5087
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5088
|
+
import { dirname as dirname6 } from "path";
|
|
4911
5089
|
function parseLock(content) {
|
|
4912
5090
|
if (!content) return void 0;
|
|
4913
5091
|
const pid = Number(content.trim());
|
|
@@ -4939,14 +5117,14 @@ var defaultLockDeps = (file) => ({
|
|
|
4939
5117
|
pid: process.pid,
|
|
4940
5118
|
readFile: (path) => {
|
|
4941
5119
|
try {
|
|
4942
|
-
return
|
|
5120
|
+
return readFileSync7(path, "utf8");
|
|
4943
5121
|
} catch {
|
|
4944
5122
|
return void 0;
|
|
4945
5123
|
}
|
|
4946
5124
|
},
|
|
4947
5125
|
writeFile: (path, content) => {
|
|
4948
|
-
if (!
|
|
4949
|
-
|
|
5126
|
+
if (!existsSync9(dirname6(path))) mkdirSync9(dirname6(path), { recursive: true, mode: 448 });
|
|
5127
|
+
writeFileSync8(path, content);
|
|
4950
5128
|
},
|
|
4951
5129
|
removeFile: (path) => rmSync6(path, { force: true }),
|
|
4952
5130
|
isAlive
|
|
@@ -4954,7 +5132,7 @@ var defaultLockDeps = (file) => ({
|
|
|
4954
5132
|
|
|
4955
5133
|
// src/logs.ts
|
|
4956
5134
|
import { spawn } from "child_process";
|
|
4957
|
-
import { copyFileSync, existsSync as
|
|
5135
|
+
import { copyFileSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync as statSync4, truncateSync } from "fs";
|
|
4958
5136
|
var MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
4959
5137
|
function logsCommand(inputs) {
|
|
4960
5138
|
return [
|
|
@@ -5035,7 +5213,7 @@ async function runStructuredLogs(inputs, deps) {
|
|
|
5035
5213
|
}
|
|
5036
5214
|
function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
5037
5215
|
try {
|
|
5038
|
-
if (!
|
|
5216
|
+
if (!existsSync10(file) || statSync4(file).size <= maxBytes) return;
|
|
5039
5217
|
copyFileSync(file, `${file}.1`);
|
|
5040
5218
|
truncateSync(file, 0);
|
|
5041
5219
|
} catch {
|
|
@@ -5044,8 +5222,8 @@ function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
|
5044
5222
|
var defaultLogsDeps = (files, jsonlFile) => ({
|
|
5045
5223
|
files,
|
|
5046
5224
|
jsonlFile,
|
|
5047
|
-
fileExists: (path) =>
|
|
5048
|
-
readFile: (path) =>
|
|
5225
|
+
fileExists: (path) => existsSync10(path),
|
|
5226
|
+
readFile: (path) => readFileSync8(path, "utf8"),
|
|
5049
5227
|
run: (argv) => new Promise((resolve3) => {
|
|
5050
5228
|
const [cmd, ...args] = argv;
|
|
5051
5229
|
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
@@ -5057,13 +5235,13 @@ var defaultLogsDeps = (files, jsonlFile) => ({
|
|
|
5057
5235
|
});
|
|
5058
5236
|
|
|
5059
5237
|
// src/process-safety.ts
|
|
5060
|
-
import { mkdirSync as
|
|
5061
|
-
import { join as
|
|
5238
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
5239
|
+
import { join as join13 } from "path";
|
|
5062
5240
|
function writeCrashRecord(dir, record) {
|
|
5063
5241
|
try {
|
|
5064
|
-
|
|
5065
|
-
const file =
|
|
5066
|
-
|
|
5242
|
+
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
5243
|
+
const file = join13(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
|
|
5244
|
+
writeFileSync9(file, `${JSON.stringify(record, null, 2)}
|
|
5067
5245
|
`, { mode: 384 });
|
|
5068
5246
|
return file;
|
|
5069
5247
|
} catch {
|
|
@@ -5118,9 +5296,9 @@ function installProcessSafetyNet(opts) {
|
|
|
5118
5296
|
// src/preflight.ts
|
|
5119
5297
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
5120
5298
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5121
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
5299
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync11, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
|
|
5122
5300
|
import { homedir as homedir4 } from "os";
|
|
5123
|
-
import { join as
|
|
5301
|
+
import { join as join14 } from "path";
|
|
5124
5302
|
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
5125
5303
|
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
5126
5304
|
var PREFLIGHT_STAGES = [
|
|
@@ -5257,8 +5435,8 @@ function commandPresent(cmd) {
|
|
|
5257
5435
|
}
|
|
5258
5436
|
function sshKeyPresent() {
|
|
5259
5437
|
try {
|
|
5260
|
-
const dir =
|
|
5261
|
-
if (
|
|
5438
|
+
const dir = join14(homedir4(), ".ssh");
|
|
5439
|
+
if (existsSync11(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
5262
5440
|
} catch {
|
|
5263
5441
|
}
|
|
5264
5442
|
try {
|
|
@@ -5277,12 +5455,12 @@ function writable(dir) {
|
|
|
5277
5455
|
}
|
|
5278
5456
|
}
|
|
5279
5457
|
function worktreeRoot(env) {
|
|
5280
|
-
return env.DAHRK_WORKTREES_DIR ??
|
|
5458
|
+
return env.DAHRK_WORKTREES_DIR ?? join14(env.DAHRK_STATE_DIR ?? join14(homedir4(), ".dahrk"), "worktrees");
|
|
5281
5459
|
}
|
|
5282
5460
|
function nearestExisting(dir) {
|
|
5283
5461
|
let cur = dir;
|
|
5284
|
-
while (!
|
|
5285
|
-
const parent =
|
|
5462
|
+
while (!existsSync11(cur)) {
|
|
5463
|
+
const parent = join14(cur, "..");
|
|
5286
5464
|
if (parent === cur) break;
|
|
5287
5465
|
cur = parent;
|
|
5288
5466
|
}
|
|
@@ -5333,52 +5511,52 @@ function gatherHostFacts(repoPath) {
|
|
|
5333
5511
|
|
|
5334
5512
|
// src/service.ts
|
|
5335
5513
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
5336
|
-
import { chmodSync as
|
|
5514
|
+
import { chmodSync as chmodSync3, existsSync as existsSync13, mkdirSync as mkdirSync12, readFileSync as readFileSync10, realpathSync as realpathSync3, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
|
|
5337
5515
|
import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
|
|
5338
|
-
import { join as
|
|
5516
|
+
import { join as join16 } from "path";
|
|
5339
5517
|
|
|
5340
5518
|
// src/state.ts
|
|
5341
|
-
import { chmodSync, existsSync as
|
|
5519
|
+
import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
5342
5520
|
import { homedir as homedir5 } from "os";
|
|
5343
|
-
import { join as
|
|
5521
|
+
import { join as join15 } from "path";
|
|
5344
5522
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
5345
5523
|
var isRuntime = (v) => RUNTIMES.includes(v);
|
|
5346
5524
|
var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
|
|
5347
5525
|
var isDesired = (v) => v === "running" || v === "stopped";
|
|
5348
|
-
var
|
|
5349
|
-
var
|
|
5526
|
+
var FILE_MODE2 = 384;
|
|
5527
|
+
var DIR_MODE2 = 448;
|
|
5350
5528
|
function stateDir(env) {
|
|
5351
|
-
return env.DAHRK_STATE_DIR ??
|
|
5529
|
+
return env.DAHRK_STATE_DIR ?? join15(homedir5(), ".dahrk");
|
|
5352
5530
|
}
|
|
5353
5531
|
function legacyStateDir(env) {
|
|
5354
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
5532
|
+
return env.DAHRK_STATE_DIR ? void 0 : join15(homedir5(), ".skakel");
|
|
5355
5533
|
}
|
|
5356
5534
|
function stateFile(env) {
|
|
5357
|
-
return
|
|
5535
|
+
return join15(stateDir(env), "node.json");
|
|
5358
5536
|
}
|
|
5359
5537
|
function logDir(env) {
|
|
5360
|
-
return
|
|
5538
|
+
return join15(stateDir(env), "logs");
|
|
5361
5539
|
}
|
|
5362
5540
|
function logFiles(env) {
|
|
5363
5541
|
const dir = logDir(env);
|
|
5364
|
-
return { out:
|
|
5542
|
+
return { out: join15(dir, "node.out.log"), err: join15(dir, "node.err.log") };
|
|
5365
5543
|
}
|
|
5366
5544
|
function jsonlLogFile(env) {
|
|
5367
|
-
return
|
|
5545
|
+
return join15(logDir(env), "node.jsonl");
|
|
5368
5546
|
}
|
|
5369
5547
|
function crashDir(env) {
|
|
5370
|
-
return
|
|
5548
|
+
return join15(logDir(env), "crashes");
|
|
5371
5549
|
}
|
|
5372
5550
|
function lockFile(env) {
|
|
5373
|
-
return
|
|
5551
|
+
return join15(stateDir(env), "node.pid");
|
|
5374
5552
|
}
|
|
5375
5553
|
function setDesired(env, desired) {
|
|
5376
5554
|
writeState(env, { desired });
|
|
5377
5555
|
}
|
|
5378
5556
|
function readState(file) {
|
|
5379
|
-
if (!
|
|
5557
|
+
if (!existsSync12(file)) return {};
|
|
5380
5558
|
try {
|
|
5381
|
-
const parsed = JSON.parse(
|
|
5559
|
+
const parsed = JSON.parse(readFileSync9(file, "utf8"));
|
|
5382
5560
|
const state = {};
|
|
5383
5561
|
for (const key of STRING_FIELDS) {
|
|
5384
5562
|
const value = parsed[key];
|
|
@@ -5398,11 +5576,11 @@ function writeState(env, patch) {
|
|
|
5398
5576
|
const dir = stateDir(env);
|
|
5399
5577
|
const file = stateFile(env);
|
|
5400
5578
|
try {
|
|
5401
|
-
|
|
5579
|
+
mkdirSync11(dir, { recursive: true, mode: DIR_MODE2 });
|
|
5402
5580
|
const next = { ...readState(file), ...patch };
|
|
5403
|
-
|
|
5404
|
-
`, { mode:
|
|
5405
|
-
|
|
5581
|
+
writeFileSync10(file, `${JSON.stringify(next, null, 2)}
|
|
5582
|
+
`, { mode: FILE_MODE2 });
|
|
5583
|
+
chmodSync2(file, FILE_MODE2);
|
|
5406
5584
|
} catch (e) {
|
|
5407
5585
|
console.warn(`could not persist node state to ${file}: ${e.message}`);
|
|
5408
5586
|
}
|
|
@@ -5481,9 +5659,9 @@ ${envEntries}
|
|
|
5481
5659
|
<key>ThrottleInterval</key>
|
|
5482
5660
|
<integer>10</integer>
|
|
5483
5661
|
<key>StandardOutPath</key>
|
|
5484
|
-
<string>${xmlEscape(
|
|
5662
|
+
<string>${xmlEscape(join16(inputs.logDir, "node.out.log"))}</string>
|
|
5485
5663
|
<key>StandardErrorPath</key>
|
|
5486
|
-
<string>${xmlEscape(
|
|
5664
|
+
<string>${xmlEscape(join16(inputs.logDir, "node.err.log"))}</string>
|
|
5487
5665
|
</dict>
|
|
5488
5666
|
</plist>
|
|
5489
5667
|
`;
|
|
@@ -5506,8 +5684,8 @@ Type=simple
|
|
|
5506
5684
|
ExecStart=${exec}
|
|
5507
5685
|
${envLines}
|
|
5508
5686
|
WorkingDirectory=${inputs.homeDir}
|
|
5509
|
-
StandardOutput=append:${
|
|
5510
|
-
StandardError=append:${
|
|
5687
|
+
StandardOutput=append:${join16(inputs.logDir, "node.out.log")}
|
|
5688
|
+
StandardError=append:${join16(inputs.logDir, "node.err.log")}
|
|
5511
5689
|
Restart=on-failure
|
|
5512
5690
|
RestartSec=3
|
|
5513
5691
|
RestartPreventExitStatus=78
|
|
@@ -5518,7 +5696,7 @@ WantedBy=default.target
|
|
|
5518
5696
|
}
|
|
5519
5697
|
function buildPlan(inputs) {
|
|
5520
5698
|
if (inputs.manager === "launchd") {
|
|
5521
|
-
const filePath2 =
|
|
5699
|
+
const filePath2 = join16(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
5522
5700
|
return {
|
|
5523
5701
|
manager: "launchd",
|
|
5524
5702
|
label: LAUNCHD_LABEL,
|
|
@@ -5535,7 +5713,7 @@ function buildPlan(inputs) {
|
|
|
5535
5713
|
logHint: "dahrk logs -f"
|
|
5536
5714
|
};
|
|
5537
5715
|
}
|
|
5538
|
-
const filePath =
|
|
5716
|
+
const filePath = join16(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
5539
5717
|
const user = userInfo().username;
|
|
5540
5718
|
return {
|
|
5541
5719
|
manager: "systemd",
|
|
@@ -5565,7 +5743,7 @@ function unitIsCurrent(plan, onDisk) {
|
|
|
5565
5743
|
return onDisk === plan.content;
|
|
5566
5744
|
}
|
|
5567
5745
|
function unitPath(manager, homeDir) {
|
|
5568
|
-
return manager === "launchd" ?
|
|
5746
|
+
return manager === "launchd" ? join16(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join16(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
5569
5747
|
}
|
|
5570
5748
|
function statusCommand(manager) {
|
|
5571
5749
|
return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
|
|
@@ -5806,23 +5984,23 @@ var defaultDeps3 = () => ({
|
|
|
5806
5984
|
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
5807
5985
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
5808
5986
|
pathEnv: process.env.PATH,
|
|
5809
|
-
mkdirp: (dir) => void
|
|
5987
|
+
mkdirp: (dir) => void mkdirSync12(dir, { recursive: true }),
|
|
5810
5988
|
// The unit's environment block carries the enrolment token, so the file is a secret: write it
|
|
5811
5989
|
// owner-only. `writeFileSync`'s `mode` applies only when it CREATES the file, so chmod explicitly
|
|
5812
5990
|
// too - re-installing over a unit an older client left at 0644 must tighten it, not keep it.
|
|
5813
5991
|
writeFile: (path, content) => {
|
|
5814
|
-
|
|
5815
|
-
|
|
5992
|
+
writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
|
|
5993
|
+
chmodSync3(path, UNIT_FILE_MODE);
|
|
5816
5994
|
},
|
|
5817
5995
|
readFile: (path) => {
|
|
5818
5996
|
try {
|
|
5819
|
-
return
|
|
5997
|
+
return readFileSync10(path, "utf8");
|
|
5820
5998
|
} catch {
|
|
5821
5999
|
return void 0;
|
|
5822
6000
|
}
|
|
5823
6001
|
},
|
|
5824
6002
|
removeFile: (path) => rmSync7(path, { force: true }),
|
|
5825
|
-
fileExists: (path) =>
|
|
6003
|
+
fileExists: (path) => existsSync13(path),
|
|
5826
6004
|
run: (argv) => {
|
|
5827
6005
|
const [cmd, ...args] = argv;
|
|
5828
6006
|
try {
|
|
@@ -6103,7 +6281,7 @@ async function runStatus(inputs, deps) {
|
|
|
6103
6281
|
}
|
|
6104
6282
|
|
|
6105
6283
|
// src/main.ts
|
|
6106
|
-
var CLIENT_VERSION = "0.1.
|
|
6284
|
+
var CLIENT_VERSION = "0.1.14";
|
|
6107
6285
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
6108
6286
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6109
6287
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -6115,7 +6293,7 @@ function resolveNodeId(env, opts = {}) {
|
|
|
6115
6293
|
if (existing) return existing;
|
|
6116
6294
|
const legacy = legacyStateDir(env);
|
|
6117
6295
|
if (legacy) {
|
|
6118
|
-
const legacyId = readState(
|
|
6296
|
+
const legacyId = readState(join17(legacy, "node.json")).nodeId;
|
|
6119
6297
|
if (legacyId) return legacyId;
|
|
6120
6298
|
}
|
|
6121
6299
|
const nodeId = randomUUID3();
|
|
@@ -6258,6 +6436,10 @@ async function startForeground(env, flags) {
|
|
|
6258
6436
|
// (a pinned override never re-probes to something else) and the mock runner path stays stable.
|
|
6259
6437
|
reprobeRuntimes: () => resolveRuntimes(env),
|
|
6260
6438
|
...env.DAHRK_RUNTIME_RECHECK_MS ? { runtimeRecheckMs: Number(env.DAHRK_RUNTIME_RECHECK_MS) } : {},
|
|
6439
|
+
// What this node is running, on disk, so a crash mid-stage does not silently re-run the stage from
|
|
6440
|
+
// scratch (DHK-416). Skipped for an ephemeral node for the same reason it never caches a token: a
|
|
6441
|
+
// one-shot CI node touches no state dir, and has no next boot to recover into.
|
|
6442
|
+
...flags.ephemeral ? {} : { jobLedger: fileJobLedger(jobLedgerFile(stateDir(env))) },
|
|
6261
6443
|
...persist ? {
|
|
6262
6444
|
onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
|
|
6263
6445
|
} : {}
|
|
@@ -6317,7 +6499,7 @@ function statusDeps(env) {
|
|
|
6317
6499
|
env,
|
|
6318
6500
|
binPath: process.argv[1],
|
|
6319
6501
|
detectRuntimes: () => resolveRuntimes(env),
|
|
6320
|
-
fileExists: (path) =>
|
|
6502
|
+
fileExists: (path) => existsSync14(path),
|
|
6321
6503
|
capture: (argv) => {
|
|
6322
6504
|
const [cmd, ...args] = argv;
|
|
6323
6505
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dahrk-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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.3.0",
|
|
38
38
|
"@openai/codex-sdk": "0.141.0",
|
|
39
39
|
"pino": "^10.3.1",
|
|
40
40
|
"ws": "^8.18.0",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.3.5",
|
|
45
45
|
"tsx": "^4.19.0",
|
|
46
|
-
"@dahrk/
|
|
47
|
-
"@dahrk/
|
|
46
|
+
"@dahrk/edge": "0.1.0",
|
|
47
|
+
"@dahrk/executor-worktree": "0.1.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup",
|