@brainervirus/workit-mcp 3.0.0 → 5.0.0
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/index.js +2381 -1258
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6620,11 +6620,11 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
6620
6620
|
|
|
6621
6621
|
// packages/workit-mcp/src/index.ts
|
|
6622
6622
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6623
|
-
import
|
|
6623
|
+
import path20 from "node:path";
|
|
6624
6624
|
|
|
6625
6625
|
// packages/workit-mcp/src/server.ts
|
|
6626
6626
|
import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync8 } from "node:fs";
|
|
6627
|
-
import
|
|
6627
|
+
import path19 from "node:path";
|
|
6628
6628
|
|
|
6629
6629
|
// node_modules/zod/v4/core/util.js
|
|
6630
6630
|
function getEnumValues(entries) {
|
|
@@ -16783,9 +16783,9 @@ class StdioServerTransport {
|
|
|
16783
16783
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16784
16784
|
|
|
16785
16785
|
// packages/workit-core/src/core.ts
|
|
16786
|
-
import { spawnSync as
|
|
16786
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
16787
16787
|
import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
|
|
16788
|
-
import
|
|
16788
|
+
import path15 from "node:path";
|
|
16789
16789
|
|
|
16790
16790
|
// packages/workit-core/src/core/task-contract.ts
|
|
16791
16791
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -17625,16 +17625,6 @@ var stateOperations = {
|
|
|
17625
17625
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
17626
17626
|
bundle: exportBundleSchema,
|
|
17627
17627
|
authorityRefs: array(refSchema)
|
|
17628
|
-
}),
|
|
17629
|
-
recover: operation({
|
|
17630
|
-
action: literal("recover"),
|
|
17631
|
-
taskId: id.optional(),
|
|
17632
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
17633
|
-
target: _enum(["task", "workspace"]),
|
|
17634
|
-
expectedBytes: digest,
|
|
17635
|
-
snapshotDigest: digest,
|
|
17636
|
-
reason: text,
|
|
17637
|
-
authorityRefs: array(refSchema)
|
|
17638
17628
|
})
|
|
17639
17629
|
};
|
|
17640
17630
|
var operationSchemas = {
|
|
@@ -17647,10 +17637,17 @@ var operationSchemas = {
|
|
|
17647
17637
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
17648
17638
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
17649
17639
|
};
|
|
17650
|
-
var {
|
|
17640
|
+
var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
|
|
17641
|
+
var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
|
|
17651
17642
|
var advertisedOperationSchemas = {
|
|
17652
|
-
|
|
17653
|
-
|
|
17643
|
+
task: advertised(taskOperations),
|
|
17644
|
+
policy: advertised(policyOperations),
|
|
17645
|
+
evidence: advertised(evidenceOperations),
|
|
17646
|
+
finding: advertised(findingOperations),
|
|
17647
|
+
decision: advertised(decisionOperations),
|
|
17648
|
+
worker: advertised(workerOperations),
|
|
17649
|
+
writer: advertised(writerOperations),
|
|
17650
|
+
state: advertised(stateOperations)
|
|
17654
17651
|
};
|
|
17655
17652
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
17656
17653
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
@@ -17865,29 +17862,10 @@ function requirementId(input) {
|
|
|
17865
17862
|
});
|
|
17866
17863
|
}
|
|
17867
17864
|
// packages/workit-core/src/core/task-store.ts
|
|
17868
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
17869
17865
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17870
|
-
import
|
|
17871
|
-
import
|
|
17872
|
-
|
|
17873
|
-
// packages/workit-core/src/core/package-root.ts
|
|
17874
|
-
import { existsSync } from "node:fs";
|
|
17875
|
-
import path from "node:path";
|
|
17876
|
-
import { fileURLToPath } from "node:url";
|
|
17877
|
-
var packageRoot = () => {
|
|
17878
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
17879
|
-
while (true) {
|
|
17880
|
-
const parent = path.dirname(dir);
|
|
17881
|
-
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
17882
|
-
return dir;
|
|
17883
|
-
dir = parent;
|
|
17884
|
-
}
|
|
17885
|
-
};
|
|
17886
|
-
var assetRoot = () => {
|
|
17887
|
-
const root = packageRoot();
|
|
17888
|
-
const assets = path.join(root, "assets");
|
|
17889
|
-
return existsSync(assets) ? assets : root;
|
|
17890
|
-
};
|
|
17866
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
|
|
17867
|
+
import * as fs7 from "node:fs";
|
|
17868
|
+
import path6 from "node:path";
|
|
17891
17869
|
|
|
17892
17870
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
17893
17871
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -17938,7 +17916,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
17938
17916
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
17939
17917
|
import { randomBytes } from "node:crypto";
|
|
17940
17918
|
import fsSync from "node:fs";
|
|
17941
|
-
import
|
|
17919
|
+
import path from "node:path";
|
|
17942
17920
|
|
|
17943
17921
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
17944
17922
|
import fs from "node:fs";
|
|
@@ -18028,13 +18006,13 @@ ${ownershipToken}
|
|
|
18028
18006
|
return { raw, ownershipToken };
|
|
18029
18007
|
}
|
|
18030
18008
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
18031
|
-
const resolved =
|
|
18032
|
-
const lexicalRelative =
|
|
18033
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
18034
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
18009
|
+
const resolved = path.resolve(lockPath);
|
|
18010
|
+
const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
|
|
18011
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
|
|
18012
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
18035
18013
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
18036
18014
|
}
|
|
18037
|
-
return relative.split(
|
|
18015
|
+
return relative.split(path.sep).join(path.posix.sep);
|
|
18038
18016
|
}
|
|
18039
18017
|
function parseSidecarLockPayload(raw, parser) {
|
|
18040
18018
|
if (parser) {
|
|
@@ -18181,7 +18159,7 @@ function getFsSafeLockConfig() {
|
|
|
18181
18159
|
|
|
18182
18160
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
18183
18161
|
import fs3 from "node:fs";
|
|
18184
|
-
import
|
|
18162
|
+
import path2 from "node:path";
|
|
18185
18163
|
|
|
18186
18164
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
18187
18165
|
function sleepSync(ms) {
|
|
@@ -18272,26 +18250,26 @@ function canonicalLockParentSync(parent) {
|
|
|
18272
18250
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
18273
18251
|
}
|
|
18274
18252
|
function normalizeTargetPath(targetPath) {
|
|
18275
|
-
const resolved =
|
|
18276
|
-
fs3.mkdirSync(
|
|
18253
|
+
const resolved = path2.resolve(targetPath);
|
|
18254
|
+
fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
18277
18255
|
try {
|
|
18278
|
-
return
|
|
18256
|
+
return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
|
|
18279
18257
|
} catch {
|
|
18280
18258
|
return resolved;
|
|
18281
18259
|
}
|
|
18282
18260
|
}
|
|
18283
18261
|
function boundedLockPath(lockPath, lockRoot) {
|
|
18284
|
-
const resolved =
|
|
18262
|
+
const resolved = path2.resolve(lockPath);
|
|
18285
18263
|
if (!lockRoot)
|
|
18286
18264
|
return resolved;
|
|
18287
18265
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
18288
|
-
const parent =
|
|
18266
|
+
const parent = path2.dirname(resolved);
|
|
18289
18267
|
const parentReal = canonicalLockParentSync(parent);
|
|
18290
|
-
const parentRelative =
|
|
18291
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
18268
|
+
const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
|
|
18269
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
|
|
18292
18270
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
18293
18271
|
}
|
|
18294
|
-
return
|
|
18272
|
+
return path2.join(parentReal, path2.basename(resolved));
|
|
18295
18273
|
}
|
|
18296
18274
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
18297
18275
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -18490,10 +18468,311 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
18490
18468
|
}
|
|
18491
18469
|
}
|
|
18492
18470
|
}
|
|
18471
|
+
// packages/workit-core/src/core/package-root.ts
|
|
18472
|
+
import { existsSync } from "node:fs";
|
|
18473
|
+
import path3 from "node:path";
|
|
18474
|
+
import { fileURLToPath } from "node:url";
|
|
18475
|
+
var packageRoot = () => {
|
|
18476
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
18477
|
+
while (true) {
|
|
18478
|
+
const parent = path3.dirname(dir);
|
|
18479
|
+
if (existsSync(path3.join(dir, "package.json")) || parent === dir)
|
|
18480
|
+
return dir;
|
|
18481
|
+
dir = parent;
|
|
18482
|
+
}
|
|
18483
|
+
};
|
|
18484
|
+
var assetRoot = () => {
|
|
18485
|
+
const root = packageRoot();
|
|
18486
|
+
const assets = path3.join(root, "assets");
|
|
18487
|
+
return existsSync(assets) ? assets : root;
|
|
18488
|
+
};
|
|
18489
|
+
|
|
18493
18490
|
// packages/workit-core/src/core/store-lock.ts
|
|
18491
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18492
|
+
import fs5 from "node:fs";
|
|
18493
|
+
import { hostname } from "node:os";
|
|
18494
|
+
|
|
18495
|
+
// packages/workit-core/src/store/paths.ts
|
|
18494
18496
|
import { spawnSync } from "node:child_process";
|
|
18497
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
18495
18498
|
import fs4 from "node:fs";
|
|
18496
|
-
import
|
|
18499
|
+
import path4 from "node:path";
|
|
18500
|
+
var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
18501
|
+
var gitRun = (cwd, args) => {
|
|
18502
|
+
const run = spawnSync("git", args, {
|
|
18503
|
+
cwd,
|
|
18504
|
+
encoding: "utf8",
|
|
18505
|
+
timeout: 20000,
|
|
18506
|
+
killSignal: "SIGKILL",
|
|
18507
|
+
windowsHide: true,
|
|
18508
|
+
env: {
|
|
18509
|
+
...process.env,
|
|
18510
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
18511
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
18512
|
+
LC_ALL: "C",
|
|
18513
|
+
LANGUAGE: "C"
|
|
18514
|
+
}
|
|
18515
|
+
});
|
|
18516
|
+
const error = run.error;
|
|
18517
|
+
return {
|
|
18518
|
+
ok: run.status === 0,
|
|
18519
|
+
stdout: run.stdout ?? "",
|
|
18520
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
18521
|
+
missing: error?.code === "ENOENT"
|
|
18522
|
+
};
|
|
18523
|
+
};
|
|
18524
|
+
var shortHash = (value) => createHash2("sha256").update(value).digest("hex").slice(0, 12);
|
|
18525
|
+
var real = (file) => {
|
|
18526
|
+
try {
|
|
18527
|
+
return fs4.realpathSync.native(file);
|
|
18528
|
+
} catch {
|
|
18529
|
+
return file;
|
|
18530
|
+
}
|
|
18531
|
+
};
|
|
18532
|
+
var DISCOVERY_ENV = [
|
|
18533
|
+
"GIT_DIR",
|
|
18534
|
+
"GIT_COMMON_DIR",
|
|
18535
|
+
"GIT_WORK_TREE",
|
|
18536
|
+
"GIT_CEILING_DIRECTORIES",
|
|
18537
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
18538
|
+
];
|
|
18539
|
+
var isGitDir = (dir) => {
|
|
18540
|
+
try {
|
|
18541
|
+
return fs4.statSync(path4.join(dir, "HEAD")).isFile() && fs4.statSync(path4.join(dir, "objects")).isDirectory() && fs4.statSync(path4.join(dir, "refs")).isDirectory();
|
|
18542
|
+
} catch {
|
|
18543
|
+
return false;
|
|
18544
|
+
}
|
|
18545
|
+
};
|
|
18546
|
+
var reftableRefs = (common) => {
|
|
18547
|
+
try {
|
|
18548
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs4.readFileSync(path4.join(common, "config"), "utf8"));
|
|
18549
|
+
} catch {
|
|
18550
|
+
return false;
|
|
18551
|
+
}
|
|
18552
|
+
};
|
|
18553
|
+
function fastGit(root) {
|
|
18554
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
18555
|
+
return null;
|
|
18556
|
+
let dir = real(root);
|
|
18557
|
+
let device = null;
|
|
18558
|
+
for (;; ) {
|
|
18559
|
+
let stat;
|
|
18560
|
+
try {
|
|
18561
|
+
stat = fs4.statSync(dir);
|
|
18562
|
+
} catch {
|
|
18563
|
+
return null;
|
|
18564
|
+
}
|
|
18565
|
+
if (device !== null && stat.dev !== device)
|
|
18566
|
+
return { kind: "none" };
|
|
18567
|
+
device = stat.dev;
|
|
18568
|
+
const dotGit = path4.join(dir, ".git");
|
|
18569
|
+
let entry = null;
|
|
18570
|
+
try {
|
|
18571
|
+
entry = fs4.statSync(dotGit);
|
|
18572
|
+
} catch {}
|
|
18573
|
+
let gitDir = null;
|
|
18574
|
+
let top = null;
|
|
18575
|
+
if (entry) {
|
|
18576
|
+
top = dir;
|
|
18577
|
+
if (entry.isDirectory())
|
|
18578
|
+
gitDir = dotGit;
|
|
18579
|
+
else if (entry.isFile()) {
|
|
18580
|
+
let text = "";
|
|
18581
|
+
try {
|
|
18582
|
+
text = fs4.readFileSync(dotGit, "utf8");
|
|
18583
|
+
} catch {
|
|
18584
|
+
return null;
|
|
18585
|
+
}
|
|
18586
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
18587
|
+
if (!match)
|
|
18588
|
+
return null;
|
|
18589
|
+
gitDir = path4.resolve(dir, match[1].trim());
|
|
18590
|
+
} else
|
|
18591
|
+
return null;
|
|
18592
|
+
if (!fs4.existsSync(path4.join(gitDir, "HEAD")))
|
|
18593
|
+
return null;
|
|
18594
|
+
} else if (isGitDir(dir))
|
|
18595
|
+
gitDir = dir;
|
|
18596
|
+
if (gitDir) {
|
|
18597
|
+
let common = gitDir;
|
|
18598
|
+
try {
|
|
18599
|
+
common = path4.resolve(gitDir, fs4.readFileSync(path4.join(gitDir, "commondir"), "utf8").trim());
|
|
18600
|
+
} catch {}
|
|
18601
|
+
if (!fs4.existsSync(path4.join(common, "objects")))
|
|
18602
|
+
return null;
|
|
18603
|
+
return {
|
|
18604
|
+
kind: "repo",
|
|
18605
|
+
common: real(common),
|
|
18606
|
+
gitDir: real(gitDir),
|
|
18607
|
+
top: top === null ? null : real(top),
|
|
18608
|
+
reftable: reftableRefs(common)
|
|
18609
|
+
};
|
|
18610
|
+
}
|
|
18611
|
+
const parent = path4.dirname(dir);
|
|
18612
|
+
if (parent === dir)
|
|
18613
|
+
return { kind: "none" };
|
|
18614
|
+
dir = parent;
|
|
18615
|
+
}
|
|
18616
|
+
}
|
|
18617
|
+
function resolveStore(root) {
|
|
18618
|
+
root = path4.resolve(root);
|
|
18619
|
+
let directory = false;
|
|
18620
|
+
try {
|
|
18621
|
+
directory = fs4.statSync(root).isDirectory();
|
|
18622
|
+
} catch {}
|
|
18623
|
+
if (!directory)
|
|
18624
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
18625
|
+
const fast = fastGit(root);
|
|
18626
|
+
if (fast?.kind === "none")
|
|
18627
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
18628
|
+
if (fast?.kind === "repo")
|
|
18629
|
+
return {
|
|
18630
|
+
dir: path4.join(fast.common, "workit"),
|
|
18631
|
+
shared: true,
|
|
18632
|
+
top: fast.top,
|
|
18633
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
18634
|
+
};
|
|
18635
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
18636
|
+
const common = run.stdout.trim();
|
|
18637
|
+
if (run.ok && common) {
|
|
18638
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
18639
|
+
return {
|
|
18640
|
+
dir: path4.join(real(path4.resolve(root, common)), "workit"),
|
|
18641
|
+
shared: true,
|
|
18642
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
18643
|
+
};
|
|
18644
|
+
}
|
|
18645
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
18646
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
18647
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
18648
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
18649
|
+
}
|
|
18650
|
+
var rebasingBranch = (gitDir) => {
|
|
18651
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
18652
|
+
try {
|
|
18653
|
+
const ref = fs4.readFileSync(path4.join(gitDir, state, "head-name"), "utf8").trim();
|
|
18654
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
18655
|
+
if (match)
|
|
18656
|
+
return match[1];
|
|
18657
|
+
} catch {}
|
|
18658
|
+
}
|
|
18659
|
+
return null;
|
|
18660
|
+
};
|
|
18661
|
+
function resolveTaskKey(root, location) {
|
|
18662
|
+
if (!location.shared)
|
|
18663
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
18664
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
18665
|
+
if (location.git && !location.git.reftable) {
|
|
18666
|
+
let head = "";
|
|
18667
|
+
try {
|
|
18668
|
+
head = fs4.readFileSync(path4.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
18669
|
+
} catch {}
|
|
18670
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
18671
|
+
if (ref && ref[1] !== ".invalid")
|
|
18672
|
+
return branchKey(ref[1]);
|
|
18673
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
18674
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
18675
|
+
if (rebasing)
|
|
18676
|
+
return branchKey(rebasing);
|
|
18677
|
+
return {
|
|
18678
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
18679
|
+
kind: "detached",
|
|
18680
|
+
branch: null
|
|
18681
|
+
};
|
|
18682
|
+
}
|
|
18683
|
+
}
|
|
18684
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
18685
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
18686
|
+
if (name && name !== ".invalid")
|
|
18687
|
+
return branchKey(name);
|
|
18688
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
18689
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
18690
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
18691
|
+
if (rebasing)
|
|
18692
|
+
return branchKey(rebasing);
|
|
18693
|
+
}
|
|
18694
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
18695
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
18696
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
18697
|
+
}
|
|
18698
|
+
function checkoutRootOf(root, location) {
|
|
18699
|
+
const resolved = path4.resolve(root);
|
|
18700
|
+
let canonical = resolved;
|
|
18701
|
+
try {
|
|
18702
|
+
canonical = fs4.realpathSync(resolved);
|
|
18703
|
+
} catch {
|
|
18704
|
+
return resolved;
|
|
18705
|
+
}
|
|
18706
|
+
const found = location ?? resolveStore(canonical);
|
|
18707
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
18708
|
+
return canonical;
|
|
18709
|
+
if (real(canonical) === found.top)
|
|
18710
|
+
return canonical;
|
|
18711
|
+
try {
|
|
18712
|
+
return fs4.realpathSync(found.top);
|
|
18713
|
+
} catch {
|
|
18714
|
+
return canonical;
|
|
18715
|
+
}
|
|
18716
|
+
}
|
|
18717
|
+
function branchRenames(location) {
|
|
18718
|
+
if (!location.shared)
|
|
18719
|
+
return [];
|
|
18720
|
+
const files = [];
|
|
18721
|
+
const walk = (dir) => {
|
|
18722
|
+
let entries;
|
|
18723
|
+
try {
|
|
18724
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
18725
|
+
} catch {
|
|
18726
|
+
return;
|
|
18727
|
+
}
|
|
18728
|
+
for (const entry of entries)
|
|
18729
|
+
if (entry.isDirectory())
|
|
18730
|
+
walk(path4.join(dir, entry.name));
|
|
18731
|
+
else
|
|
18732
|
+
files.push(path4.join(dir, entry.name));
|
|
18733
|
+
};
|
|
18734
|
+
walk(path4.join(path4.dirname(location.dir), "logs", "refs", "heads"));
|
|
18735
|
+
if (location.git)
|
|
18736
|
+
files.push(path4.join(location.git.gitDir, "logs", "HEAD"));
|
|
18737
|
+
const seen = new Set;
|
|
18738
|
+
const renames = [];
|
|
18739
|
+
for (const file of files) {
|
|
18740
|
+
let text = "";
|
|
18741
|
+
try {
|
|
18742
|
+
text = fs4.readFileSync(file, "utf8");
|
|
18743
|
+
} catch {
|
|
18744
|
+
continue;
|
|
18745
|
+
}
|
|
18746
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
18747
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
18748
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
18749
|
+
if (seen.has(id))
|
|
18750
|
+
continue;
|
|
18751
|
+
seen.add(id);
|
|
18752
|
+
renames.push(rename);
|
|
18753
|
+
}
|
|
18754
|
+
}
|
|
18755
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
18756
|
+
}
|
|
18757
|
+
function followRenames(key, boundAt, renames) {
|
|
18758
|
+
let current = key;
|
|
18759
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
18760
|
+
for (const rename of renames)
|
|
18761
|
+
if (rename.from === current && rename.at >= since) {
|
|
18762
|
+
current = rename.to;
|
|
18763
|
+
since = rename.at;
|
|
18764
|
+
}
|
|
18765
|
+
return current;
|
|
18766
|
+
}
|
|
18767
|
+
var checkoutSlug = (root) => {
|
|
18768
|
+
let canonical = real(root);
|
|
18769
|
+
if (process.platform === "win32")
|
|
18770
|
+
canonical = canonical.toLowerCase();
|
|
18771
|
+
const base = path4.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
18772
|
+
return `${base}-${shortHash(canonical)}`;
|
|
18773
|
+
};
|
|
18774
|
+
|
|
18775
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
18497
18776
|
var metadataLockSchema = object2({
|
|
18498
18777
|
pid: number2().int().nonnegative().safe(),
|
|
18499
18778
|
processStart: string2().nullable(),
|
|
@@ -18525,10 +18804,6 @@ var parseMetadataLockOrNull = (raw) => {
|
|
|
18525
18804
|
return null;
|
|
18526
18805
|
}
|
|
18527
18806
|
};
|
|
18528
|
-
var sameMetadataLock = (left, right) => {
|
|
18529
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
18530
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
18531
|
-
};
|
|
18532
18807
|
var parseProcStatStart = (stat) => {
|
|
18533
18808
|
const close = stat.lastIndexOf(")");
|
|
18534
18809
|
if (close < 0)
|
|
@@ -18538,14 +18813,14 @@ var parseProcStatStart = (stat) => {
|
|
|
18538
18813
|
var processStartOf = (pid) => {
|
|
18539
18814
|
if (process.platform === "linux") {
|
|
18540
18815
|
try {
|
|
18541
|
-
return parseProcStatStart(
|
|
18816
|
+
return parseProcStatStart(fs5.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
18542
18817
|
} catch {
|
|
18543
18818
|
return null;
|
|
18544
18819
|
}
|
|
18545
18820
|
}
|
|
18546
18821
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
18547
18822
|
try {
|
|
18548
|
-
const run =
|
|
18823
|
+
const run = spawnSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
18549
18824
|
encoding: "utf8",
|
|
18550
18825
|
timeout: 1000
|
|
18551
18826
|
});
|
|
@@ -18559,7 +18834,7 @@ var processStartOf = (pid) => {
|
|
|
18559
18834
|
};
|
|
18560
18835
|
var readTrimmed = (file) => {
|
|
18561
18836
|
try {
|
|
18562
|
-
return
|
|
18837
|
+
return fs5.readFileSync(file, "utf8").trim() || null;
|
|
18563
18838
|
} catch {
|
|
18564
18839
|
return null;
|
|
18565
18840
|
}
|
|
@@ -18570,7 +18845,7 @@ var localLockHost = () => {
|
|
|
18570
18845
|
return cachedLockHost;
|
|
18571
18846
|
let pidns = null;
|
|
18572
18847
|
try {
|
|
18573
|
-
pidns = /\[(\d+)\]/.exec(
|
|
18848
|
+
pidns = /\[(\d+)\]/.exec(fs5.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
18574
18849
|
} catch {}
|
|
18575
18850
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
18576
18851
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -18610,7 +18885,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
|
18610
18885
|
};
|
|
18611
18886
|
var ageOf = (file, nowMs) => {
|
|
18612
18887
|
try {
|
|
18613
|
-
return nowMs -
|
|
18888
|
+
return nowMs - fs5.lstatSync(file).mtimeMs;
|
|
18614
18889
|
} catch {
|
|
18615
18890
|
return null;
|
|
18616
18891
|
}
|
|
@@ -18621,117 +18896,151 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
|
18621
18896
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
18622
18897
|
return false;
|
|
18623
18898
|
try {
|
|
18624
|
-
|
|
18899
|
+
fs5.rmdirSync(guard);
|
|
18625
18900
|
return true;
|
|
18626
18901
|
} catch {
|
|
18627
18902
|
return false;
|
|
18628
18903
|
}
|
|
18629
18904
|
};
|
|
18630
18905
|
|
|
18631
|
-
// packages/workit-core/src/
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18635
|
-
var
|
|
18636
|
-
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
18643
|
-
var indexEntry = (task, file) => {
|
|
18644
|
-
const sessions = [];
|
|
18645
|
-
const intent = hostSession(task.intent.provenance.session);
|
|
18646
|
-
if (intent)
|
|
18647
|
-
sessions.push({ ...intent, workerId: null });
|
|
18648
|
-
for (const worker of task.workers) {
|
|
18649
|
-
const session = hostSession(worker.data.session);
|
|
18650
|
-
if (session)
|
|
18651
|
-
sessions.push({ ...session, workerId: worker.id });
|
|
18906
|
+
// packages/workit-core/src/store/event-log.ts
|
|
18907
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
18908
|
+
import fs6 from "node:fs";
|
|
18909
|
+
import path5 from "node:path";
|
|
18910
|
+
var EVENT_VERSION = 1;
|
|
18911
|
+
|
|
18912
|
+
class LogDamage extends Error {
|
|
18913
|
+
upgrade;
|
|
18914
|
+
constructor(message, upgrade = false) {
|
|
18915
|
+
super(message);
|
|
18916
|
+
this.upgrade = upgrade;
|
|
18652
18917
|
}
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18657
|
-
createdAt: task.createdAt,
|
|
18658
|
-
updatedAt: task.updatedAt,
|
|
18659
|
-
objective: task.intent.data.objective,
|
|
18660
|
-
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
18661
|
-
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
18662
|
-
sessions,
|
|
18663
|
-
file
|
|
18664
|
-
};
|
|
18665
|
-
};
|
|
18666
|
-
var fileSignature = (file) => {
|
|
18918
|
+
}
|
|
18919
|
+
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18920
|
+
var parseEvent = (line, file, offset) => {
|
|
18921
|
+
let value;
|
|
18667
18922
|
try {
|
|
18668
|
-
|
|
18669
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
18923
|
+
value = JSON.parse(line);
|
|
18670
18924
|
} catch {
|
|
18671
|
-
|
|
18672
|
-
}
|
|
18673
|
-
|
|
18674
|
-
|
|
18675
|
-
|
|
18676
|
-
|
|
18677
|
-
|
|
18678
|
-
|
|
18925
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
18926
|
+
}
|
|
18927
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
18928
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
18929
|
+
if (value.v > EVENT_VERSION)
|
|
18930
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
18931
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
18932
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
18933
|
+
return value;
|
|
18679
18934
|
};
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18935
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
18936
|
+
let fd;
|
|
18937
|
+
try {
|
|
18938
|
+
fd = fs6.openSync(file, "r");
|
|
18939
|
+
} catch (error) {
|
|
18940
|
+
if (error.code === "ENOENT")
|
|
18941
|
+
return null;
|
|
18942
|
+
throw error;
|
|
18943
|
+
}
|
|
18944
|
+
let bytes;
|
|
18945
|
+
let size;
|
|
18946
|
+
try {
|
|
18947
|
+
size = fs6.fstatSync(fd).size;
|
|
18948
|
+
if (from > size)
|
|
18949
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
18950
|
+
bytes = Buffer.alloc(size - from);
|
|
18951
|
+
let read = 0;
|
|
18952
|
+
while (read < bytes.length) {
|
|
18953
|
+
const got = fs6.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
18954
|
+
if (got === 0)
|
|
18955
|
+
break;
|
|
18956
|
+
read += got;
|
|
18957
|
+
}
|
|
18958
|
+
bytes = bytes.subarray(0, read);
|
|
18959
|
+
size = from + read;
|
|
18960
|
+
} finally {
|
|
18961
|
+
fs6.closeSync(fd);
|
|
18962
|
+
}
|
|
18963
|
+
const events = [];
|
|
18964
|
+
let start = 0;
|
|
18965
|
+
let lastStart = from;
|
|
18966
|
+
let seq = expectSeq;
|
|
18967
|
+
for (;; ) {
|
|
18968
|
+
const newline = bytes.indexOf(10, start);
|
|
18969
|
+
if (newline < 0)
|
|
18970
|
+
break;
|
|
18971
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
18972
|
+
if (line.trim() !== "") {
|
|
18973
|
+
const event = parseEvent(line, file, from + start);
|
|
18974
|
+
if (seq !== null && event.seq !== seq)
|
|
18975
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
18976
|
+
seq = event.seq + 1;
|
|
18977
|
+
events.push(event);
|
|
18978
|
+
lastStart = from + start;
|
|
18979
|
+
}
|
|
18980
|
+
start = newline + 1;
|
|
18981
|
+
}
|
|
18982
|
+
return { events, end: from + start, lastStart, size };
|
|
18983
|
+
}
|
|
18984
|
+
var fsyncDirectory = (dir) => {
|
|
18985
|
+
try {
|
|
18986
|
+
const fd = fs6.openSync(dir, "r");
|
|
18684
18987
|
try {
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
cachedRuntimeVersion = "0.0.0";
|
|
18988
|
+
fs6.fsyncSync(fd);
|
|
18989
|
+
} finally {
|
|
18990
|
+
fs6.closeSync(fd);
|
|
18689
18991
|
}
|
|
18992
|
+
} catch (error) {
|
|
18993
|
+
if (process.platform !== "win32")
|
|
18994
|
+
throw error;
|
|
18690
18995
|
}
|
|
18691
|
-
return cachedRuntimeVersion;
|
|
18692
18996
|
};
|
|
18693
|
-
|
|
18694
|
-
|
|
18695
|
-
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18699
|
-
|
|
18700
|
-
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18997
|
+
function appendEvent(file, event, validEnd) {
|
|
18998
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
18999
|
+
`, "utf8");
|
|
19000
|
+
const created = !fs6.existsSync(file);
|
|
19001
|
+
if (!created && fs6.statSync(file).size !== validEnd) {
|
|
19002
|
+
const cut = fs6.openSync(file, "r+");
|
|
19003
|
+
try {
|
|
19004
|
+
fs6.ftruncateSync(cut, validEnd);
|
|
19005
|
+
fs6.fsyncSync(cut);
|
|
19006
|
+
} finally {
|
|
19007
|
+
fs6.closeSync(cut);
|
|
19008
|
+
}
|
|
18705
19009
|
}
|
|
18706
|
-
|
|
18707
|
-
};
|
|
18708
|
-
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
18709
|
-
`;
|
|
18710
|
-
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
18711
|
-
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
18712
|
-
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
18713
|
-
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
18714
|
-
var sameDirectoryIdentity = (left, right) => {
|
|
18715
|
-
if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
|
|
18716
|
-
return false;
|
|
18717
|
-
const normalizedLeft = path4.resolve(left);
|
|
18718
|
-
const normalizedRight = path4.resolve(right);
|
|
19010
|
+
const fd = fs6.openSync(file, "a", 384);
|
|
18719
19011
|
try {
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
const canonicalLeft = fs5.realpathSync(normalizedLeft);
|
|
18727
|
-
const canonicalRight = fs5.realpathSync(normalizedRight);
|
|
18728
|
-
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
18729
|
-
return true;
|
|
18730
|
-
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
18731
|
-
} catch {
|
|
18732
|
-
return false;
|
|
19012
|
+
let written = 0;
|
|
19013
|
+
while (written < line.length)
|
|
19014
|
+
written += fs6.writeSync(fd, line, written, line.length - written);
|
|
19015
|
+
fs6.fsyncSync(fd);
|
|
19016
|
+
} finally {
|
|
19017
|
+
fs6.closeSync(fd);
|
|
18733
19018
|
}
|
|
18734
|
-
|
|
19019
|
+
if (created)
|
|
19020
|
+
fsyncDirectory(path5.dirname(file));
|
|
19021
|
+
return validEnd;
|
|
19022
|
+
}
|
|
19023
|
+
function replaceFile(file, bytes, durable) {
|
|
19024
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
19025
|
+
try {
|
|
19026
|
+
const fd = fs6.openSync(temporary, "wx", 384);
|
|
19027
|
+
try {
|
|
19028
|
+
fs6.writeSync(fd, bytes);
|
|
19029
|
+
if (durable)
|
|
19030
|
+
fs6.fsyncSync(fd);
|
|
19031
|
+
} finally {
|
|
19032
|
+
fs6.closeSync(fd);
|
|
19033
|
+
}
|
|
19034
|
+
renameWithRetry(temporary, file);
|
|
19035
|
+
if (durable)
|
|
19036
|
+
fsyncDirectory(path5.dirname(file));
|
|
19037
|
+
} catch (error) {
|
|
19038
|
+
try {
|
|
19039
|
+
fs6.unlinkSync(temporary);
|
|
19040
|
+
} catch {}
|
|
19041
|
+
throw error;
|
|
19042
|
+
}
|
|
19043
|
+
}
|
|
18735
19044
|
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
18736
19045
|
var isTransientWindowsError = (error) => {
|
|
18737
19046
|
if (process.platform !== "win32")
|
|
@@ -18752,138 +19061,596 @@ var retryTransient = (run) => {
|
|
|
18752
19061
|
}
|
|
18753
19062
|
}
|
|
18754
19063
|
};
|
|
19064
|
+
var renameWithRetry = (from, to) => retryTransient(() => fs6.renameSync(from, to));
|
|
19065
|
+
|
|
19066
|
+
// packages/workit-core/src/store/patch.ts
|
|
19067
|
+
var isPlainObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19068
|
+
var equal = (left, right) => {
|
|
19069
|
+
if (left === right)
|
|
19070
|
+
return true;
|
|
19071
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
19072
|
+
return false;
|
|
19073
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
19074
|
+
return false;
|
|
19075
|
+
if (Array.isArray(left)) {
|
|
19076
|
+
const other = right;
|
|
19077
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
19078
|
+
}
|
|
19079
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
19080
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
19081
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
19082
|
+
};
|
|
19083
|
+
function diff(before, after, path = []) {
|
|
19084
|
+
if (equal(before, after))
|
|
19085
|
+
return [];
|
|
19086
|
+
if (isPlainObject3(before) && isPlainObject3(after)) {
|
|
19087
|
+
const ops = [];
|
|
19088
|
+
for (const key of Object.keys(before))
|
|
19089
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
19090
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
19091
|
+
for (const key of Object.keys(after)) {
|
|
19092
|
+
if (after[key] === undefined)
|
|
19093
|
+
continue;
|
|
19094
|
+
if (before[key] === undefined)
|
|
19095
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
19096
|
+
else
|
|
19097
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
19098
|
+
}
|
|
19099
|
+
return ops;
|
|
19100
|
+
}
|
|
19101
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
19102
|
+
const ops = [];
|
|
19103
|
+
if (after.length < before.length)
|
|
19104
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
19105
|
+
const shared = Math.min(before.length, after.length);
|
|
19106
|
+
let changed = 0;
|
|
19107
|
+
for (let index = 0;index < shared; index += 1)
|
|
19108
|
+
if (!equal(before[index], after[index]))
|
|
19109
|
+
changed += 1;
|
|
19110
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
19111
|
+
return [{ op: "set", path, value: after }];
|
|
19112
|
+
for (let index = 0;index < shared; index += 1)
|
|
19113
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
19114
|
+
if (after.length > before.length)
|
|
19115
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
19116
|
+
return ops;
|
|
19117
|
+
}
|
|
19118
|
+
return [{ op: "set", path, value: after }];
|
|
19119
|
+
}
|
|
19120
|
+
var clone2 = (value) => structuredClone(value);
|
|
19121
|
+
function apply(target, ops) {
|
|
19122
|
+
let root = target;
|
|
19123
|
+
for (const op of ops) {
|
|
19124
|
+
if (op.path.length === 0) {
|
|
19125
|
+
if (op.op === "set")
|
|
19126
|
+
root = clone2(op.value);
|
|
19127
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
19128
|
+
root.push(...clone2(op.values));
|
|
19129
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
19130
|
+
root.length = op.length;
|
|
19131
|
+
else
|
|
19132
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
19133
|
+
continue;
|
|
19134
|
+
}
|
|
19135
|
+
let parent = root;
|
|
19136
|
+
for (const key of op.path.slice(0, -1)) {
|
|
19137
|
+
if (typeof parent !== "object" || parent === null)
|
|
19138
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
19139
|
+
parent = parent[key];
|
|
19140
|
+
}
|
|
19141
|
+
if (typeof parent !== "object" || parent === null)
|
|
19142
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
19143
|
+
const key = op.path.at(-1);
|
|
19144
|
+
const container = parent;
|
|
19145
|
+
if (op.op === "set")
|
|
19146
|
+
container[key] = clone2(op.value);
|
|
19147
|
+
else if (op.op === "del")
|
|
19148
|
+
delete container[key];
|
|
19149
|
+
else {
|
|
19150
|
+
const array = container[key];
|
|
19151
|
+
if (!Array.isArray(array))
|
|
19152
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
19153
|
+
if (op.op === "push")
|
|
19154
|
+
array.push(...clone2(op.values));
|
|
19155
|
+
else
|
|
19156
|
+
array.length = op.length;
|
|
19157
|
+
}
|
|
19158
|
+
}
|
|
19159
|
+
return root;
|
|
19160
|
+
}
|
|
19161
|
+
var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
|
|
19162
|
+
var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
19163
|
+
if (!isPlainObject3(op) || !isPath(op.path))
|
|
19164
|
+
return false;
|
|
19165
|
+
if (op.op === "set")
|
|
19166
|
+
return "value" in op;
|
|
19167
|
+
if (op.op === "del")
|
|
19168
|
+
return op.path.length > 0;
|
|
19169
|
+
if (op.op === "push")
|
|
19170
|
+
return Array.isArray(op.values);
|
|
19171
|
+
if (op.op === "cut")
|
|
19172
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
19173
|
+
return false;
|
|
19174
|
+
});
|
|
19175
|
+
|
|
19176
|
+
// packages/workit-core/src/store/reduce.ts
|
|
19177
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
19178
|
+
var OPENING_TYPES = new Set([
|
|
19179
|
+
"task.opened",
|
|
19180
|
+
"task.imported",
|
|
19181
|
+
"migrated.from_v2",
|
|
19182
|
+
"task.checkpoint"
|
|
19183
|
+
]);
|
|
19184
|
+
var KNOWN_TYPES = new Set([
|
|
19185
|
+
...OPENING_TYPES,
|
|
19186
|
+
"task.bound",
|
|
19187
|
+
"task.noted",
|
|
19188
|
+
"task.revised",
|
|
19189
|
+
"task.judged",
|
|
19190
|
+
"task.paused",
|
|
19191
|
+
"task.resumed",
|
|
19192
|
+
"task.closed",
|
|
19193
|
+
"task.updated",
|
|
19194
|
+
"check.ran",
|
|
19195
|
+
"evidence.recorded",
|
|
19196
|
+
"finding.recorded",
|
|
19197
|
+
"finding.updated",
|
|
19198
|
+
"decision.recorded",
|
|
19199
|
+
"decision.updated",
|
|
19200
|
+
"worker.updated"
|
|
19201
|
+
]);
|
|
19202
|
+
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19203
|
+
var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
|
|
19204
|
+
var BLOB_REF = "$blob";
|
|
19205
|
+
var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
|
|
19206
|
+
var blobDigest = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
19207
|
+
var toBlob = (value, blobs) => {
|
|
19208
|
+
if (!isObject3(value))
|
|
19209
|
+
return value;
|
|
19210
|
+
const digest = blobDigest(value);
|
|
19211
|
+
blobs.write(digest, value);
|
|
19212
|
+
return { [BLOB_REF]: digest };
|
|
19213
|
+
};
|
|
19214
|
+
var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
|
|
19215
|
+
var encodeOps = (ops, blobs) => ops.map((op) => {
|
|
19216
|
+
if (op.path[0] !== "candidates")
|
|
19217
|
+
return op;
|
|
19218
|
+
if (op.op === "push" && op.path.length === 1)
|
|
19219
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
19220
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
19221
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
19222
|
+
if (op.op === "set" && op.path.length === 2)
|
|
19223
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
19224
|
+
return op;
|
|
19225
|
+
});
|
|
19226
|
+
var decode3 = (value, blobs) => {
|
|
19227
|
+
const digest = blobRef(value);
|
|
19228
|
+
if (digest)
|
|
19229
|
+
return blobs.read(digest);
|
|
19230
|
+
if (Array.isArray(value))
|
|
19231
|
+
return value.map((item) => decode3(item, blobs));
|
|
19232
|
+
if (isObject3(value)) {
|
|
19233
|
+
const out = {};
|
|
19234
|
+
for (const [key, item] of Object.entries(value))
|
|
19235
|
+
out[key] = decode3(item, blobs);
|
|
19236
|
+
return out;
|
|
19237
|
+
}
|
|
19238
|
+
return value;
|
|
19239
|
+
};
|
|
19240
|
+
function reduce(state, event, blobs) {
|
|
19241
|
+
const data = event.data;
|
|
19242
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
19243
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
19244
|
+
if (!known && !carries) {
|
|
19245
|
+
if (event.critical === true)
|
|
19246
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
19247
|
+
return state;
|
|
19248
|
+
}
|
|
19249
|
+
let next = state;
|
|
19250
|
+
if ("record" in data) {
|
|
19251
|
+
if (!isObject3(data.record))
|
|
19252
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
19253
|
+
next = {
|
|
19254
|
+
record: decode3(data.record, blobs),
|
|
19255
|
+
key: null,
|
|
19256
|
+
legacy: null
|
|
19257
|
+
};
|
|
19258
|
+
} else if (next === null) {
|
|
19259
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
19260
|
+
} else
|
|
19261
|
+
next = { ...next };
|
|
19262
|
+
if ("ops" in data) {
|
|
19263
|
+
if (!isPatch(data.ops))
|
|
19264
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
19265
|
+
try {
|
|
19266
|
+
next.record = apply(next.record, decode3(data.ops, blobs));
|
|
19267
|
+
} catch (error) {
|
|
19268
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
19269
|
+
}
|
|
19270
|
+
}
|
|
19271
|
+
if ("key" in data)
|
|
19272
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
19273
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
19274
|
+
next.legacy = {
|
|
19275
|
+
path: String(data.legacy.path ?? ""),
|
|
19276
|
+
digest: String(data.legacy.digest ?? "")
|
|
19277
|
+
};
|
|
19278
|
+
return next;
|
|
19279
|
+
}
|
|
19280
|
+
function eventType(before, after) {
|
|
19281
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
19282
|
+
if (before.status !== after.status)
|
|
19283
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
19284
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
19285
|
+
const last = after.evidence.at(-1);
|
|
19286
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
19287
|
+
}
|
|
19288
|
+
if (count(after.findings) > count(before.findings))
|
|
19289
|
+
return "finding.recorded";
|
|
19290
|
+
if (count(after.decisions) > count(before.decisions))
|
|
19291
|
+
return "decision.recorded";
|
|
19292
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
19293
|
+
return "finding.updated";
|
|
19294
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
19295
|
+
return "decision.updated";
|
|
19296
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
19297
|
+
return "worker.updated";
|
|
19298
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
19299
|
+
return "task.revised";
|
|
19300
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
19301
|
+
return "task.judged";
|
|
19302
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
19303
|
+
return "task.noted";
|
|
19304
|
+
return "task.updated";
|
|
19305
|
+
}
|
|
19306
|
+
|
|
19307
|
+
// packages/workit-core/src/store/notes.ts
|
|
19308
|
+
var SLOT = Symbol.for("workit.migrationReporter");
|
|
19309
|
+
var reportMigration = (report) => {
|
|
19310
|
+
try {
|
|
19311
|
+
const reporter = globalThis[SLOT];
|
|
19312
|
+
if (typeof reporter === "function")
|
|
19313
|
+
reporter(report);
|
|
19314
|
+
} catch {}
|
|
19315
|
+
};
|
|
19316
|
+
var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
19317
|
+
|
|
19318
|
+
// packages/workit-core/src/core/task-store.ts
|
|
19319
|
+
var MIGRATION_PENDING = "workit migration pending";
|
|
19320
|
+
var STORE_FORMAT = 3;
|
|
19321
|
+
var STORE_MIN_RUNTIME = "3.0.0";
|
|
19322
|
+
var SNAPSHOT_EVERY = 32;
|
|
19323
|
+
var COMPACT_ABOVE = 200;
|
|
19324
|
+
var COMPACT_KEEP = 50;
|
|
19325
|
+
var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
19326
|
+
var TOMBSTONE = Symbol("tombstone");
|
|
19327
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
19328
|
+
var INDEX_VERSION = 3;
|
|
19329
|
+
var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19330
|
+
var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
19331
|
+
var indexEntry = (task, key, legacy, file) => {
|
|
19332
|
+
const sessions = [];
|
|
19333
|
+
const intent = hostSession(task.intent.provenance.session);
|
|
19334
|
+
if (intent)
|
|
19335
|
+
sessions.push({ ...intent, workerId: null });
|
|
19336
|
+
for (const worker of task.workers) {
|
|
19337
|
+
const session = hostSession(worker.data.session);
|
|
19338
|
+
if (session)
|
|
19339
|
+
sessions.push({ ...session, workerId: worker.id });
|
|
19340
|
+
}
|
|
19341
|
+
return {
|
|
19342
|
+
id: task.id,
|
|
19343
|
+
revision: task.revision,
|
|
19344
|
+
status: task.status,
|
|
19345
|
+
createdAt: task.createdAt,
|
|
19346
|
+
updatedAt: task.updatedAt,
|
|
19347
|
+
objective: task.intent.data.objective,
|
|
19348
|
+
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
19349
|
+
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
19350
|
+
sessions,
|
|
19351
|
+
workspaceId: task.workspaceId,
|
|
19352
|
+
key: key?.key ?? null,
|
|
19353
|
+
branch: key?.branch ?? null,
|
|
19354
|
+
boundAt: key?.boundAt ?? null,
|
|
19355
|
+
legacy,
|
|
19356
|
+
file
|
|
19357
|
+
};
|
|
19358
|
+
};
|
|
19359
|
+
var fileSignature = (file) => {
|
|
19360
|
+
try {
|
|
19361
|
+
const stat = fs7.statSync(file, { bigint: true });
|
|
19362
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
19363
|
+
} catch {
|
|
19364
|
+
return null;
|
|
19365
|
+
}
|
|
19366
|
+
};
|
|
19367
|
+
var RACY_WINDOW_NS = 2000000000n;
|
|
19368
|
+
var racySignature = (signature) => {
|
|
19369
|
+
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
19370
|
+
const changed = mtime > ctime ? mtime : ctime;
|
|
19371
|
+
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
19372
|
+
};
|
|
19373
|
+
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
19374
|
+
var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
19375
|
+
var cachedRuntimeVersion = null;
|
|
19376
|
+
var runtimeVersion = () => {
|
|
19377
|
+
if (cachedRuntimeVersion === null) {
|
|
19378
|
+
try {
|
|
19379
|
+
const pkg = JSON.parse(fs7.readFileSync(path6.join(packageRoot(), "package.json"), "utf8"));
|
|
19380
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
19381
|
+
} catch {
|
|
19382
|
+
cachedRuntimeVersion = "0.0.0";
|
|
19383
|
+
}
|
|
19384
|
+
}
|
|
19385
|
+
return cachedRuntimeVersion;
|
|
19386
|
+
};
|
|
19387
|
+
var stampNew = (record) => ({
|
|
19388
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
19389
|
+
updatedWith: runtimeVersion()
|
|
19390
|
+
});
|
|
19391
|
+
var isNewerVersion = (candidate, current) => {
|
|
19392
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
19393
|
+
const [candidateParts, currentParts] = [parts(candidate), parts(current)];
|
|
19394
|
+
for (let index = 0;index < 3; index += 1) {
|
|
19395
|
+
const left = candidateParts[index] ?? 0;
|
|
19396
|
+
const right = currentParts[index] ?? 0;
|
|
19397
|
+
if (left !== right)
|
|
19398
|
+
return left > right;
|
|
19399
|
+
}
|
|
19400
|
+
return false;
|
|
19401
|
+
};
|
|
19402
|
+
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
19403
|
+
var sameDirectoryIdentity = (left, right) => {
|
|
19404
|
+
if (!path6.isAbsolute(left) || !path6.isAbsolute(right))
|
|
19405
|
+
return false;
|
|
19406
|
+
const normalizedLeft = path6.resolve(left);
|
|
19407
|
+
const normalizedRight = path6.resolve(right);
|
|
19408
|
+
try {
|
|
19409
|
+
const a = fs7.statSync(normalizedLeft, { bigint: true });
|
|
19410
|
+
const b = fs7.statSync(normalizedRight, { bigint: true });
|
|
19411
|
+
if (!a.isDirectory() || !b.isDirectory())
|
|
19412
|
+
return false;
|
|
19413
|
+
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
19414
|
+
return true;
|
|
19415
|
+
const canonicalLeft = fs7.realpathSync(normalizedLeft);
|
|
19416
|
+
const canonicalRight = fs7.realpathSync(normalizedRight);
|
|
19417
|
+
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
19418
|
+
return true;
|
|
19419
|
+
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
19420
|
+
} catch {
|
|
19421
|
+
return false;
|
|
19422
|
+
}
|
|
19423
|
+
};
|
|
18755
19424
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
18756
19425
|
var heldInProcess = new Map;
|
|
18757
|
-
var
|
|
18758
|
-
var
|
|
18759
|
-
const count = (heldInProcess.get(
|
|
19426
|
+
var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
|
|
19427
|
+
var drop = (lock) => {
|
|
19428
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
18760
19429
|
if (count > 0)
|
|
18761
|
-
heldInProcess.set(
|
|
19430
|
+
heldInProcess.set(lock, count);
|
|
18762
19431
|
else
|
|
18763
|
-
heldInProcess.delete(
|
|
19432
|
+
heldInProcess.delete(lock);
|
|
18764
19433
|
};
|
|
18765
|
-
var
|
|
18766
|
-
const
|
|
18767
|
-
return
|
|
19434
|
+
var envActor = () => {
|
|
19435
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
19436
|
+
return {
|
|
19437
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
19438
|
+
session: value("WORKIT_SESSION_ID"),
|
|
19439
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
19440
|
+
};
|
|
18768
19441
|
};
|
|
18769
19442
|
|
|
18770
19443
|
class TaskStore {
|
|
18771
19444
|
root;
|
|
19445
|
+
origin;
|
|
18772
19446
|
lockTimeoutMs;
|
|
19447
|
+
actor;
|
|
19448
|
+
migrateOnRead;
|
|
19449
|
+
located;
|
|
19450
|
+
migrationChecked = false;
|
|
19451
|
+
compactDue = new Set;
|
|
18773
19452
|
constructor(root, options = {}) {
|
|
18774
|
-
|
|
19453
|
+
const resolved = path6.resolve(root);
|
|
19454
|
+
this.origin = fs7.existsSync(resolved) ? fs7.realpathSync(resolved) : resolved;
|
|
19455
|
+
this.located = resolveStore(this.origin);
|
|
19456
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
18775
19457
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
19458
|
+
this.actor = options.actor ?? envActor();
|
|
19459
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
19460
|
+
}
|
|
19461
|
+
location() {
|
|
19462
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs7.existsSync(path6.join(this.root, ".git"))) {
|
|
19463
|
+
this.located = undefined;
|
|
19464
|
+
this.migrationChecked = false;
|
|
19465
|
+
}
|
|
19466
|
+
if (this.located === undefined)
|
|
19467
|
+
this.located = resolveStore(this.root);
|
|
19468
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
19469
|
+
}
|
|
19470
|
+
currentKey() {
|
|
19471
|
+
const location = this.location();
|
|
19472
|
+
if (!location.ok)
|
|
19473
|
+
return location;
|
|
19474
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
18776
19475
|
}
|
|
18777
19476
|
readTask(taskId) {
|
|
18778
19477
|
if (!validId(taskId))
|
|
18779
19478
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
18780
|
-
const
|
|
18781
|
-
if (!
|
|
19479
|
+
const ready = this.ready();
|
|
19480
|
+
if (!ready.ok)
|
|
19481
|
+
return ready;
|
|
19482
|
+
const loaded = this.load(taskId);
|
|
19483
|
+
if (!loaded.ok)
|
|
19484
|
+
return loaded;
|
|
19485
|
+
if (!loaded.data)
|
|
18782
19486
|
return failure2("not_found", "task not found", { taskId });
|
|
18783
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
18784
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
18785
|
-
if (result.result.ok) {
|
|
18786
|
-
const workspace = this.readWorkspace();
|
|
18787
|
-
if (!workspace.ok)
|
|
18788
|
-
return workspace;
|
|
18789
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
18790
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
18791
|
-
}
|
|
18792
|
-
return result.result;
|
|
18793
|
-
}
|
|
18794
|
-
listTasks() {
|
|
18795
|
-
if (!fs5.existsSync(this.tasksDir))
|
|
18796
|
-
return success(null, null, []);
|
|
18797
|
-
let names;
|
|
18798
|
-
try {
|
|
18799
|
-
names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
18800
|
-
} catch (error) {
|
|
18801
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
18802
|
-
path: this.tasksDir
|
|
18803
|
-
});
|
|
18804
|
-
}
|
|
18805
19487
|
const workspace = this.readWorkspace();
|
|
18806
19488
|
if (!workspace.ok)
|
|
18807
19489
|
return workspace;
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
19490
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
19491
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
19492
|
+
}
|
|
19493
|
+
listTasks() {
|
|
19494
|
+
const listed = this.listTaskIndex();
|
|
19495
|
+
if (!listed.ok)
|
|
19496
|
+
return listed;
|
|
18812
19497
|
const tasks = [];
|
|
18813
|
-
for (const
|
|
18814
|
-
const
|
|
18815
|
-
if (!
|
|
18816
|
-
|
|
18817
|
-
if (
|
|
18818
|
-
|
|
18819
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
18820
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
18821
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
18822
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
18823
|
-
tasks.push(item.result.data);
|
|
19498
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
19499
|
+
const loaded = this.load(entry.id);
|
|
19500
|
+
if (!loaded.ok)
|
|
19501
|
+
return loaded;
|
|
19502
|
+
if (loaded.data)
|
|
19503
|
+
tasks.push(loaded.data.record);
|
|
18824
19504
|
}
|
|
18825
19505
|
return success(null, null, tasks);
|
|
18826
19506
|
}
|
|
18827
19507
|
listTaskIndex() {
|
|
18828
|
-
|
|
19508
|
+
const all = this.listStoreIndex();
|
|
19509
|
+
if (!all.ok)
|
|
19510
|
+
return all;
|
|
19511
|
+
const workspace = this.readWorkspace();
|
|
19512
|
+
if (!workspace.ok)
|
|
19513
|
+
return workspace;
|
|
19514
|
+
if (!workspace.data)
|
|
18829
19515
|
return success(null, null, []);
|
|
19516
|
+
const id = workspace.data.id;
|
|
19517
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
19518
|
+
}
|
|
19519
|
+
listStoreIndex() {
|
|
19520
|
+
const ready = this.ready();
|
|
19521
|
+
if (!ready.ok)
|
|
19522
|
+
return ready;
|
|
19523
|
+
const tasksDir = this.paths().tasks;
|
|
18830
19524
|
let names;
|
|
18831
19525
|
try {
|
|
18832
|
-
names =
|
|
19526
|
+
names = fs7.existsSync(tasksDir) ? fs7.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
18833
19527
|
} catch (error) {
|
|
18834
19528
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
18835
|
-
path:
|
|
19529
|
+
path: tasksDir
|
|
18836
19530
|
});
|
|
18837
19531
|
}
|
|
18838
|
-
const
|
|
18839
|
-
if (!workspace.ok)
|
|
18840
|
-
return workspace;
|
|
18841
|
-
if (!workspace.data)
|
|
18842
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
18843
|
-
path: this.workspacePath
|
|
18844
|
-
});
|
|
18845
|
-
const stored = this.readIndex(workspace.data.id);
|
|
19532
|
+
const stored = this.readIndex();
|
|
18846
19533
|
const entries = [];
|
|
18847
19534
|
let changed = Object.keys(stored).length !== names.length;
|
|
18848
19535
|
for (const name of names.toSorted()) {
|
|
18849
|
-
const
|
|
18850
|
-
const signature = fileSignature(file);
|
|
19536
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
18851
19537
|
if (signature === null) {
|
|
18852
19538
|
changed = true;
|
|
18853
19539
|
continue;
|
|
18854
19540
|
}
|
|
18855
|
-
const cached = stored[name
|
|
19541
|
+
const cached = stored[name];
|
|
18856
19542
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
18857
19543
|
entries.push(cached);
|
|
18858
19544
|
continue;
|
|
18859
19545
|
}
|
|
18860
|
-
const
|
|
18861
|
-
if (!
|
|
19546
|
+
const loaded = this.load(name);
|
|
19547
|
+
if (!loaded.ok)
|
|
19548
|
+
return loaded;
|
|
19549
|
+
if (!loaded.data)
|
|
18862
19550
|
continue;
|
|
18863
|
-
|
|
18864
|
-
return item.result;
|
|
18865
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
18866
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
18867
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
18868
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
18869
|
-
const entry = indexEntry(item.result.data, signature);
|
|
19551
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
18870
19552
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
18871
19553
|
changed = true;
|
|
18872
19554
|
entries.push(entry);
|
|
18873
19555
|
}
|
|
18874
19556
|
if (changed)
|
|
18875
|
-
this.writeIndex(
|
|
19557
|
+
this.writeIndex(entries);
|
|
18876
19558
|
return success(null, null, entries);
|
|
18877
19559
|
}
|
|
18878
19560
|
readWorkspace() {
|
|
18879
|
-
const
|
|
18880
|
-
if (!
|
|
18881
|
-
return
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
19561
|
+
const ready = this.ready();
|
|
19562
|
+
if (!ready.ok)
|
|
19563
|
+
return ready;
|
|
19564
|
+
const file = this.paths().workspace;
|
|
19565
|
+
let bytes;
|
|
19566
|
+
try {
|
|
19567
|
+
bytes = retryTransient(() => fs7.readFileSync(file, "utf8"));
|
|
19568
|
+
} catch (error) {
|
|
19569
|
+
if (error.code === "ENOENT")
|
|
19570
|
+
return success(null, null, null);
|
|
19571
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
19572
|
+
path: file
|
|
18885
19573
|
});
|
|
18886
|
-
|
|
19574
|
+
}
|
|
19575
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
19576
|
+
if (!parsed.ok)
|
|
19577
|
+
return parsed;
|
|
19578
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
19579
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
19580
|
+
return parsed;
|
|
19581
|
+
}
|
|
19582
|
+
implicitTask(input) {
|
|
19583
|
+
const ready = this.ready(input.create !== false);
|
|
19584
|
+
if (!ready.ok)
|
|
19585
|
+
return ready;
|
|
19586
|
+
const key = this.currentKey();
|
|
19587
|
+
if (!key.ok)
|
|
19588
|
+
return key;
|
|
19589
|
+
const found = this.openTaskForKey(key.data.key);
|
|
19590
|
+
if (!found.ok)
|
|
19591
|
+
return found;
|
|
19592
|
+
const workspace = this.readWorkspace();
|
|
19593
|
+
if (!workspace.ok)
|
|
19594
|
+
return workspace;
|
|
19595
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
19596
|
+
const task = this.readTask(found.data.id);
|
|
19597
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
19598
|
+
}
|
|
19599
|
+
if (input.create === false) {
|
|
19600
|
+
if (!found.data)
|
|
19601
|
+
return success(null, null, null);
|
|
19602
|
+
const loaded = this.load(found.data.id);
|
|
19603
|
+
if (!loaded.ok)
|
|
19604
|
+
return loaded;
|
|
19605
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
19606
|
+
}
|
|
19607
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
19608
|
+
}
|
|
19609
|
+
implicitLocked(input, key) {
|
|
19610
|
+
{
|
|
19611
|
+
const again = this.openTaskForKey(key.key);
|
|
19612
|
+
if (!again.ok)
|
|
19613
|
+
return again;
|
|
19614
|
+
if (again.data) {
|
|
19615
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
19616
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
19617
|
+
}
|
|
19618
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path6.basename(this.root) || this.root;
|
|
19619
|
+
const intent = {
|
|
19620
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
19621
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
19622
|
+
authorityRefs: []
|
|
19623
|
+
};
|
|
19624
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
19625
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
19626
|
+
}
|
|
19627
|
+
}
|
|
19628
|
+
adoptTask(taskId) {
|
|
19629
|
+
if (!validId(taskId))
|
|
19630
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
19631
|
+
const ready = this.ready(true);
|
|
19632
|
+
if (!ready.ok)
|
|
19633
|
+
return ready;
|
|
19634
|
+
const key = this.currentKey();
|
|
19635
|
+
if (!key.ok)
|
|
19636
|
+
return key;
|
|
19637
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
19638
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
19639
|
+
if (!holder.ok)
|
|
19640
|
+
return holder;
|
|
19641
|
+
if (holder.data && holder.data.id !== taskId)
|
|
19642
|
+
return failure2("invalid_transition", `${key.data.key} already has open task ${holder.data.id}; close it (\`workit task close\`) before adopting another`, { taskId: holder.data.id });
|
|
19643
|
+
return this.bindLocked(taskId, key.data);
|
|
19644
|
+
}));
|
|
19645
|
+
}
|
|
19646
|
+
keyReport() {
|
|
19647
|
+
const key = this.currentKey();
|
|
19648
|
+
if (!key.ok)
|
|
19649
|
+
return key;
|
|
19650
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
19651
|
+
if (!bound.ok)
|
|
19652
|
+
return bound;
|
|
19653
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
18887
19654
|
}
|
|
18888
19655
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
18889
19656
|
const value = "intent" in input ? input : {
|
|
@@ -18891,82 +19658,16 @@ class TaskStore {
|
|
|
18891
19658
|
provenance,
|
|
18892
19659
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
18893
19660
|
};
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
18899
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
18900
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
18901
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
18902
|
-
});
|
|
18903
|
-
}
|
|
18904
|
-
if (!value.provenance)
|
|
18905
|
-
return failure2("invalid_input", "provenance is required");
|
|
18906
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
18907
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
18908
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
18909
|
-
if (current.data && !previousWorkspaceBytes)
|
|
18910
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
18911
|
-
const workspace = current.data ? {
|
|
18912
|
-
...current.data,
|
|
18913
|
-
revision: newRevision(),
|
|
18914
|
-
root: this.root,
|
|
18915
|
-
runtime: stampNew(current.data)
|
|
18916
|
-
} : {
|
|
18917
|
-
schemaVersion: SCHEMA_VERSION,
|
|
18918
|
-
id: newId(),
|
|
18919
|
-
revision: newRevision(),
|
|
18920
|
-
root: this.root,
|
|
18921
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
18922
|
-
writer: null
|
|
18923
|
-
};
|
|
18924
|
-
const timestamp = value.now ?? now();
|
|
18925
|
-
const task = {
|
|
18926
|
-
schemaVersion: SCHEMA_VERSION,
|
|
18927
|
-
id: newId(),
|
|
18928
|
-
workspaceId: workspace.id,
|
|
18929
|
-
revision: newRevision(),
|
|
18930
|
-
createdAt: timestamp,
|
|
18931
|
-
updatedAt: timestamp,
|
|
18932
|
-
origin: null,
|
|
18933
|
-
intent: {
|
|
18934
|
-
id: newId(),
|
|
18935
|
-
recordedAt: timestamp,
|
|
18936
|
-
provenance: value.provenance,
|
|
18937
|
-
data: value.intent
|
|
18938
|
-
},
|
|
18939
|
-
constraints: [],
|
|
18940
|
-
status: "active",
|
|
18941
|
-
closure: null,
|
|
18942
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
18943
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
18944
|
-
assessments: [],
|
|
18945
|
-
policy: null,
|
|
18946
|
-
policyChanges: [],
|
|
18947
|
-
candidates: [],
|
|
18948
|
-
evidence: [],
|
|
18949
|
-
decisions: [],
|
|
18950
|
-
findings: [],
|
|
18951
|
-
workers: []
|
|
18952
|
-
};
|
|
18953
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
18954
|
-
if (!writtenWorkspace.ok)
|
|
18955
|
-
return writtenWorkspace;
|
|
18956
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
18957
|
-
if (!writtenTask.ok) {
|
|
18958
|
-
if (current.data && previousWorkspaceBytes) {
|
|
18959
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
18960
|
-
if (!restored.ok)
|
|
18961
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
18962
|
-
}
|
|
18963
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
18964
|
-
}
|
|
18965
|
-
return success(task.revision, workspace.revision, task);
|
|
18966
|
-
});
|
|
19661
|
+
const ready = this.ready(true);
|
|
19662
|
+
if (!ready.ok)
|
|
19663
|
+
return ready;
|
|
19664
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
18967
19665
|
}
|
|
18968
19666
|
importTask(input) {
|
|
18969
|
-
|
|
19667
|
+
const ready = this.ready(true);
|
|
19668
|
+
if (!ready.ok)
|
|
19669
|
+
return ready;
|
|
19670
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
18970
19671
|
const current = this.readWorkspace();
|
|
18971
19672
|
if (!current.ok)
|
|
18972
19673
|
return current;
|
|
@@ -18998,23 +19699,8 @@ class TaskStore {
|
|
|
18998
19699
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
18999
19700
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
19000
19701
|
});
|
|
19001
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
19002
|
-
if (current.data && !previousWorkspaceBytes)
|
|
19003
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
19004
19702
|
const timestamp = input.now ?? now();
|
|
19005
|
-
const workspace = current.data
|
|
19006
|
-
...current.data,
|
|
19007
|
-
revision: newRevision(),
|
|
19008
|
-
root: this.root,
|
|
19009
|
-
runtime: stampNew(current.data)
|
|
19010
|
-
} : {
|
|
19011
|
-
schemaVersion: SCHEMA_VERSION,
|
|
19012
|
-
id: input.workspaceId ?? newId(),
|
|
19013
|
-
revision: newRevision(),
|
|
19014
|
-
root: this.root,
|
|
19015
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
19016
|
-
writer: null
|
|
19017
|
-
};
|
|
19703
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
19018
19704
|
const task = {
|
|
19019
19705
|
...input.task,
|
|
19020
19706
|
workspaceId: workspace.id,
|
|
@@ -19029,62 +19715,46 @@ class TaskStore {
|
|
|
19029
19715
|
const validTask = taskRecordSchema.safeParse(task);
|
|
19030
19716
|
if (!validTask.success)
|
|
19031
19717
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
19036
|
-
if (!writtenTask.ok) {
|
|
19037
|
-
if (current.data && previousWorkspaceBytes) {
|
|
19038
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
19039
|
-
if (!restored.ok)
|
|
19040
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
19041
|
-
}
|
|
19042
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
19043
|
-
operation: "import",
|
|
19044
|
-
outcome: "unknown",
|
|
19045
|
-
path: writtenTask.details.path
|
|
19046
|
-
});
|
|
19047
|
-
}
|
|
19048
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
19718
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
19719
|
+
operation: "import"
|
|
19720
|
+
});
|
|
19049
19721
|
});
|
|
19050
19722
|
}
|
|
19051
19723
|
mutateTask(taskId, expected, update, timestamp) {
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
const
|
|
19059
|
-
if (!
|
|
19060
|
-
return
|
|
19724
|
+
if (!validId(taskId))
|
|
19725
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
19726
|
+
const ready = this.ready(true);
|
|
19727
|
+
if (!ready.ok)
|
|
19728
|
+
return ready;
|
|
19729
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
19730
|
+
const loaded = this.loadBound(taskId);
|
|
19731
|
+
if (!loaded.ok)
|
|
19732
|
+
return loaded;
|
|
19733
|
+
if (loaded.data.record.revision !== expected)
|
|
19734
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
19061
19735
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
19062
19736
|
let changed;
|
|
19063
19737
|
try {
|
|
19064
|
-
changed = update(
|
|
19738
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
19065
19739
|
} catch (error) {
|
|
19066
19740
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
19067
19741
|
}
|
|
19068
19742
|
if (!changed.ok)
|
|
19069
19743
|
return changed;
|
|
19070
|
-
const
|
|
19071
|
-
|
|
19072
|
-
id: taskId,
|
|
19073
|
-
workspaceId: current.data.workspaceId,
|
|
19074
|
-
createdAt: current.data.createdAt,
|
|
19075
|
-
revision: context.revision,
|
|
19076
|
-
updatedAt: context.now,
|
|
19077
|
-
runtime: stampNew(current.data)
|
|
19078
|
-
};
|
|
19079
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
19080
|
-
if (!valid.success)
|
|
19744
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
19745
|
+
if (!valid)
|
|
19081
19746
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
19082
|
-
const written = this.
|
|
19083
|
-
return written.ok ? success(valid.
|
|
19747
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
19748
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
19084
19749
|
});
|
|
19750
|
+
this.compactPending();
|
|
19751
|
+
return result;
|
|
19085
19752
|
}
|
|
19086
19753
|
mutateWorkspace(expected, update) {
|
|
19087
|
-
|
|
19754
|
+
const ready = this.ready(true);
|
|
19755
|
+
if (!ready.ok)
|
|
19756
|
+
return ready;
|
|
19757
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
19088
19758
|
const current = this.readWorkspace();
|
|
19089
19759
|
if (!current.ok)
|
|
19090
19760
|
return current;
|
|
@@ -19092,9 +19762,6 @@ class TaskStore {
|
|
|
19092
19762
|
return failure2("not_found", "workspace not found");
|
|
19093
19763
|
if (current.data.revision !== expected)
|
|
19094
19764
|
return this.workspaceConflict(expected, current.data.revision);
|
|
19095
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
19096
|
-
if (!previousBytes)
|
|
19097
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
19098
19765
|
const context = { now: now(), revision: newRevision() };
|
|
19099
19766
|
let changed;
|
|
19100
19767
|
try {
|
|
@@ -19104,23 +19771,27 @@ class TaskStore {
|
|
|
19104
19771
|
}
|
|
19105
19772
|
if (!changed.ok)
|
|
19106
19773
|
return changed;
|
|
19107
|
-
const
|
|
19774
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
19108
19775
|
...changed.data,
|
|
19109
19776
|
id: current.data.id,
|
|
19110
19777
|
root: this.root,
|
|
19111
19778
|
revision: context.revision,
|
|
19112
19779
|
runtime: stampNew(current.data)
|
|
19113
|
-
};
|
|
19114
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
19780
|
+
});
|
|
19115
19781
|
if (!valid.success)
|
|
19116
19782
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
19117
|
-
const written = this.
|
|
19783
|
+
const written = this.writeWorkspace(valid.data);
|
|
19118
19784
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
19119
19785
|
});
|
|
19120
19786
|
}
|
|
19121
19787
|
mutateTaskAndWorkspace(input) {
|
|
19122
|
-
|
|
19123
|
-
|
|
19788
|
+
if (!validId(input.taskId))
|
|
19789
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
19790
|
+
const ready = this.ready(true);
|
|
19791
|
+
if (!ready.ok)
|
|
19792
|
+
return ready;
|
|
19793
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
19794
|
+
const task = this.loadBound(input.taskId);
|
|
19124
19795
|
if (!task.ok)
|
|
19125
19796
|
return task;
|
|
19126
19797
|
const workspace = this.readWorkspace();
|
|
@@ -19131,14 +19802,10 @@ class TaskStore {
|
|
|
19131
19802
|
const expected = input.expectedRevision;
|
|
19132
19803
|
if (!expected)
|
|
19133
19804
|
return failure2("invalid_input", "task revision is required");
|
|
19134
|
-
if (task.data.revision !== expected)
|
|
19135
|
-
return this.conflict(expected, task.data.revision);
|
|
19805
|
+
if (task.data.record.revision !== expected)
|
|
19806
|
+
return this.conflict(expected, task.data.record.revision);
|
|
19136
19807
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
19137
19808
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
19138
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
19139
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
19140
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
19141
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
19142
19809
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
19143
19810
|
let changedWorkspace;
|
|
19144
19811
|
try {
|
|
@@ -19157,286 +19824,69 @@ class TaskStore {
|
|
|
19157
19824
|
});
|
|
19158
19825
|
if (!nextWorkspace.success)
|
|
19159
19826
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
19160
|
-
const reserved = this.
|
|
19827
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
19161
19828
|
if (!reserved.ok)
|
|
19162
19829
|
return reserved;
|
|
19830
|
+
const uncertain = (message) => {
|
|
19831
|
+
this.markUncertain(nextWorkspace.data);
|
|
19832
|
+
return failure2("external_outcome_unknown", message, {
|
|
19833
|
+
operation: "coupled_mutation",
|
|
19834
|
+
outcome: "unknown"
|
|
19835
|
+
});
|
|
19836
|
+
};
|
|
19163
19837
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
19164
19838
|
let changedTask;
|
|
19165
19839
|
try {
|
|
19166
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
19840
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
19167
19841
|
} catch (error) {
|
|
19168
|
-
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
|
|
19172
|
-
|
|
19173
|
-
|
|
19174
|
-
|
|
19175
|
-
const
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19179
|
-
|
|
19180
|
-
|
|
19181
|
-
updatedAt: taskContext.now,
|
|
19182
|
-
runtime: stampNew(task.data)
|
|
19842
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
19843
|
+
}
|
|
19844
|
+
if (!changedTask.ok)
|
|
19845
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
19846
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
19847
|
+
if (!nextTask)
|
|
19848
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
19849
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
19850
|
+
if (!written.ok)
|
|
19851
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
19852
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
19853
|
+
task: nextTask,
|
|
19854
|
+
workspace: nextWorkspace.data
|
|
19183
19855
|
});
|
|
19184
|
-
|
|
19185
|
-
|
|
19186
|
-
|
|
19187
|
-
}
|
|
19188
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
19189
|
-
if (!written.ok) {
|
|
19190
|
-
this.markUncertain(nextWorkspace.data);
|
|
19191
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
19192
|
-
}
|
|
19193
|
-
return success(nextTask.data.revision, nextWorkspace.data.revision, {
|
|
19194
|
-
task: nextTask.data,
|
|
19195
|
-
workspace: nextWorkspace.data
|
|
19196
|
-
});
|
|
19197
|
-
});
|
|
19198
|
-
}
|
|
19199
|
-
recoveryCandidates() {
|
|
19200
|
-
if (!fs5.existsSync(this.recoveryDir))
|
|
19201
|
-
return success(null, null, []);
|
|
19202
|
-
try {
|
|
19203
|
-
const candidates = [];
|
|
19204
|
-
for (const name of fs5.readdirSync(this.recoveryDir)) {
|
|
19205
|
-
const match = RECOVERY_NAME.exec(name);
|
|
19206
|
-
if (match)
|
|
19207
|
-
candidates.push({
|
|
19208
|
-
target: match[1],
|
|
19209
|
-
path: path4.join(this.recoveryDir, name),
|
|
19210
|
-
digest: match[3]
|
|
19211
|
-
});
|
|
19212
|
-
}
|
|
19213
|
-
return success(null, null, candidates);
|
|
19214
|
-
} catch (error) {
|
|
19215
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
19216
|
-
path: this.recoveryDir
|
|
19217
|
-
});
|
|
19218
|
-
}
|
|
19219
|
-
}
|
|
19220
|
-
recoverTask(taskId, input) {
|
|
19221
|
-
return this.recover("task", taskId, input);
|
|
19222
|
-
}
|
|
19223
|
-
recoverWorkspace(input) {
|
|
19224
|
-
return this.recover("workspace", null, input);
|
|
19225
|
-
}
|
|
19226
|
-
recover(target, taskId, input) {
|
|
19227
|
-
if (taskId !== null && !validId(taskId))
|
|
19228
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
19229
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
19230
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
19231
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
19232
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
19233
|
-
try {
|
|
19234
|
-
const workspace = this.readWorkspace();
|
|
19235
|
-
if (!workspace.ok && target === "task")
|
|
19236
|
-
return workspace;
|
|
19237
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
19238
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
19239
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
19240
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
19241
|
-
const currentBytes = this.snapshotBytes(file);
|
|
19242
|
-
if (!currentBytes)
|
|
19243
|
-
return failure2("not_found", "snapshot not found");
|
|
19244
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
19245
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
19246
|
-
let selectedBytes = currentBytes;
|
|
19247
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
19248
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
19249
|
-
if (!candidate)
|
|
19250
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
19251
|
-
selectedBytes = candidate;
|
|
19252
|
-
}
|
|
19253
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
19254
|
-
if (!selectedRecord.ok)
|
|
19255
|
-
return selectedRecord;
|
|
19256
|
-
if (target === "workspace") {
|
|
19257
|
-
const parsed = selectedRecord;
|
|
19258
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
19259
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
19260
|
-
} else {
|
|
19261
|
-
const parsed = selectedRecord;
|
|
19262
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
19263
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
19264
|
-
}
|
|
19265
|
-
const lock = this.readLockSnapshot();
|
|
19266
|
-
if (!lock.ok)
|
|
19267
|
-
return lock;
|
|
19268
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
19269
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
19270
|
-
if (!evidence.ok)
|
|
19271
|
-
return evidence;
|
|
19272
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
19273
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
19274
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
19275
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
19276
|
-
const recoveryGate = { reclaimed: false };
|
|
19277
|
-
const result = this.withLock((handle) => {
|
|
19278
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
19279
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
19280
|
-
path: this.lockPath
|
|
19281
|
-
});
|
|
19282
|
-
if (!handle.verifyStillHeld())
|
|
19283
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
19284
|
-
path: this.lockPath
|
|
19285
|
-
});
|
|
19286
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
19287
|
-
if (!reacquiredBytes)
|
|
19288
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
19289
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
19290
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
19291
|
-
if (target === "task") {
|
|
19292
|
-
const currentWorkspace = this.readWorkspace();
|
|
19293
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
19294
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
19295
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
19296
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
19297
|
-
}
|
|
19298
|
-
if (target === "workspace") {
|
|
19299
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
19300
|
-
if (!parsed.ok)
|
|
19301
|
-
return parsed;
|
|
19302
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
19303
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
19304
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
19305
|
-
}
|
|
19306
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
19307
|
-
if (!parsed.ok)
|
|
19308
|
-
return parsed;
|
|
19309
|
-
const value = {
|
|
19310
|
-
...parsed.data,
|
|
19311
|
-
revision: newRevision(),
|
|
19312
|
-
updatedAt: now(),
|
|
19313
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
19314
|
-
};
|
|
19315
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
19316
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
19317
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
19318
|
-
return result;
|
|
19319
|
-
} catch (error) {
|
|
19320
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
19321
|
-
}
|
|
19322
|
-
}
|
|
19323
|
-
processEvidence(input, lock, writer) {
|
|
19324
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
19325
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
19326
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
19327
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
19328
|
-
try {
|
|
19329
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
19330
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
19331
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
19332
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
19333
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
19334
|
-
if (!result.ok)
|
|
19335
|
-
return result;
|
|
19336
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
19337
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
19338
|
-
} catch (error) {
|
|
19339
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
19340
|
-
}
|
|
19341
|
-
}
|
|
19342
|
-
deepFreeze(value) {
|
|
19343
|
-
if (value && typeof value === "object") {
|
|
19344
|
-
for (const child of Object.values(value))
|
|
19345
|
-
this.deepFreeze(child);
|
|
19346
|
-
Object.freeze(value);
|
|
19347
|
-
}
|
|
19348
|
-
return value;
|
|
19349
|
-
}
|
|
19350
|
-
readLockSnapshot() {
|
|
19351
|
-
try {
|
|
19352
|
-
const stat = fs5.lstatSync(this.lockPath);
|
|
19353
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
19354
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
19355
|
-
path: this.lockPath
|
|
19356
|
-
});
|
|
19357
|
-
const raw = fs5.readFileSync(this.lockPath, "utf8");
|
|
19358
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
19359
|
-
} catch (error) {
|
|
19360
|
-
if (error?.code === "ENOENT")
|
|
19361
|
-
return success(null, null, null);
|
|
19362
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
19363
|
-
path: this.lockPath
|
|
19364
|
-
});
|
|
19365
|
-
}
|
|
19366
|
-
}
|
|
19367
|
-
metadataLockOptions() {
|
|
19368
|
-
const inProcess = heldInProcess.has(this.root);
|
|
19369
|
-
return {
|
|
19370
|
-
lockPath: this.lockPath,
|
|
19371
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
19372
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
19373
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
19374
|
-
staleRecovery: "remove-if-unchanged",
|
|
19375
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
19376
|
-
let ageMs = null;
|
|
19377
|
-
try {
|
|
19378
|
-
ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
|
|
19379
|
-
} catch {}
|
|
19380
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
19381
|
-
},
|
|
19382
|
-
shouldRemoveStaleLock: () => true,
|
|
19383
|
-
parsePayload: parseMetadataLockOrNull,
|
|
19384
|
-
payload: () => ({
|
|
19385
|
-
pid: process.pid,
|
|
19386
|
-
processStart: processStartOf(process.pid),
|
|
19387
|
-
host: localLockHost(),
|
|
19388
|
-
nonce: randomUUID2()
|
|
19389
|
-
})
|
|
19390
|
-
};
|
|
19391
|
-
}
|
|
19392
|
-
acquireMetadataLock(options) {
|
|
19393
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
19394
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
19395
|
-
while (true) {
|
|
19396
|
-
try {
|
|
19397
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
19398
|
-
...options,
|
|
19399
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
19400
|
-
});
|
|
19401
|
-
} catch (error) {
|
|
19402
|
-
const code = error?.code;
|
|
19403
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
19404
|
-
throw error;
|
|
19405
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
19406
|
-
throw error;
|
|
19407
|
-
if (code !== "file_lock_stale")
|
|
19408
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
19409
|
-
}
|
|
19410
|
-
}
|
|
19411
|
-
}
|
|
19412
|
-
externalActionLockOptions() {
|
|
19413
|
-
const options = this.metadataLockOptions();
|
|
19414
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
19856
|
+
}));
|
|
19857
|
+
this.compactPending();
|
|
19858
|
+
return result;
|
|
19415
19859
|
}
|
|
19416
19860
|
async withExternalActionLock(operation, reentrant = false) {
|
|
19417
19861
|
const activeRoots = externalActionLockRoots.getStore();
|
|
19418
19862
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
19419
19863
|
return operation();
|
|
19864
|
+
const ready = this.ready(true);
|
|
19865
|
+
if (!ready.ok)
|
|
19866
|
+
return ready;
|
|
19867
|
+
const lockPath = this.paths().checkoutLock;
|
|
19420
19868
|
try {
|
|
19421
|
-
this.
|
|
19869
|
+
this.initializeStorage();
|
|
19422
19870
|
} catch (error) {
|
|
19423
19871
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
19424
|
-
path: this.
|
|
19872
|
+
path: this.paths().dir
|
|
19425
19873
|
});
|
|
19426
19874
|
}
|
|
19427
19875
|
let handle;
|
|
19428
19876
|
try {
|
|
19429
|
-
|
|
19877
|
+
const options = this.lockOptions(lockPath);
|
|
19878
|
+
handle = this.acquire(lockPath, {
|
|
19879
|
+
...options,
|
|
19880
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
19881
|
+
});
|
|
19430
19882
|
} catch (error) {
|
|
19431
|
-
return this.lockFailure(error);
|
|
19883
|
+
return this.lockFailure(lockPath, error);
|
|
19432
19884
|
}
|
|
19433
|
-
|
|
19885
|
+
hold(lockPath);
|
|
19434
19886
|
let result;
|
|
19435
19887
|
try {
|
|
19436
19888
|
if (!handle.verifyStillHeld())
|
|
19437
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
19438
|
-
path: this.lockPath
|
|
19439
|
-
});
|
|
19889
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
19440
19890
|
else {
|
|
19441
19891
|
try {
|
|
19442
19892
|
const run = () => operation();
|
|
@@ -19450,526 +19900,1144 @@ class TaskStore {
|
|
|
19450
19900
|
if (!handle.verifyStillHeld())
|
|
19451
19901
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
19452
19902
|
outcome: "unknown",
|
|
19453
|
-
path:
|
|
19903
|
+
path: lockPath
|
|
19454
19904
|
});
|
|
19455
19905
|
} catch {
|
|
19456
19906
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
19457
19907
|
outcome: "unknown",
|
|
19458
|
-
path:
|
|
19908
|
+
path: lockPath
|
|
19459
19909
|
});
|
|
19460
19910
|
}
|
|
19461
19911
|
}
|
|
19462
19912
|
} catch {
|
|
19463
19913
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
19464
19914
|
outcome: "unknown",
|
|
19465
|
-
path:
|
|
19915
|
+
path: lockPath
|
|
19466
19916
|
});
|
|
19467
19917
|
}
|
|
19468
|
-
|
|
19918
|
+
drop(lockPath);
|
|
19469
19919
|
try {
|
|
19470
19920
|
retryTransient(() => handle.release());
|
|
19471
19921
|
} catch (error) {
|
|
19472
|
-
const released = this.lockFailure(error);
|
|
19922
|
+
const released = this.lockFailure(lockPath, error);
|
|
19473
19923
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
19474
19924
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
19475
19925
|
outcome: "unknown",
|
|
19476
|
-
path:
|
|
19926
|
+
path: lockPath
|
|
19477
19927
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
19478
19928
|
}
|
|
19479
19929
|
return result;
|
|
19480
19930
|
}
|
|
19481
|
-
|
|
19482
|
-
const
|
|
19483
|
-
options.
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
return true;
|
|
19931
|
+
collectGarbage(options = {}) {
|
|
19932
|
+
const dryRun = options.dryRun === true;
|
|
19933
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
19934
|
+
const report = {
|
|
19935
|
+
dryRun,
|
|
19936
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
19937
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
19938
|
+
temporary: { removed: 0 },
|
|
19939
|
+
legacyRecovery: null,
|
|
19940
|
+
retried: [],
|
|
19941
|
+
failed: []
|
|
19493
19942
|
};
|
|
19494
|
-
|
|
19943
|
+
const ready = this.ready(!dryRun);
|
|
19944
|
+
if (!ready.ok)
|
|
19945
|
+
return ready;
|
|
19946
|
+
const dirs = this.paths();
|
|
19947
|
+
const recovery = path6.join(this.root, ".workit", "recovery");
|
|
19948
|
+
if (fs7.existsSync(recovery)) {
|
|
19949
|
+
let files = 0;
|
|
19950
|
+
let bytes = 0;
|
|
19951
|
+
try {
|
|
19952
|
+
for (const name of fs7.readdirSync(recovery)) {
|
|
19953
|
+
const stat = fs7.lstatSync(path6.join(recovery, name));
|
|
19954
|
+
if (!stat.isFile())
|
|
19955
|
+
continue;
|
|
19956
|
+
files += 1;
|
|
19957
|
+
bytes += stat.size;
|
|
19958
|
+
}
|
|
19959
|
+
} catch {}
|
|
19960
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
19961
|
+
if (remove)
|
|
19962
|
+
fs7.rmSync(recovery, { recursive: true, force: true });
|
|
19963
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
19964
|
+
}
|
|
19965
|
+
if (!fs7.existsSync(dirs.tasks))
|
|
19966
|
+
return success(null, null, report);
|
|
19967
|
+
const names = fs7.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
19968
|
+
for (const name of names) {
|
|
19969
|
+
const file = this.eventsPath(name);
|
|
19970
|
+
let size = 0;
|
|
19971
|
+
try {
|
|
19972
|
+
size = fs7.statSync(file).size;
|
|
19973
|
+
} catch {
|
|
19974
|
+
continue;
|
|
19975
|
+
}
|
|
19976
|
+
const lines = this.countEvents(file);
|
|
19977
|
+
if (lines > compactAbove) {
|
|
19978
|
+
if (dryRun) {
|
|
19979
|
+
report.compacted.tasks.push(name);
|
|
19980
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
19981
|
+
report.compacted.bytesBefore += size;
|
|
19982
|
+
} else {
|
|
19983
|
+
const compacted = this.compact(name);
|
|
19984
|
+
if (!compacted.ok)
|
|
19985
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
19986
|
+
else {
|
|
19987
|
+
report.compacted.tasks.push(name);
|
|
19988
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
19989
|
+
report.compacted.bytesBefore += size;
|
|
19990
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
19991
|
+
}
|
|
19992
|
+
}
|
|
19993
|
+
}
|
|
19994
|
+
}
|
|
19995
|
+
const nowMs = Date.now();
|
|
19996
|
+
const sweep = () => {
|
|
19997
|
+
const references = new Set;
|
|
19998
|
+
for (const name of names)
|
|
19999
|
+
try {
|
|
20000
|
+
for (const match of fs7.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
20001
|
+
references.add(match[1]);
|
|
20002
|
+
} catch {}
|
|
20003
|
+
if (fs7.existsSync(dirs.blobs))
|
|
20004
|
+
for (const name of fs7.readdirSync(dirs.blobs)) {
|
|
20005
|
+
const digest = name.replace(/\.json$/, "");
|
|
20006
|
+
const file = path6.join(dirs.blobs, name);
|
|
20007
|
+
let stat;
|
|
20008
|
+
try {
|
|
20009
|
+
stat = fs7.lstatSync(file);
|
|
20010
|
+
} catch {
|
|
20011
|
+
continue;
|
|
20012
|
+
}
|
|
20013
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
20014
|
+
report.blobs.kept += 1;
|
|
20015
|
+
continue;
|
|
20016
|
+
}
|
|
20017
|
+
report.blobs.removed += 1;
|
|
20018
|
+
report.blobs.removedBytes += stat.size;
|
|
20019
|
+
if (!dryRun)
|
|
20020
|
+
fs7.rmSync(file, { force: true });
|
|
20021
|
+
}
|
|
20022
|
+
return success(null, null, null);
|
|
20023
|
+
};
|
|
20024
|
+
const swept = dryRun ? sweep() : this.withLock(path6.join(dirs.dir, "gc.lock"), sweep);
|
|
20025
|
+
if (!swept.ok)
|
|
20026
|
+
return swept;
|
|
20027
|
+
const temporaryDirs = [
|
|
20028
|
+
dirs.dir,
|
|
20029
|
+
dirs.tasks,
|
|
20030
|
+
dirs.blobs,
|
|
20031
|
+
dirs.checkout,
|
|
20032
|
+
...names.map((name) => path6.join(dirs.tasks, name))
|
|
20033
|
+
];
|
|
20034
|
+
for (const directory of temporaryDirs) {
|
|
20035
|
+
if (!fs7.existsSync(directory))
|
|
20036
|
+
continue;
|
|
20037
|
+
for (const name of fs7.readdirSync(directory)) {
|
|
20038
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
20039
|
+
continue;
|
|
20040
|
+
const file = path6.join(directory, name);
|
|
20041
|
+
try {
|
|
20042
|
+
const stat = fs7.lstatSync(file);
|
|
20043
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
20044
|
+
continue;
|
|
20045
|
+
} catch {
|
|
20046
|
+
continue;
|
|
20047
|
+
}
|
|
20048
|
+
report.temporary.removed += 1;
|
|
20049
|
+
if (!dryRun)
|
|
20050
|
+
fs7.rmSync(file, { force: true });
|
|
20051
|
+
}
|
|
20052
|
+
}
|
|
20053
|
+
return success(null, null, report);
|
|
19495
20054
|
}
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
const
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
20055
|
+
paths() {
|
|
20056
|
+
const location = this.location();
|
|
20057
|
+
const dir = location.ok ? location.data.dir : path6.join(this.root, ".workit");
|
|
20058
|
+
const checkout = path6.join(dir, "checkouts", checkoutSlug(this.root));
|
|
20059
|
+
return {
|
|
20060
|
+
dir,
|
|
20061
|
+
shared: location.ok ? location.data.shared : false,
|
|
20062
|
+
marker: path6.join(dir, "store.json"),
|
|
20063
|
+
checkout,
|
|
20064
|
+
workspace: path6.join(checkout, "workspace.json"),
|
|
20065
|
+
checkoutLock: path6.join(checkout, "metadata.lock"),
|
|
20066
|
+
tasks: path6.join(dir, "tasks"),
|
|
20067
|
+
index: path6.join(dir, "task-index.json"),
|
|
20068
|
+
blobs: path6.join(dir, "blobs", "candidates"),
|
|
20069
|
+
legacy: path6.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
20070
|
+
v2: path6.join(this.root, ".workit")
|
|
19504
20071
|
};
|
|
19505
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
19506
20072
|
}
|
|
19507
|
-
|
|
20073
|
+
eventsPath(taskId) {
|
|
20074
|
+
return path6.join(this.paths().tasks, taskId, "events.jsonl");
|
|
20075
|
+
}
|
|
20076
|
+
snapshotPath(taskId) {
|
|
20077
|
+
return path6.join(this.paths().tasks, taskId, "snapshot.json");
|
|
20078
|
+
}
|
|
20079
|
+
taskLockPath(taskId) {
|
|
20080
|
+
return path6.join(this.paths().tasks, taskId, "lock");
|
|
20081
|
+
}
|
|
20082
|
+
ready(write = false) {
|
|
20083
|
+
const location = this.location();
|
|
20084
|
+
if (!location.ok)
|
|
20085
|
+
return location;
|
|
20086
|
+
if (this.migrationChecked)
|
|
20087
|
+
return success(null, null, null);
|
|
20088
|
+
const dirs = this.paths();
|
|
19508
20089
|
try {
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
19512
|
-
|
|
19513
|
-
|
|
20090
|
+
const marker = JSON.parse(fs7.readFileSync(dirs.marker, "utf8"));
|
|
20091
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
20092
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
20093
|
+
} catch {}
|
|
20094
|
+
this.migrationChecked = true;
|
|
20095
|
+
const settled = this.settleMigrations(write);
|
|
20096
|
+
if (!settled.ok)
|
|
20097
|
+
this.migrationChecked = false;
|
|
20098
|
+
return settled;
|
|
20099
|
+
}
|
|
20100
|
+
settleMigrations(write) {
|
|
20101
|
+
const moved = this.adoptLocalStore();
|
|
20102
|
+
if (!moved.ok)
|
|
20103
|
+
return moved;
|
|
20104
|
+
const pending = this.pendingMigrations();
|
|
20105
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
20106
|
+
return failure2("needs_input", `${MIGRATION_PENDING}: the task store at ${pending[0]} has not been migrated yet; run \`workit task status\` to migrate it`, { path: pending[0], guidance: "workit task status" });
|
|
20107
|
+
for (const v2 of this.v2Dirs()) {
|
|
20108
|
+
const migrated = this.migrateV2(v2);
|
|
20109
|
+
if (!migrated.ok)
|
|
20110
|
+
return migrated;
|
|
19514
20111
|
}
|
|
19515
|
-
|
|
19516
|
-
|
|
20112
|
+
return success(null, null, null);
|
|
20113
|
+
}
|
|
20114
|
+
blobStore() {
|
|
20115
|
+
const dir = this.paths().blobs;
|
|
20116
|
+
return {
|
|
20117
|
+
read: (digest) => {
|
|
20118
|
+
try {
|
|
20119
|
+
return JSON.parse(fs7.readFileSync(path6.join(dir, `${digest}.json`), "utf8"));
|
|
20120
|
+
} catch (error) {
|
|
20121
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
20122
|
+
}
|
|
20123
|
+
},
|
|
20124
|
+
write: (digest, value) => {
|
|
20125
|
+
const file = path6.join(dir, `${digest}.json`);
|
|
20126
|
+
if (fs7.existsSync(file)) {
|
|
20127
|
+
const stamp = new Date;
|
|
20128
|
+
try {
|
|
20129
|
+
fs7.utimesSync(file, stamp, stamp);
|
|
20130
|
+
return;
|
|
20131
|
+
} catch {}
|
|
20132
|
+
}
|
|
20133
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
20134
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
20135
|
+
}
|
|
20136
|
+
};
|
|
20137
|
+
}
|
|
20138
|
+
load(taskId) {
|
|
19517
20139
|
try {
|
|
19518
|
-
|
|
20140
|
+
const loaded = this.loadOrThrow(taskId);
|
|
20141
|
+
if (!loaded)
|
|
20142
|
+
return success(null, null, null);
|
|
20143
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
20144
|
+
if (!parsed.ok)
|
|
20145
|
+
return parsed;
|
|
20146
|
+
return success(null, null, {
|
|
20147
|
+
record: parsed.data,
|
|
20148
|
+
key: loaded.state.key,
|
|
20149
|
+
legacy: loaded.state.legacy,
|
|
20150
|
+
seq: loaded.seq,
|
|
20151
|
+
lastId: loaded.lastId,
|
|
20152
|
+
lastStart: loaded.lastStart,
|
|
20153
|
+
end: loaded.end,
|
|
20154
|
+
size: loaded.size,
|
|
20155
|
+
snapshotSeq: loaded.snapshotSeq
|
|
20156
|
+
});
|
|
19519
20157
|
} catch (error) {
|
|
19520
|
-
|
|
20158
|
+
if (error instanceof LogDamage)
|
|
20159
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
20160
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
20161
|
+
taskId
|
|
20162
|
+
});
|
|
19521
20163
|
}
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
20164
|
+
}
|
|
20165
|
+
loadOrThrow(taskId) {
|
|
20166
|
+
const blobs = this.blobStore();
|
|
20167
|
+
const file = this.eventsPath(taskId);
|
|
20168
|
+
const snapshot = this.readSnapshot(taskId);
|
|
20169
|
+
if (snapshot) {
|
|
19525
20170
|
try {
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
}
|
|
19534
|
-
|
|
19535
|
-
|
|
20171
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
20172
|
+
const first = tail?.events[0];
|
|
20173
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
20174
|
+
let state = {
|
|
20175
|
+
record: snapshot.record,
|
|
20176
|
+
key: snapshot.key,
|
|
20177
|
+
legacy: snapshot.legacy
|
|
20178
|
+
};
|
|
20179
|
+
for (const event of tail.events.slice(1))
|
|
20180
|
+
state = reduce(state, event, blobs);
|
|
20181
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
19536
20182
|
}
|
|
19537
20183
|
} catch (error) {
|
|
19538
|
-
|
|
20184
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
20185
|
+
throw error;
|
|
19539
20186
|
}
|
|
19540
20187
|
}
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
const releaseFailure = this.lockFailure(error);
|
|
19547
|
-
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
19548
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
19549
|
-
}
|
|
20188
|
+
const log = readLog(file, 0, null);
|
|
20189
|
+
if (!log || log.events.length === 0) {
|
|
20190
|
+
if (log && log.size > 0)
|
|
20191
|
+
throw new LogDamage("the log holds no complete event");
|
|
20192
|
+
return null;
|
|
19550
20193
|
}
|
|
19551
|
-
|
|
20194
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
20195
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
20196
|
+
let state = null;
|
|
20197
|
+
for (const event of log.events)
|
|
20198
|
+
state = reduce(state, event, blobs);
|
|
20199
|
+
return this.loadedFrom(state, log, 0);
|
|
19552
20200
|
}
|
|
19553
|
-
|
|
19554
|
-
const
|
|
20201
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
20202
|
+
const last = log.events.at(-1);
|
|
20203
|
+
return {
|
|
20204
|
+
state,
|
|
20205
|
+
seq: last.seq,
|
|
20206
|
+
lastId: last.id,
|
|
20207
|
+
lastStart: log.lastStart,
|
|
20208
|
+
end: log.end,
|
|
20209
|
+
size: log.size,
|
|
20210
|
+
snapshotSeq
|
|
20211
|
+
};
|
|
20212
|
+
}
|
|
20213
|
+
readSnapshot(taskId) {
|
|
19555
20214
|
try {
|
|
19556
|
-
|
|
20215
|
+
const value = JSON.parse(fs7.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
20216
|
+
if (!isObject4(value) || value.v !== 1 || value.task !== taskId || typeof value.seq !== "number" || typeof value.lastId !== "string" || typeof value.lastStart !== "number" || typeof value.end !== "number" || !isObject4(value.record))
|
|
20217
|
+
return null;
|
|
20218
|
+
return value;
|
|
19557
20219
|
} catch {
|
|
19558
|
-
return
|
|
20220
|
+
return null;
|
|
19559
20221
|
}
|
|
20222
|
+
}
|
|
20223
|
+
writeSnapshot(taskId, loaded) {
|
|
20224
|
+
const snapshot = {
|
|
20225
|
+
v: 1,
|
|
20226
|
+
task: taskId,
|
|
20227
|
+
seq: loaded.seq,
|
|
20228
|
+
lastId: loaded.lastId,
|
|
20229
|
+
lastStart: loaded.lastStart,
|
|
20230
|
+
end: loaded.end,
|
|
20231
|
+
key: loaded.key,
|
|
20232
|
+
legacy: loaded.legacy,
|
|
20233
|
+
record: loaded.record
|
|
20234
|
+
};
|
|
19560
20235
|
try {
|
|
19561
|
-
|
|
20236
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
19562
20237
|
} catch {}
|
|
19563
|
-
return true;
|
|
19564
20238
|
}
|
|
19565
|
-
|
|
20239
|
+
loadBound(taskId) {
|
|
20240
|
+
const loaded = this.load(taskId);
|
|
20241
|
+
if (!loaded.ok)
|
|
20242
|
+
return loaded;
|
|
20243
|
+
if (!loaded.data)
|
|
20244
|
+
return failure2("not_found", "task not found", { taskId });
|
|
20245
|
+
const workspace = this.readWorkspace();
|
|
20246
|
+
if (!workspace.ok)
|
|
20247
|
+
return workspace;
|
|
20248
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
20249
|
+
return bound ?? success(null, null, loaded.data);
|
|
20250
|
+
}
|
|
20251
|
+
bindingError(task, workspace) {
|
|
20252
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
20253
|
+
return null;
|
|
20254
|
+
return failure2("not_found", `task ${task.id} belongs to another checkout of this repository; run there, or move it here with \`workit task adopt ${task.id}\``, { taskId: task.id });
|
|
20255
|
+
}
|
|
20256
|
+
nextTask(current, changed, context) {
|
|
20257
|
+
const valid = taskRecordSchema.safeParse({
|
|
20258
|
+
...changed,
|
|
20259
|
+
id: current.id,
|
|
20260
|
+
workspaceId: current.workspaceId,
|
|
20261
|
+
createdAt: current.createdAt,
|
|
20262
|
+
revision: context.revision,
|
|
20263
|
+
updatedAt: context.now,
|
|
20264
|
+
runtime: stampNew(current)
|
|
20265
|
+
});
|
|
20266
|
+
return valid.success ? valid.data : null;
|
|
20267
|
+
}
|
|
20268
|
+
event(taskId, seq, type, data) {
|
|
20269
|
+
return {
|
|
20270
|
+
v: EVENT_VERSION,
|
|
20271
|
+
seq,
|
|
20272
|
+
at: new Date().toISOString(),
|
|
20273
|
+
id: newId(),
|
|
20274
|
+
task: taskId,
|
|
20275
|
+
actor: this.actor,
|
|
20276
|
+
type,
|
|
20277
|
+
data
|
|
20278
|
+
};
|
|
20279
|
+
}
|
|
20280
|
+
appendPatch(loaded, next, extra = {}) {
|
|
20281
|
+
const taskId = next.id;
|
|
19566
20282
|
try {
|
|
19567
|
-
|
|
19568
|
-
|
|
20283
|
+
const blobs = this.blobStore();
|
|
20284
|
+
const before = loaded.record;
|
|
20285
|
+
const after = next;
|
|
20286
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
20287
|
+
const { type, ...data } = extra;
|
|
20288
|
+
if (data.key)
|
|
20289
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
20290
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
20291
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
20292
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
20293
|
+
`);
|
|
20294
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
20295
|
+
const state = {
|
|
20296
|
+
record: next,
|
|
20297
|
+
key,
|
|
20298
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
20299
|
+
seq: event.seq,
|
|
20300
|
+
lastId: event.id,
|
|
20301
|
+
lastStart: start,
|
|
20302
|
+
end
|
|
20303
|
+
};
|
|
20304
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
20305
|
+
this.writeSnapshot(taskId, state);
|
|
20306
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
20307
|
+
this.compactDue.add(taskId);
|
|
20308
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
20309
|
+
return success(null, null, null);
|
|
19569
20310
|
} catch (error) {
|
|
19570
|
-
return error
|
|
20311
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
20312
|
+
path: this.eventsPath(taskId)
|
|
20313
|
+
});
|
|
19571
20314
|
}
|
|
19572
20315
|
}
|
|
19573
|
-
|
|
19574
|
-
const
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
20316
|
+
openTask(record, type, key, legacy = null) {
|
|
20317
|
+
const dir = path6.join(this.paths().tasks, record.id);
|
|
20318
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
20319
|
+
const blobs = this.blobStore();
|
|
20320
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
20321
|
+
const event = this.event(record.id, 1, type, {
|
|
20322
|
+
record: encodeRecord(record, blobs),
|
|
20323
|
+
key: bound,
|
|
20324
|
+
...legacy ? { legacy } : {}
|
|
20325
|
+
});
|
|
20326
|
+
const file = this.eventsPath(record.id);
|
|
20327
|
+
const existing = readLog(file, 0, null);
|
|
20328
|
+
if (existing && existing.events.length > 0)
|
|
20329
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
20330
|
+
appendEvent(file, event, 0);
|
|
20331
|
+
this.indexTask(record, bound, legacy !== null);
|
|
20332
|
+
}
|
|
20333
|
+
compactPending() {
|
|
20334
|
+
for (const taskId of this.compactDue) {
|
|
20335
|
+
this.compactDue.delete(taskId);
|
|
20336
|
+
this.compact(taskId);
|
|
20337
|
+
}
|
|
20338
|
+
}
|
|
20339
|
+
compact(taskId) {
|
|
20340
|
+
try {
|
|
20341
|
+
const file = this.eventsPath(taskId);
|
|
20342
|
+
const log = readLog(file, 0, null);
|
|
20343
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
20344
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
20345
|
+
const blobs = this.blobStore();
|
|
20346
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
20347
|
+
let state = null;
|
|
20348
|
+
for (const event of log.events.slice(0, cut))
|
|
20349
|
+
state = reduce(state, event, blobs);
|
|
20350
|
+
const folded = log.events[cut - 1];
|
|
20351
|
+
const checkpoint = {
|
|
20352
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
20353
|
+
record: encodeRecord(state.record, blobs),
|
|
20354
|
+
key: state.key,
|
|
20355
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
20356
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
20357
|
+
})
|
|
20358
|
+
};
|
|
20359
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
20360
|
+
`)}
|
|
20361
|
+
`;
|
|
20362
|
+
const last = log.events.at(-1);
|
|
20363
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
20364
|
+
let latest = null;
|
|
20365
|
+
try {
|
|
20366
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
20367
|
+
} catch {}
|
|
20368
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
20369
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
20370
|
+
const appended = fs7.readFileSync(file).subarray(log.end, latest.end);
|
|
20371
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
20372
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
20373
|
+
try {
|
|
20374
|
+
fs7.rmSync(this.snapshotPath(taskId), { force: true });
|
|
20375
|
+
} catch {}
|
|
20376
|
+
const reloaded = this.load(taskId);
|
|
20377
|
+
if (reloaded.ok && reloaded.data)
|
|
20378
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
20379
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
19580
20380
|
});
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19588
|
-
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
20381
|
+
} catch (error) {
|
|
20382
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
20383
|
+
}
|
|
20384
|
+
}
|
|
20385
|
+
countEvents(file) {
|
|
20386
|
+
try {
|
|
20387
|
+
const bytes = fs7.readFileSync(file);
|
|
20388
|
+
let count = 0;
|
|
20389
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
20390
|
+
count += 1;
|
|
20391
|
+
return count;
|
|
20392
|
+
} catch {
|
|
20393
|
+
return 0;
|
|
20394
|
+
}
|
|
20395
|
+
}
|
|
20396
|
+
openTasksForKey(key) {
|
|
20397
|
+
const all = this.listStoreIndex();
|
|
20398
|
+
if (!all.ok)
|
|
20399
|
+
return all;
|
|
20400
|
+
const location = this.location();
|
|
20401
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
20402
|
+
return success(null, null, all.data.filter((entry) => entry.key !== null && entry.status !== "closed").filter((entry) => (renames.length ? followRenames(entry.key, entry.boundAt ?? entry.createdAt, renames) : entry.key) === key).toSorted(oldestFirst));
|
|
20403
|
+
}
|
|
20404
|
+
openTaskForKey(key) {
|
|
20405
|
+
const bound = this.openTasksForKey(key);
|
|
20406
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
20407
|
+
}
|
|
20408
|
+
withKeyLock(key, operation) {
|
|
20409
|
+
const digest = createHash4("sha256").update(key).digest("hex").slice(0, 32);
|
|
20410
|
+
return this.withLock(path6.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
20411
|
+
}
|
|
20412
|
+
bindLocked(taskId, key, timestamp) {
|
|
20413
|
+
const current = this.readWorkspace();
|
|
20414
|
+
if (!current.ok)
|
|
20415
|
+
return current;
|
|
20416
|
+
let workspace = current.data;
|
|
20417
|
+
if (!workspace) {
|
|
20418
|
+
workspace = this.nextWorkspace(null);
|
|
20419
|
+
const written = this.writeWorkspace(workspace);
|
|
20420
|
+
if (!written.ok)
|
|
20421
|
+
return written;
|
|
20422
|
+
}
|
|
20423
|
+
const target = workspace;
|
|
20424
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
20425
|
+
const loaded = this.load(taskId);
|
|
20426
|
+
if (!loaded.ok)
|
|
20427
|
+
return loaded;
|
|
20428
|
+
if (!loaded.data)
|
|
20429
|
+
return failure2("not_found", "task not found", { taskId });
|
|
20430
|
+
if (loaded.data.record.status === "closed")
|
|
20431
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
20432
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
20433
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
20434
|
+
const next = taskRecordSchema.safeParse({
|
|
20435
|
+
...loaded.data.record,
|
|
20436
|
+
workspaceId: target.id,
|
|
20437
|
+
revision: newRevision(),
|
|
20438
|
+
updatedAt: timestamp ?? now(),
|
|
20439
|
+
runtime: stampNew(loaded.data.record)
|
|
20440
|
+
});
|
|
20441
|
+
if (!next.success)
|
|
20442
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
20443
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
20444
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
20445
|
+
});
|
|
20446
|
+
}
|
|
20447
|
+
unbindLocked(taskId) {
|
|
20448
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
20449
|
+
const loaded = this.load(taskId);
|
|
20450
|
+
if (!loaded.ok)
|
|
20451
|
+
return loaded;
|
|
20452
|
+
if (!loaded.data || loaded.data.key === null)
|
|
20453
|
+
return success(null, null, null);
|
|
20454
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
20455
|
+
});
|
|
20456
|
+
}
|
|
20457
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
20458
|
+
const current = this.readWorkspace();
|
|
20459
|
+
if (!current.ok)
|
|
20460
|
+
return current;
|
|
20461
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
20462
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
20463
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
20464
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
20465
|
+
});
|
|
20466
|
+
if (!value.provenance)
|
|
20467
|
+
return failure2("invalid_input", "provenance is required");
|
|
20468
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
20469
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
20470
|
+
if (bindCurrent) {
|
|
20471
|
+
const currentKey = this.currentKey();
|
|
20472
|
+
if (currentKey.ok)
|
|
20473
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
20474
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
20475
|
+
if (!holder.ok)
|
|
20476
|
+
return holder;
|
|
20477
|
+
if (holder.data) {
|
|
20478
|
+
const released = this.unbindLocked(holder.data.id);
|
|
20479
|
+
if (!released.ok)
|
|
20480
|
+
return released;
|
|
20481
|
+
}
|
|
20482
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
19596
20483
|
});
|
|
19597
20484
|
}
|
|
19598
|
-
|
|
19599
|
-
|
|
20485
|
+
return this.newTask(value, current.data, key);
|
|
20486
|
+
}
|
|
20487
|
+
newTask(value, previous, binding) {
|
|
20488
|
+
const current = { data: previous };
|
|
20489
|
+
const workspace = this.nextWorkspace(current.data);
|
|
20490
|
+
const timestamp = value.now ?? now();
|
|
20491
|
+
const task = {
|
|
20492
|
+
schemaVersion: SCHEMA_VERSION,
|
|
20493
|
+
id: newId(),
|
|
20494
|
+
workspaceId: workspace.id,
|
|
20495
|
+
revision: newRevision(),
|
|
20496
|
+
createdAt: timestamp,
|
|
20497
|
+
updatedAt: timestamp,
|
|
20498
|
+
origin: null,
|
|
20499
|
+
intent: {
|
|
20500
|
+
id: newId(),
|
|
20501
|
+
recordedAt: timestamp,
|
|
20502
|
+
provenance: value.provenance,
|
|
20503
|
+
data: value.intent
|
|
20504
|
+
},
|
|
20505
|
+
constraints: [],
|
|
20506
|
+
status: "active",
|
|
20507
|
+
closure: null,
|
|
20508
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
20509
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
20510
|
+
assessments: [],
|
|
20511
|
+
policy: null,
|
|
20512
|
+
policyChanges: [],
|
|
20513
|
+
candidates: [],
|
|
20514
|
+
evidence: [],
|
|
20515
|
+
decisions: [],
|
|
20516
|
+
findings: [],
|
|
20517
|
+
workers: []
|
|
20518
|
+
};
|
|
20519
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
20520
|
+
operation: "create"
|
|
20521
|
+
});
|
|
20522
|
+
}
|
|
20523
|
+
nextWorkspace(current, id) {
|
|
20524
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
20525
|
+
schemaVersion: SCHEMA_VERSION,
|
|
20526
|
+
id: id ?? newId(),
|
|
20527
|
+
revision: newRevision(),
|
|
20528
|
+
root: this.root,
|
|
20529
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
20530
|
+
writer: null
|
|
20531
|
+
};
|
|
19600
20532
|
}
|
|
19601
|
-
|
|
19602
|
-
|
|
19603
|
-
|
|
19604
|
-
|
|
20533
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
20534
|
+
const written = this.writeWorkspace(workspace);
|
|
20535
|
+
if (!written.ok)
|
|
20536
|
+
return written;
|
|
19605
20537
|
try {
|
|
19606
|
-
|
|
19607
|
-
} catch {
|
|
19608
|
-
|
|
19609
|
-
|
|
19610
|
-
|
|
19611
|
-
`)
|
|
20538
|
+
this.openTask(task, type, key);
|
|
20539
|
+
} catch (error) {
|
|
20540
|
+
if (previous) {
|
|
20541
|
+
const restored = this.writeWorkspace(previous);
|
|
20542
|
+
if (!restored.ok)
|
|
20543
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
20544
|
+
}
|
|
20545
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
20546
|
+
}
|
|
20547
|
+
return success(task.revision, workspace.revision, task);
|
|
19612
20548
|
}
|
|
19613
|
-
|
|
19614
|
-
|
|
20549
|
+
writeWorkspace(record) {
|
|
20550
|
+
const file = this.paths().workspace;
|
|
19615
20551
|
try {
|
|
19616
|
-
|
|
19617
|
-
|
|
19618
|
-
|
|
19619
|
-
|
|
19620
|
-
else if (typeof previous === "string")
|
|
19621
|
-
this.saveRecovery(file, previous);
|
|
19622
|
-
temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
19623
|
-
const fd = fs5.openSync(temporary, "wx", 384);
|
|
19624
|
-
try {
|
|
19625
|
-
fs5.writeSync(fd, jsonBytes(value));
|
|
19626
|
-
fs5.fsyncSync(fd);
|
|
19627
|
-
} finally {
|
|
19628
|
-
fs5.closeSync(fd);
|
|
19629
|
-
}
|
|
19630
|
-
retryTransient(() => fs5.renameSync(temporary, file));
|
|
19631
|
-
temporary = undefined;
|
|
19632
|
-
this.fsyncDirectory(path4.dirname(file));
|
|
19633
|
-
if (path4.dirname(file) === this.tasksDir)
|
|
19634
|
-
this.indexTaskWrite(file, value);
|
|
19635
|
-
return success(null, null, value);
|
|
20552
|
+
fs7.mkdirSync(path6.dirname(file), { recursive: true });
|
|
20553
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
20554
|
+
`, true);
|
|
20555
|
+
return success(null, null, null);
|
|
19636
20556
|
} catch (error) {
|
|
19637
|
-
return failure2("storage_error", `
|
|
19638
|
-
path: file
|
|
19639
|
-
});
|
|
19640
|
-
} finally {
|
|
19641
|
-
if (temporary)
|
|
19642
|
-
try {
|
|
19643
|
-
fs5.unlinkSync(temporary);
|
|
19644
|
-
} catch {}
|
|
20557
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
19645
20558
|
}
|
|
19646
20559
|
}
|
|
19647
|
-
|
|
20560
|
+
markUncertain(workspace) {
|
|
20561
|
+
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
20562
|
+
return;
|
|
20563
|
+
this.writeWorkspace({
|
|
20564
|
+
...workspace,
|
|
20565
|
+
writer: { ...workspace.writer, state: "uncertain" },
|
|
20566
|
+
revision: newRevision(),
|
|
20567
|
+
runtime: stampNew(workspace)
|
|
20568
|
+
});
|
|
20569
|
+
}
|
|
20570
|
+
readIndex() {
|
|
19648
20571
|
try {
|
|
19649
|
-
const value = JSON.parse(
|
|
19650
|
-
if (!
|
|
20572
|
+
const value = JSON.parse(fs7.readFileSync(this.paths().index, "utf8"));
|
|
20573
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
19651
20574
|
return {};
|
|
19652
20575
|
const tasks = {};
|
|
19653
20576
|
for (const [id, entry] of Object.entries(value.tasks))
|
|
19654
|
-
if (
|
|
20577
|
+
if (isObject4(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && typeof entry.workspaceId === "string" && Array.isArray(entry.sessions) && isObject4(entry.progress) && isObject4(entry.source))
|
|
19655
20578
|
tasks[id] = entry;
|
|
19656
20579
|
return tasks;
|
|
19657
20580
|
} catch {
|
|
19658
20581
|
return {};
|
|
19659
20582
|
}
|
|
19660
20583
|
}
|
|
19661
|
-
writeIndex(
|
|
19662
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
20584
|
+
writeIndex(entries) {
|
|
19663
20585
|
try {
|
|
19664
|
-
|
|
20586
|
+
fs7.mkdirSync(this.paths().tasks, { recursive: true });
|
|
20587
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
19665
20588
|
version: INDEX_VERSION,
|
|
19666
|
-
workspaceId,
|
|
19667
20589
|
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
19668
|
-
}),
|
|
19669
|
-
|
|
20590
|
+
}), false);
|
|
20591
|
+
} catch {}
|
|
20592
|
+
}
|
|
20593
|
+
indexTask(task, key, legacy) {
|
|
20594
|
+
try {
|
|
20595
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
20596
|
+
if (signature === null)
|
|
20597
|
+
return;
|
|
20598
|
+
const tasks = this.readIndex();
|
|
20599
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
20600
|
+
this.writeIndex(Object.values(tasks));
|
|
20601
|
+
} catch {}
|
|
20602
|
+
}
|
|
20603
|
+
parseTask(record, taskId) {
|
|
20604
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
20605
|
+
if (!parsed.ok)
|
|
20606
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
20607
|
+
if (parsed.data.id !== taskId)
|
|
20608
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
20609
|
+
return parsed;
|
|
20610
|
+
}
|
|
20611
|
+
parseRecord(bytes, schema, label) {
|
|
20612
|
+
let value;
|
|
20613
|
+
try {
|
|
20614
|
+
value = JSON.parse(bytes);
|
|
20615
|
+
} catch {
|
|
20616
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
20617
|
+
}
|
|
20618
|
+
return this.parseValue(value, schema, label);
|
|
20619
|
+
}
|
|
20620
|
+
parseValue(value, schema, label) {
|
|
20621
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
20622
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
20623
|
+
const parsed = parseStoredRecord(schema, value);
|
|
20624
|
+
if (parsed.success)
|
|
20625
|
+
return success(null, null, parsed.data);
|
|
20626
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
20627
|
+
if (parsed.critical.length > 0)
|
|
20628
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
20629
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
20630
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
20631
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
20632
|
+
}
|
|
20633
|
+
conflict(expected, actual) {
|
|
20634
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
20635
|
+
}
|
|
20636
|
+
workspaceConflict(expected, actual) {
|
|
20637
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
20638
|
+
}
|
|
20639
|
+
v2Dirs() {
|
|
20640
|
+
return [...new Set([this.root, this.origin])].map((dir) => path6.join(dir, ".workit"));
|
|
20641
|
+
}
|
|
20642
|
+
pendingMigrations() {
|
|
20643
|
+
return this.v2Dirs().filter((dir) => {
|
|
20644
|
+
const raw = this.readV2Workspace(path6.join(dir, "workspace.json"));
|
|
20645
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
20646
|
+
});
|
|
20647
|
+
}
|
|
20648
|
+
migrateV2(v2) {
|
|
20649
|
+
const dirs = this.paths();
|
|
20650
|
+
const v2Workspace = path6.join(v2, "workspace.json");
|
|
20651
|
+
const v2Tasks = path6.join(v2, "tasks");
|
|
20652
|
+
const legacy = path6.join(dirs.dir, "legacy", checkoutSlug(path6.dirname(v2)), "v2");
|
|
20653
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
20654
|
+
if (raw === null || raw === TOMBSTONE)
|
|
20655
|
+
return success(null, null, null);
|
|
20656
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path6.join(v2, "metadata.lock"), () => {
|
|
20657
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
20658
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
20659
|
+
return success(null, null, null);
|
|
20660
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
20661
|
+
if (!parsedWorkspace.ok)
|
|
20662
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
20663
|
+
let files = [];
|
|
20664
|
+
try {
|
|
20665
|
+
files = fs7.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path6.join(v2Tasks, name)));
|
|
20666
|
+
} catch {}
|
|
20667
|
+
const current = this.readWorkspace();
|
|
20668
|
+
if (!current.ok)
|
|
20669
|
+
return current;
|
|
20670
|
+
let workspaceMoved = false;
|
|
20671
|
+
let target = current.data;
|
|
20672
|
+
if (!target) {
|
|
20673
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
20674
|
+
const written = this.writeWorkspace(target);
|
|
20675
|
+
if (!written.ok)
|
|
20676
|
+
return written;
|
|
20677
|
+
workspaceMoved = true;
|
|
20678
|
+
}
|
|
20679
|
+
let migrated = 0;
|
|
20680
|
+
let skipped = 0;
|
|
20681
|
+
const done = new Map;
|
|
20682
|
+
try {
|
|
20683
|
+
fs7.mkdirSync(path6.join(legacy, "tasks"), { recursive: true });
|
|
20684
|
+
for (const name of ["workspace.json", "index.json"])
|
|
20685
|
+
if (fs7.existsSync(path6.join(v2, name)))
|
|
20686
|
+
fs7.copyFileSync(path6.join(v2, name), path6.join(legacy, name));
|
|
20687
|
+
for (const name of files) {
|
|
20688
|
+
const id = name.slice(0, -5);
|
|
20689
|
+
const file = path6.join(v2Tasks, name);
|
|
20690
|
+
const bytes = fs7.readFileSync(file, "utf8");
|
|
20691
|
+
const digest = sha256(bytes);
|
|
20692
|
+
fs7.writeFileSync(path6.join(legacy, "tasks", name), bytes);
|
|
20693
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
20694
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
20695
|
+
return failure2("recovery_required", `cannot migrate 2.x task ${id}: ${parsed.ok ? "file name and record ID differ" : parsed.error}; move ${file} aside to continue`, { path: file });
|
|
20696
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
20697
|
+
const origin = { path: path6.join(legacy, "tasks", name), digest };
|
|
20698
|
+
if (!fs7.existsSync(this.eventsPath(id))) {
|
|
20699
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
20700
|
+
migrated += 1;
|
|
20701
|
+
} else {
|
|
20702
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
20703
|
+
const loaded = this.load(id);
|
|
20704
|
+
if (!loaded.ok)
|
|
20705
|
+
return loaded;
|
|
20706
|
+
if (!loaded.data)
|
|
20707
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
20708
|
+
if (loaded.data.legacy?.digest === digest)
|
|
20709
|
+
return success(null, null, false);
|
|
20710
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
20711
|
+
type: "migrated.from_v2",
|
|
20712
|
+
legacy: origin
|
|
20713
|
+
});
|
|
20714
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
20715
|
+
});
|
|
20716
|
+
if (!resynced.ok)
|
|
20717
|
+
return resynced;
|
|
20718
|
+
if (resynced.data)
|
|
20719
|
+
migrated += 1;
|
|
20720
|
+
else
|
|
20721
|
+
skipped += 1;
|
|
20722
|
+
}
|
|
20723
|
+
done.set(name, digest);
|
|
20724
|
+
}
|
|
20725
|
+
} catch (error) {
|
|
20726
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
20727
|
+
path: v2
|
|
20728
|
+
});
|
|
20729
|
+
}
|
|
20730
|
+
try {
|
|
20731
|
+
let complete = true;
|
|
20732
|
+
for (const [name, digest] of done) {
|
|
20733
|
+
const file = path6.join(v2Tasks, name);
|
|
20734
|
+
if (sha256(fs7.readFileSync(file, "utf8")) !== digest) {
|
|
20735
|
+
complete = false;
|
|
20736
|
+
continue;
|
|
20737
|
+
}
|
|
20738
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
20739
|
+
}
|
|
20740
|
+
if (!complete)
|
|
20741
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
20742
|
+
path: v2
|
|
20743
|
+
});
|
|
20744
|
+
fs7.rmSync(path6.join(v2, "index.json"), { force: true });
|
|
20745
|
+
this.writeTombstone(v2, target.id);
|
|
20746
|
+
} catch (error) {
|
|
20747
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
20748
|
+
path: v2
|
|
20749
|
+
});
|
|
20750
|
+
}
|
|
20751
|
+
reportMigration({
|
|
20752
|
+
from: v2,
|
|
20753
|
+
to: dirs.dir,
|
|
20754
|
+
backup: legacy,
|
|
20755
|
+
tasks: migrated,
|
|
20756
|
+
skipped,
|
|
20757
|
+
workspace: workspaceMoved
|
|
20758
|
+
});
|
|
20759
|
+
return success(null, null, null);
|
|
20760
|
+
}));
|
|
20761
|
+
}
|
|
20762
|
+
adoptLocalStore() {
|
|
20763
|
+
const dirs = this.paths();
|
|
20764
|
+
const marker = path6.join(dirs.v2, "store.json");
|
|
20765
|
+
if (!dirs.shared || !fs7.existsSync(marker))
|
|
20766
|
+
return success(null, null, null);
|
|
20767
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
20768
|
+
if (!fs7.existsSync(marker))
|
|
20769
|
+
return success(null, null, null);
|
|
20770
|
+
let tasks = 0;
|
|
20771
|
+
try {
|
|
20772
|
+
const merge = (from, to) => {
|
|
20773
|
+
if (!fs7.existsSync(to)) {
|
|
20774
|
+
fs7.mkdirSync(path6.dirname(to), { recursive: true });
|
|
20775
|
+
try {
|
|
20776
|
+
fs7.renameSync(from, to);
|
|
20777
|
+
} catch {
|
|
20778
|
+
fs7.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
20779
|
+
fs7.rmSync(from, { recursive: true, force: true });
|
|
20780
|
+
}
|
|
20781
|
+
return;
|
|
20782
|
+
}
|
|
20783
|
+
if (fs7.lstatSync(from).isDirectory() && fs7.lstatSync(to).isDirectory())
|
|
20784
|
+
for (const name of fs7.readdirSync(from))
|
|
20785
|
+
merge(path6.join(from, name), path6.join(to, name));
|
|
20786
|
+
};
|
|
20787
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
20788
|
+
const from = path6.join(dirs.v2, sub);
|
|
20789
|
+
if (!fs7.existsSync(from))
|
|
20790
|
+
continue;
|
|
20791
|
+
if (sub === "tasks") {
|
|
20792
|
+
for (const name of fs7.readdirSync(from))
|
|
20793
|
+
if (validId(name) && !fs7.existsSync(path6.join(dirs.dir, sub, name)))
|
|
20794
|
+
tasks += 1;
|
|
20795
|
+
}
|
|
20796
|
+
merge(from, path6.join(dirs.dir, sub));
|
|
20797
|
+
}
|
|
20798
|
+
const ledger = path6.join(dirs.v2, "ledger");
|
|
20799
|
+
if (fs7.existsSync(ledger) && !fs7.existsSync(path6.join(dirs.dir, "ledger")))
|
|
20800
|
+
merge(ledger, path6.join(dirs.dir, "ledger"));
|
|
20801
|
+
fs7.rmSync(path6.join(dirs.v2, "task-index.json"), { force: true });
|
|
20802
|
+
fs7.renameSync(marker, path6.join(dirs.v2, "store.moved.json"));
|
|
20803
|
+
} catch (error) {
|
|
20804
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
20805
|
+
path: dirs.v2
|
|
20806
|
+
});
|
|
20807
|
+
}
|
|
20808
|
+
reportMigration({
|
|
20809
|
+
from: dirs.v2,
|
|
20810
|
+
to: dirs.dir,
|
|
20811
|
+
backup: dirs.v2,
|
|
20812
|
+
tasks,
|
|
20813
|
+
skipped: 0,
|
|
20814
|
+
workspace: false
|
|
20815
|
+
});
|
|
20816
|
+
return success(null, null, null);
|
|
20817
|
+
});
|
|
20818
|
+
}
|
|
20819
|
+
readV2Workspace(file) {
|
|
20820
|
+
let bytes;
|
|
20821
|
+
try {
|
|
20822
|
+
bytes = fs7.readFileSync(file, "utf8");
|
|
19670
20823
|
} catch {
|
|
19671
|
-
|
|
19672
|
-
fs5.unlinkSync(temporary);
|
|
19673
|
-
} catch {}
|
|
20824
|
+
return null;
|
|
19674
20825
|
}
|
|
19675
|
-
}
|
|
19676
|
-
indexTaskWrite(file, task) {
|
|
19677
20826
|
try {
|
|
19678
|
-
const
|
|
19679
|
-
if (
|
|
19680
|
-
return;
|
|
19681
|
-
const tasks = this.readIndex(task.workspaceId);
|
|
19682
|
-
tasks[task.id] = indexEntry(task, signature);
|
|
19683
|
-
this.writeIndex(task.workspaceId, Object.values(tasks));
|
|
20827
|
+
const value = JSON.parse(bytes);
|
|
20828
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
20829
|
+
return TOMBSTONE;
|
|
19684
20830
|
} catch {}
|
|
20831
|
+
return { bytes };
|
|
20832
|
+
}
|
|
20833
|
+
writeTombstone(v2, workspaceId) {
|
|
20834
|
+
const dirs = this.paths();
|
|
20835
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
20836
|
+
const marker = {
|
|
20837
|
+
schemaVersion: SCHEMA_VERSION,
|
|
20838
|
+
id: workspaceId ?? newId(),
|
|
20839
|
+
revision: newRevision(),
|
|
20840
|
+
root: path6.dirname(v2),
|
|
20841
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
20842
|
+
writer: null,
|
|
20843
|
+
store: {
|
|
20844
|
+
format: "workit-store",
|
|
20845
|
+
version: STORE_FORMAT,
|
|
20846
|
+
path: dirs.dir,
|
|
20847
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
20848
|
+
},
|
|
20849
|
+
critical: ["store"]
|
|
20850
|
+
};
|
|
20851
|
+
fs7.mkdirSync(v2, { recursive: true });
|
|
20852
|
+
const ignore = path6.join(v2, ".gitignore");
|
|
20853
|
+
if (!fs7.existsSync(ignore))
|
|
20854
|
+
fs7.writeFileSync(ignore, `*
|
|
20855
|
+
`);
|
|
20856
|
+
replaceFile(path6.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
20857
|
+
`, true);
|
|
20858
|
+
}
|
|
20859
|
+
stubFor(taskId) {
|
|
20860
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
20861
|
+
return `${JSON.stringify({
|
|
20862
|
+
schemaVersion: SCHEMA_VERSION,
|
|
20863
|
+
id: taskId,
|
|
20864
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
20865
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
20866
|
+
critical: ["store"]
|
|
20867
|
+
})}
|
|
20868
|
+
`;
|
|
19685
20869
|
}
|
|
19686
|
-
|
|
19687
|
-
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
19688
|
-
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
19689
|
-
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
19690
|
-
if (fs5.existsSync(destination)) {
|
|
19691
|
-
if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
|
|
19692
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
19693
|
-
const stamp = new Date;
|
|
19694
|
-
retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
|
|
19695
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
19696
|
-
return;
|
|
19697
|
-
}
|
|
19698
|
-
let temporary;
|
|
20870
|
+
isStub(file) {
|
|
19699
20871
|
try {
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
|
|
19704
|
-
fs5.fsyncSync(fd);
|
|
19705
|
-
} finally {
|
|
19706
|
-
fs5.closeSync(fd);
|
|
19707
|
-
}
|
|
19708
|
-
retryTransient(() => fs5.renameSync(temporary, destination));
|
|
19709
|
-
temporary = undefined;
|
|
19710
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
19711
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
19712
|
-
} finally {
|
|
19713
|
-
if (temporary)
|
|
19714
|
-
try {
|
|
19715
|
-
fs5.unlinkSync(temporary);
|
|
19716
|
-
} catch {}
|
|
20872
|
+
const value = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
20873
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
20874
|
+
} catch {
|
|
20875
|
+
return false;
|
|
19717
20876
|
}
|
|
19718
20877
|
}
|
|
19719
|
-
|
|
20878
|
+
writeTombstoneIfMissing() {
|
|
19720
20879
|
try {
|
|
19721
|
-
const
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
20880
|
+
const v2 = path6.join(this.root, ".workit");
|
|
20881
|
+
if (fs7.existsSync(path6.join(v2, "workspace.json")))
|
|
20882
|
+
return;
|
|
20883
|
+
let id = null;
|
|
20884
|
+
try {
|
|
20885
|
+
id = JSON.parse(fs7.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
20886
|
+
} catch {}
|
|
20887
|
+
this.writeTombstone(v2, id);
|
|
19725
20888
|
} catch {}
|
|
19726
20889
|
}
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
const file = path4.join(this.recoveryDir, name);
|
|
20890
|
+
initializeStorage() {
|
|
20891
|
+
const dirs = this.paths();
|
|
20892
|
+
retryTransient(() => fs7.mkdirSync(dirs.checkout, { recursive: true }));
|
|
20893
|
+
retryTransient(() => fs7.mkdirSync(dirs.tasks, { recursive: true }));
|
|
20894
|
+
if (!fs7.existsSync(dirs.marker))
|
|
20895
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
20896
|
+
`, false);
|
|
20897
|
+
if (!dirs.shared) {
|
|
20898
|
+
const ignore = path6.join(dirs.dir, ".gitignore");
|
|
20899
|
+
let current = null;
|
|
19738
20900
|
try {
|
|
19739
|
-
|
|
19740
|
-
if (!stat.isFile())
|
|
19741
|
-
continue;
|
|
19742
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
20901
|
+
current = fs7.readFileSync(ignore, "utf8");
|
|
19743
20902
|
} catch {}
|
|
20903
|
+
if (current !== `*
|
|
20904
|
+
`)
|
|
20905
|
+
retryTransient(() => fs7.writeFileSync(ignore, `*
|
|
20906
|
+
`));
|
|
19744
20907
|
}
|
|
19745
|
-
|
|
19746
|
-
}
|
|
19747
|
-
surplusCopies(copies, keep) {
|
|
19748
|
-
const ordered = copies.toSorted((left, right) => left.name === keep ? -1 : right.name === keep ? 1 : left.mtimeNs === right.mtimeNs ? left.name.localeCompare(right.name) : left.mtimeNs > right.mtimeNs ? -1 : 1);
|
|
19749
|
-
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
20908
|
+
this.writeTombstoneIfMissing();
|
|
19750
20909
|
}
|
|
19751
|
-
|
|
19752
|
-
const
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
20910
|
+
lockOptions(lockPath) {
|
|
20911
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
20912
|
+
return {
|
|
20913
|
+
lockPath,
|
|
20914
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
20915
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
20916
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
20917
|
+
staleRecovery: "remove-if-unchanged",
|
|
20918
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
20919
|
+
let ageMs = null;
|
|
20920
|
+
try {
|
|
20921
|
+
ageMs = nowMs - fs7.lstatSync(lockPath).mtimeMs;
|
|
20922
|
+
} catch {}
|
|
20923
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
20924
|
+
},
|
|
20925
|
+
shouldRemoveStaleLock: () => true,
|
|
20926
|
+
parsePayload: parseMetadataLockOrNull,
|
|
20927
|
+
payload: () => ({
|
|
20928
|
+
pid: process.pid,
|
|
20929
|
+
processStart: processStartOf(process.pid),
|
|
20930
|
+
host: localLockHost(),
|
|
20931
|
+
nonce: randomUUID3()
|
|
20932
|
+
})
|
|
19758
20933
|
};
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
20934
|
+
}
|
|
20935
|
+
acquire(lockPath, options) {
|
|
20936
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
20937
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
20938
|
+
while (true) {
|
|
19762
20939
|
try {
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
for (const copies of groups.values()) {
|
|
19767
|
-
const surplus = this.surplusCopies(copies);
|
|
19768
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
19769
|
-
for (const copy of surplus) {
|
|
19770
|
-
report.recovery.removed += 1;
|
|
19771
|
-
try {
|
|
19772
|
-
report.recovery.removedBytes += fs5.statSync(copy.path).size;
|
|
19773
|
-
} catch {}
|
|
19774
|
-
if (!dryRun)
|
|
19775
|
-
fs5.rmSync(copy.path, { force: true });
|
|
19776
|
-
}
|
|
19777
|
-
}
|
|
19778
|
-
const nowMs = Date.now();
|
|
19779
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
19780
|
-
if (!fs5.existsSync(directory))
|
|
19781
|
-
continue;
|
|
19782
|
-
for (const name of fs5.readdirSync(directory)) {
|
|
19783
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
19784
|
-
continue;
|
|
19785
|
-
const file = path4.join(directory, name);
|
|
19786
|
-
const stat = fs5.lstatSync(file);
|
|
19787
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
19788
|
-
continue;
|
|
19789
|
-
report.temporary.removed += 1;
|
|
19790
|
-
if (!dryRun)
|
|
19791
|
-
fs5.rmSync(file, { force: true });
|
|
19792
|
-
}
|
|
19793
|
-
}
|
|
19794
|
-
return success(null, null, null);
|
|
19795
|
-
} catch (error) {
|
|
19796
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
19797
|
-
path: this.recoveryDir
|
|
20940
|
+
return acquireFileLockSync(lockPath, {
|
|
20941
|
+
...options,
|
|
20942
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
19798
20943
|
});
|
|
20944
|
+
} catch (error) {
|
|
20945
|
+
const code = error?.code;
|
|
20946
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
20947
|
+
throw error;
|
|
20948
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
20949
|
+
throw error;
|
|
20950
|
+
if (code !== "file_lock_stale")
|
|
20951
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
19799
20952
|
}
|
|
19800
|
-
};
|
|
19801
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
19802
|
-
if (!pruned.ok)
|
|
19803
|
-
return pruned;
|
|
19804
|
-
const tasks = this.listTasks();
|
|
19805
|
-
if (!tasks.ok)
|
|
19806
|
-
return tasks;
|
|
19807
|
-
for (const task of tasks.data) {
|
|
19808
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
19809
|
-
const removed = task.candidates.length - deduped.length;
|
|
19810
|
-
if (removed === 0)
|
|
19811
|
-
continue;
|
|
19812
|
-
if (task.status === "active") {
|
|
19813
|
-
report.candidates.skippedActive.push(task.id);
|
|
19814
|
-
continue;
|
|
19815
|
-
}
|
|
19816
|
-
if (task.status === "closed") {
|
|
19817
|
-
report.candidates.skippedClosed.push(task.id);
|
|
19818
|
-
continue;
|
|
19819
|
-
}
|
|
19820
|
-
if (!dryRun) {
|
|
19821
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
19822
|
-
...current,
|
|
19823
|
-
candidates: dedupeCandidates(current.candidates)
|
|
19824
|
-
}));
|
|
19825
|
-
if (!written.ok) {
|
|
19826
|
-
report.candidates.failed.push(task.id);
|
|
19827
|
-
continue;
|
|
19828
|
-
}
|
|
19829
|
-
}
|
|
19830
|
-
report.candidates.removed += removed;
|
|
19831
|
-
report.candidates.tasks.push(task.id);
|
|
19832
20953
|
}
|
|
19833
|
-
return success(null, null, report);
|
|
19834
20954
|
}
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
return
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
return null;
|
|
19844
|
-
for (const candidate of candidates.data) {
|
|
19845
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
19846
|
-
continue;
|
|
19847
|
-
try {
|
|
19848
|
-
if (!fs5.lstatSync(candidate.path).isFile())
|
|
19849
|
-
continue;
|
|
19850
|
-
const bytes = fs5.readFileSync(candidate.path);
|
|
19851
|
-
if (digestBytes(bytes) !== digest)
|
|
19852
|
-
continue;
|
|
19853
|
-
if (target === "workspace") {
|
|
19854
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
19855
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
19856
|
-
return bytes;
|
|
19857
|
-
} else {
|
|
19858
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
19859
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
19860
|
-
return bytes;
|
|
19861
|
-
}
|
|
19862
|
-
} catch {}
|
|
20955
|
+
withLock(lockPath, operation) {
|
|
20956
|
+
try {
|
|
20957
|
+
this.initializeStorage();
|
|
20958
|
+
fs7.mkdirSync(path6.dirname(lockPath), { recursive: true });
|
|
20959
|
+
} catch (error) {
|
|
20960
|
+
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
20961
|
+
path: this.paths().dir
|
|
20962
|
+
});
|
|
19863
20963
|
}
|
|
19864
|
-
|
|
19865
|
-
}
|
|
19866
|
-
fsyncDirectory(directory) {
|
|
20964
|
+
let handle;
|
|
19867
20965
|
try {
|
|
19868
|
-
|
|
19869
|
-
try {
|
|
19870
|
-
fs5.fsyncSync(fd);
|
|
19871
|
-
} finally {
|
|
19872
|
-
fs5.closeSync(fd);
|
|
19873
|
-
}
|
|
20966
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
19874
20967
|
} catch (error) {
|
|
19875
|
-
|
|
19876
|
-
throw error;
|
|
20968
|
+
return this.lockFailure(lockPath, error);
|
|
19877
20969
|
}
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
if (!fs5.existsSync(this.tasksDir))
|
|
19881
|
-
return null;
|
|
19882
|
-
const ids = new Set;
|
|
20970
|
+
hold(lockPath);
|
|
20971
|
+
let result;
|
|
19883
20972
|
try {
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19889
|
-
|
|
19890
|
-
|
|
20973
|
+
if (!handle.verifyStillHeld())
|
|
20974
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
20975
|
+
else {
|
|
20976
|
+
try {
|
|
20977
|
+
result = operation();
|
|
20978
|
+
} catch (error) {
|
|
20979
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
20980
|
+
}
|
|
19891
20981
|
}
|
|
19892
|
-
} catch {
|
|
19893
|
-
|
|
20982
|
+
} catch (error) {
|
|
20983
|
+
result = this.lockFailure(lockPath, error);
|
|
20984
|
+
}
|
|
20985
|
+
drop(lockPath);
|
|
20986
|
+
try {
|
|
20987
|
+
retryTransient(() => handle.release());
|
|
20988
|
+
} catch (error) {
|
|
20989
|
+
const released = this.lockFailure(lockPath, error);
|
|
20990
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
20991
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
19894
20992
|
}
|
|
19895
|
-
return
|
|
20993
|
+
return result;
|
|
19896
20994
|
}
|
|
19897
|
-
|
|
20995
|
+
lockDirWritable(lockPath) {
|
|
20996
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
19898
20997
|
try {
|
|
19899
|
-
|
|
20998
|
+
fs7.closeSync(fs7.openSync(probe, "wx", 384));
|
|
19900
20999
|
} catch {
|
|
19901
|
-
return
|
|
21000
|
+
return false;
|
|
19902
21001
|
}
|
|
21002
|
+
try {
|
|
21003
|
+
fs7.rmSync(probe, { force: true });
|
|
21004
|
+
} catch {}
|
|
21005
|
+
return true;
|
|
19903
21006
|
}
|
|
19904
|
-
|
|
21007
|
+
lockHolderPresent(lockPath) {
|
|
19905
21008
|
try {
|
|
19906
|
-
|
|
19907
|
-
return
|
|
21009
|
+
fs7.lstatSync(lockPath);
|
|
21010
|
+
return true;
|
|
19908
21011
|
} catch (error) {
|
|
19909
|
-
|
|
19910
|
-
return { exists: false, result: success(null, null, null) };
|
|
19911
|
-
return {
|
|
19912
|
-
exists: true,
|
|
19913
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
19914
|
-
path: file
|
|
19915
|
-
})
|
|
19916
|
-
};
|
|
21012
|
+
return error?.code !== "ENOENT";
|
|
19917
21013
|
}
|
|
19918
21014
|
}
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
21015
|
+
lockFailure(lockPath, error) {
|
|
21016
|
+
const value = error;
|
|
21017
|
+
const code = typeof value?.code === "string" ? value.code : "";
|
|
21018
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
21019
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
21020
|
+
path: lockPath,
|
|
21021
|
+
guidance: `Check permissions and the read-only attribute on ${path6.dirname(lockPath)}.`
|
|
21022
|
+
});
|
|
21023
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
21024
|
+
let lock = null;
|
|
21025
|
+
try {
|
|
21026
|
+
lock = parseMetadataLockOrNull(fs7.readFileSync(lockPath, "utf8"));
|
|
21027
|
+
} catch {}
|
|
21028
|
+
if (lock?.externalAction)
|
|
21029
|
+
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
21030
|
+
outcome: "not_started",
|
|
21031
|
+
path: lockPath
|
|
21032
|
+
});
|
|
21033
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
21034
|
+
outcome: "not_started",
|
|
21035
|
+
path: lockPath,
|
|
21036
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
21037
|
+
});
|
|
19925
21038
|
}
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
const parsed = parseStoredRecord(schema, value);
|
|
19929
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
19930
|
-
return success(null, null, parsed.data);
|
|
19931
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
19932
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
19933
|
-
if (parsed.success)
|
|
19934
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
19935
|
-
if (parsed.critical.length > 0)
|
|
19936
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
19937
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
19938
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
19939
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
19940
|
-
}
|
|
19941
|
-
conflict(expected, actual) {
|
|
19942
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
19943
|
-
expectedRevision: expected,
|
|
19944
|
-
actualRevision: actual
|
|
19945
|
-
});
|
|
19946
|
-
}
|
|
19947
|
-
workspaceConflict(expected, actual) {
|
|
19948
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
19949
|
-
}
|
|
19950
|
-
taskPath(taskId) {
|
|
19951
|
-
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
19952
|
-
}
|
|
19953
|
-
get workitDir() {
|
|
19954
|
-
return path4.join(this.root, ".workit");
|
|
19955
|
-
}
|
|
19956
|
-
get tasksDir() {
|
|
19957
|
-
return path4.join(this.workitDir, "tasks");
|
|
19958
|
-
}
|
|
19959
|
-
get recoveryDir() {
|
|
19960
|
-
return path4.join(this.workitDir, "recovery");
|
|
19961
|
-
}
|
|
19962
|
-
get workspacePath() {
|
|
19963
|
-
return path4.join(this.workitDir, "workspace.json");
|
|
19964
|
-
}
|
|
19965
|
-
get indexPath() {
|
|
19966
|
-
return path4.join(this.workitDir, "index.json");
|
|
19967
|
-
}
|
|
19968
|
-
get lockPath() {
|
|
19969
|
-
return path4.join(this.workitDir, "metadata.lock");
|
|
19970
|
-
}
|
|
19971
|
-
get gitignorePath() {
|
|
19972
|
-
return path4.join(this.workitDir, ".gitignore");
|
|
21039
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
21040
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
19973
21041
|
}
|
|
19974
21042
|
}
|
|
19975
21043
|
// packages/workit-core/src/core/workers.ts
|
|
@@ -20033,30 +21101,30 @@ function assertProductWriteAllowed(input) {
|
|
|
20033
21101
|
}
|
|
20034
21102
|
|
|
20035
21103
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
20036
|
-
import { createHash as
|
|
20037
|
-
import * as
|
|
20038
|
-
import { spawnSync as
|
|
20039
|
-
import
|
|
21104
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
21105
|
+
import * as fs12 from "node:fs";
|
|
21106
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
21107
|
+
import path14 from "node:path";
|
|
20040
21108
|
|
|
20041
21109
|
// packages/workit-core/src/core/authority.ts
|
|
20042
|
-
import { createHash as
|
|
20043
|
-
import * as
|
|
20044
|
-
import
|
|
21110
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
21111
|
+
import * as fs9 from "node:fs";
|
|
21112
|
+
import path11 from "node:path";
|
|
20045
21113
|
|
|
20046
21114
|
// packages/workit-core/src/core/auto-approval.ts
|
|
20047
|
-
import { createHash as
|
|
21115
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
20048
21116
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
20049
|
-
import
|
|
21117
|
+
import path10 from "node:path";
|
|
20050
21118
|
|
|
20051
21119
|
// packages/workit-core/src/core/workspaces.ts
|
|
20052
|
-
import { createHash as
|
|
21120
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
20053
21121
|
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
20054
|
-
import
|
|
21122
|
+
import path8 from "node:path";
|
|
20055
21123
|
|
|
20056
21124
|
// packages/workit-core/src/core/config.ts
|
|
20057
21125
|
import {
|
|
20058
|
-
copyFileSync,
|
|
20059
|
-
cpSync,
|
|
21126
|
+
copyFileSync as copyFileSync2,
|
|
21127
|
+
cpSync as cpSync2,
|
|
20060
21128
|
existsSync as existsSync3,
|
|
20061
21129
|
mkdirSync as mkdirSync2,
|
|
20062
21130
|
readFileSync as readFileSync2,
|
|
@@ -20064,7 +21132,7 @@ import {
|
|
|
20064
21132
|
writeFileSync as writeFileSync2
|
|
20065
21133
|
} from "node:fs";
|
|
20066
21134
|
import os from "node:os";
|
|
20067
|
-
import
|
|
21135
|
+
import path7 from "node:path";
|
|
20068
21136
|
|
|
20069
21137
|
// packages/workit-core/src/core/boundary.ts
|
|
20070
21138
|
import { statSync as statSync2 } from "node:fs";
|
|
@@ -20123,7 +21191,7 @@ var mergePreset = (preset, input = {}, current = {
|
|
|
20123
21191
|
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
20124
21192
|
};
|
|
20125
21193
|
};
|
|
20126
|
-
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
21194
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
|
|
20127
21195
|
var migratedDir = null;
|
|
20128
21196
|
var migrationFailed = false;
|
|
20129
21197
|
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
@@ -20133,7 +21201,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
20133
21201
|
migratedDir = dir;
|
|
20134
21202
|
return dir;
|
|
20135
21203
|
}
|
|
20136
|
-
const legacy =
|
|
21204
|
+
const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
20137
21205
|
if (!existsSync3(legacy)) {
|
|
20138
21206
|
migratedDir = dir;
|
|
20139
21207
|
return dir;
|
|
@@ -20146,15 +21214,15 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
20146
21214
|
mkdirSync2(dir, { recursive: true });
|
|
20147
21215
|
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
20148
21216
|
for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
|
|
20149
|
-
const src =
|
|
20150
|
-
const dest =
|
|
21217
|
+
const src = path7.join(legacy, entry.name);
|
|
21218
|
+
const dest = path7.join(dir, entry.name);
|
|
20151
21219
|
if (existsSync3(dest))
|
|
20152
21220
|
continue;
|
|
20153
21221
|
try {
|
|
20154
21222
|
if (entry.isDirectory())
|
|
20155
|
-
|
|
21223
|
+
cpSync2(src, dest, { recursive: true });
|
|
20156
21224
|
else if (entry.isFile())
|
|
20157
|
-
|
|
21225
|
+
copyFileSync2(src, dest);
|
|
20158
21226
|
} catch (err) {
|
|
20159
21227
|
migrationFailed = true;
|
|
20160
21228
|
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
@@ -20219,7 +21287,7 @@ var parseConfigResult = (raw, file) => {
|
|
|
20219
21287
|
};
|
|
20220
21288
|
};
|
|
20221
21289
|
var readConfigTyped = (dir) => {
|
|
20222
|
-
const file =
|
|
21290
|
+
const file = path7.join(dir ?? configDir(), "config.json");
|
|
20223
21291
|
return parseConfigResult(readSafe(file), file);
|
|
20224
21292
|
};
|
|
20225
21293
|
var readConfig = () => {
|
|
@@ -20316,7 +21384,7 @@ var releaseTrackSchema = object2({
|
|
|
20316
21384
|
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
20317
21385
|
}
|
|
20318
21386
|
}
|
|
20319
|
-
if (track.versionSource.kind === "package-json" && (
|
|
21387
|
+
if (track.versionSource.kind === "package-json" && (path8.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
|
|
20320
21388
|
context.addIssue({
|
|
20321
21389
|
code: "custom",
|
|
20322
21390
|
path: ["versionSource", "path"],
|
|
@@ -20370,7 +21438,7 @@ var workspacesFileSchema = object2({ workspaces: array(workspaceConfigSchema).op
|
|
|
20370
21438
|
names.add(workspace.name);
|
|
20371
21439
|
}
|
|
20372
21440
|
});
|
|
20373
|
-
var workspacesRevision = (contents) =>
|
|
21441
|
+
var workspacesRevision = (contents) => createHash5("sha256").update(contents ?? "<missing>").digest("hex");
|
|
20374
21442
|
var validateWorkspacesDocument = (parsed, file) => {
|
|
20375
21443
|
if (!isConfigObject(parsed)) {
|
|
20376
21444
|
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
@@ -20408,7 +21476,7 @@ var validateWorkspacesDocument = (parsed, file) => {
|
|
|
20408
21476
|
};
|
|
20409
21477
|
};
|
|
20410
21478
|
var readWorkspacesResult = (dir = configDir()) => {
|
|
20411
|
-
const file =
|
|
21479
|
+
const file = path8.join(dir, "workspaces.json");
|
|
20412
21480
|
let raw;
|
|
20413
21481
|
try {
|
|
20414
21482
|
raw = readFileSync3(file, "utf8");
|
|
@@ -20579,7 +21647,7 @@ var runtimeWorkspaceIndexSchema = object2({
|
|
|
20579
21647
|
workspaces: array(object2({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
|
|
20580
21648
|
}).passthrough();
|
|
20581
21649
|
var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
|
|
20582
|
-
const file =
|
|
21650
|
+
const file = path8.join(configDir(), "workspaces.json");
|
|
20583
21651
|
let raw;
|
|
20584
21652
|
try {
|
|
20585
21653
|
raw = readFileSync3(file, "utf8");
|
|
@@ -20710,13 +21778,13 @@ var gitContext = (workspaceRoot, paths = []) => {
|
|
|
20710
21778
|
};
|
|
20711
21779
|
|
|
20712
21780
|
// packages/workit-core/src/core/vcs-config.ts
|
|
20713
|
-
import
|
|
20714
|
-
import
|
|
20715
|
-
import { spawnSync as
|
|
20716
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ??
|
|
21781
|
+
import fs8 from "node:fs";
|
|
21782
|
+
import path9 from "node:path";
|
|
21783
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
21784
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
|
|
20717
21785
|
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
20718
21786
|
var remoteProvider = (cwd) => {
|
|
20719
|
-
const r =
|
|
21787
|
+
const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
20720
21788
|
if (r.status !== 0)
|
|
20721
21789
|
return null;
|
|
20722
21790
|
const url = (r.stdout ?? "").trim();
|
|
@@ -20753,7 +21821,7 @@ var remoteEndpoint = (remote) => {
|
|
|
20753
21821
|
var configuredSshEndpoint = (host) => {
|
|
20754
21822
|
if (host.startsWith("-"))
|
|
20755
21823
|
return null;
|
|
20756
|
-
const resolved =
|
|
21824
|
+
const resolved = spawnSync3("ssh", ["-G", host], {
|
|
20757
21825
|
encoding: "utf8",
|
|
20758
21826
|
timeout: 5000,
|
|
20759
21827
|
maxBuffer: 1024 * 1024,
|
|
@@ -20803,7 +21871,7 @@ var readVcsConfig = () => {
|
|
|
20803
21871
|
const file = vcsConfigPath();
|
|
20804
21872
|
let raw;
|
|
20805
21873
|
try {
|
|
20806
|
-
raw =
|
|
21874
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
20807
21875
|
} catch {
|
|
20808
21876
|
return { status: "missing", path: file, config: {} };
|
|
20809
21877
|
}
|
|
@@ -20869,7 +21937,7 @@ function vcsConfig(mode, cwd) {
|
|
|
20869
21937
|
const prov = cfg[provider] ?? {};
|
|
20870
21938
|
const out = {
|
|
20871
21939
|
ok: true,
|
|
20872
|
-
configPath:
|
|
21940
|
+
configPath: path9.resolve(cfgPath),
|
|
20873
21941
|
provider,
|
|
20874
21942
|
defaultTargetBranch: defaultTarget,
|
|
20875
21943
|
pr: cfg.pr ?? {},
|
|
@@ -20904,7 +21972,7 @@ function vcsCliIdentity(cwd) {
|
|
|
20904
21972
|
PATH: process.env.PATH ?? "",
|
|
20905
21973
|
...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
|
|
20906
21974
|
};
|
|
20907
|
-
const result =
|
|
21975
|
+
const result = spawnSync3(bin, ["api", "user"], {
|
|
20908
21976
|
cwd: root,
|
|
20909
21977
|
encoding: "utf8",
|
|
20910
21978
|
env
|
|
@@ -20946,7 +22014,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20946
22014
|
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
20947
22015
|
descriptionPreview: desc.slice(0, 600)
|
|
20948
22016
|
});
|
|
20949
|
-
const remoteResult =
|
|
22017
|
+
const remoteResult = spawnSync3("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
20950
22018
|
cwd,
|
|
20951
22019
|
encoding: "utf8"
|
|
20952
22020
|
});
|
|
@@ -20962,7 +22030,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20962
22030
|
if (!hostingApiHostMatches(remote, host))
|
|
20963
22031
|
return { ok: false, error: "PR style target does not match the provider host" };
|
|
20964
22032
|
if (provider === "gitlab") {
|
|
20965
|
-
const run = (args) =>
|
|
22033
|
+
const run = (args) => spawnSync3("glab", ["api", ...args], {
|
|
20966
22034
|
cwd,
|
|
20967
22035
|
encoding: "utf8",
|
|
20968
22036
|
env
|
|
@@ -20982,7 +22050,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20982
22050
|
});
|
|
20983
22051
|
}
|
|
20984
22052
|
} else if (provider === "github") {
|
|
20985
|
-
const r =
|
|
22053
|
+
const r = spawnSync3("gh", [
|
|
20986
22054
|
"pr",
|
|
20987
22055
|
"list",
|
|
20988
22056
|
"--repo",
|
|
@@ -21054,7 +22122,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
|
|
|
21054
22122
|
var digestOf = (configDirPath) => {
|
|
21055
22123
|
try {
|
|
21056
22124
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
21057
|
-
return
|
|
22125
|
+
return createHash6("sha256").update(raw).digest("hex");
|
|
21058
22126
|
} catch {
|
|
21059
22127
|
return null;
|
|
21060
22128
|
}
|
|
@@ -21098,7 +22166,7 @@ var targetOf = (root, descriptor) => {
|
|
|
21098
22166
|
const cwd = parsed?.payload?.cwd;
|
|
21099
22167
|
if (cwd === undefined)
|
|
21100
22168
|
return root;
|
|
21101
|
-
if (typeof cwd !== "string" || !
|
|
22169
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
21102
22170
|
return null;
|
|
21103
22171
|
return realpathSync3(cwd);
|
|
21104
22172
|
} catch {
|
|
@@ -21323,11 +22391,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
|
21323
22391
|
return [];
|
|
21324
22392
|
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
21325
22393
|
}
|
|
21326
|
-
var
|
|
22394
|
+
var digestBytes = (bytes) => createHash7("sha256").update(bytes).digest("hex");
|
|
21327
22395
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
21328
22396
|
let root;
|
|
21329
22397
|
try {
|
|
21330
|
-
root =
|
|
22398
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
21331
22399
|
} catch {
|
|
21332
22400
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
21333
22401
|
}
|
|
@@ -21336,23 +22404,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
21336
22404
|
continue;
|
|
21337
22405
|
if (!reference.digest)
|
|
21338
22406
|
return failure2("invalid_input", "document references require a byte digest");
|
|
21339
|
-
const target =
|
|
21340
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
22407
|
+
const target = path11.resolve(root, reference.path);
|
|
22408
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
21341
22409
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
21342
22410
|
fields: [
|
|
21343
22411
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
21344
22412
|
]
|
|
21345
22413
|
});
|
|
21346
22414
|
try {
|
|
21347
|
-
const relative =
|
|
22415
|
+
const relative = path11.relative(root, target);
|
|
21348
22416
|
let current = root;
|
|
21349
|
-
for (const segment of relative.split(
|
|
22417
|
+
for (const segment of relative.split(path11.sep)) {
|
|
21350
22418
|
if (!segment)
|
|
21351
22419
|
continue;
|
|
21352
|
-
current =
|
|
21353
|
-
const stat =
|
|
21354
|
-
const real =
|
|
21355
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
22420
|
+
current = path11.join(current, segment);
|
|
22421
|
+
const stat = fs9.lstatSync(current);
|
|
22422
|
+
const real = fs9.realpathSync(current);
|
|
22423
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
21356
22424
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
21357
22425
|
fields: [
|
|
21358
22426
|
{
|
|
@@ -21368,7 +22436,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
21368
22436
|
if (current !== target && !stat.isDirectory())
|
|
21369
22437
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
21370
22438
|
}
|
|
21371
|
-
if (
|
|
22439
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
21372
22440
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
21373
22441
|
} catch {
|
|
21374
22442
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -21732,9 +22800,9 @@ var bindingCovers = scopeCovers;
|
|
|
21732
22800
|
var verifyDecisionContent = verifyContentRefs;
|
|
21733
22801
|
|
|
21734
22802
|
// packages/workit-core/src/check-config.ts
|
|
21735
|
-
import { spawnSync as
|
|
21736
|
-
import
|
|
21737
|
-
import
|
|
22803
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
22804
|
+
import fs10 from "node:fs";
|
|
22805
|
+
import path12 from "node:path";
|
|
21738
22806
|
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
21739
22807
|
var CHECKS_FILE = "workit.checks.json";
|
|
21740
22808
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -21776,7 +22844,7 @@ function splitCommand(command) {
|
|
|
21776
22844
|
return argv.length ? argv : new Error("empty command");
|
|
21777
22845
|
}
|
|
21778
22846
|
var git = (cwd, args) => {
|
|
21779
|
-
const run =
|
|
22847
|
+
const run = spawnSync4("git", args, {
|
|
21780
22848
|
cwd,
|
|
21781
22849
|
encoding: "utf8",
|
|
21782
22850
|
timeout: 20000,
|
|
@@ -21787,10 +22855,10 @@ var git = (cwd, args) => {
|
|
|
21787
22855
|
return value || null;
|
|
21788
22856
|
};
|
|
21789
22857
|
function checkRoot(cwd) {
|
|
21790
|
-
return
|
|
22858
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
21791
22859
|
}
|
|
21792
22860
|
var packageManager = (root) => {
|
|
21793
|
-
const has = (file) =>
|
|
22861
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
21794
22862
|
if (has("bun.lock") || has("bun.lockb"))
|
|
21795
22863
|
return "bun";
|
|
21796
22864
|
if (has("pnpm-lock.yaml"))
|
|
@@ -21810,7 +22878,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
|
21810
22878
|
function detectedChecks(root) {
|
|
21811
22879
|
const read = (file) => {
|
|
21812
22880
|
try {
|
|
21813
|
-
return
|
|
22881
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
21814
22882
|
} catch {
|
|
21815
22883
|
return null;
|
|
21816
22884
|
}
|
|
@@ -21849,7 +22917,7 @@ function detectedChecks(root) {
|
|
|
21849
22917
|
var GATES = ["testing", "verification"];
|
|
21850
22918
|
function loadCheckConfig(cwd) {
|
|
21851
22919
|
const root = checkRoot(cwd);
|
|
21852
|
-
const file =
|
|
22920
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
21853
22921
|
const none = (error = null) => ({
|
|
21854
22922
|
source: error ? CHECKS_FILE : "none",
|
|
21855
22923
|
root,
|
|
@@ -21859,7 +22927,7 @@ function loadCheckConfig(cwd) {
|
|
|
21859
22927
|
});
|
|
21860
22928
|
let text = null;
|
|
21861
22929
|
try {
|
|
21862
|
-
text =
|
|
22930
|
+
text = fs10.readFileSync(file, "utf8");
|
|
21863
22931
|
} catch (error) {
|
|
21864
22932
|
if (error.code !== "ENOENT")
|
|
21865
22933
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -21922,10 +22990,10 @@ function workitAvailable(env = process.env) {
|
|
|
21922
22990
|
return workitOnPath;
|
|
21923
22991
|
const windows = process.platform === "win32";
|
|
21924
22992
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
21925
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
22993
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
21926
22994
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
21927
22995
|
try {
|
|
21928
|
-
return
|
|
22996
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
21929
22997
|
} catch {
|
|
21930
22998
|
return false;
|
|
21931
22999
|
}
|
|
@@ -21937,15 +23005,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
21937
23005
|
}
|
|
21938
23006
|
|
|
21939
23007
|
// packages/workit-core/src/git/rev.ts
|
|
21940
|
-
import { spawnSync as
|
|
21941
|
-
import { createHash as
|
|
21942
|
-
import
|
|
23008
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
23009
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
23010
|
+
import fs11 from "node:fs";
|
|
21943
23011
|
import os2 from "node:os";
|
|
21944
|
-
import
|
|
23012
|
+
import path13 from "node:path";
|
|
21945
23013
|
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
21946
23014
|
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
21947
23015
|
var git2 = (cwd, args, options = {}) => {
|
|
21948
|
-
const result =
|
|
23016
|
+
const result = spawnSync5("git", args, {
|
|
21949
23017
|
cwd,
|
|
21950
23018
|
encoding: "utf8",
|
|
21951
23019
|
env: options.env ?? process.env,
|
|
@@ -21975,10 +23043,10 @@ function headSha(cwd) {
|
|
|
21975
23043
|
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
21976
23044
|
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
21977
23045
|
var seedIndex = (from, to) => {
|
|
21978
|
-
|
|
21979
|
-
const { atimeNs, mtimeNs } =
|
|
23046
|
+
fs11.copyFileSync(from, to);
|
|
23047
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
21980
23048
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
21981
|
-
|
|
23049
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
21982
23050
|
};
|
|
21983
23051
|
function worktreeTree(cwd, options = {}) {
|
|
21984
23052
|
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
@@ -21987,10 +23055,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
21987
23055
|
return null;
|
|
21988
23056
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
21989
23057
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
21990
|
-
const scratch =
|
|
23058
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
21991
23059
|
try {
|
|
21992
|
-
const tempIndex =
|
|
21993
|
-
if (
|
|
23060
|
+
const tempIndex = path13.join(scratch, "index");
|
|
23061
|
+
if (fs11.existsSync(indexPath))
|
|
21994
23062
|
seedIndex(indexPath, tempIndex);
|
|
21995
23063
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
21996
23064
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -22002,7 +23070,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
22002
23070
|
const skipped = [];
|
|
22003
23071
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
22004
23072
|
try {
|
|
22005
|
-
const stat =
|
|
23073
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
22006
23074
|
if (stat.isFile() && stat.size > cap)
|
|
22007
23075
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
22008
23076
|
} catch {}
|
|
@@ -22019,10 +23087,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
22019
23087
|
if (!tree)
|
|
22020
23088
|
return null;
|
|
22021
23089
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
22022
|
-
const key = skipped.length ? `sha256:${
|
|
23090
|
+
const key = skipped.length ? `sha256:${createHash8("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
22023
23091
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
22024
23092
|
} finally {
|
|
22025
|
-
|
|
23093
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
22026
23094
|
}
|
|
22027
23095
|
}
|
|
22028
23096
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -22042,16 +23110,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
22042
23110
|
});
|
|
22043
23111
|
if (!status.ok)
|
|
22044
23112
|
return null;
|
|
22045
|
-
const hash =
|
|
23113
|
+
const hash = createHash8("sha256");
|
|
22046
23114
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
22047
23115
|
for (const entry of status.stdout.split("\x00")) {
|
|
22048
23116
|
if (entry.length < 4)
|
|
22049
23117
|
continue;
|
|
22050
23118
|
const file = entry.slice(3);
|
|
22051
|
-
const absolute =
|
|
23119
|
+
const absolute = path13.join(top, file);
|
|
22052
23120
|
let stat = "-";
|
|
22053
23121
|
try {
|
|
22054
|
-
const info =
|
|
23122
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
22055
23123
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
22056
23124
|
if (info.isDirectory())
|
|
22057
23125
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -22062,15 +23130,15 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
22062
23130
|
}
|
|
22063
23131
|
|
|
22064
23132
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
22065
|
-
var
|
|
22066
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
22067
|
-
var relative = (root, candidate) =>
|
|
23133
|
+
var digestBytes2 = (value) => createHash9("sha256").update(value).digest("hex");
|
|
23134
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
|
|
23135
|
+
var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
|
|
22068
23136
|
var canonicalPath = (value) => {
|
|
22069
23137
|
if (typeof value !== "string" || !value)
|
|
22070
23138
|
return null;
|
|
22071
23139
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
22072
23140
|
return null;
|
|
22073
|
-
const normalized =
|
|
23141
|
+
const normalized = path14.posix.normalize(value);
|
|
22074
23142
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
22075
23143
|
return null;
|
|
22076
23144
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -22106,7 +23174,7 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
22106
23174
|
return left.length - right.length;
|
|
22107
23175
|
};
|
|
22108
23176
|
var gitPaths = (root) => {
|
|
22109
|
-
const result =
|
|
23177
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
22110
23178
|
cwd: root,
|
|
22111
23179
|
encoding: "buffer"
|
|
22112
23180
|
});
|
|
@@ -22118,21 +23186,21 @@ var gitPaths = (root) => {
|
|
|
22118
23186
|
git: !/not a git repository/i.test(stderr)
|
|
22119
23187
|
};
|
|
22120
23188
|
}
|
|
22121
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
22122
|
-
const stagedDeleted =
|
|
23189
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
23190
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
22123
23191
|
if (stagedDeleted.status !== 0) {
|
|
22124
23192
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
22125
23193
|
if (!/not a git repository/i.test(stderr))
|
|
22126
23194
|
return { paths, uncertain: true, git: true };
|
|
22127
23195
|
} else if (stagedDeleted.stdout) {
|
|
22128
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
23196
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
22129
23197
|
}
|
|
22130
23198
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
22131
23199
|
};
|
|
22132
23200
|
var walk = (root, directory, output) => {
|
|
22133
23201
|
let names;
|
|
22134
23202
|
try {
|
|
22135
|
-
names =
|
|
23203
|
+
names = fs12.readdirSync(directory);
|
|
22136
23204
|
} catch {
|
|
22137
23205
|
return true;
|
|
22138
23206
|
}
|
|
@@ -22140,10 +23208,10 @@ var walk = (root, directory, output) => {
|
|
|
22140
23208
|
for (const name of names) {
|
|
22141
23209
|
if (name === ".git" || name === ".workit")
|
|
22142
23210
|
continue;
|
|
22143
|
-
const target =
|
|
23211
|
+
const target = path14.join(directory, name);
|
|
22144
23212
|
let stat;
|
|
22145
23213
|
try {
|
|
22146
|
-
stat =
|
|
23214
|
+
stat = fs12.lstatSync(target);
|
|
22147
23215
|
} catch {
|
|
22148
23216
|
uncertain = true;
|
|
22149
23217
|
continue;
|
|
@@ -22158,14 +23226,14 @@ var walk = (root, directory, output) => {
|
|
|
22158
23226
|
var scopeRoots = (root, scope) => {
|
|
22159
23227
|
const roots = [];
|
|
22160
23228
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
22161
|
-
const target =
|
|
23229
|
+
const target = path14.resolve(root, item);
|
|
22162
23230
|
if (!inside(root, target))
|
|
22163
23231
|
return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
|
|
22164
23232
|
let ancestor = target;
|
|
22165
|
-
while (!
|
|
22166
|
-
ancestor =
|
|
23233
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
23234
|
+
ancestor = path14.dirname(ancestor);
|
|
22167
23235
|
try {
|
|
22168
|
-
if (!inside(root,
|
|
23236
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
22169
23237
|
return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
22170
23238
|
} catch {
|
|
22171
23239
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -22177,7 +23245,7 @@ var scopeRoots = (root, scope) => {
|
|
|
22177
23245
|
function captureCandidate(root, scope, environment = []) {
|
|
22178
23246
|
let checkout;
|
|
22179
23247
|
try {
|
|
22180
|
-
checkout =
|
|
23248
|
+
checkout = fs12.realpathSync(root);
|
|
22181
23249
|
} catch {
|
|
22182
23250
|
return failure2("invalid_input", "candidate root does not exist");
|
|
22183
23251
|
}
|
|
@@ -22195,11 +23263,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
22195
23263
|
if (item === ".git" || item === ".workit")
|
|
22196
23264
|
continue;
|
|
22197
23265
|
names.add(item);
|
|
22198
|
-
if (!
|
|
23266
|
+
if (!fs12.existsSync(target))
|
|
22199
23267
|
continue;
|
|
22200
23268
|
let stat;
|
|
22201
23269
|
try {
|
|
22202
|
-
stat =
|
|
23270
|
+
stat = fs12.lstatSync(target);
|
|
22203
23271
|
} catch {
|
|
22204
23272
|
uncertain = true;
|
|
22205
23273
|
continue;
|
|
@@ -22215,17 +23283,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
22215
23283
|
for (const item of [...names].toSorted()) {
|
|
22216
23284
|
if (!scopeMatches(item, normalizedScope))
|
|
22217
23285
|
continue;
|
|
22218
|
-
const target =
|
|
23286
|
+
const target = path14.join(checkout, item);
|
|
22219
23287
|
try {
|
|
22220
|
-
const stat =
|
|
23288
|
+
const stat = fs12.lstatSync(target);
|
|
22221
23289
|
if (stat.isSymbolicLink()) {
|
|
22222
|
-
const link =
|
|
22223
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
23290
|
+
const link = fs12.readlinkSync(target);
|
|
23291
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
22224
23292
|
} else if (stat.isFile()) {
|
|
22225
23293
|
files.push({
|
|
22226
23294
|
path: item,
|
|
22227
23295
|
kind: "file",
|
|
22228
|
-
digest:
|
|
23296
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
22229
23297
|
executable: (stat.mode & 73) !== 0
|
|
22230
23298
|
});
|
|
22231
23299
|
} else if (!stat.isDirectory()) {
|
|
@@ -22250,7 +23318,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
22250
23318
|
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
22251
23319
|
if (values.some(({ value }) => value === null))
|
|
22252
23320
|
completeness = "uncertain";
|
|
22253
|
-
const headResult =
|
|
23321
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
22254
23322
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
22255
23323
|
completeness = "uncertain";
|
|
22256
23324
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -22649,13 +23717,13 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
22649
23717
|
|
|
22650
23718
|
// packages/workit-core/src/core/methods.ts
|
|
22651
23719
|
var METHODS = {
|
|
22652
|
-
"workit-
|
|
22653
|
-
"workit-
|
|
23720
|
+
"workit-shape": { dimensions: ["challenge", "decisions", "artifacts", "continuity"] },
|
|
23721
|
+
"workit-bdd": { dimensions: ["testing"] },
|
|
22654
23722
|
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
22655
|
-
"workit-
|
|
22656
|
-
"workit-
|
|
23723
|
+
"workit-implement": { ruleIds: ["mechanical-existing-checks"] },
|
|
23724
|
+
"workit-fanout": { dimensions: ["delegation"] },
|
|
22657
23725
|
"workit-debug": {},
|
|
22658
|
-
"workit-
|
|
23726
|
+
"workit-continue": {},
|
|
22659
23727
|
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
22660
23728
|
};
|
|
22661
23729
|
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
@@ -23421,6 +24489,38 @@ var applyWorkerLifecycle = (input) => {
|
|
|
23421
24489
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
23422
24490
|
};
|
|
23423
24491
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
24492
|
+
var TASK_SCOPED_ACTIONS = new Set([
|
|
24493
|
+
"task.inspect",
|
|
24494
|
+
"task.revise",
|
|
24495
|
+
"task.progress",
|
|
24496
|
+
"task.pause",
|
|
24497
|
+
"task.resume",
|
|
24498
|
+
"task.close",
|
|
24499
|
+
"policy.assess",
|
|
24500
|
+
"policy.preview",
|
|
24501
|
+
"policy.explain",
|
|
24502
|
+
"evidence.record",
|
|
24503
|
+
"finding.record",
|
|
24504
|
+
"finding.resolve",
|
|
24505
|
+
"decision.record",
|
|
24506
|
+
"decision.revoke",
|
|
24507
|
+
"worker.assign",
|
|
24508
|
+
"worker.report",
|
|
24509
|
+
"worker.cancel",
|
|
24510
|
+
"writer.acquire",
|
|
24511
|
+
"writer.release",
|
|
24512
|
+
"state.export"
|
|
24513
|
+
]);
|
|
24514
|
+
var CREATING_ACTIONS = new Set([
|
|
24515
|
+
"task.revise",
|
|
24516
|
+
"task.progress",
|
|
24517
|
+
"policy.assess",
|
|
24518
|
+
"evidence.record",
|
|
24519
|
+
"finding.record",
|
|
24520
|
+
"decision.record",
|
|
24521
|
+
"worker.assign",
|
|
24522
|
+
"writer.acquire"
|
|
24523
|
+
]);
|
|
23424
24524
|
var REVISION_RETRY_ATTEMPTS = 8;
|
|
23425
24525
|
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
23426
24526
|
var filledRevisions = (request) => {
|
|
@@ -23672,7 +24772,9 @@ class WorkitCore {
|
|
|
23672
24772
|
}
|
|
23673
24773
|
contextRootError() {
|
|
23674
24774
|
try {
|
|
23675
|
-
|
|
24775
|
+
realpathSync6(this.context.root);
|
|
24776
|
+
const root = checkoutRootOf(this.context.root);
|
|
24777
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
23676
24778
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
23677
24779
|
} catch {
|
|
23678
24780
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -23694,6 +24796,30 @@ class WorkitCore {
|
|
|
23694
24796
|
retryOmittedRevisions(request, run) {
|
|
23695
24797
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
23696
24798
|
}
|
|
24799
|
+
implicitTask(family, request) {
|
|
24800
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
24801
|
+
return success(null, null, request);
|
|
24802
|
+
const value = request;
|
|
24803
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
24804
|
+
return success(null, null, request);
|
|
24805
|
+
const action = `${family}.${value.action}`;
|
|
24806
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
24807
|
+
return success(null, null, request);
|
|
24808
|
+
const root = this.contextRootError();
|
|
24809
|
+
if (!root.ok)
|
|
24810
|
+
return root;
|
|
24811
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
24812
|
+
const found = this.store.implicitTask({
|
|
24813
|
+
provenance: provenance(this.context),
|
|
24814
|
+
create,
|
|
24815
|
+
now: trustedNow(this.context)
|
|
24816
|
+
});
|
|
24817
|
+
if (!found.ok)
|
|
24818
|
+
return found;
|
|
24819
|
+
if (!found.data)
|
|
24820
|
+
return failure2("not_found", 'no task for this branch yet; pass taskId, or record something first (`workit task start "<objective>"`, a note, a check) to create it');
|
|
24821
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
24822
|
+
}
|
|
23697
24823
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
23698
24824
|
const workerId = this.context.workerId ?? null;
|
|
23699
24825
|
if (workerId === null)
|
|
@@ -23725,7 +24851,11 @@ class WorkitCore {
|
|
|
23725
24851
|
return null;
|
|
23726
24852
|
}
|
|
23727
24853
|
state(request) {
|
|
23728
|
-
|
|
24854
|
+
const resolved = this.implicitTask("state", request);
|
|
24855
|
+
if (!resolved.ok)
|
|
24856
|
+
return resolved;
|
|
24857
|
+
const value = resolved.data;
|
|
24858
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
23729
24859
|
}
|
|
23730
24860
|
stateOnce(request) {
|
|
23731
24861
|
const root = this.contextRootError();
|
|
@@ -23792,44 +24922,14 @@ class WorkitCore {
|
|
|
23792
24922
|
return imported;
|
|
23793
24923
|
return this.summary(imported.data);
|
|
23794
24924
|
}
|
|
23795
|
-
|
|
23796
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
23797
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
23798
|
-
lock,
|
|
23799
|
-
writer,
|
|
23800
|
-
reason: input.reason,
|
|
23801
|
-
authorityRefs: input.authorityRefs
|
|
23802
|
-
});
|
|
23803
|
-
if (input.target === "workspace")
|
|
23804
|
-
return this.store.recoverWorkspace({
|
|
23805
|
-
expectedBytes: input.expectedBytes,
|
|
23806
|
-
snapshotDigest: input.snapshotDigest,
|
|
23807
|
-
reason: input.reason,
|
|
23808
|
-
authorityRefs: input.authorityRefs,
|
|
23809
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
23810
|
-
processEvidence
|
|
23811
|
-
});
|
|
23812
|
-
const workspace = this.store.readWorkspace();
|
|
23813
|
-
if (!workspace.ok)
|
|
23814
|
-
return workspace;
|
|
23815
|
-
if (!workspace.data)
|
|
23816
|
-
return failure2("not_found", "workspace not found");
|
|
23817
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
23818
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
23819
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
23820
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
23821
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
23822
|
-
return this.store.recoverTask(input.taskId, {
|
|
23823
|
-
expectedBytes: input.expectedBytes,
|
|
23824
|
-
snapshotDigest: input.snapshotDigest,
|
|
23825
|
-
reason: input.reason,
|
|
23826
|
-
authorityRefs: input.authorityRefs,
|
|
23827
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
23828
|
-
processEvidence
|
|
23829
|
-
});
|
|
24925
|
+
return failure2("invalid_input", "unknown state action");
|
|
23830
24926
|
}
|
|
23831
24927
|
task(request) {
|
|
23832
|
-
|
|
24928
|
+
const resolved = this.implicitTask("task", request);
|
|
24929
|
+
if (!resolved.ok)
|
|
24930
|
+
return resolved;
|
|
24931
|
+
const value = resolved.data;
|
|
24932
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
23833
24933
|
}
|
|
23834
24934
|
taskOnce(request) {
|
|
23835
24935
|
const root = this.contextRootError();
|
|
@@ -23931,7 +25031,11 @@ class WorkitCore {
|
|
|
23931
25031
|
}
|
|
23932
25032
|
}
|
|
23933
25033
|
policy(request) {
|
|
23934
|
-
|
|
25034
|
+
const resolved = this.implicitTask("policy", request);
|
|
25035
|
+
if (!resolved.ok)
|
|
25036
|
+
return resolved;
|
|
25037
|
+
const value = resolved.data;
|
|
25038
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
23935
25039
|
}
|
|
23936
25040
|
policyOnce(request) {
|
|
23937
25041
|
const root = this.contextRootError();
|
|
@@ -23980,7 +25084,11 @@ class WorkitCore {
|
|
|
23980
25084
|
return success(changed.data.revision, null, changed.data.policy);
|
|
23981
25085
|
}
|
|
23982
25086
|
evidence(request) {
|
|
23983
|
-
|
|
25087
|
+
const resolved = this.implicitTask("evidence", request);
|
|
25088
|
+
if (!resolved.ok)
|
|
25089
|
+
return resolved;
|
|
25090
|
+
const value = resolved.data;
|
|
25091
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
23984
25092
|
}
|
|
23985
25093
|
evidenceOnce(request) {
|
|
23986
25094
|
const root = this.contextRootError();
|
|
@@ -24121,13 +25229,16 @@ class WorkitCore {
|
|
|
24121
25229
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
24122
25230
|
}
|
|
24123
25231
|
decision(request) {
|
|
24124
|
-
|
|
25232
|
+
const resolved = this.implicitTask("decision", request);
|
|
25233
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
24125
25234
|
}
|
|
24126
25235
|
observeDecision(request, observation) {
|
|
24127
|
-
|
|
25236
|
+
const resolved = this.implicitTask("decision", request);
|
|
25237
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
24128
25238
|
}
|
|
24129
25239
|
observeStandingDecision(request) {
|
|
24130
|
-
|
|
25240
|
+
const resolved = this.implicitTask("decision", request);
|
|
25241
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
24131
25242
|
}
|
|
24132
25243
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
24133
25244
|
let revision = expected;
|
|
@@ -24289,7 +25400,11 @@ class WorkitCore {
|
|
|
24289
25400
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
24290
25401
|
}
|
|
24291
25402
|
finding(request) {
|
|
24292
|
-
|
|
25403
|
+
const resolved = this.implicitTask("finding", request);
|
|
25404
|
+
if (!resolved.ok)
|
|
25405
|
+
return resolved;
|
|
25406
|
+
const value = resolved.data;
|
|
25407
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
24293
25408
|
}
|
|
24294
25409
|
findingOnce(request) {
|
|
24295
25410
|
const root = this.contextRootError();
|
|
@@ -24415,7 +25530,11 @@ class WorkitCore {
|
|
|
24415
25530
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
24416
25531
|
}
|
|
24417
25532
|
worker(request) {
|
|
24418
|
-
|
|
25533
|
+
const resolved = this.implicitTask("worker", request);
|
|
25534
|
+
if (!resolved.ok)
|
|
25535
|
+
return resolved;
|
|
25536
|
+
const value = resolved.data;
|
|
25537
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
24419
25538
|
}
|
|
24420
25539
|
workerOnce(request) {
|
|
24421
25540
|
const root = this.contextRootError();
|
|
@@ -24765,7 +25884,11 @@ class WorkitCore {
|
|
|
24765
25884
|
return applied;
|
|
24766
25885
|
}
|
|
24767
25886
|
writer(request) {
|
|
24768
|
-
|
|
25887
|
+
const resolved = this.implicitTask("writer", request);
|
|
25888
|
+
if (!resolved.ok)
|
|
25889
|
+
return resolved;
|
|
25890
|
+
const value = resolved.data;
|
|
25891
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
24769
25892
|
}
|
|
24770
25893
|
writerOnce(request) {
|
|
24771
25894
|
const root = this.contextRootError();
|
|
@@ -25401,22 +26524,22 @@ function resolveGitRevision(root, value) {
|
|
|
25401
26524
|
}
|
|
25402
26525
|
function resolveInside(root, candidate) {
|
|
25403
26526
|
const base = realpathSync7(root);
|
|
25404
|
-
const target =
|
|
25405
|
-
if (target !== base && !target.startsWith(base +
|
|
26527
|
+
const target = path15.resolve(base, candidate);
|
|
26528
|
+
if (target !== base && !target.startsWith(base + path15.sep)) {
|
|
25406
26529
|
throw new Error("path must stay inside repository root");
|
|
25407
26530
|
}
|
|
25408
26531
|
let ancestor = target;
|
|
25409
26532
|
while (!existsSync5(ancestor))
|
|
25410
|
-
ancestor =
|
|
26533
|
+
ancestor = path15.dirname(ancestor);
|
|
25411
26534
|
const canonicalAncestor = realpathSync7(ancestor);
|
|
25412
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
26535
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path15.sep)) {
|
|
25413
26536
|
throw new Error("path must stay inside repository root");
|
|
25414
26537
|
}
|
|
25415
26538
|
return target;
|
|
25416
26539
|
}
|
|
25417
26540
|
function run2(root, executable, args, env = {}) {
|
|
25418
26541
|
const cwd = realpathSync7(root);
|
|
25419
|
-
const result =
|
|
26542
|
+
const result = spawnSync7(executable, args, {
|
|
25420
26543
|
cwd,
|
|
25421
26544
|
encoding: "utf8",
|
|
25422
26545
|
env: { ...process.env, ...env }
|
|
@@ -25487,9 +26610,9 @@ var applyPatterns = (value) => {
|
|
|
25487
26610
|
var redactSecrets = (value) => applyPatterns(value);
|
|
25488
26611
|
|
|
25489
26612
|
// packages/workit-core/src/core/pr-create.ts
|
|
25490
|
-
import
|
|
25491
|
-
import
|
|
25492
|
-
import { spawnSync as
|
|
26613
|
+
import fs13 from "node:fs";
|
|
26614
|
+
import path16 from "node:path";
|
|
26615
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
25493
26616
|
// packages/workit-core/src/forge/pr-body.ts
|
|
25494
26617
|
function parseGhIssue(value) {
|
|
25495
26618
|
const m = /issues\/(\d+)/.exec(value);
|
|
@@ -25499,13 +26622,13 @@ function parseGhIssue(value) {
|
|
|
25499
26622
|
// packages/workit-core/src/core/pr-create.ts
|
|
25500
26623
|
function whichOnPath(tool) {
|
|
25501
26624
|
const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
|
|
25502
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
26625
|
+
for (const dir of (process.env.PATH ?? "").split(path16.delimiter)) {
|
|
25503
26626
|
if (!dir)
|
|
25504
26627
|
continue;
|
|
25505
26628
|
for (const name of names) {
|
|
25506
|
-
const candidate =
|
|
26629
|
+
const candidate = path16.join(dir, name);
|
|
25507
26630
|
try {
|
|
25508
|
-
|
|
26631
|
+
fs13.accessSync(candidate, fs13.constants.X_OK);
|
|
25509
26632
|
return candidate;
|
|
25510
26633
|
} catch {}
|
|
25511
26634
|
}
|
|
@@ -25514,7 +26637,7 @@ function whichOnPath(tool) {
|
|
|
25514
26637
|
}
|
|
25515
26638
|
var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
|
|
25516
26639
|
var pushRemote = (cwd) => {
|
|
25517
|
-
const result =
|
|
26640
|
+
const result = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
25518
26641
|
cwd,
|
|
25519
26642
|
encoding: "utf8"
|
|
25520
26643
|
});
|
|
@@ -25525,9 +26648,9 @@ var pushRemote = (cwd) => {
|
|
|
25525
26648
|
};
|
|
25526
26649
|
|
|
25527
26650
|
// packages/workit-core/src/core/repo-context.ts
|
|
25528
|
-
import { spawnSync as
|
|
26651
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
25529
26652
|
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
25530
|
-
import
|
|
26653
|
+
import path17 from "node:path";
|
|
25531
26654
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
25532
26655
|
try {
|
|
25533
26656
|
gitRevisionParts(value);
|
|
@@ -25554,7 +26677,7 @@ var invalidRange = (cwd) => ({
|
|
|
25554
26677
|
cwd
|
|
25555
26678
|
});
|
|
25556
26679
|
var runGit = (cwd, args) => {
|
|
25557
|
-
const result =
|
|
26680
|
+
const result = spawnSync9("git", args, { cwd, encoding: "utf8" });
|
|
25558
26681
|
return {
|
|
25559
26682
|
stdout: (result.stdout ?? "").trimEnd(),
|
|
25560
26683
|
stderr: (result.stderr ?? "").trimEnd(),
|
|
@@ -25661,7 +26784,7 @@ var findPrTemplate = (cwd) => {
|
|
|
25661
26784
|
]) {
|
|
25662
26785
|
const parts = rel.split("/");
|
|
25663
26786
|
const base = parts[parts.length - 1];
|
|
25664
|
-
const dir = parts.length > 1 ?
|
|
26787
|
+
const dir = parts.length > 1 ? path17.join(cwd, ...parts.slice(0, -1)) : cwd;
|
|
25665
26788
|
const actual = probeCaseInsensitive(dir, base);
|
|
25666
26789
|
if (actual)
|
|
25667
26790
|
return [...parts.slice(0, -1), actual].join("/");
|
|
@@ -25692,7 +26815,7 @@ var diffStatForRange = (cwd, range) => {
|
|
|
25692
26815
|
return runGit(cwd, ["diff", "--stat", "--"]).stdout;
|
|
25693
26816
|
};
|
|
25694
26817
|
var packageScripts = (cwd, keys) => {
|
|
25695
|
-
const pkgPath =
|
|
26818
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
25696
26819
|
if (!existsSync6(pkgPath))
|
|
25697
26820
|
return [];
|
|
25698
26821
|
try {
|
|
@@ -25718,9 +26841,9 @@ var documentationFiles = (cwd) => {
|
|
|
25718
26841
|
if (entry.isDirectory()) {
|
|
25719
26842
|
if (excluded.has(entry.name))
|
|
25720
26843
|
continue;
|
|
25721
|
-
walk(
|
|
26844
|
+
walk(path17.join(dir, entry.name), depth + 1);
|
|
25722
26845
|
} else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
|
|
25723
|
-
matches.push("./" +
|
|
26846
|
+
matches.push("./" + path17.relative(cwd, path17.join(dir, entry.name)).replaceAll("\\", "/"));
|
|
25724
26847
|
}
|
|
25725
26848
|
}
|
|
25726
26849
|
};
|
|
@@ -25736,7 +26859,7 @@ var readTrimmed2 = (file, maxLines) => {
|
|
|
25736
26859
|
`);
|
|
25737
26860
|
};
|
|
25738
26861
|
function prReadyContext(root, range) {
|
|
25739
|
-
const cwd =
|
|
26862
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25740
26863
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25741
26864
|
return invalidRange(cwd);
|
|
25742
26865
|
let stdout = "";
|
|
@@ -25794,7 +26917,7 @@ ${status.stderr}` : "");
|
|
|
25794
26917
|
stdout += `template_path: ${template}
|
|
25795
26918
|
|
|
25796
26919
|
`;
|
|
25797
|
-
stdout += readTrimmed2(
|
|
26920
|
+
stdout += readTrimmed2(path17.join(cwd, template), 220);
|
|
25798
26921
|
} else {
|
|
25799
26922
|
stdout += `template_path: none
|
|
25800
26923
|
|
|
@@ -25804,7 +26927,7 @@ ${status.stderr}` : "");
|
|
|
25804
26927
|
stdout += `
|
|
25805
26928
|
`;
|
|
25806
26929
|
stdout += printSection("Recent Validation Signals");
|
|
25807
|
-
if (existsSync6(
|
|
26930
|
+
if (existsSync6(path17.join(cwd, "package.json"))) {
|
|
25808
26931
|
stdout += `package.json detected. Common scripts:
|
|
25809
26932
|
`;
|
|
25810
26933
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -25813,7 +26936,7 @@ ${status.stderr}` : "");
|
|
|
25813
26936
|
`) + `
|
|
25814
26937
|
`;
|
|
25815
26938
|
}
|
|
25816
|
-
if (existsSync6(
|
|
26939
|
+
if (existsSync6(path17.join(cwd, "Cargo.toml")) || existsSync6(path17.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
25817
26940
|
stdout += `Rust project detected.
|
|
25818
26941
|
`;
|
|
25819
26942
|
}
|
|
@@ -25846,7 +26969,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
|
|
|
25846
26969
|
- Apply with the host-native editor, preserving the repository changelog conventions.
|
|
25847
26970
|
- If Unreleased already has duplicate category headings, normalize_only first.`;
|
|
25848
26971
|
function changelogContext(root, range) {
|
|
25849
|
-
const cwd =
|
|
26972
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25850
26973
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25851
26974
|
return invalidRange(cwd);
|
|
25852
26975
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -25864,7 +26987,7 @@ function changelogContext(root, range) {
|
|
|
25864
26987
|
stdout += CHANGELOG_RULES + `
|
|
25865
26988
|
`;
|
|
25866
26989
|
stdout += printSection("Existing CHANGELOG.md");
|
|
25867
|
-
const changelogPath =
|
|
26990
|
+
const changelogPath = path17.join(cwd, "CHANGELOG.md");
|
|
25868
26991
|
if (existsSync6(changelogPath)) {
|
|
25869
26992
|
stdout += readTrimmed2(changelogPath, 260) + `
|
|
25870
26993
|
`;
|
|
@@ -25884,7 +27007,7 @@ function changelogContext(root, range) {
|
|
|
25884
27007
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
25885
27008
|
}
|
|
25886
27009
|
function docsRefreshContext(root, range) {
|
|
25887
|
-
const cwd =
|
|
27010
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25888
27011
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25889
27012
|
return invalidRange(cwd);
|
|
25890
27013
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -25906,7 +27029,7 @@ function docsRefreshContext(root, range) {
|
|
|
25906
27029
|
`) + `
|
|
25907
27030
|
`;
|
|
25908
27031
|
stdout += printSection("README Preview");
|
|
25909
|
-
const readme =
|
|
27032
|
+
const readme = path17.join(cwd, "README.md");
|
|
25910
27033
|
if (existsSync6(readme)) {
|
|
25911
27034
|
stdout += readTrimmed2(readme, 220) + `
|
|
25912
27035
|
`;
|
|
@@ -25915,7 +27038,7 @@ function docsRefreshContext(root, range) {
|
|
|
25915
27038
|
`;
|
|
25916
27039
|
}
|
|
25917
27040
|
stdout += printSection("Package Scripts");
|
|
25918
|
-
const pkgPath =
|
|
27041
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
25919
27042
|
if (existsSync6(pkgPath)) {
|
|
25920
27043
|
try {
|
|
25921
27044
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
@@ -25929,7 +27052,7 @@ function docsRefreshContext(root, range) {
|
|
|
25929
27052
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
25930
27053
|
}
|
|
25931
27054
|
function releaseNotesContext(root, rangeOrTag) {
|
|
25932
|
-
const cwd =
|
|
27055
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25933
27056
|
if (!rangeOrTag) {
|
|
25934
27057
|
return { stdout: "", stderr: `ERROR: release tag or range required
|
|
25935
27058
|
`, exitCode: 1, cwd };
|
|
@@ -25967,7 +27090,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
25967
27090
|
`;
|
|
25968
27091
|
stdout += printSection("Existing Release Files");
|
|
25969
27092
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
25970
|
-
if (existsSync6(
|
|
27093
|
+
if (existsSync6(path17.join(cwd, rel)))
|
|
25971
27094
|
stdout += rel + `
|
|
25972
27095
|
`;
|
|
25973
27096
|
}
|
|
@@ -25995,7 +27118,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
25995
27118
|
}
|
|
25996
27119
|
|
|
25997
27120
|
// packages/workit-core/src/core/tracker-issues.ts
|
|
25998
|
-
import { spawnSync as
|
|
27121
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
25999
27122
|
var repoPathFromRemote = (remote) => {
|
|
26000
27123
|
let rest = (remote || "").trim().replace(/\/+$/, "");
|
|
26001
27124
|
if (!rest)
|
|
@@ -26016,7 +27139,7 @@ var defaultCli = (bin, available) => {
|
|
|
26016
27139
|
if (!available)
|
|
26017
27140
|
return null;
|
|
26018
27141
|
return (args, root) => {
|
|
26019
|
-
const out =
|
|
27142
|
+
const out = spawnSync10(bin, args, {
|
|
26020
27143
|
cwd: root,
|
|
26021
27144
|
encoding: "utf8",
|
|
26022
27145
|
env: { ...process.env, PATH: process.env.PATH ?? "" }
|
|
@@ -26094,8 +27217,8 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
|
|
|
26094
27217
|
};
|
|
26095
27218
|
|
|
26096
27219
|
// packages/workit-core/src/core/youtrack.ts
|
|
26097
|
-
import
|
|
26098
|
-
import
|
|
27220
|
+
import fs14 from "node:fs";
|
|
27221
|
+
import path18 from "node:path";
|
|
26099
27222
|
|
|
26100
27223
|
// packages/workit-core/src/core/templates.ts
|
|
26101
27224
|
var repoRoot2 = assetRoot();
|
|
@@ -26108,22 +27231,22 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
|
|
|
26108
27231
|
// packages/workit-core/src/core/youtrack.ts
|
|
26109
27232
|
var ISSUE_RE = /^[A-Z]+-\d+$/;
|
|
26110
27233
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
26111
|
-
var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
26112
|
-
var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
27234
|
+
var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(configDir(), "youtrack.json");
|
|
27235
|
+
var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(resolveConfigDir(), "youtrack.json");
|
|
26113
27236
|
var youTrackTokenModeOk = (p) => {
|
|
26114
27237
|
if (process.platform === "win32")
|
|
26115
27238
|
return true;
|
|
26116
|
-
const mode =
|
|
27239
|
+
const mode = fs14.statSync(p).mode & 511;
|
|
26117
27240
|
return mode === 384;
|
|
26118
27241
|
};
|
|
26119
27242
|
function readYouTrackConfig(required) {
|
|
26120
27243
|
const cfgPath = youTrackConfigPath();
|
|
26121
|
-
if (!
|
|
27244
|
+
if (!fs14.existsSync(cfgPath)) {
|
|
26122
27245
|
return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
|
|
26123
27246
|
}
|
|
26124
27247
|
let parsed;
|
|
26125
27248
|
try {
|
|
26126
|
-
parsed = JSON.parse(
|
|
27249
|
+
parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
|
|
26127
27250
|
} catch {
|
|
26128
27251
|
return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
|
|
26129
27252
|
}
|
|
@@ -26134,11 +27257,11 @@ function readYouTrackConfig(required) {
|
|
|
26134
27257
|
}
|
|
26135
27258
|
var readYouTrackContextConfig = () => {
|
|
26136
27259
|
const cfgPath = youTrackReadOnlyConfigPath();
|
|
26137
|
-
if (!
|
|
27260
|
+
if (!fs14.existsSync(cfgPath))
|
|
26138
27261
|
return { data: {} };
|
|
26139
27262
|
let parsed;
|
|
26140
27263
|
try {
|
|
26141
|
-
parsed = JSON.parse(
|
|
27264
|
+
parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
|
|
26142
27265
|
} catch {
|
|
26143
27266
|
return { error: "YouTrack configuration is unavailable" };
|
|
26144
27267
|
}
|
|
@@ -26177,12 +27300,12 @@ var youTrackToken = () => {
|
|
|
26177
27300
|
if ("error" in loaded)
|
|
26178
27301
|
return loaded;
|
|
26179
27302
|
const tokenFile = String(loaded.config.tokenFile ?? "");
|
|
26180
|
-
const tokenPath = tokenFile ?
|
|
26181
|
-
if (!tokenPath || !
|
|
27303
|
+
const tokenPath = tokenFile ? path18.isAbsolute(tokenFile) ? path18.resolve(tokenFile) : path18.resolve(process.cwd(), tokenFile) : "";
|
|
27304
|
+
if (!tokenPath || !fs14.existsSync(tokenPath))
|
|
26182
27305
|
return { error: "missing youtrack.token" };
|
|
26183
27306
|
if (!youTrackTokenModeOk(tokenPath))
|
|
26184
27307
|
return { error: "youtrack.token mode must be 0600" };
|
|
26185
|
-
const token =
|
|
27308
|
+
const token = fs14.readFileSync(tokenPath, "utf8").trim();
|
|
26186
27309
|
if (!token)
|
|
26187
27310
|
return { error: "empty token file" };
|
|
26188
27311
|
if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
|
|
@@ -26247,9 +27370,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
|
|
|
26247
27370
|
} catch {
|
|
26248
27371
|
continue;
|
|
26249
27372
|
}
|
|
26250
|
-
if (!
|
|
27373
|
+
if (!fs14.existsSync(full))
|
|
26251
27374
|
continue;
|
|
26252
|
-
const text =
|
|
27375
|
+
const text = fs14.readFileSync(full, "utf8");
|
|
26253
27376
|
const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
|
|
26254
27377
|
if (m)
|
|
26255
27378
|
return m[1];
|
|
@@ -26543,13 +27666,13 @@ var rootVariants = (workspaceRoot) => {
|
|
|
26543
27666
|
if (!workspaceRoot)
|
|
26544
27667
|
return [];
|
|
26545
27668
|
const roots = new Set;
|
|
26546
|
-
for (const candidate of [workspaceRoot,
|
|
26547
|
-
if (
|
|
27669
|
+
for (const candidate of [workspaceRoot, path19.resolve(workspaceRoot)]) {
|
|
27670
|
+
if (path19.isAbsolute(candidate) && candidate !== path19.parse(candidate).root)
|
|
26548
27671
|
roots.add(candidate);
|
|
26549
27672
|
}
|
|
26550
27673
|
try {
|
|
26551
27674
|
const canonical = realpathSync8(workspaceRoot);
|
|
26552
|
-
if (canonical !==
|
|
27675
|
+
if (canonical !== path19.parse(canonical).root)
|
|
26553
27676
|
roots.add(canonical);
|
|
26554
27677
|
} catch {}
|
|
26555
27678
|
return [...roots].toSorted((left, right) => right.length - left.length);
|
|
@@ -26650,11 +27773,11 @@ var thrownResult = (tool, error, workspaceRoot) => {
|
|
|
26650
27773
|
};
|
|
26651
27774
|
function createMcpServer(host, contextProvider) {
|
|
26652
27775
|
assertMcpHost(host);
|
|
26653
|
-
const here =
|
|
27776
|
+
const here = path19.dirname(fileURLToPath2(import.meta.url));
|
|
26654
27777
|
const sourceMarker = markSourcesLoaded([
|
|
26655
|
-
|
|
26656
|
-
|
|
26657
|
-
|
|
27778
|
+
path19.join(here, "server.ts"),
|
|
27779
|
+
path19.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
|
|
27780
|
+
path19.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
|
|
26658
27781
|
].filter((file) => existsSync7(file)));
|
|
26659
27782
|
let staleWarned = false;
|
|
26660
27783
|
const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
@@ -26834,8 +27957,8 @@ var main = async () => {
|
|
|
26834
27957
|
process.exitCode = 2;
|
|
26835
27958
|
}
|
|
26836
27959
|
};
|
|
26837
|
-
var entryPath = process.argv[1] ?
|
|
26838
|
-
if (entryPath ===
|
|
27960
|
+
var entryPath = process.argv[1] ? path20.resolve(process.argv[1]) : "";
|
|
27961
|
+
if (entryPath === path20.resolve(fileURLToPath3(import.meta.url)))
|
|
26839
27962
|
await main();
|
|
26840
27963
|
export {
|
|
26841
27964
|
McpCapabilityUnavailableError,
|