@brainervirus/workit-cursor 2.9.0 → 4.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/.cursor-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/dist/cursor-session-start.js +2241 -1114
- package/dist/mcp-server.js +2376 -1253
- package/dist/workit-hook.js +2355 -1210
- package/package.json +4 -4
package/dist/workit-hook.js
CHANGED
|
@@ -8345,16 +8345,6 @@ var stateOperations = {
|
|
|
8345
8345
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
8346
8346
|
bundle: exportBundleSchema,
|
|
8347
8347
|
authorityRefs: array(refSchema)
|
|
8348
|
-
}),
|
|
8349
|
-
recover: operation({
|
|
8350
|
-
action: literal("recover"),
|
|
8351
|
-
taskId: id.optional(),
|
|
8352
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
8353
|
-
target: _enum(["task", "workspace"]),
|
|
8354
|
-
expectedBytes: digest,
|
|
8355
|
-
snapshotDigest: digest,
|
|
8356
|
-
reason: text,
|
|
8357
|
-
authorityRefs: array(refSchema)
|
|
8358
8348
|
})
|
|
8359
8349
|
};
|
|
8360
8350
|
var operationSchemas = {
|
|
@@ -8367,10 +8357,17 @@ var operationSchemas = {
|
|
|
8367
8357
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
8368
8358
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
8369
8359
|
};
|
|
8370
|
-
var {
|
|
8360
|
+
var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
|
|
8361
|
+
var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
|
|
8371
8362
|
var advertisedOperationSchemas = {
|
|
8372
|
-
|
|
8373
|
-
|
|
8363
|
+
task: advertised(taskOperations),
|
|
8364
|
+
policy: advertised(policyOperations),
|
|
8365
|
+
evidence: advertised(evidenceOperations),
|
|
8366
|
+
finding: advertised(findingOperations),
|
|
8367
|
+
decision: advertised(decisionOperations),
|
|
8368
|
+
worker: advertised(workerOperations),
|
|
8369
|
+
writer: advertised(writerOperations),
|
|
8370
|
+
state: advertised(stateOperations)
|
|
8374
8371
|
};
|
|
8375
8372
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
8376
8373
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
@@ -8492,14 +8489,14 @@ function requirementId(input) {
|
|
|
8492
8489
|
import { realpathSync as realpathSync6 } from "node:fs";
|
|
8493
8490
|
|
|
8494
8491
|
// packages/workit-core/src/core/authority.ts
|
|
8495
|
-
import { createHash as
|
|
8496
|
-
import * as
|
|
8497
|
-
import
|
|
8492
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8493
|
+
import * as fs9 from "node:fs";
|
|
8494
|
+
import path11 from "node:path";
|
|
8498
8495
|
|
|
8499
8496
|
// packages/workit-core/src/core/auto-approval.ts
|
|
8500
|
-
import { createHash as
|
|
8497
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8501
8498
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
8502
|
-
import
|
|
8499
|
+
import path10 from "node:path";
|
|
8503
8500
|
|
|
8504
8501
|
// packages/workit-core/src/core/workspaces.ts
|
|
8505
8502
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -9313,24 +9310,10 @@ var verifyPushIdentity = (root, provider, account) => {
|
|
|
9313
9310
|
};
|
|
9314
9311
|
|
|
9315
9312
|
// packages/workit-core/src/core/task-store.ts
|
|
9316
|
-
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
9317
9313
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9318
|
-
import
|
|
9319
|
-
import
|
|
9320
|
-
|
|
9321
|
-
// packages/workit-core/src/core/package-root.ts
|
|
9322
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
9323
|
-
import path4 from "node:path";
|
|
9324
|
-
import { fileURLToPath } from "node:url";
|
|
9325
|
-
var packageRoot = () => {
|
|
9326
|
-
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
9327
|
-
while (true) {
|
|
9328
|
-
const parent = path4.dirname(dir);
|
|
9329
|
-
if (existsSync2(path4.join(dir, "package.json")) || parent === dir)
|
|
9330
|
-
return dir;
|
|
9331
|
-
dir = parent;
|
|
9332
|
-
}
|
|
9333
|
-
};
|
|
9314
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypto";
|
|
9315
|
+
import * as fs8 from "node:fs";
|
|
9316
|
+
import path9 from "node:path";
|
|
9334
9317
|
|
|
9335
9318
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
9336
9319
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -9381,7 +9364,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
9381
9364
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
9382
9365
|
import { randomBytes } from "node:crypto";
|
|
9383
9366
|
import fsSync from "node:fs";
|
|
9384
|
-
import
|
|
9367
|
+
import path4 from "node:path";
|
|
9385
9368
|
|
|
9386
9369
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
9387
9370
|
import fs2 from "node:fs";
|
|
@@ -9471,13 +9454,13 @@ ${ownershipToken}
|
|
|
9471
9454
|
return { raw, ownershipToken };
|
|
9472
9455
|
}
|
|
9473
9456
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
9474
|
-
const resolved =
|
|
9475
|
-
const lexicalRelative =
|
|
9476
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
9477
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
9457
|
+
const resolved = path4.resolve(lockPath);
|
|
9458
|
+
const lexicalRelative = path4.relative(lockRoot.rootDir, resolved);
|
|
9459
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(lexicalRelative) ? lexicalRelative : path4.relative(lockRoot.rootReal, resolved);
|
|
9460
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative)) {
|
|
9478
9461
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
9479
9462
|
}
|
|
9480
|
-
return relative.split(
|
|
9463
|
+
return relative.split(path4.sep).join(path4.posix.sep);
|
|
9481
9464
|
}
|
|
9482
9465
|
function parseSidecarLockPayload(raw, parser) {
|
|
9483
9466
|
if (parser) {
|
|
@@ -9624,7 +9607,7 @@ function getFsSafeLockConfig() {
|
|
|
9624
9607
|
|
|
9625
9608
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
9626
9609
|
import fs4 from "node:fs";
|
|
9627
|
-
import
|
|
9610
|
+
import path5 from "node:path";
|
|
9628
9611
|
|
|
9629
9612
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
9630
9613
|
function sleepSync(ms) {
|
|
@@ -9715,26 +9698,26 @@ function canonicalLockParentSync(parent) {
|
|
|
9715
9698
|
return process.platform === "win32" ? fs4.realpathSync.native(parent) : fs4.realpathSync(parent);
|
|
9716
9699
|
}
|
|
9717
9700
|
function normalizeTargetPath(targetPath) {
|
|
9718
|
-
const resolved =
|
|
9719
|
-
fs4.mkdirSync(
|
|
9701
|
+
const resolved = path5.resolve(targetPath);
|
|
9702
|
+
fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
9720
9703
|
try {
|
|
9721
|
-
return
|
|
9704
|
+
return path5.join(canonicalLockParentSync(path5.dirname(resolved)), path5.basename(resolved));
|
|
9722
9705
|
} catch {
|
|
9723
9706
|
return resolved;
|
|
9724
9707
|
}
|
|
9725
9708
|
}
|
|
9726
9709
|
function boundedLockPath(lockPath, lockRoot) {
|
|
9727
|
-
const resolved =
|
|
9710
|
+
const resolved = path5.resolve(lockPath);
|
|
9728
9711
|
if (!lockRoot)
|
|
9729
9712
|
return resolved;
|
|
9730
9713
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
9731
|
-
const parent =
|
|
9714
|
+
const parent = path5.dirname(resolved);
|
|
9732
9715
|
const parentReal = canonicalLockParentSync(parent);
|
|
9733
|
-
const parentRelative =
|
|
9734
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
9716
|
+
const parentRelative = path5.relative(lockRoot.rootReal, parentReal);
|
|
9717
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path5.sep}`) || path5.isAbsolute(parentRelative)) {
|
|
9735
9718
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
9736
9719
|
}
|
|
9737
|
-
return
|
|
9720
|
+
return path5.join(parentReal, path5.basename(resolved));
|
|
9738
9721
|
}
|
|
9739
9722
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
9740
9723
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -9933,10 +9916,306 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
9933
9916
|
}
|
|
9934
9917
|
}
|
|
9935
9918
|
}
|
|
9919
|
+
// packages/workit-core/src/core/package-root.ts
|
|
9920
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
9921
|
+
import path6 from "node:path";
|
|
9922
|
+
import { fileURLToPath } from "node:url";
|
|
9923
|
+
var packageRoot = () => {
|
|
9924
|
+
let dir = path6.dirname(fileURLToPath(import.meta.url));
|
|
9925
|
+
while (true) {
|
|
9926
|
+
const parent = path6.dirname(dir);
|
|
9927
|
+
if (existsSync2(path6.join(dir, "package.json")) || parent === dir)
|
|
9928
|
+
return dir;
|
|
9929
|
+
dir = parent;
|
|
9930
|
+
}
|
|
9931
|
+
};
|
|
9932
|
+
|
|
9936
9933
|
// packages/workit-core/src/core/store-lock.ts
|
|
9934
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9935
|
+
import fs6 from "node:fs";
|
|
9936
|
+
import { hostname } from "node:os";
|
|
9937
|
+
|
|
9938
|
+
// packages/workit-core/src/store/paths.ts
|
|
9937
9939
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
9940
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9938
9941
|
import fs5 from "node:fs";
|
|
9939
|
-
import
|
|
9942
|
+
import path7 from "node:path";
|
|
9943
|
+
var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
9944
|
+
var gitRun = (cwd, args) => {
|
|
9945
|
+
const run = spawnSync2("git", args, {
|
|
9946
|
+
cwd,
|
|
9947
|
+
encoding: "utf8",
|
|
9948
|
+
timeout: 20000,
|
|
9949
|
+
killSignal: "SIGKILL",
|
|
9950
|
+
windowsHide: true,
|
|
9951
|
+
env: {
|
|
9952
|
+
...process.env,
|
|
9953
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
9954
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
9955
|
+
LC_ALL: "C",
|
|
9956
|
+
LANGUAGE: "C"
|
|
9957
|
+
}
|
|
9958
|
+
});
|
|
9959
|
+
const error = run.error;
|
|
9960
|
+
return {
|
|
9961
|
+
ok: run.status === 0,
|
|
9962
|
+
stdout: run.stdout ?? "",
|
|
9963
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
9964
|
+
missing: error?.code === "ENOENT"
|
|
9965
|
+
};
|
|
9966
|
+
};
|
|
9967
|
+
var shortHash = (value) => createHash4("sha256").update(value).digest("hex").slice(0, 12);
|
|
9968
|
+
var real = (file) => {
|
|
9969
|
+
try {
|
|
9970
|
+
return fs5.realpathSync.native(file);
|
|
9971
|
+
} catch {
|
|
9972
|
+
return file;
|
|
9973
|
+
}
|
|
9974
|
+
};
|
|
9975
|
+
var DISCOVERY_ENV = [
|
|
9976
|
+
"GIT_DIR",
|
|
9977
|
+
"GIT_COMMON_DIR",
|
|
9978
|
+
"GIT_WORK_TREE",
|
|
9979
|
+
"GIT_CEILING_DIRECTORIES",
|
|
9980
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
9981
|
+
];
|
|
9982
|
+
var isGitDir = (dir) => {
|
|
9983
|
+
try {
|
|
9984
|
+
return fs5.statSync(path7.join(dir, "HEAD")).isFile() && fs5.statSync(path7.join(dir, "objects")).isDirectory() && fs5.statSync(path7.join(dir, "refs")).isDirectory();
|
|
9985
|
+
} catch {
|
|
9986
|
+
return false;
|
|
9987
|
+
}
|
|
9988
|
+
};
|
|
9989
|
+
var reftableRefs = (common) => {
|
|
9990
|
+
try {
|
|
9991
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs5.readFileSync(path7.join(common, "config"), "utf8"));
|
|
9992
|
+
} catch {
|
|
9993
|
+
return false;
|
|
9994
|
+
}
|
|
9995
|
+
};
|
|
9996
|
+
function fastGit(root) {
|
|
9997
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
9998
|
+
return null;
|
|
9999
|
+
let dir = real(root);
|
|
10000
|
+
let device = null;
|
|
10001
|
+
for (;; ) {
|
|
10002
|
+
let stat;
|
|
10003
|
+
try {
|
|
10004
|
+
stat = fs5.statSync(dir);
|
|
10005
|
+
} catch {
|
|
10006
|
+
return null;
|
|
10007
|
+
}
|
|
10008
|
+
if (device !== null && stat.dev !== device)
|
|
10009
|
+
return { kind: "none" };
|
|
10010
|
+
device = stat.dev;
|
|
10011
|
+
const dotGit = path7.join(dir, ".git");
|
|
10012
|
+
let entry = null;
|
|
10013
|
+
try {
|
|
10014
|
+
entry = fs5.statSync(dotGit);
|
|
10015
|
+
} catch {}
|
|
10016
|
+
let gitDir = null;
|
|
10017
|
+
let top = null;
|
|
10018
|
+
if (entry) {
|
|
10019
|
+
top = dir;
|
|
10020
|
+
if (entry.isDirectory())
|
|
10021
|
+
gitDir = dotGit;
|
|
10022
|
+
else if (entry.isFile()) {
|
|
10023
|
+
let text = "";
|
|
10024
|
+
try {
|
|
10025
|
+
text = fs5.readFileSync(dotGit, "utf8");
|
|
10026
|
+
} catch {
|
|
10027
|
+
return null;
|
|
10028
|
+
}
|
|
10029
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
10030
|
+
if (!match)
|
|
10031
|
+
return null;
|
|
10032
|
+
gitDir = path7.resolve(dir, match[1].trim());
|
|
10033
|
+
} else
|
|
10034
|
+
return null;
|
|
10035
|
+
if (!fs5.existsSync(path7.join(gitDir, "HEAD")))
|
|
10036
|
+
return null;
|
|
10037
|
+
} else if (isGitDir(dir))
|
|
10038
|
+
gitDir = dir;
|
|
10039
|
+
if (gitDir) {
|
|
10040
|
+
let common = gitDir;
|
|
10041
|
+
try {
|
|
10042
|
+
common = path7.resolve(gitDir, fs5.readFileSync(path7.join(gitDir, "commondir"), "utf8").trim());
|
|
10043
|
+
} catch {}
|
|
10044
|
+
if (!fs5.existsSync(path7.join(common, "objects")))
|
|
10045
|
+
return null;
|
|
10046
|
+
return {
|
|
10047
|
+
kind: "repo",
|
|
10048
|
+
common: real(common),
|
|
10049
|
+
gitDir: real(gitDir),
|
|
10050
|
+
top: top === null ? null : real(top),
|
|
10051
|
+
reftable: reftableRefs(common)
|
|
10052
|
+
};
|
|
10053
|
+
}
|
|
10054
|
+
const parent = path7.dirname(dir);
|
|
10055
|
+
if (parent === dir)
|
|
10056
|
+
return { kind: "none" };
|
|
10057
|
+
dir = parent;
|
|
10058
|
+
}
|
|
10059
|
+
}
|
|
10060
|
+
function resolveStore(root) {
|
|
10061
|
+
root = path7.resolve(root);
|
|
10062
|
+
let directory = false;
|
|
10063
|
+
try {
|
|
10064
|
+
directory = fs5.statSync(root).isDirectory();
|
|
10065
|
+
} catch {}
|
|
10066
|
+
if (!directory)
|
|
10067
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10068
|
+
const fast = fastGit(root);
|
|
10069
|
+
if (fast?.kind === "none")
|
|
10070
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10071
|
+
if (fast?.kind === "repo")
|
|
10072
|
+
return {
|
|
10073
|
+
dir: path7.join(fast.common, "workit"),
|
|
10074
|
+
shared: true,
|
|
10075
|
+
top: fast.top,
|
|
10076
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
10077
|
+
};
|
|
10078
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
10079
|
+
const common = run.stdout.trim();
|
|
10080
|
+
if (run.ok && common) {
|
|
10081
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10082
|
+
return {
|
|
10083
|
+
dir: path7.join(real(path7.resolve(root, common)), "workit"),
|
|
10084
|
+
shared: true,
|
|
10085
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
10086
|
+
};
|
|
10087
|
+
}
|
|
10088
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
10089
|
+
return { dir: path7.join(root, ".workit"), shared: false, top: null };
|
|
10090
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
10091
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
10092
|
+
}
|
|
10093
|
+
var rebasingBranch = (gitDir) => {
|
|
10094
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
10095
|
+
try {
|
|
10096
|
+
const ref = fs5.readFileSync(path7.join(gitDir, state, "head-name"), "utf8").trim();
|
|
10097
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
10098
|
+
if (match)
|
|
10099
|
+
return match[1];
|
|
10100
|
+
} catch {}
|
|
10101
|
+
}
|
|
10102
|
+
return null;
|
|
10103
|
+
};
|
|
10104
|
+
function resolveTaskKey(root, location) {
|
|
10105
|
+
if (!location.shared)
|
|
10106
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
10107
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
10108
|
+
if (location.git && !location.git.reftable) {
|
|
10109
|
+
let head = "";
|
|
10110
|
+
try {
|
|
10111
|
+
head = fs5.readFileSync(path7.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10112
|
+
} catch {}
|
|
10113
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10114
|
+
if (ref && ref[1] !== ".invalid")
|
|
10115
|
+
return branchKey(ref[1]);
|
|
10116
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10117
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10118
|
+
if (rebasing)
|
|
10119
|
+
return branchKey(rebasing);
|
|
10120
|
+
return {
|
|
10121
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
10122
|
+
kind: "detached",
|
|
10123
|
+
branch: null
|
|
10124
|
+
};
|
|
10125
|
+
}
|
|
10126
|
+
}
|
|
10127
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
10128
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
10129
|
+
if (name && name !== ".invalid")
|
|
10130
|
+
return branchKey(name);
|
|
10131
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
10132
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
10133
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
10134
|
+
if (rebasing)
|
|
10135
|
+
return branchKey(rebasing);
|
|
10136
|
+
}
|
|
10137
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10138
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
10139
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
10140
|
+
}
|
|
10141
|
+
function checkoutRootOf(root, location) {
|
|
10142
|
+
const resolved = path7.resolve(root);
|
|
10143
|
+
let canonical = resolved;
|
|
10144
|
+
try {
|
|
10145
|
+
canonical = fs5.realpathSync(resolved);
|
|
10146
|
+
} catch {
|
|
10147
|
+
return resolved;
|
|
10148
|
+
}
|
|
10149
|
+
const found = location ?? resolveStore(canonical);
|
|
10150
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
10151
|
+
return canonical;
|
|
10152
|
+
if (real(canonical) === found.top)
|
|
10153
|
+
return canonical;
|
|
10154
|
+
try {
|
|
10155
|
+
return fs5.realpathSync(found.top);
|
|
10156
|
+
} catch {
|
|
10157
|
+
return canonical;
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
function branchRenames(location) {
|
|
10161
|
+
if (!location.shared)
|
|
10162
|
+
return [];
|
|
10163
|
+
const files = [];
|
|
10164
|
+
const walk = (dir) => {
|
|
10165
|
+
let entries;
|
|
10166
|
+
try {
|
|
10167
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
10168
|
+
} catch {
|
|
10169
|
+
return;
|
|
10170
|
+
}
|
|
10171
|
+
for (const entry of entries)
|
|
10172
|
+
if (entry.isDirectory())
|
|
10173
|
+
walk(path7.join(dir, entry.name));
|
|
10174
|
+
else
|
|
10175
|
+
files.push(path7.join(dir, entry.name));
|
|
10176
|
+
};
|
|
10177
|
+
walk(path7.join(path7.dirname(location.dir), "logs", "refs", "heads"));
|
|
10178
|
+
if (location.git)
|
|
10179
|
+
files.push(path7.join(location.git.gitDir, "logs", "HEAD"));
|
|
10180
|
+
const seen = new Set;
|
|
10181
|
+
const renames = [];
|
|
10182
|
+
for (const file of files) {
|
|
10183
|
+
let text = "";
|
|
10184
|
+
try {
|
|
10185
|
+
text = fs5.readFileSync(file, "utf8");
|
|
10186
|
+
} catch {
|
|
10187
|
+
continue;
|
|
10188
|
+
}
|
|
10189
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
10190
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
10191
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
10192
|
+
if (seen.has(id))
|
|
10193
|
+
continue;
|
|
10194
|
+
seen.add(id);
|
|
10195
|
+
renames.push(rename);
|
|
10196
|
+
}
|
|
10197
|
+
}
|
|
10198
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
10199
|
+
}
|
|
10200
|
+
function followRenames(key, boundAt, renames) {
|
|
10201
|
+
let current = key;
|
|
10202
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
10203
|
+
for (const rename of renames)
|
|
10204
|
+
if (rename.from === current && rename.at >= since) {
|
|
10205
|
+
current = rename.to;
|
|
10206
|
+
since = rename.at;
|
|
10207
|
+
}
|
|
10208
|
+
return current;
|
|
10209
|
+
}
|
|
10210
|
+
var checkoutSlug = (root) => {
|
|
10211
|
+
let canonical = real(root);
|
|
10212
|
+
if (process.platform === "win32")
|
|
10213
|
+
canonical = canonical.toLowerCase();
|
|
10214
|
+
const base = path7.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
10215
|
+
return `${base}-${shortHash(canonical)}`;
|
|
10216
|
+
};
|
|
10217
|
+
|
|
10218
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
9940
10219
|
var metadataLockSchema = object({
|
|
9941
10220
|
pid: number2().int().nonnegative().safe(),
|
|
9942
10221
|
processStart: string2().nullable(),
|
|
@@ -9968,10 +10247,6 @@ var parseMetadataLockOrNull = (raw) => {
|
|
|
9968
10247
|
return null;
|
|
9969
10248
|
}
|
|
9970
10249
|
};
|
|
9971
|
-
var sameMetadataLock = (left, right) => {
|
|
9972
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
9973
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
9974
|
-
};
|
|
9975
10250
|
var parseProcStatStart = (stat) => {
|
|
9976
10251
|
const close = stat.lastIndexOf(")");
|
|
9977
10252
|
if (close < 0)
|
|
@@ -9981,14 +10256,14 @@ var parseProcStatStart = (stat) => {
|
|
|
9981
10256
|
var processStartOf = (pid) => {
|
|
9982
10257
|
if (process.platform === "linux") {
|
|
9983
10258
|
try {
|
|
9984
|
-
return parseProcStatStart(
|
|
10259
|
+
return parseProcStatStart(fs6.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
9985
10260
|
} catch {
|
|
9986
10261
|
return null;
|
|
9987
10262
|
}
|
|
9988
10263
|
}
|
|
9989
10264
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
9990
10265
|
try {
|
|
9991
|
-
const run =
|
|
10266
|
+
const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
9992
10267
|
encoding: "utf8",
|
|
9993
10268
|
timeout: 1000
|
|
9994
10269
|
});
|
|
@@ -10002,7 +10277,7 @@ var processStartOf = (pid) => {
|
|
|
10002
10277
|
};
|
|
10003
10278
|
var readTrimmed = (file) => {
|
|
10004
10279
|
try {
|
|
10005
|
-
return
|
|
10280
|
+
return fs6.readFileSync(file, "utf8").trim() || null;
|
|
10006
10281
|
} catch {
|
|
10007
10282
|
return null;
|
|
10008
10283
|
}
|
|
@@ -10013,7 +10288,7 @@ var localLockHost = () => {
|
|
|
10013
10288
|
return cachedLockHost;
|
|
10014
10289
|
let pidns = null;
|
|
10015
10290
|
try {
|
|
10016
|
-
pidns = /\[(\d+)\]/.exec(
|
|
10291
|
+
pidns = /\[(\d+)\]/.exec(fs6.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
10017
10292
|
} catch {}
|
|
10018
10293
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
10019
10294
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -10053,7 +10328,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
|
10053
10328
|
};
|
|
10054
10329
|
var ageOf = (file, nowMs) => {
|
|
10055
10330
|
try {
|
|
10056
|
-
return nowMs -
|
|
10331
|
+
return nowMs - fs6.lstatSync(file).mtimeMs;
|
|
10057
10332
|
} catch {
|
|
10058
10333
|
return null;
|
|
10059
10334
|
}
|
|
@@ -10064,124 +10339,158 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
|
10064
10339
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
10065
10340
|
return false;
|
|
10066
10341
|
try {
|
|
10067
|
-
|
|
10342
|
+
fs6.rmdirSync(guard);
|
|
10068
10343
|
return true;
|
|
10069
10344
|
} catch {
|
|
10070
10345
|
return false;
|
|
10071
10346
|
}
|
|
10072
10347
|
};
|
|
10073
10348
|
|
|
10074
|
-
// packages/workit-core/src/
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
var
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10086
|
-
var indexEntry = (task, file) => {
|
|
10087
|
-
const sessions = [];
|
|
10088
|
-
const intent = hostSession(task.intent.provenance.session);
|
|
10089
|
-
if (intent)
|
|
10090
|
-
sessions.push({ ...intent, workerId: null });
|
|
10091
|
-
for (const worker of task.workers) {
|
|
10092
|
-
const session = hostSession(worker.data.session);
|
|
10093
|
-
if (session)
|
|
10094
|
-
sessions.push({ ...session, workerId: worker.id });
|
|
10349
|
+
// packages/workit-core/src/store/event-log.ts
|
|
10350
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10351
|
+
import fs7 from "node:fs";
|
|
10352
|
+
import path8 from "node:path";
|
|
10353
|
+
var EVENT_VERSION = 1;
|
|
10354
|
+
|
|
10355
|
+
class LogDamage extends Error {
|
|
10356
|
+
upgrade;
|
|
10357
|
+
constructor(message, upgrade = false) {
|
|
10358
|
+
super(message);
|
|
10359
|
+
this.upgrade = upgrade;
|
|
10095
10360
|
}
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
createdAt: task.createdAt,
|
|
10101
|
-
updatedAt: task.updatedAt,
|
|
10102
|
-
objective: task.intent.data.objective,
|
|
10103
|
-
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10104
|
-
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10105
|
-
sessions,
|
|
10106
|
-
file
|
|
10107
|
-
};
|
|
10108
|
-
};
|
|
10109
|
-
var fileSignature = (file) => {
|
|
10361
|
+
}
|
|
10362
|
+
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10363
|
+
var parseEvent = (line, file, offset) => {
|
|
10364
|
+
let value;
|
|
10110
10365
|
try {
|
|
10111
|
-
|
|
10112
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10366
|
+
value = JSON.parse(line);
|
|
10113
10367
|
} catch {
|
|
10114
|
-
|
|
10115
|
-
}
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10368
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
10369
|
+
}
|
|
10370
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
10371
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10372
|
+
if (value.v > EVENT_VERSION)
|
|
10373
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
10374
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
10375
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
10376
|
+
return value;
|
|
10122
10377
|
};
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10378
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
10379
|
+
let fd;
|
|
10380
|
+
try {
|
|
10381
|
+
fd = fs7.openSync(file, "r");
|
|
10382
|
+
} catch (error) {
|
|
10383
|
+
if (error.code === "ENOENT")
|
|
10384
|
+
return null;
|
|
10385
|
+
throw error;
|
|
10386
|
+
}
|
|
10387
|
+
let bytes;
|
|
10388
|
+
let size;
|
|
10389
|
+
try {
|
|
10390
|
+
size = fs7.fstatSync(fd).size;
|
|
10391
|
+
if (from > size)
|
|
10392
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
10393
|
+
bytes = Buffer.alloc(size - from);
|
|
10394
|
+
let read = 0;
|
|
10395
|
+
while (read < bytes.length) {
|
|
10396
|
+
const got = fs7.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
10397
|
+
if (got === 0)
|
|
10398
|
+
break;
|
|
10399
|
+
read += got;
|
|
10400
|
+
}
|
|
10401
|
+
bytes = bytes.subarray(0, read);
|
|
10402
|
+
size = from + read;
|
|
10403
|
+
} finally {
|
|
10404
|
+
fs7.closeSync(fd);
|
|
10405
|
+
}
|
|
10406
|
+
const events = [];
|
|
10407
|
+
let start = 0;
|
|
10408
|
+
let lastStart = from;
|
|
10409
|
+
let seq = expectSeq;
|
|
10410
|
+
for (;; ) {
|
|
10411
|
+
const newline = bytes.indexOf(10, start);
|
|
10412
|
+
if (newline < 0)
|
|
10413
|
+
break;
|
|
10414
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
10415
|
+
if (line.trim() !== "") {
|
|
10416
|
+
const event = parseEvent(line, file, from + start);
|
|
10417
|
+
if (seq !== null && event.seq !== seq)
|
|
10418
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
10419
|
+
seq = event.seq + 1;
|
|
10420
|
+
events.push(event);
|
|
10421
|
+
lastStart = from + start;
|
|
10422
|
+
}
|
|
10423
|
+
start = newline + 1;
|
|
10424
|
+
}
|
|
10425
|
+
return { events, end: from + start, lastStart, size };
|
|
10426
|
+
}
|
|
10427
|
+
var fsyncDirectory = (dir) => {
|
|
10428
|
+
try {
|
|
10429
|
+
const fd = fs7.openSync(dir, "r");
|
|
10127
10430
|
try {
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
cachedRuntimeVersion = "0.0.0";
|
|
10431
|
+
fs7.fsyncSync(fd);
|
|
10432
|
+
} finally {
|
|
10433
|
+
fs7.closeSync(fd);
|
|
10132
10434
|
}
|
|
10435
|
+
} catch (error) {
|
|
10436
|
+
if (process.platform !== "win32")
|
|
10437
|
+
throw error;
|
|
10133
10438
|
}
|
|
10134
|
-
return cachedRuntimeVersion;
|
|
10135
10439
|
};
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10440
|
+
function appendEvent(file, event, validEnd) {
|
|
10441
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
10442
|
+
`, "utf8");
|
|
10443
|
+
const created = !fs7.existsSync(file);
|
|
10444
|
+
if (!created && fs7.statSync(file).size !== validEnd) {
|
|
10445
|
+
const cut = fs7.openSync(file, "r+");
|
|
10446
|
+
try {
|
|
10447
|
+
fs7.ftruncateSync(cut, validEnd);
|
|
10448
|
+
fs7.fsyncSync(cut);
|
|
10449
|
+
} finally {
|
|
10450
|
+
fs7.closeSync(cut);
|
|
10451
|
+
}
|
|
10148
10452
|
}
|
|
10149
|
-
|
|
10150
|
-
};
|
|
10151
|
-
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
10152
|
-
`;
|
|
10153
|
-
var digestBytes = (value) => createHash4("sha256").update(value).digest("hex");
|
|
10154
|
-
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
10155
|
-
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);
|
|
10156
|
-
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
10157
|
-
var sameDirectoryIdentity = (left, right) => {
|
|
10158
|
-
if (!path7.isAbsolute(left) || !path7.isAbsolute(right))
|
|
10159
|
-
return false;
|
|
10160
|
-
const normalizedLeft = path7.resolve(left);
|
|
10161
|
-
const normalizedRight = path7.resolve(right);
|
|
10453
|
+
const fd = fs7.openSync(file, "a", 384);
|
|
10162
10454
|
try {
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
const canonicalLeft = fs6.realpathSync(normalizedLeft);
|
|
10170
|
-
const canonicalRight = fs6.realpathSync(normalizedRight);
|
|
10171
|
-
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
10172
|
-
return true;
|
|
10173
|
-
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
10174
|
-
} catch {
|
|
10175
|
-
return false;
|
|
10455
|
+
let written = 0;
|
|
10456
|
+
while (written < line.length)
|
|
10457
|
+
written += fs7.writeSync(fd, line, written, line.length - written);
|
|
10458
|
+
fs7.fsyncSync(fd);
|
|
10459
|
+
} finally {
|
|
10460
|
+
fs7.closeSync(fd);
|
|
10176
10461
|
}
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
const
|
|
10183
|
-
|
|
10184
|
-
|
|
10462
|
+
if (created)
|
|
10463
|
+
fsyncDirectory(path8.dirname(file));
|
|
10464
|
+
return validEnd;
|
|
10465
|
+
}
|
|
10466
|
+
function replaceFile(file, bytes, durable) {
|
|
10467
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
10468
|
+
try {
|
|
10469
|
+
const fd = fs7.openSync(temporary, "wx", 384);
|
|
10470
|
+
try {
|
|
10471
|
+
fs7.writeSync(fd, bytes);
|
|
10472
|
+
if (durable)
|
|
10473
|
+
fs7.fsyncSync(fd);
|
|
10474
|
+
} finally {
|
|
10475
|
+
fs7.closeSync(fd);
|
|
10476
|
+
}
|
|
10477
|
+
renameWithRetry(temporary, file);
|
|
10478
|
+
if (durable)
|
|
10479
|
+
fsyncDirectory(path8.dirname(file));
|
|
10480
|
+
} catch (error) {
|
|
10481
|
+
try {
|
|
10482
|
+
fs7.unlinkSync(temporary);
|
|
10483
|
+
} catch {}
|
|
10484
|
+
throw error;
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
10488
|
+
var isTransientWindowsError = (error) => {
|
|
10489
|
+
if (process.platform !== "win32")
|
|
10490
|
+
return false;
|
|
10491
|
+
const code = error?.code;
|
|
10492
|
+
return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
|
|
10493
|
+
};
|
|
10185
10494
|
var retryTransient = (run) => {
|
|
10186
10495
|
if (process.platform !== "win32")
|
|
10187
10496
|
return run();
|
|
@@ -10195,138 +10504,596 @@ var retryTransient = (run) => {
|
|
|
10195
10504
|
}
|
|
10196
10505
|
}
|
|
10197
10506
|
};
|
|
10507
|
+
var renameWithRetry = (from, to) => retryTransient(() => fs7.renameSync(from, to));
|
|
10508
|
+
|
|
10509
|
+
// packages/workit-core/src/store/patch.ts
|
|
10510
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10511
|
+
var equal = (left, right) => {
|
|
10512
|
+
if (left === right)
|
|
10513
|
+
return true;
|
|
10514
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
10515
|
+
return false;
|
|
10516
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
10517
|
+
return false;
|
|
10518
|
+
if (Array.isArray(left)) {
|
|
10519
|
+
const other = right;
|
|
10520
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
10521
|
+
}
|
|
10522
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
10523
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
10524
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
10525
|
+
};
|
|
10526
|
+
function diff(before, after, path = []) {
|
|
10527
|
+
if (equal(before, after))
|
|
10528
|
+
return [];
|
|
10529
|
+
if (isPlainObject2(before) && isPlainObject2(after)) {
|
|
10530
|
+
const ops = [];
|
|
10531
|
+
for (const key of Object.keys(before))
|
|
10532
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
10533
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
10534
|
+
for (const key of Object.keys(after)) {
|
|
10535
|
+
if (after[key] === undefined)
|
|
10536
|
+
continue;
|
|
10537
|
+
if (before[key] === undefined)
|
|
10538
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
10539
|
+
else
|
|
10540
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
10541
|
+
}
|
|
10542
|
+
return ops;
|
|
10543
|
+
}
|
|
10544
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
10545
|
+
const ops = [];
|
|
10546
|
+
if (after.length < before.length)
|
|
10547
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
10548
|
+
const shared = Math.min(before.length, after.length);
|
|
10549
|
+
let changed = 0;
|
|
10550
|
+
for (let index = 0;index < shared; index += 1)
|
|
10551
|
+
if (!equal(before[index], after[index]))
|
|
10552
|
+
changed += 1;
|
|
10553
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
10554
|
+
return [{ op: "set", path, value: after }];
|
|
10555
|
+
for (let index = 0;index < shared; index += 1)
|
|
10556
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
10557
|
+
if (after.length > before.length)
|
|
10558
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
10559
|
+
return ops;
|
|
10560
|
+
}
|
|
10561
|
+
return [{ op: "set", path, value: after }];
|
|
10562
|
+
}
|
|
10563
|
+
var clone2 = (value) => structuredClone(value);
|
|
10564
|
+
function apply(target, ops) {
|
|
10565
|
+
let root = target;
|
|
10566
|
+
for (const op of ops) {
|
|
10567
|
+
if (op.path.length === 0) {
|
|
10568
|
+
if (op.op === "set")
|
|
10569
|
+
root = clone2(op.value);
|
|
10570
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
10571
|
+
root.push(...clone2(op.values));
|
|
10572
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
10573
|
+
root.length = op.length;
|
|
10574
|
+
else
|
|
10575
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
10576
|
+
continue;
|
|
10577
|
+
}
|
|
10578
|
+
let parent = root;
|
|
10579
|
+
for (const key of op.path.slice(0, -1)) {
|
|
10580
|
+
if (typeof parent !== "object" || parent === null)
|
|
10581
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10582
|
+
parent = parent[key];
|
|
10583
|
+
}
|
|
10584
|
+
if (typeof parent !== "object" || parent === null)
|
|
10585
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
10586
|
+
const key = op.path.at(-1);
|
|
10587
|
+
const container = parent;
|
|
10588
|
+
if (op.op === "set")
|
|
10589
|
+
container[key] = clone2(op.value);
|
|
10590
|
+
else if (op.op === "del")
|
|
10591
|
+
delete container[key];
|
|
10592
|
+
else {
|
|
10593
|
+
const array = container[key];
|
|
10594
|
+
if (!Array.isArray(array))
|
|
10595
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
10596
|
+
if (op.op === "push")
|
|
10597
|
+
array.push(...clone2(op.values));
|
|
10598
|
+
else
|
|
10599
|
+
array.length = op.length;
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
return root;
|
|
10603
|
+
}
|
|
10604
|
+
var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
|
|
10605
|
+
var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
10606
|
+
if (!isPlainObject2(op) || !isPath(op.path))
|
|
10607
|
+
return false;
|
|
10608
|
+
if (op.op === "set")
|
|
10609
|
+
return "value" in op;
|
|
10610
|
+
if (op.op === "del")
|
|
10611
|
+
return op.path.length > 0;
|
|
10612
|
+
if (op.op === "push")
|
|
10613
|
+
return Array.isArray(op.values);
|
|
10614
|
+
if (op.op === "cut")
|
|
10615
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
10616
|
+
return false;
|
|
10617
|
+
});
|
|
10618
|
+
|
|
10619
|
+
// packages/workit-core/src/store/reduce.ts
|
|
10620
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10621
|
+
var OPENING_TYPES = new Set([
|
|
10622
|
+
"task.opened",
|
|
10623
|
+
"task.imported",
|
|
10624
|
+
"migrated.from_v2",
|
|
10625
|
+
"task.checkpoint"
|
|
10626
|
+
]);
|
|
10627
|
+
var KNOWN_TYPES = new Set([
|
|
10628
|
+
...OPENING_TYPES,
|
|
10629
|
+
"task.bound",
|
|
10630
|
+
"task.noted",
|
|
10631
|
+
"task.revised",
|
|
10632
|
+
"task.judged",
|
|
10633
|
+
"task.paused",
|
|
10634
|
+
"task.resumed",
|
|
10635
|
+
"task.closed",
|
|
10636
|
+
"task.updated",
|
|
10637
|
+
"check.ran",
|
|
10638
|
+
"evidence.recorded",
|
|
10639
|
+
"finding.recorded",
|
|
10640
|
+
"finding.updated",
|
|
10641
|
+
"decision.recorded",
|
|
10642
|
+
"decision.updated",
|
|
10643
|
+
"worker.updated"
|
|
10644
|
+
]);
|
|
10645
|
+
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10646
|
+
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");
|
|
10647
|
+
var BLOB_REF = "$blob";
|
|
10648
|
+
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;
|
|
10649
|
+
var blobDigest = (value) => createHash5("sha256").update(JSON.stringify(value)).digest("hex");
|
|
10650
|
+
var toBlob = (value, blobs) => {
|
|
10651
|
+
if (!isObject3(value))
|
|
10652
|
+
return value;
|
|
10653
|
+
const digest = blobDigest(value);
|
|
10654
|
+
blobs.write(digest, value);
|
|
10655
|
+
return { [BLOB_REF]: digest };
|
|
10656
|
+
};
|
|
10657
|
+
var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
|
|
10658
|
+
var encodeOps = (ops, blobs) => ops.map((op) => {
|
|
10659
|
+
if (op.path[0] !== "candidates")
|
|
10660
|
+
return op;
|
|
10661
|
+
if (op.op === "push" && op.path.length === 1)
|
|
10662
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
10663
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
10664
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
10665
|
+
if (op.op === "set" && op.path.length === 2)
|
|
10666
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
10667
|
+
return op;
|
|
10668
|
+
});
|
|
10669
|
+
var decode2 = (value, blobs) => {
|
|
10670
|
+
const digest = blobRef(value);
|
|
10671
|
+
if (digest)
|
|
10672
|
+
return blobs.read(digest);
|
|
10673
|
+
if (Array.isArray(value))
|
|
10674
|
+
return value.map((item) => decode2(item, blobs));
|
|
10675
|
+
if (isObject3(value)) {
|
|
10676
|
+
const out = {};
|
|
10677
|
+
for (const [key, item] of Object.entries(value))
|
|
10678
|
+
out[key] = decode2(item, blobs);
|
|
10679
|
+
return out;
|
|
10680
|
+
}
|
|
10681
|
+
return value;
|
|
10682
|
+
};
|
|
10683
|
+
function reduce(state, event, blobs) {
|
|
10684
|
+
const data = event.data;
|
|
10685
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
10686
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
10687
|
+
if (!known && !carries) {
|
|
10688
|
+
if (event.critical === true)
|
|
10689
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
10690
|
+
return state;
|
|
10691
|
+
}
|
|
10692
|
+
let next = state;
|
|
10693
|
+
if ("record" in data) {
|
|
10694
|
+
if (!isObject3(data.record))
|
|
10695
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
10696
|
+
next = {
|
|
10697
|
+
record: decode2(data.record, blobs),
|
|
10698
|
+
key: null,
|
|
10699
|
+
legacy: null
|
|
10700
|
+
};
|
|
10701
|
+
} else if (next === null) {
|
|
10702
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
10703
|
+
} else
|
|
10704
|
+
next = { ...next };
|
|
10705
|
+
if ("ops" in data) {
|
|
10706
|
+
if (!isPatch(data.ops))
|
|
10707
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
10708
|
+
try {
|
|
10709
|
+
next.record = apply(next.record, decode2(data.ops, blobs));
|
|
10710
|
+
} catch (error) {
|
|
10711
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
10712
|
+
}
|
|
10713
|
+
}
|
|
10714
|
+
if ("key" in data)
|
|
10715
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
10716
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
10717
|
+
next.legacy = {
|
|
10718
|
+
path: String(data.legacy.path ?? ""),
|
|
10719
|
+
digest: String(data.legacy.digest ?? "")
|
|
10720
|
+
};
|
|
10721
|
+
return next;
|
|
10722
|
+
}
|
|
10723
|
+
function eventType(before, after) {
|
|
10724
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
10725
|
+
if (before.status !== after.status)
|
|
10726
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
10727
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
10728
|
+
const last = after.evidence.at(-1);
|
|
10729
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
10730
|
+
}
|
|
10731
|
+
if (count(after.findings) > count(before.findings))
|
|
10732
|
+
return "finding.recorded";
|
|
10733
|
+
if (count(after.decisions) > count(before.decisions))
|
|
10734
|
+
return "decision.recorded";
|
|
10735
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
10736
|
+
return "finding.updated";
|
|
10737
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
10738
|
+
return "decision.updated";
|
|
10739
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
10740
|
+
return "worker.updated";
|
|
10741
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
10742
|
+
return "task.revised";
|
|
10743
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
10744
|
+
return "task.judged";
|
|
10745
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
10746
|
+
return "task.noted";
|
|
10747
|
+
return "task.updated";
|
|
10748
|
+
}
|
|
10749
|
+
|
|
10750
|
+
// packages/workit-core/src/store/notes.ts
|
|
10751
|
+
var SLOT = Symbol.for("workit.migrationReporter");
|
|
10752
|
+
var reportMigration = (report) => {
|
|
10753
|
+
try {
|
|
10754
|
+
const reporter = globalThis[SLOT];
|
|
10755
|
+
if (typeof reporter === "function")
|
|
10756
|
+
reporter(report);
|
|
10757
|
+
} catch {}
|
|
10758
|
+
};
|
|
10759
|
+
var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
10760
|
+
|
|
10761
|
+
// packages/workit-core/src/core/task-store.ts
|
|
10762
|
+
var MIGRATION_PENDING = "workit migration pending";
|
|
10763
|
+
var STORE_FORMAT = 3;
|
|
10764
|
+
var STORE_MIN_RUNTIME = "3.0.0";
|
|
10765
|
+
var SNAPSHOT_EVERY = 32;
|
|
10766
|
+
var COMPACT_ABOVE = 200;
|
|
10767
|
+
var COMPACT_KEEP = 50;
|
|
10768
|
+
var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
10769
|
+
var TOMBSTONE = Symbol("tombstone");
|
|
10770
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
10771
|
+
var INDEX_VERSION = 3;
|
|
10772
|
+
var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10773
|
+
var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
10774
|
+
var indexEntry = (task, key, legacy, file) => {
|
|
10775
|
+
const sessions = [];
|
|
10776
|
+
const intent = hostSession(task.intent.provenance.session);
|
|
10777
|
+
if (intent)
|
|
10778
|
+
sessions.push({ ...intent, workerId: null });
|
|
10779
|
+
for (const worker of task.workers) {
|
|
10780
|
+
const session = hostSession(worker.data.session);
|
|
10781
|
+
if (session)
|
|
10782
|
+
sessions.push({ ...session, workerId: worker.id });
|
|
10783
|
+
}
|
|
10784
|
+
return {
|
|
10785
|
+
id: task.id,
|
|
10786
|
+
revision: task.revision,
|
|
10787
|
+
status: task.status,
|
|
10788
|
+
createdAt: task.createdAt,
|
|
10789
|
+
updatedAt: task.updatedAt,
|
|
10790
|
+
objective: task.intent.data.objective,
|
|
10791
|
+
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
10792
|
+
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
10793
|
+
sessions,
|
|
10794
|
+
workspaceId: task.workspaceId,
|
|
10795
|
+
key: key?.key ?? null,
|
|
10796
|
+
branch: key?.branch ?? null,
|
|
10797
|
+
boundAt: key?.boundAt ?? null,
|
|
10798
|
+
legacy,
|
|
10799
|
+
file
|
|
10800
|
+
};
|
|
10801
|
+
};
|
|
10802
|
+
var fileSignature = (file) => {
|
|
10803
|
+
try {
|
|
10804
|
+
const stat = fs8.statSync(file, { bigint: true });
|
|
10805
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
10806
|
+
} catch {
|
|
10807
|
+
return null;
|
|
10808
|
+
}
|
|
10809
|
+
};
|
|
10810
|
+
var RACY_WINDOW_NS = 2000000000n;
|
|
10811
|
+
var racySignature = (signature) => {
|
|
10812
|
+
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
10813
|
+
const changed = mtime > ctime ? mtime : ctime;
|
|
10814
|
+
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
10815
|
+
};
|
|
10816
|
+
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
10817
|
+
var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
10818
|
+
var cachedRuntimeVersion = null;
|
|
10819
|
+
var runtimeVersion = () => {
|
|
10820
|
+
if (cachedRuntimeVersion === null) {
|
|
10821
|
+
try {
|
|
10822
|
+
const pkg = JSON.parse(fs8.readFileSync(path9.join(packageRoot(), "package.json"), "utf8"));
|
|
10823
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
10824
|
+
} catch {
|
|
10825
|
+
cachedRuntimeVersion = "0.0.0";
|
|
10826
|
+
}
|
|
10827
|
+
}
|
|
10828
|
+
return cachedRuntimeVersion;
|
|
10829
|
+
};
|
|
10830
|
+
var stampNew = (record) => ({
|
|
10831
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
10832
|
+
updatedWith: runtimeVersion()
|
|
10833
|
+
});
|
|
10834
|
+
var isNewerVersion = (candidate, current) => {
|
|
10835
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
10836
|
+
const [candidateParts, currentParts] = [parts(candidate), parts(current)];
|
|
10837
|
+
for (let index = 0;index < 3; index += 1) {
|
|
10838
|
+
const left = candidateParts[index] ?? 0;
|
|
10839
|
+
const right = currentParts[index] ?? 0;
|
|
10840
|
+
if (left !== right)
|
|
10841
|
+
return left > right;
|
|
10842
|
+
}
|
|
10843
|
+
return false;
|
|
10844
|
+
};
|
|
10845
|
+
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);
|
|
10846
|
+
var sameDirectoryIdentity = (left, right) => {
|
|
10847
|
+
if (!path9.isAbsolute(left) || !path9.isAbsolute(right))
|
|
10848
|
+
return false;
|
|
10849
|
+
const normalizedLeft = path9.resolve(left);
|
|
10850
|
+
const normalizedRight = path9.resolve(right);
|
|
10851
|
+
try {
|
|
10852
|
+
const a = fs8.statSync(normalizedLeft, { bigint: true });
|
|
10853
|
+
const b = fs8.statSync(normalizedRight, { bigint: true });
|
|
10854
|
+
if (!a.isDirectory() || !b.isDirectory())
|
|
10855
|
+
return false;
|
|
10856
|
+
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
10857
|
+
return true;
|
|
10858
|
+
const canonicalLeft = fs8.realpathSync(normalizedLeft);
|
|
10859
|
+
const canonicalRight = fs8.realpathSync(normalizedRight);
|
|
10860
|
+
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
10861
|
+
return true;
|
|
10862
|
+
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
10863
|
+
} catch {
|
|
10864
|
+
return false;
|
|
10865
|
+
}
|
|
10866
|
+
};
|
|
10198
10867
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
10199
10868
|
var heldInProcess = new Map;
|
|
10200
|
-
var
|
|
10201
|
-
var
|
|
10202
|
-
const count = (heldInProcess.get(
|
|
10869
|
+
var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
|
|
10870
|
+
var drop = (lock) => {
|
|
10871
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
10203
10872
|
if (count > 0)
|
|
10204
|
-
heldInProcess.set(
|
|
10873
|
+
heldInProcess.set(lock, count);
|
|
10205
10874
|
else
|
|
10206
|
-
heldInProcess.delete(
|
|
10875
|
+
heldInProcess.delete(lock);
|
|
10207
10876
|
};
|
|
10208
|
-
var
|
|
10209
|
-
const
|
|
10210
|
-
return
|
|
10877
|
+
var envActor = () => {
|
|
10878
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
10879
|
+
return {
|
|
10880
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
10881
|
+
session: value("WORKIT_SESSION_ID"),
|
|
10882
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
10883
|
+
};
|
|
10211
10884
|
};
|
|
10212
10885
|
|
|
10213
10886
|
class TaskStore {
|
|
10214
10887
|
root;
|
|
10888
|
+
origin;
|
|
10215
10889
|
lockTimeoutMs;
|
|
10890
|
+
actor;
|
|
10891
|
+
migrateOnRead;
|
|
10892
|
+
located;
|
|
10893
|
+
migrationChecked = false;
|
|
10894
|
+
compactDue = new Set;
|
|
10216
10895
|
constructor(root, options = {}) {
|
|
10217
|
-
|
|
10896
|
+
const resolved = path9.resolve(root);
|
|
10897
|
+
this.origin = fs8.existsSync(resolved) ? fs8.realpathSync(resolved) : resolved;
|
|
10898
|
+
this.located = resolveStore(this.origin);
|
|
10899
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
10218
10900
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
10901
|
+
this.actor = options.actor ?? envActor();
|
|
10902
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
10903
|
+
}
|
|
10904
|
+
location() {
|
|
10905
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs8.existsSync(path9.join(this.root, ".git"))) {
|
|
10906
|
+
this.located = undefined;
|
|
10907
|
+
this.migrationChecked = false;
|
|
10908
|
+
}
|
|
10909
|
+
if (this.located === undefined)
|
|
10910
|
+
this.located = resolveStore(this.root);
|
|
10911
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
10912
|
+
}
|
|
10913
|
+
currentKey() {
|
|
10914
|
+
const location = this.location();
|
|
10915
|
+
if (!location.ok)
|
|
10916
|
+
return location;
|
|
10917
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
10219
10918
|
}
|
|
10220
10919
|
readTask(taskId) {
|
|
10221
10920
|
if (!validId(taskId))
|
|
10222
10921
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10223
|
-
const
|
|
10224
|
-
if (!
|
|
10922
|
+
const ready = this.ready();
|
|
10923
|
+
if (!ready.ok)
|
|
10924
|
+
return ready;
|
|
10925
|
+
const loaded = this.load(taskId);
|
|
10926
|
+
if (!loaded.ok)
|
|
10927
|
+
return loaded;
|
|
10928
|
+
if (!loaded.data)
|
|
10225
10929
|
return failure2("not_found", "task not found", { taskId });
|
|
10226
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
10227
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
10228
|
-
if (result.result.ok) {
|
|
10229
|
-
const workspace = this.readWorkspace();
|
|
10230
|
-
if (!workspace.ok)
|
|
10231
|
-
return workspace;
|
|
10232
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
10233
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
10234
|
-
}
|
|
10235
|
-
return result.result;
|
|
10236
|
-
}
|
|
10237
|
-
listTasks() {
|
|
10238
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
10239
|
-
return success(null, null, []);
|
|
10240
|
-
let names;
|
|
10241
|
-
try {
|
|
10242
|
-
names = fs6.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
10243
|
-
} catch (error) {
|
|
10244
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10245
|
-
path: this.tasksDir
|
|
10246
|
-
});
|
|
10247
|
-
}
|
|
10248
10930
|
const workspace = this.readWorkspace();
|
|
10249
10931
|
if (!workspace.ok)
|
|
10250
10932
|
return workspace;
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10933
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
10934
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
10935
|
+
}
|
|
10936
|
+
listTasks() {
|
|
10937
|
+
const listed = this.listTaskIndex();
|
|
10938
|
+
if (!listed.ok)
|
|
10939
|
+
return listed;
|
|
10255
10940
|
const tasks = [];
|
|
10256
|
-
for (const
|
|
10257
|
-
const
|
|
10258
|
-
if (!
|
|
10259
|
-
|
|
10260
|
-
if (
|
|
10261
|
-
|
|
10262
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10263
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10264
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10265
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10266
|
-
tasks.push(item.result.data);
|
|
10941
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
10942
|
+
const loaded = this.load(entry.id);
|
|
10943
|
+
if (!loaded.ok)
|
|
10944
|
+
return loaded;
|
|
10945
|
+
if (loaded.data)
|
|
10946
|
+
tasks.push(loaded.data.record);
|
|
10267
10947
|
}
|
|
10268
10948
|
return success(null, null, tasks);
|
|
10269
10949
|
}
|
|
10270
10950
|
listTaskIndex() {
|
|
10271
|
-
|
|
10951
|
+
const all = this.listStoreIndex();
|
|
10952
|
+
if (!all.ok)
|
|
10953
|
+
return all;
|
|
10954
|
+
const workspace = this.readWorkspace();
|
|
10955
|
+
if (!workspace.ok)
|
|
10956
|
+
return workspace;
|
|
10957
|
+
if (!workspace.data)
|
|
10272
10958
|
return success(null, null, []);
|
|
10959
|
+
const id = workspace.data.id;
|
|
10960
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
10961
|
+
}
|
|
10962
|
+
listStoreIndex() {
|
|
10963
|
+
const ready = this.ready();
|
|
10964
|
+
if (!ready.ok)
|
|
10965
|
+
return ready;
|
|
10966
|
+
const tasksDir = this.paths().tasks;
|
|
10273
10967
|
let names;
|
|
10274
10968
|
try {
|
|
10275
|
-
names =
|
|
10969
|
+
names = fs8.existsSync(tasksDir) ? fs8.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
10276
10970
|
} catch (error) {
|
|
10277
10971
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
10278
|
-
path:
|
|
10972
|
+
path: tasksDir
|
|
10279
10973
|
});
|
|
10280
10974
|
}
|
|
10281
|
-
const
|
|
10282
|
-
if (!workspace.ok)
|
|
10283
|
-
return workspace;
|
|
10284
|
-
if (!workspace.data)
|
|
10285
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
10286
|
-
path: this.workspacePath
|
|
10287
|
-
});
|
|
10288
|
-
const stored = this.readIndex(workspace.data.id);
|
|
10975
|
+
const stored = this.readIndex();
|
|
10289
10976
|
const entries = [];
|
|
10290
10977
|
let changed = Object.keys(stored).length !== names.length;
|
|
10291
10978
|
for (const name of names.toSorted()) {
|
|
10292
|
-
const
|
|
10293
|
-
const signature = fileSignature(file);
|
|
10979
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
10294
10980
|
if (signature === null) {
|
|
10295
10981
|
changed = true;
|
|
10296
10982
|
continue;
|
|
10297
10983
|
}
|
|
10298
|
-
const cached = stored[name
|
|
10984
|
+
const cached = stored[name];
|
|
10299
10985
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
10300
10986
|
entries.push(cached);
|
|
10301
10987
|
continue;
|
|
10302
10988
|
}
|
|
10303
|
-
const
|
|
10304
|
-
if (!
|
|
10989
|
+
const loaded = this.load(name);
|
|
10990
|
+
if (!loaded.ok)
|
|
10991
|
+
return loaded;
|
|
10992
|
+
if (!loaded.data)
|
|
10305
10993
|
continue;
|
|
10306
|
-
|
|
10307
|
-
return item.result;
|
|
10308
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
10309
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
10310
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
10311
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
10312
|
-
const entry = indexEntry(item.result.data, signature);
|
|
10994
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
10313
10995
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
10314
10996
|
changed = true;
|
|
10315
10997
|
entries.push(entry);
|
|
10316
10998
|
}
|
|
10317
10999
|
if (changed)
|
|
10318
|
-
this.writeIndex(
|
|
11000
|
+
this.writeIndex(entries);
|
|
10319
11001
|
return success(null, null, entries);
|
|
10320
11002
|
}
|
|
10321
11003
|
readWorkspace() {
|
|
10322
|
-
const
|
|
10323
|
-
if (!
|
|
10324
|
-
return
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
11004
|
+
const ready = this.ready();
|
|
11005
|
+
if (!ready.ok)
|
|
11006
|
+
return ready;
|
|
11007
|
+
const file = this.paths().workspace;
|
|
11008
|
+
let bytes;
|
|
11009
|
+
try {
|
|
11010
|
+
bytes = retryTransient(() => fs8.readFileSync(file, "utf8"));
|
|
11011
|
+
} catch (error) {
|
|
11012
|
+
if (error.code === "ENOENT")
|
|
11013
|
+
return success(null, null, null);
|
|
11014
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
11015
|
+
path: file
|
|
10328
11016
|
});
|
|
10329
|
-
|
|
11017
|
+
}
|
|
11018
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
11019
|
+
if (!parsed.ok)
|
|
11020
|
+
return parsed;
|
|
11021
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
11022
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
11023
|
+
return parsed;
|
|
11024
|
+
}
|
|
11025
|
+
implicitTask(input) {
|
|
11026
|
+
const ready = this.ready(input.create !== false);
|
|
11027
|
+
if (!ready.ok)
|
|
11028
|
+
return ready;
|
|
11029
|
+
const key = this.currentKey();
|
|
11030
|
+
if (!key.ok)
|
|
11031
|
+
return key;
|
|
11032
|
+
const found = this.openTaskForKey(key.data.key);
|
|
11033
|
+
if (!found.ok)
|
|
11034
|
+
return found;
|
|
11035
|
+
const workspace = this.readWorkspace();
|
|
11036
|
+
if (!workspace.ok)
|
|
11037
|
+
return workspace;
|
|
11038
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
11039
|
+
const task = this.readTask(found.data.id);
|
|
11040
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
11041
|
+
}
|
|
11042
|
+
if (input.create === false) {
|
|
11043
|
+
if (!found.data)
|
|
11044
|
+
return success(null, null, null);
|
|
11045
|
+
const loaded = this.load(found.data.id);
|
|
11046
|
+
if (!loaded.ok)
|
|
11047
|
+
return loaded;
|
|
11048
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
11049
|
+
}
|
|
11050
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
11051
|
+
}
|
|
11052
|
+
implicitLocked(input, key) {
|
|
11053
|
+
{
|
|
11054
|
+
const again = this.openTaskForKey(key.key);
|
|
11055
|
+
if (!again.ok)
|
|
11056
|
+
return again;
|
|
11057
|
+
if (again.data) {
|
|
11058
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
11059
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
11060
|
+
}
|
|
11061
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path9.basename(this.root) || this.root;
|
|
11062
|
+
const intent = {
|
|
11063
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
11064
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
11065
|
+
authorityRefs: []
|
|
11066
|
+
};
|
|
11067
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
11068
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
11069
|
+
}
|
|
11070
|
+
}
|
|
11071
|
+
adoptTask(taskId) {
|
|
11072
|
+
if (!validId(taskId))
|
|
11073
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11074
|
+
const ready = this.ready(true);
|
|
11075
|
+
if (!ready.ok)
|
|
11076
|
+
return ready;
|
|
11077
|
+
const key = this.currentKey();
|
|
11078
|
+
if (!key.ok)
|
|
11079
|
+
return key;
|
|
11080
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
11081
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
11082
|
+
if (!holder.ok)
|
|
11083
|
+
return holder;
|
|
11084
|
+
if (holder.data && holder.data.id !== taskId)
|
|
11085
|
+
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 });
|
|
11086
|
+
return this.bindLocked(taskId, key.data);
|
|
11087
|
+
}));
|
|
11088
|
+
}
|
|
11089
|
+
keyReport() {
|
|
11090
|
+
const key = this.currentKey();
|
|
11091
|
+
if (!key.ok)
|
|
11092
|
+
return key;
|
|
11093
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
11094
|
+
if (!bound.ok)
|
|
11095
|
+
return bound;
|
|
11096
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
10330
11097
|
}
|
|
10331
11098
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
10332
11099
|
const value = "intent" in input ? input : {
|
|
@@ -10334,82 +11101,16 @@ class TaskStore {
|
|
|
10334
11101
|
provenance,
|
|
10335
11102
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
10336
11103
|
};
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
10342
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
10343
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
10344
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10345
|
-
});
|
|
10346
|
-
}
|
|
10347
|
-
if (!value.provenance)
|
|
10348
|
-
return failure2("invalid_input", "provenance is required");
|
|
10349
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
10350
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
10351
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10352
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10353
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
10354
|
-
const workspace = current.data ? {
|
|
10355
|
-
...current.data,
|
|
10356
|
-
revision: newRevision(),
|
|
10357
|
-
root: this.root,
|
|
10358
|
-
runtime: stampNew(current.data)
|
|
10359
|
-
} : {
|
|
10360
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10361
|
-
id: newId(),
|
|
10362
|
-
revision: newRevision(),
|
|
10363
|
-
root: this.root,
|
|
10364
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10365
|
-
writer: null
|
|
10366
|
-
};
|
|
10367
|
-
const timestamp = value.now ?? now();
|
|
10368
|
-
const task = {
|
|
10369
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10370
|
-
id: newId(),
|
|
10371
|
-
workspaceId: workspace.id,
|
|
10372
|
-
revision: newRevision(),
|
|
10373
|
-
createdAt: timestamp,
|
|
10374
|
-
updatedAt: timestamp,
|
|
10375
|
-
origin: null,
|
|
10376
|
-
intent: {
|
|
10377
|
-
id: newId(),
|
|
10378
|
-
recordedAt: timestamp,
|
|
10379
|
-
provenance: value.provenance,
|
|
10380
|
-
data: value.intent
|
|
10381
|
-
},
|
|
10382
|
-
constraints: [],
|
|
10383
|
-
status: "active",
|
|
10384
|
-
closure: null,
|
|
10385
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
10386
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10387
|
-
assessments: [],
|
|
10388
|
-
policy: null,
|
|
10389
|
-
policyChanges: [],
|
|
10390
|
-
candidates: [],
|
|
10391
|
-
evidence: [],
|
|
10392
|
-
decisions: [],
|
|
10393
|
-
findings: [],
|
|
10394
|
-
workers: []
|
|
10395
|
-
};
|
|
10396
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
10397
|
-
if (!writtenWorkspace.ok)
|
|
10398
|
-
return writtenWorkspace;
|
|
10399
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
10400
|
-
if (!writtenTask.ok) {
|
|
10401
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10402
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10403
|
-
if (!restored.ok)
|
|
10404
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
10405
|
-
}
|
|
10406
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
10407
|
-
}
|
|
10408
|
-
return success(task.revision, workspace.revision, task);
|
|
10409
|
-
});
|
|
11104
|
+
const ready = this.ready(true);
|
|
11105
|
+
if (!ready.ok)
|
|
11106
|
+
return ready;
|
|
11107
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
10410
11108
|
}
|
|
10411
11109
|
importTask(input) {
|
|
10412
|
-
|
|
11110
|
+
const ready = this.ready(true);
|
|
11111
|
+
if (!ready.ok)
|
|
11112
|
+
return ready;
|
|
11113
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10413
11114
|
const current = this.readWorkspace();
|
|
10414
11115
|
if (!current.ok)
|
|
10415
11116
|
return current;
|
|
@@ -10441,23 +11142,8 @@ class TaskStore {
|
|
|
10441
11142
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
10442
11143
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
10443
11144
|
});
|
|
10444
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
10445
|
-
if (current.data && !previousWorkspaceBytes)
|
|
10446
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
10447
11145
|
const timestamp = input.now ?? now();
|
|
10448
|
-
const workspace = current.data
|
|
10449
|
-
...current.data,
|
|
10450
|
-
revision: newRevision(),
|
|
10451
|
-
root: this.root,
|
|
10452
|
-
runtime: stampNew(current.data)
|
|
10453
|
-
} : {
|
|
10454
|
-
schemaVersion: SCHEMA_VERSION,
|
|
10455
|
-
id: input.workspaceId ?? newId(),
|
|
10456
|
-
revision: newRevision(),
|
|
10457
|
-
root: this.root,
|
|
10458
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
10459
|
-
writer: null
|
|
10460
|
-
};
|
|
11146
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
10461
11147
|
const task = {
|
|
10462
11148
|
...input.task,
|
|
10463
11149
|
workspaceId: workspace.id,
|
|
@@ -10472,62 +11158,46 @@ class TaskStore {
|
|
|
10472
11158
|
const validTask = taskRecordSchema.safeParse(task);
|
|
10473
11159
|
if (!validTask.success)
|
|
10474
11160
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
10479
|
-
if (!writtenTask.ok) {
|
|
10480
|
-
if (current.data && previousWorkspaceBytes) {
|
|
10481
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
10482
|
-
if (!restored.ok)
|
|
10483
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
10484
|
-
}
|
|
10485
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
10486
|
-
operation: "import",
|
|
10487
|
-
outcome: "unknown",
|
|
10488
|
-
path: writtenTask.details.path
|
|
10489
|
-
});
|
|
10490
|
-
}
|
|
10491
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
11161
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
11162
|
+
operation: "import"
|
|
11163
|
+
});
|
|
10492
11164
|
});
|
|
10493
11165
|
}
|
|
10494
11166
|
mutateTask(taskId, expected, update, timestamp) {
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
const
|
|
10502
|
-
if (!
|
|
10503
|
-
return
|
|
11167
|
+
if (!validId(taskId))
|
|
11168
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
11169
|
+
const ready = this.ready(true);
|
|
11170
|
+
if (!ready.ok)
|
|
11171
|
+
return ready;
|
|
11172
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
11173
|
+
const loaded = this.loadBound(taskId);
|
|
11174
|
+
if (!loaded.ok)
|
|
11175
|
+
return loaded;
|
|
11176
|
+
if (loaded.data.record.revision !== expected)
|
|
11177
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
10504
11178
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
10505
11179
|
let changed;
|
|
10506
11180
|
try {
|
|
10507
|
-
changed = update(
|
|
11181
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
10508
11182
|
} catch (error) {
|
|
10509
11183
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
10510
11184
|
}
|
|
10511
11185
|
if (!changed.ok)
|
|
10512
11186
|
return changed;
|
|
10513
|
-
const
|
|
10514
|
-
|
|
10515
|
-
id: taskId,
|
|
10516
|
-
workspaceId: current.data.workspaceId,
|
|
10517
|
-
createdAt: current.data.createdAt,
|
|
10518
|
-
revision: context.revision,
|
|
10519
|
-
updatedAt: context.now,
|
|
10520
|
-
runtime: stampNew(current.data)
|
|
10521
|
-
};
|
|
10522
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
10523
|
-
if (!valid.success)
|
|
11187
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
11188
|
+
if (!valid)
|
|
10524
11189
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
10525
|
-
const written = this.
|
|
10526
|
-
return written.ok ? success(valid.
|
|
11190
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
11191
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
10527
11192
|
});
|
|
11193
|
+
this.compactPending();
|
|
11194
|
+
return result;
|
|
10528
11195
|
}
|
|
10529
11196
|
mutateWorkspace(expected, update) {
|
|
10530
|
-
|
|
11197
|
+
const ready = this.ready(true);
|
|
11198
|
+
if (!ready.ok)
|
|
11199
|
+
return ready;
|
|
11200
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
10531
11201
|
const current = this.readWorkspace();
|
|
10532
11202
|
if (!current.ok)
|
|
10533
11203
|
return current;
|
|
@@ -10535,9 +11205,6 @@ class TaskStore {
|
|
|
10535
11205
|
return failure2("not_found", "workspace not found");
|
|
10536
11206
|
if (current.data.revision !== expected)
|
|
10537
11207
|
return this.workspaceConflict(expected, current.data.revision);
|
|
10538
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
10539
|
-
if (!previousBytes)
|
|
10540
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
10541
11208
|
const context = { now: now(), revision: newRevision() };
|
|
10542
11209
|
let changed;
|
|
10543
11210
|
try {
|
|
@@ -10547,23 +11214,27 @@ class TaskStore {
|
|
|
10547
11214
|
}
|
|
10548
11215
|
if (!changed.ok)
|
|
10549
11216
|
return changed;
|
|
10550
|
-
const
|
|
11217
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
10551
11218
|
...changed.data,
|
|
10552
11219
|
id: current.data.id,
|
|
10553
11220
|
root: this.root,
|
|
10554
11221
|
revision: context.revision,
|
|
10555
11222
|
runtime: stampNew(current.data)
|
|
10556
|
-
};
|
|
10557
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
11223
|
+
});
|
|
10558
11224
|
if (!valid.success)
|
|
10559
11225
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10560
|
-
const written = this.
|
|
11226
|
+
const written = this.writeWorkspace(valid.data);
|
|
10561
11227
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
10562
11228
|
});
|
|
10563
11229
|
}
|
|
10564
11230
|
mutateTaskAndWorkspace(input) {
|
|
10565
|
-
|
|
10566
|
-
|
|
11231
|
+
if (!validId(input.taskId))
|
|
11232
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
11233
|
+
const ready = this.ready(true);
|
|
11234
|
+
if (!ready.ok)
|
|
11235
|
+
return ready;
|
|
11236
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
11237
|
+
const task = this.loadBound(input.taskId);
|
|
10567
11238
|
if (!task.ok)
|
|
10568
11239
|
return task;
|
|
10569
11240
|
const workspace = this.readWorkspace();
|
|
@@ -10574,14 +11245,10 @@ class TaskStore {
|
|
|
10574
11245
|
const expected = input.expectedRevision;
|
|
10575
11246
|
if (!expected)
|
|
10576
11247
|
return failure2("invalid_input", "task revision is required");
|
|
10577
|
-
if (task.data.revision !== expected)
|
|
10578
|
-
return this.conflict(expected, task.data.revision);
|
|
11248
|
+
if (task.data.record.revision !== expected)
|
|
11249
|
+
return this.conflict(expected, task.data.record.revision);
|
|
10579
11250
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10580
11251
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
10581
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
10582
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
10583
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
10584
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
10585
11252
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10586
11253
|
let changedWorkspace;
|
|
10587
11254
|
try {
|
|
@@ -10600,286 +11267,69 @@ class TaskStore {
|
|
|
10600
11267
|
});
|
|
10601
11268
|
if (!nextWorkspace.success)
|
|
10602
11269
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
10603
|
-
const reserved = this.
|
|
11270
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
10604
11271
|
if (!reserved.ok)
|
|
10605
11272
|
return reserved;
|
|
11273
|
+
const uncertain = (message) => {
|
|
11274
|
+
this.markUncertain(nextWorkspace.data);
|
|
11275
|
+
return failure2("external_outcome_unknown", message, {
|
|
11276
|
+
operation: "coupled_mutation",
|
|
11277
|
+
outcome: "unknown"
|
|
11278
|
+
});
|
|
11279
|
+
};
|
|
10606
11280
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
10607
11281
|
let changedTask;
|
|
10608
11282
|
try {
|
|
10609
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
11283
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
10610
11284
|
} catch (error) {
|
|
10611
|
-
|
|
10612
|
-
return failure2("external_outcome_unknown", `workspace reserved but task mutation threw: ${String(error)}`, { operation: "coupled_mutation", outcome: "unknown" });
|
|
11285
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
10613
11286
|
}
|
|
10614
|
-
if (!changedTask.ok)
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
updatedAt: taskContext.now,
|
|
10625
|
-
runtime: stampNew(task.data)
|
|
10626
|
-
});
|
|
10627
|
-
if (!nextTask.success) {
|
|
10628
|
-
this.markUncertain(nextWorkspace.data);
|
|
10629
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10630
|
-
}
|
|
10631
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
10632
|
-
if (!written.ok) {
|
|
10633
|
-
this.markUncertain(nextWorkspace.data);
|
|
10634
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
10635
|
-
}
|
|
10636
|
-
return success(nextTask.data.revision, nextWorkspace.data.revision, {
|
|
10637
|
-
task: nextTask.data,
|
|
11287
|
+
if (!changedTask.ok)
|
|
11288
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11289
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
11290
|
+
if (!nextTask)
|
|
11291
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
11292
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
11293
|
+
if (!written.ok)
|
|
11294
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
11295
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
11296
|
+
task: nextTask,
|
|
10638
11297
|
workspace: nextWorkspace.data
|
|
10639
11298
|
});
|
|
10640
|
-
});
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
10644
|
-
return success(null, null, []);
|
|
10645
|
-
try {
|
|
10646
|
-
const candidates = [];
|
|
10647
|
-
for (const name of fs6.readdirSync(this.recoveryDir)) {
|
|
10648
|
-
const match = RECOVERY_NAME.exec(name);
|
|
10649
|
-
if (match)
|
|
10650
|
-
candidates.push({
|
|
10651
|
-
target: match[1],
|
|
10652
|
-
path: path7.join(this.recoveryDir, name),
|
|
10653
|
-
digest: match[3]
|
|
10654
|
-
});
|
|
10655
|
-
}
|
|
10656
|
-
return success(null, null, candidates);
|
|
10657
|
-
} catch (error) {
|
|
10658
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
10659
|
-
path: this.recoveryDir
|
|
10660
|
-
});
|
|
10661
|
-
}
|
|
10662
|
-
}
|
|
10663
|
-
recoverTask(taskId, input) {
|
|
10664
|
-
return this.recover("task", taskId, input);
|
|
10665
|
-
}
|
|
10666
|
-
recoverWorkspace(input) {
|
|
10667
|
-
return this.recover("workspace", null, input);
|
|
10668
|
-
}
|
|
10669
|
-
recover(target, taskId, input) {
|
|
10670
|
-
if (taskId !== null && !validId(taskId))
|
|
10671
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
10672
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
10673
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
10674
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
10675
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
10676
|
-
try {
|
|
10677
|
-
const workspace = this.readWorkspace();
|
|
10678
|
-
if (!workspace.ok && target === "task")
|
|
10679
|
-
return workspace;
|
|
10680
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
10681
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
10682
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
10683
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
10684
|
-
const currentBytes = this.snapshotBytes(file);
|
|
10685
|
-
if (!currentBytes)
|
|
10686
|
-
return failure2("not_found", "snapshot not found");
|
|
10687
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
10688
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
10689
|
-
let selectedBytes = currentBytes;
|
|
10690
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
10691
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
10692
|
-
if (!candidate)
|
|
10693
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
10694
|
-
selectedBytes = candidate;
|
|
10695
|
-
}
|
|
10696
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10697
|
-
if (!selectedRecord.ok)
|
|
10698
|
-
return selectedRecord;
|
|
10699
|
-
if (target === "workspace") {
|
|
10700
|
-
const parsed = selectedRecord;
|
|
10701
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
10702
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
10703
|
-
} else {
|
|
10704
|
-
const parsed = selectedRecord;
|
|
10705
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
10706
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
10707
|
-
}
|
|
10708
|
-
const lock = this.readLockSnapshot();
|
|
10709
|
-
if (!lock.ok)
|
|
10710
|
-
return lock;
|
|
10711
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
10712
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
10713
|
-
if (!evidence.ok)
|
|
10714
|
-
return evidence;
|
|
10715
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
10716
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
10717
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
10718
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
10719
|
-
const recoveryGate = { reclaimed: false };
|
|
10720
|
-
const result = this.withLock((handle) => {
|
|
10721
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
10722
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
10723
|
-
path: this.lockPath
|
|
10724
|
-
});
|
|
10725
|
-
if (!handle.verifyStillHeld())
|
|
10726
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
10727
|
-
path: this.lockPath
|
|
10728
|
-
});
|
|
10729
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
10730
|
-
if (!reacquiredBytes)
|
|
10731
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
10732
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
10733
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
10734
|
-
if (target === "task") {
|
|
10735
|
-
const currentWorkspace = this.readWorkspace();
|
|
10736
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
10737
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
10738
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
10739
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
10740
|
-
}
|
|
10741
|
-
if (target === "workspace") {
|
|
10742
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
10743
|
-
if (!parsed.ok)
|
|
10744
|
-
return parsed;
|
|
10745
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
10746
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10747
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
10748
|
-
}
|
|
10749
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
10750
|
-
if (!parsed.ok)
|
|
10751
|
-
return parsed;
|
|
10752
|
-
const value = {
|
|
10753
|
-
...parsed.data,
|
|
10754
|
-
revision: newRevision(),
|
|
10755
|
-
updatedAt: now(),
|
|
10756
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
10757
|
-
};
|
|
10758
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
10759
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
10760
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
10761
|
-
return result;
|
|
10762
|
-
} catch (error) {
|
|
10763
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
10764
|
-
}
|
|
10765
|
-
}
|
|
10766
|
-
processEvidence(input, lock, writer) {
|
|
10767
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
10768
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
10769
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
10770
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
10771
|
-
try {
|
|
10772
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
10773
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
10774
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
10775
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
10776
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
10777
|
-
if (!result.ok)
|
|
10778
|
-
return result;
|
|
10779
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
10780
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
10781
|
-
} catch (error) {
|
|
10782
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
10783
|
-
}
|
|
10784
|
-
}
|
|
10785
|
-
deepFreeze(value) {
|
|
10786
|
-
if (value && typeof value === "object") {
|
|
10787
|
-
for (const child of Object.values(value))
|
|
10788
|
-
this.deepFreeze(child);
|
|
10789
|
-
Object.freeze(value);
|
|
10790
|
-
}
|
|
10791
|
-
return value;
|
|
10792
|
-
}
|
|
10793
|
-
readLockSnapshot() {
|
|
10794
|
-
try {
|
|
10795
|
-
const stat = fs6.lstatSync(this.lockPath);
|
|
10796
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
10797
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
10798
|
-
path: this.lockPath
|
|
10799
|
-
});
|
|
10800
|
-
const raw = fs6.readFileSync(this.lockPath, "utf8");
|
|
10801
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
10802
|
-
} catch (error) {
|
|
10803
|
-
if (error?.code === "ENOENT")
|
|
10804
|
-
return success(null, null, null);
|
|
10805
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
10806
|
-
path: this.lockPath
|
|
10807
|
-
});
|
|
10808
|
-
}
|
|
10809
|
-
}
|
|
10810
|
-
metadataLockOptions() {
|
|
10811
|
-
const inProcess = heldInProcess.has(this.root);
|
|
10812
|
-
return {
|
|
10813
|
-
lockPath: this.lockPath,
|
|
10814
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
10815
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
10816
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
10817
|
-
staleRecovery: "remove-if-unchanged",
|
|
10818
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
10819
|
-
let ageMs = null;
|
|
10820
|
-
try {
|
|
10821
|
-
ageMs = nowMs - fs6.lstatSync(this.lockPath).mtimeMs;
|
|
10822
|
-
} catch {}
|
|
10823
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
10824
|
-
},
|
|
10825
|
-
shouldRemoveStaleLock: () => true,
|
|
10826
|
-
parsePayload: parseMetadataLockOrNull,
|
|
10827
|
-
payload: () => ({
|
|
10828
|
-
pid: process.pid,
|
|
10829
|
-
processStart: processStartOf(process.pid),
|
|
10830
|
-
host: localLockHost(),
|
|
10831
|
-
nonce: randomUUID2()
|
|
10832
|
-
})
|
|
10833
|
-
};
|
|
10834
|
-
}
|
|
10835
|
-
acquireMetadataLock(options) {
|
|
10836
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
10837
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
10838
|
-
while (true) {
|
|
10839
|
-
try {
|
|
10840
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
10841
|
-
...options,
|
|
10842
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
10843
|
-
});
|
|
10844
|
-
} catch (error) {
|
|
10845
|
-
const code = error?.code;
|
|
10846
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
10847
|
-
throw error;
|
|
10848
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
10849
|
-
throw error;
|
|
10850
|
-
if (code !== "file_lock_stale")
|
|
10851
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
10852
|
-
}
|
|
10853
|
-
}
|
|
10854
|
-
}
|
|
10855
|
-
externalActionLockOptions() {
|
|
10856
|
-
const options = this.metadataLockOptions();
|
|
10857
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
11299
|
+
}));
|
|
11300
|
+
this.compactPending();
|
|
11301
|
+
return result;
|
|
10858
11302
|
}
|
|
10859
11303
|
async withExternalActionLock(operation, reentrant = false) {
|
|
10860
11304
|
const activeRoots = externalActionLockRoots.getStore();
|
|
10861
11305
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
10862
11306
|
return operation();
|
|
11307
|
+
const ready = this.ready(true);
|
|
11308
|
+
if (!ready.ok)
|
|
11309
|
+
return ready;
|
|
11310
|
+
const lockPath = this.paths().checkoutLock;
|
|
10863
11311
|
try {
|
|
10864
|
-
this.
|
|
11312
|
+
this.initializeStorage();
|
|
10865
11313
|
} catch (error) {
|
|
10866
11314
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
10867
|
-
path: this.
|
|
11315
|
+
path: this.paths().dir
|
|
10868
11316
|
});
|
|
10869
11317
|
}
|
|
10870
11318
|
let handle;
|
|
10871
11319
|
try {
|
|
10872
|
-
|
|
11320
|
+
const options = this.lockOptions(lockPath);
|
|
11321
|
+
handle = this.acquire(lockPath, {
|
|
11322
|
+
...options,
|
|
11323
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
11324
|
+
});
|
|
10873
11325
|
} catch (error) {
|
|
10874
|
-
return this.lockFailure(error);
|
|
11326
|
+
return this.lockFailure(lockPath, error);
|
|
10875
11327
|
}
|
|
10876
|
-
|
|
11328
|
+
hold(lockPath);
|
|
10877
11329
|
let result;
|
|
10878
11330
|
try {
|
|
10879
11331
|
if (!handle.verifyStillHeld())
|
|
10880
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
10881
|
-
path: this.lockPath
|
|
10882
|
-
});
|
|
11332
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
10883
11333
|
else {
|
|
10884
11334
|
try {
|
|
10885
11335
|
const run = () => operation();
|
|
@@ -10893,526 +11343,1144 @@ class TaskStore {
|
|
|
10893
11343
|
if (!handle.verifyStillHeld())
|
|
10894
11344
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
10895
11345
|
outcome: "unknown",
|
|
10896
|
-
path:
|
|
11346
|
+
path: lockPath
|
|
10897
11347
|
});
|
|
10898
11348
|
} catch {
|
|
10899
11349
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
10900
11350
|
outcome: "unknown",
|
|
10901
|
-
path:
|
|
11351
|
+
path: lockPath
|
|
10902
11352
|
});
|
|
10903
11353
|
}
|
|
10904
11354
|
}
|
|
10905
11355
|
} catch {
|
|
10906
11356
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
10907
11357
|
outcome: "unknown",
|
|
10908
|
-
path:
|
|
11358
|
+
path: lockPath
|
|
10909
11359
|
});
|
|
10910
11360
|
}
|
|
10911
|
-
|
|
11361
|
+
drop(lockPath);
|
|
10912
11362
|
try {
|
|
10913
11363
|
retryTransient(() => handle.release());
|
|
10914
11364
|
} catch (error) {
|
|
10915
|
-
const released = this.lockFailure(error);
|
|
11365
|
+
const released = this.lockFailure(lockPath, error);
|
|
10916
11366
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
10917
11367
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
10918
11368
|
outcome: "unknown",
|
|
10919
|
-
path:
|
|
11369
|
+
path: lockPath
|
|
10920
11370
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
10921
11371
|
}
|
|
10922
11372
|
return result;
|
|
10923
11373
|
}
|
|
10924
|
-
|
|
10925
|
-
const
|
|
10926
|
-
options.
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
11374
|
+
collectGarbage(options = {}) {
|
|
11375
|
+
const dryRun = options.dryRun === true;
|
|
11376
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
11377
|
+
const report = {
|
|
11378
|
+
dryRun,
|
|
11379
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
11380
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
11381
|
+
temporary: { removed: 0 },
|
|
11382
|
+
legacyRecovery: null,
|
|
11383
|
+
retried: [],
|
|
11384
|
+
failed: []
|
|
11385
|
+
};
|
|
11386
|
+
const ready = this.ready(!dryRun);
|
|
11387
|
+
if (!ready.ok)
|
|
11388
|
+
return ready;
|
|
11389
|
+
const dirs = this.paths();
|
|
11390
|
+
const recovery = path9.join(this.root, ".workit", "recovery");
|
|
11391
|
+
if (fs8.existsSync(recovery)) {
|
|
11392
|
+
let files = 0;
|
|
11393
|
+
let bytes = 0;
|
|
11394
|
+
try {
|
|
11395
|
+
for (const name of fs8.readdirSync(recovery)) {
|
|
11396
|
+
const stat = fs8.lstatSync(path9.join(recovery, name));
|
|
11397
|
+
if (!stat.isFile())
|
|
11398
|
+
continue;
|
|
11399
|
+
files += 1;
|
|
11400
|
+
bytes += stat.size;
|
|
11401
|
+
}
|
|
11402
|
+
} catch {}
|
|
11403
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
11404
|
+
if (remove)
|
|
11405
|
+
fs8.rmSync(recovery, { recursive: true, force: true });
|
|
11406
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
11407
|
+
}
|
|
11408
|
+
if (!fs8.existsSync(dirs.tasks))
|
|
11409
|
+
return success(null, null, report);
|
|
11410
|
+
const names = fs8.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
11411
|
+
for (const name of names) {
|
|
11412
|
+
const file = this.eventsPath(name);
|
|
11413
|
+
let size = 0;
|
|
11414
|
+
try {
|
|
11415
|
+
size = fs8.statSync(file).size;
|
|
11416
|
+
} catch {
|
|
11417
|
+
continue;
|
|
11418
|
+
}
|
|
11419
|
+
const lines = this.countEvents(file);
|
|
11420
|
+
if (lines > compactAbove) {
|
|
11421
|
+
if (dryRun) {
|
|
11422
|
+
report.compacted.tasks.push(name);
|
|
11423
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
11424
|
+
report.compacted.bytesBefore += size;
|
|
11425
|
+
} else {
|
|
11426
|
+
const compacted = this.compact(name);
|
|
11427
|
+
if (!compacted.ok)
|
|
11428
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
11429
|
+
else {
|
|
11430
|
+
report.compacted.tasks.push(name);
|
|
11431
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
11432
|
+
report.compacted.bytesBefore += size;
|
|
11433
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
11434
|
+
}
|
|
11435
|
+
}
|
|
11436
|
+
}
|
|
11437
|
+
}
|
|
11438
|
+
const nowMs = Date.now();
|
|
11439
|
+
const sweep = () => {
|
|
11440
|
+
const references = new Set;
|
|
11441
|
+
for (const name of names)
|
|
11442
|
+
try {
|
|
11443
|
+
for (const match of fs8.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
11444
|
+
references.add(match[1]);
|
|
11445
|
+
} catch {}
|
|
11446
|
+
if (fs8.existsSync(dirs.blobs))
|
|
11447
|
+
for (const name of fs8.readdirSync(dirs.blobs)) {
|
|
11448
|
+
const digest = name.replace(/\.json$/, "");
|
|
11449
|
+
const file = path9.join(dirs.blobs, name);
|
|
11450
|
+
let stat;
|
|
11451
|
+
try {
|
|
11452
|
+
stat = fs8.lstatSync(file);
|
|
11453
|
+
} catch {
|
|
11454
|
+
continue;
|
|
11455
|
+
}
|
|
11456
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
11457
|
+
report.blobs.kept += 1;
|
|
11458
|
+
continue;
|
|
11459
|
+
}
|
|
11460
|
+
report.blobs.removed += 1;
|
|
11461
|
+
report.blobs.removedBytes += stat.size;
|
|
11462
|
+
if (!dryRun)
|
|
11463
|
+
fs8.rmSync(file, { force: true });
|
|
11464
|
+
}
|
|
11465
|
+
return success(null, null, null);
|
|
10936
11466
|
};
|
|
10937
|
-
|
|
11467
|
+
const swept = dryRun ? sweep() : this.withLock(path9.join(dirs.dir, "gc.lock"), sweep);
|
|
11468
|
+
if (!swept.ok)
|
|
11469
|
+
return swept;
|
|
11470
|
+
const temporaryDirs = [
|
|
11471
|
+
dirs.dir,
|
|
11472
|
+
dirs.tasks,
|
|
11473
|
+
dirs.blobs,
|
|
11474
|
+
dirs.checkout,
|
|
11475
|
+
...names.map((name) => path9.join(dirs.tasks, name))
|
|
11476
|
+
];
|
|
11477
|
+
for (const directory of temporaryDirs) {
|
|
11478
|
+
if (!fs8.existsSync(directory))
|
|
11479
|
+
continue;
|
|
11480
|
+
for (const name of fs8.readdirSync(directory)) {
|
|
11481
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11482
|
+
continue;
|
|
11483
|
+
const file = path9.join(directory, name);
|
|
11484
|
+
try {
|
|
11485
|
+
const stat = fs8.lstatSync(file);
|
|
11486
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11487
|
+
continue;
|
|
11488
|
+
} catch {
|
|
11489
|
+
continue;
|
|
11490
|
+
}
|
|
11491
|
+
report.temporary.removed += 1;
|
|
11492
|
+
if (!dryRun)
|
|
11493
|
+
fs8.rmSync(file, { force: true });
|
|
11494
|
+
}
|
|
11495
|
+
}
|
|
11496
|
+
return success(null, null, report);
|
|
11497
|
+
}
|
|
11498
|
+
paths() {
|
|
11499
|
+
const location = this.location();
|
|
11500
|
+
const dir = location.ok ? location.data.dir : path9.join(this.root, ".workit");
|
|
11501
|
+
const checkout = path9.join(dir, "checkouts", checkoutSlug(this.root));
|
|
11502
|
+
return {
|
|
11503
|
+
dir,
|
|
11504
|
+
shared: location.ok ? location.data.shared : false,
|
|
11505
|
+
marker: path9.join(dir, "store.json"),
|
|
11506
|
+
checkout,
|
|
11507
|
+
workspace: path9.join(checkout, "workspace.json"),
|
|
11508
|
+
checkoutLock: path9.join(checkout, "metadata.lock"),
|
|
11509
|
+
tasks: path9.join(dir, "tasks"),
|
|
11510
|
+
index: path9.join(dir, "task-index.json"),
|
|
11511
|
+
blobs: path9.join(dir, "blobs", "candidates"),
|
|
11512
|
+
legacy: path9.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
11513
|
+
v2: path9.join(this.root, ".workit")
|
|
11514
|
+
};
|
|
11515
|
+
}
|
|
11516
|
+
eventsPath(taskId) {
|
|
11517
|
+
return path9.join(this.paths().tasks, taskId, "events.jsonl");
|
|
11518
|
+
}
|
|
11519
|
+
snapshotPath(taskId) {
|
|
11520
|
+
return path9.join(this.paths().tasks, taskId, "snapshot.json");
|
|
11521
|
+
}
|
|
11522
|
+
taskLockPath(taskId) {
|
|
11523
|
+
return path9.join(this.paths().tasks, taskId, "lock");
|
|
11524
|
+
}
|
|
11525
|
+
ready(write = false) {
|
|
11526
|
+
const location = this.location();
|
|
11527
|
+
if (!location.ok)
|
|
11528
|
+
return location;
|
|
11529
|
+
if (this.migrationChecked)
|
|
11530
|
+
return success(null, null, null);
|
|
11531
|
+
const dirs = this.paths();
|
|
11532
|
+
try {
|
|
11533
|
+
const marker = JSON.parse(fs8.readFileSync(dirs.marker, "utf8"));
|
|
11534
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
11535
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
11536
|
+
} catch {}
|
|
11537
|
+
this.migrationChecked = true;
|
|
11538
|
+
const settled = this.settleMigrations(write);
|
|
11539
|
+
if (!settled.ok)
|
|
11540
|
+
this.migrationChecked = false;
|
|
11541
|
+
return settled;
|
|
11542
|
+
}
|
|
11543
|
+
settleMigrations(write) {
|
|
11544
|
+
const moved = this.adoptLocalStore();
|
|
11545
|
+
if (!moved.ok)
|
|
11546
|
+
return moved;
|
|
11547
|
+
const pending = this.pendingMigrations();
|
|
11548
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
11549
|
+
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" });
|
|
11550
|
+
for (const v2 of this.v2Dirs()) {
|
|
11551
|
+
const migrated = this.migrateV2(v2);
|
|
11552
|
+
if (!migrated.ok)
|
|
11553
|
+
return migrated;
|
|
11554
|
+
}
|
|
11555
|
+
return success(null, null, null);
|
|
11556
|
+
}
|
|
11557
|
+
blobStore() {
|
|
11558
|
+
const dir = this.paths().blobs;
|
|
11559
|
+
return {
|
|
11560
|
+
read: (digest) => {
|
|
11561
|
+
try {
|
|
11562
|
+
return JSON.parse(fs8.readFileSync(path9.join(dir, `${digest}.json`), "utf8"));
|
|
11563
|
+
} catch (error) {
|
|
11564
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
11565
|
+
}
|
|
11566
|
+
},
|
|
11567
|
+
write: (digest, value) => {
|
|
11568
|
+
const file = path9.join(dir, `${digest}.json`);
|
|
11569
|
+
if (fs8.existsSync(file)) {
|
|
11570
|
+
const stamp = new Date;
|
|
11571
|
+
try {
|
|
11572
|
+
fs8.utimesSync(file, stamp, stamp);
|
|
11573
|
+
return;
|
|
11574
|
+
} catch {}
|
|
11575
|
+
}
|
|
11576
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11577
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
11578
|
+
}
|
|
11579
|
+
};
|
|
11580
|
+
}
|
|
11581
|
+
load(taskId) {
|
|
11582
|
+
try {
|
|
11583
|
+
const loaded = this.loadOrThrow(taskId);
|
|
11584
|
+
if (!loaded)
|
|
11585
|
+
return success(null, null, null);
|
|
11586
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
11587
|
+
if (!parsed.ok)
|
|
11588
|
+
return parsed;
|
|
11589
|
+
return success(null, null, {
|
|
11590
|
+
record: parsed.data,
|
|
11591
|
+
key: loaded.state.key,
|
|
11592
|
+
legacy: loaded.state.legacy,
|
|
11593
|
+
seq: loaded.seq,
|
|
11594
|
+
lastId: loaded.lastId,
|
|
11595
|
+
lastStart: loaded.lastStart,
|
|
11596
|
+
end: loaded.end,
|
|
11597
|
+
size: loaded.size,
|
|
11598
|
+
snapshotSeq: loaded.snapshotSeq
|
|
11599
|
+
});
|
|
11600
|
+
} catch (error) {
|
|
11601
|
+
if (error instanceof LogDamage)
|
|
11602
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
11603
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
11604
|
+
taskId
|
|
11605
|
+
});
|
|
11606
|
+
}
|
|
11607
|
+
}
|
|
11608
|
+
loadOrThrow(taskId) {
|
|
11609
|
+
const blobs = this.blobStore();
|
|
11610
|
+
const file = this.eventsPath(taskId);
|
|
11611
|
+
const snapshot = this.readSnapshot(taskId);
|
|
11612
|
+
if (snapshot) {
|
|
11613
|
+
try {
|
|
11614
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
11615
|
+
const first = tail?.events[0];
|
|
11616
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
11617
|
+
let state = {
|
|
11618
|
+
record: snapshot.record,
|
|
11619
|
+
key: snapshot.key,
|
|
11620
|
+
legacy: snapshot.legacy
|
|
11621
|
+
};
|
|
11622
|
+
for (const event of tail.events.slice(1))
|
|
11623
|
+
state = reduce(state, event, blobs);
|
|
11624
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
11625
|
+
}
|
|
11626
|
+
} catch (error) {
|
|
11627
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
11628
|
+
throw error;
|
|
11629
|
+
}
|
|
11630
|
+
}
|
|
11631
|
+
const log = readLog(file, 0, null);
|
|
11632
|
+
if (!log || log.events.length === 0) {
|
|
11633
|
+
if (log && log.size > 0)
|
|
11634
|
+
throw new LogDamage("the log holds no complete event");
|
|
11635
|
+
return null;
|
|
11636
|
+
}
|
|
11637
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
11638
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
11639
|
+
let state = null;
|
|
11640
|
+
for (const event of log.events)
|
|
11641
|
+
state = reduce(state, event, blobs);
|
|
11642
|
+
return this.loadedFrom(state, log, 0);
|
|
11643
|
+
}
|
|
11644
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
11645
|
+
const last = log.events.at(-1);
|
|
11646
|
+
return {
|
|
11647
|
+
state,
|
|
11648
|
+
seq: last.seq,
|
|
11649
|
+
lastId: last.id,
|
|
11650
|
+
lastStart: log.lastStart,
|
|
11651
|
+
end: log.end,
|
|
11652
|
+
size: log.size,
|
|
11653
|
+
snapshotSeq
|
|
11654
|
+
};
|
|
11655
|
+
}
|
|
11656
|
+
readSnapshot(taskId) {
|
|
11657
|
+
try {
|
|
11658
|
+
const value = JSON.parse(fs8.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
11659
|
+
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))
|
|
11660
|
+
return null;
|
|
11661
|
+
return value;
|
|
11662
|
+
} catch {
|
|
11663
|
+
return null;
|
|
11664
|
+
}
|
|
11665
|
+
}
|
|
11666
|
+
writeSnapshot(taskId, loaded) {
|
|
11667
|
+
const snapshot = {
|
|
11668
|
+
v: 1,
|
|
11669
|
+
task: taskId,
|
|
11670
|
+
seq: loaded.seq,
|
|
11671
|
+
lastId: loaded.lastId,
|
|
11672
|
+
lastStart: loaded.lastStart,
|
|
11673
|
+
end: loaded.end,
|
|
11674
|
+
key: loaded.key,
|
|
11675
|
+
legacy: loaded.legacy,
|
|
11676
|
+
record: loaded.record
|
|
11677
|
+
};
|
|
11678
|
+
try {
|
|
11679
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
11680
|
+
} catch {}
|
|
11681
|
+
}
|
|
11682
|
+
loadBound(taskId) {
|
|
11683
|
+
const loaded = this.load(taskId);
|
|
11684
|
+
if (!loaded.ok)
|
|
11685
|
+
return loaded;
|
|
11686
|
+
if (!loaded.data)
|
|
11687
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11688
|
+
const workspace = this.readWorkspace();
|
|
11689
|
+
if (!workspace.ok)
|
|
11690
|
+
return workspace;
|
|
11691
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
11692
|
+
return bound ?? success(null, null, loaded.data);
|
|
11693
|
+
}
|
|
11694
|
+
bindingError(task, workspace) {
|
|
11695
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
11696
|
+
return null;
|
|
11697
|
+
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 });
|
|
11698
|
+
}
|
|
11699
|
+
nextTask(current, changed, context) {
|
|
11700
|
+
const valid = taskRecordSchema.safeParse({
|
|
11701
|
+
...changed,
|
|
11702
|
+
id: current.id,
|
|
11703
|
+
workspaceId: current.workspaceId,
|
|
11704
|
+
createdAt: current.createdAt,
|
|
11705
|
+
revision: context.revision,
|
|
11706
|
+
updatedAt: context.now,
|
|
11707
|
+
runtime: stampNew(current)
|
|
11708
|
+
});
|
|
11709
|
+
return valid.success ? valid.data : null;
|
|
11710
|
+
}
|
|
11711
|
+
event(taskId, seq, type, data) {
|
|
11712
|
+
return {
|
|
11713
|
+
v: EVENT_VERSION,
|
|
11714
|
+
seq,
|
|
11715
|
+
at: new Date().toISOString(),
|
|
11716
|
+
id: newId(),
|
|
11717
|
+
task: taskId,
|
|
11718
|
+
actor: this.actor,
|
|
11719
|
+
type,
|
|
11720
|
+
data
|
|
11721
|
+
};
|
|
11722
|
+
}
|
|
11723
|
+
appendPatch(loaded, next, extra = {}) {
|
|
11724
|
+
const taskId = next.id;
|
|
11725
|
+
try {
|
|
11726
|
+
const blobs = this.blobStore();
|
|
11727
|
+
const before = loaded.record;
|
|
11728
|
+
const after = next;
|
|
11729
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
11730
|
+
const { type, ...data } = extra;
|
|
11731
|
+
if (data.key)
|
|
11732
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
11733
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
11734
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
11735
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
11736
|
+
`);
|
|
11737
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
11738
|
+
const state = {
|
|
11739
|
+
record: next,
|
|
11740
|
+
key,
|
|
11741
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
11742
|
+
seq: event.seq,
|
|
11743
|
+
lastId: event.id,
|
|
11744
|
+
lastStart: start,
|
|
11745
|
+
end
|
|
11746
|
+
};
|
|
11747
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
11748
|
+
this.writeSnapshot(taskId, state);
|
|
11749
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
11750
|
+
this.compactDue.add(taskId);
|
|
11751
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
11752
|
+
return success(null, null, null);
|
|
11753
|
+
} catch (error) {
|
|
11754
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
11755
|
+
path: this.eventsPath(taskId)
|
|
11756
|
+
});
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
openTask(record, type, key, legacy = null) {
|
|
11760
|
+
const dir = path9.join(this.paths().tasks, record.id);
|
|
11761
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
11762
|
+
const blobs = this.blobStore();
|
|
11763
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
11764
|
+
const event = this.event(record.id, 1, type, {
|
|
11765
|
+
record: encodeRecord(record, blobs),
|
|
11766
|
+
key: bound,
|
|
11767
|
+
...legacy ? { legacy } : {}
|
|
11768
|
+
});
|
|
11769
|
+
const file = this.eventsPath(record.id);
|
|
11770
|
+
const existing = readLog(file, 0, null);
|
|
11771
|
+
if (existing && existing.events.length > 0)
|
|
11772
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
11773
|
+
appendEvent(file, event, 0);
|
|
11774
|
+
this.indexTask(record, bound, legacy !== null);
|
|
11775
|
+
}
|
|
11776
|
+
compactPending() {
|
|
11777
|
+
for (const taskId of this.compactDue) {
|
|
11778
|
+
this.compactDue.delete(taskId);
|
|
11779
|
+
this.compact(taskId);
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
compact(taskId) {
|
|
11783
|
+
try {
|
|
11784
|
+
const file = this.eventsPath(taskId);
|
|
11785
|
+
const log = readLog(file, 0, null);
|
|
11786
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
11787
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
11788
|
+
const blobs = this.blobStore();
|
|
11789
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
11790
|
+
let state = null;
|
|
11791
|
+
for (const event of log.events.slice(0, cut))
|
|
11792
|
+
state = reduce(state, event, blobs);
|
|
11793
|
+
const folded = log.events[cut - 1];
|
|
11794
|
+
const checkpoint = {
|
|
11795
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
11796
|
+
record: encodeRecord(state.record, blobs),
|
|
11797
|
+
key: state.key,
|
|
11798
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
11799
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
11800
|
+
})
|
|
11801
|
+
};
|
|
11802
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
11803
|
+
`)}
|
|
11804
|
+
`;
|
|
11805
|
+
const last = log.events.at(-1);
|
|
11806
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11807
|
+
let latest = null;
|
|
11808
|
+
try {
|
|
11809
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
11810
|
+
} catch {}
|
|
11811
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
11812
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
11813
|
+
const appended = fs8.readFileSync(file).subarray(log.end, latest.end);
|
|
11814
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
11815
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
11816
|
+
try {
|
|
11817
|
+
fs8.rmSync(this.snapshotPath(taskId), { force: true });
|
|
11818
|
+
} catch {}
|
|
11819
|
+
const reloaded = this.load(taskId);
|
|
11820
|
+
if (reloaded.ok && reloaded.data)
|
|
11821
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
11822
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
11823
|
+
});
|
|
11824
|
+
} catch (error) {
|
|
11825
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
countEvents(file) {
|
|
11829
|
+
try {
|
|
11830
|
+
const bytes = fs8.readFileSync(file);
|
|
11831
|
+
let count = 0;
|
|
11832
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
11833
|
+
count += 1;
|
|
11834
|
+
return count;
|
|
11835
|
+
} catch {
|
|
11836
|
+
return 0;
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
openTasksForKey(key) {
|
|
11840
|
+
const all = this.listStoreIndex();
|
|
11841
|
+
if (!all.ok)
|
|
11842
|
+
return all;
|
|
11843
|
+
const location = this.location();
|
|
11844
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
11845
|
+
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));
|
|
11846
|
+
}
|
|
11847
|
+
openTaskForKey(key) {
|
|
11848
|
+
const bound = this.openTasksForKey(key);
|
|
11849
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
11850
|
+
}
|
|
11851
|
+
withKeyLock(key, operation) {
|
|
11852
|
+
const digest = createHash6("sha256").update(key).digest("hex").slice(0, 32);
|
|
11853
|
+
return this.withLock(path9.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
11854
|
+
}
|
|
11855
|
+
bindLocked(taskId, key, timestamp) {
|
|
11856
|
+
const current = this.readWorkspace();
|
|
11857
|
+
if (!current.ok)
|
|
11858
|
+
return current;
|
|
11859
|
+
let workspace = current.data;
|
|
11860
|
+
if (!workspace) {
|
|
11861
|
+
workspace = this.nextWorkspace(null);
|
|
11862
|
+
const written = this.writeWorkspace(workspace);
|
|
11863
|
+
if (!written.ok)
|
|
11864
|
+
return written;
|
|
11865
|
+
}
|
|
11866
|
+
const target = workspace;
|
|
11867
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11868
|
+
const loaded = this.load(taskId);
|
|
11869
|
+
if (!loaded.ok)
|
|
11870
|
+
return loaded;
|
|
11871
|
+
if (!loaded.data)
|
|
11872
|
+
return failure2("not_found", "task not found", { taskId });
|
|
11873
|
+
if (loaded.data.record.status === "closed")
|
|
11874
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
11875
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
11876
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
11877
|
+
const next = taskRecordSchema.safeParse({
|
|
11878
|
+
...loaded.data.record,
|
|
11879
|
+
workspaceId: target.id,
|
|
11880
|
+
revision: newRevision(),
|
|
11881
|
+
updatedAt: timestamp ?? now(),
|
|
11882
|
+
runtime: stampNew(loaded.data.record)
|
|
11883
|
+
});
|
|
11884
|
+
if (!next.success)
|
|
11885
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
11886
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
11887
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
11888
|
+
});
|
|
11889
|
+
}
|
|
11890
|
+
unbindLocked(taskId) {
|
|
11891
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
11892
|
+
const loaded = this.load(taskId);
|
|
11893
|
+
if (!loaded.ok)
|
|
11894
|
+
return loaded;
|
|
11895
|
+
if (!loaded.data || loaded.data.key === null)
|
|
11896
|
+
return success(null, null, null);
|
|
11897
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
11898
|
+
});
|
|
11899
|
+
}
|
|
11900
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
11901
|
+
const current = this.readWorkspace();
|
|
11902
|
+
if (!current.ok)
|
|
11903
|
+
return current;
|
|
11904
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
11905
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
11906
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
11907
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
11908
|
+
});
|
|
11909
|
+
if (!value.provenance)
|
|
11910
|
+
return failure2("invalid_input", "provenance is required");
|
|
11911
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
11912
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
11913
|
+
if (bindCurrent) {
|
|
11914
|
+
const currentKey = this.currentKey();
|
|
11915
|
+
if (currentKey.ok)
|
|
11916
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
11917
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
11918
|
+
if (!holder.ok)
|
|
11919
|
+
return holder;
|
|
11920
|
+
if (holder.data) {
|
|
11921
|
+
const released = this.unbindLocked(holder.data.id);
|
|
11922
|
+
if (!released.ok)
|
|
11923
|
+
return released;
|
|
11924
|
+
}
|
|
11925
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
11926
|
+
});
|
|
11927
|
+
}
|
|
11928
|
+
return this.newTask(value, current.data, key);
|
|
11929
|
+
}
|
|
11930
|
+
newTask(value, previous, binding) {
|
|
11931
|
+
const current = { data: previous };
|
|
11932
|
+
const workspace = this.nextWorkspace(current.data);
|
|
11933
|
+
const timestamp = value.now ?? now();
|
|
11934
|
+
const task = {
|
|
11935
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11936
|
+
id: newId(),
|
|
11937
|
+
workspaceId: workspace.id,
|
|
11938
|
+
revision: newRevision(),
|
|
11939
|
+
createdAt: timestamp,
|
|
11940
|
+
updatedAt: timestamp,
|
|
11941
|
+
origin: null,
|
|
11942
|
+
intent: {
|
|
11943
|
+
id: newId(),
|
|
11944
|
+
recordedAt: timestamp,
|
|
11945
|
+
provenance: value.provenance,
|
|
11946
|
+
data: value.intent
|
|
11947
|
+
},
|
|
11948
|
+
constraints: [],
|
|
11949
|
+
status: "active",
|
|
11950
|
+
closure: null,
|
|
11951
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
11952
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
11953
|
+
assessments: [],
|
|
11954
|
+
policy: null,
|
|
11955
|
+
policyChanges: [],
|
|
11956
|
+
candidates: [],
|
|
11957
|
+
evidence: [],
|
|
11958
|
+
decisions: [],
|
|
11959
|
+
findings: [],
|
|
11960
|
+
workers: []
|
|
11961
|
+
};
|
|
11962
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
11963
|
+
operation: "create"
|
|
11964
|
+
});
|
|
11965
|
+
}
|
|
11966
|
+
nextWorkspace(current, id) {
|
|
11967
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
11968
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11969
|
+
id: id ?? newId(),
|
|
11970
|
+
revision: newRevision(),
|
|
11971
|
+
root: this.root,
|
|
11972
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
11973
|
+
writer: null
|
|
11974
|
+
};
|
|
11975
|
+
}
|
|
11976
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
11977
|
+
const written = this.writeWorkspace(workspace);
|
|
11978
|
+
if (!written.ok)
|
|
11979
|
+
return written;
|
|
11980
|
+
try {
|
|
11981
|
+
this.openTask(task, type, key);
|
|
11982
|
+
} catch (error) {
|
|
11983
|
+
if (previous) {
|
|
11984
|
+
const restored = this.writeWorkspace(previous);
|
|
11985
|
+
if (!restored.ok)
|
|
11986
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
11987
|
+
}
|
|
11988
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
11989
|
+
}
|
|
11990
|
+
return success(task.revision, workspace.revision, task);
|
|
11991
|
+
}
|
|
11992
|
+
writeWorkspace(record) {
|
|
11993
|
+
const file = this.paths().workspace;
|
|
11994
|
+
try {
|
|
11995
|
+
fs8.mkdirSync(path9.dirname(file), { recursive: true });
|
|
11996
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
11997
|
+
`, true);
|
|
11998
|
+
return success(null, null, null);
|
|
11999
|
+
} catch (error) {
|
|
12000
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
12001
|
+
}
|
|
10938
12002
|
}
|
|
10939
12003
|
markUncertain(workspace) {
|
|
10940
12004
|
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
10941
12005
|
return;
|
|
10942
|
-
|
|
12006
|
+
this.writeWorkspace({
|
|
10943
12007
|
...workspace,
|
|
10944
12008
|
writer: { ...workspace.writer, state: "uncertain" },
|
|
10945
12009
|
revision: newRevision(),
|
|
10946
12010
|
runtime: stampNew(workspace)
|
|
12011
|
+
});
|
|
12012
|
+
}
|
|
12013
|
+
readIndex() {
|
|
12014
|
+
try {
|
|
12015
|
+
const value = JSON.parse(fs8.readFileSync(this.paths().index, "utf8"));
|
|
12016
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
12017
|
+
return {};
|
|
12018
|
+
const tasks = {};
|
|
12019
|
+
for (const [id, entry] of Object.entries(value.tasks))
|
|
12020
|
+
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))
|
|
12021
|
+
tasks[id] = entry;
|
|
12022
|
+
return tasks;
|
|
12023
|
+
} catch {
|
|
12024
|
+
return {};
|
|
12025
|
+
}
|
|
12026
|
+
}
|
|
12027
|
+
writeIndex(entries) {
|
|
12028
|
+
try {
|
|
12029
|
+
fs8.mkdirSync(this.paths().tasks, { recursive: true });
|
|
12030
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
12031
|
+
version: INDEX_VERSION,
|
|
12032
|
+
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
12033
|
+
}), false);
|
|
12034
|
+
} catch {}
|
|
12035
|
+
}
|
|
12036
|
+
indexTask(task, key, legacy) {
|
|
12037
|
+
try {
|
|
12038
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
12039
|
+
if (signature === null)
|
|
12040
|
+
return;
|
|
12041
|
+
const tasks = this.readIndex();
|
|
12042
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
12043
|
+
this.writeIndex(Object.values(tasks));
|
|
12044
|
+
} catch {}
|
|
12045
|
+
}
|
|
12046
|
+
parseTask(record, taskId) {
|
|
12047
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
12048
|
+
if (!parsed.ok)
|
|
12049
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
12050
|
+
if (parsed.data.id !== taskId)
|
|
12051
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
12052
|
+
return parsed;
|
|
12053
|
+
}
|
|
12054
|
+
parseRecord(bytes, schema, label) {
|
|
12055
|
+
let value;
|
|
12056
|
+
try {
|
|
12057
|
+
value = JSON.parse(bytes);
|
|
12058
|
+
} catch {
|
|
12059
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
12060
|
+
}
|
|
12061
|
+
return this.parseValue(value, schema, label);
|
|
12062
|
+
}
|
|
12063
|
+
parseValue(value, schema, label) {
|
|
12064
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
12065
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
12066
|
+
const parsed = parseStoredRecord(schema, value);
|
|
12067
|
+
if (parsed.success)
|
|
12068
|
+
return success(null, null, parsed.data);
|
|
12069
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
12070
|
+
if (parsed.critical.length > 0)
|
|
12071
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
12072
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
12073
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
12074
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
12075
|
+
}
|
|
12076
|
+
conflict(expected, actual) {
|
|
12077
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
12078
|
+
}
|
|
12079
|
+
workspaceConflict(expected, actual) {
|
|
12080
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
12081
|
+
}
|
|
12082
|
+
v2Dirs() {
|
|
12083
|
+
return [...new Set([this.root, this.origin])].map((dir) => path9.join(dir, ".workit"));
|
|
12084
|
+
}
|
|
12085
|
+
pendingMigrations() {
|
|
12086
|
+
return this.v2Dirs().filter((dir) => {
|
|
12087
|
+
const raw = this.readV2Workspace(path9.join(dir, "workspace.json"));
|
|
12088
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
12089
|
+
});
|
|
12090
|
+
}
|
|
12091
|
+
migrateV2(v2) {
|
|
12092
|
+
const dirs = this.paths();
|
|
12093
|
+
const v2Workspace = path9.join(v2, "workspace.json");
|
|
12094
|
+
const v2Tasks = path9.join(v2, "tasks");
|
|
12095
|
+
const legacy = path9.join(dirs.dir, "legacy", checkoutSlug(path9.dirname(v2)), "v2");
|
|
12096
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
12097
|
+
if (raw === null || raw === TOMBSTONE)
|
|
12098
|
+
return success(null, null, null);
|
|
12099
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path9.join(v2, "metadata.lock"), () => {
|
|
12100
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
12101
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
12102
|
+
return success(null, null, null);
|
|
12103
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
12104
|
+
if (!parsedWorkspace.ok)
|
|
12105
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
12106
|
+
let files = [];
|
|
12107
|
+
try {
|
|
12108
|
+
files = fs8.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path9.join(v2Tasks, name)));
|
|
12109
|
+
} catch {}
|
|
12110
|
+
const current = this.readWorkspace();
|
|
12111
|
+
if (!current.ok)
|
|
12112
|
+
return current;
|
|
12113
|
+
let workspaceMoved = false;
|
|
12114
|
+
let target = current.data;
|
|
12115
|
+
if (!target) {
|
|
12116
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
12117
|
+
const written = this.writeWorkspace(target);
|
|
12118
|
+
if (!written.ok)
|
|
12119
|
+
return written;
|
|
12120
|
+
workspaceMoved = true;
|
|
12121
|
+
}
|
|
12122
|
+
let migrated = 0;
|
|
12123
|
+
let skipped = 0;
|
|
12124
|
+
const done = new Map;
|
|
12125
|
+
try {
|
|
12126
|
+
fs8.mkdirSync(path9.join(legacy, "tasks"), { recursive: true });
|
|
12127
|
+
for (const name of ["workspace.json", "index.json"])
|
|
12128
|
+
if (fs8.existsSync(path9.join(v2, name)))
|
|
12129
|
+
fs8.copyFileSync(path9.join(v2, name), path9.join(legacy, name));
|
|
12130
|
+
for (const name of files) {
|
|
12131
|
+
const id = name.slice(0, -5);
|
|
12132
|
+
const file = path9.join(v2Tasks, name);
|
|
12133
|
+
const bytes = fs8.readFileSync(file, "utf8");
|
|
12134
|
+
const digest = sha256(bytes);
|
|
12135
|
+
fs8.writeFileSync(path9.join(legacy, "tasks", name), bytes);
|
|
12136
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
12137
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
12138
|
+
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 });
|
|
12139
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
12140
|
+
const origin = { path: path9.join(legacy, "tasks", name), digest };
|
|
12141
|
+
if (!fs8.existsSync(this.eventsPath(id))) {
|
|
12142
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
12143
|
+
migrated += 1;
|
|
12144
|
+
} else {
|
|
12145
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
12146
|
+
const loaded = this.load(id);
|
|
12147
|
+
if (!loaded.ok)
|
|
12148
|
+
return loaded;
|
|
12149
|
+
if (!loaded.data)
|
|
12150
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
12151
|
+
if (loaded.data.legacy?.digest === digest)
|
|
12152
|
+
return success(null, null, false);
|
|
12153
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
12154
|
+
type: "migrated.from_v2",
|
|
12155
|
+
legacy: origin
|
|
12156
|
+
});
|
|
12157
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
12158
|
+
});
|
|
12159
|
+
if (!resynced.ok)
|
|
12160
|
+
return resynced;
|
|
12161
|
+
if (resynced.data)
|
|
12162
|
+
migrated += 1;
|
|
12163
|
+
else
|
|
12164
|
+
skipped += 1;
|
|
12165
|
+
}
|
|
12166
|
+
done.set(name, digest);
|
|
12167
|
+
}
|
|
12168
|
+
} catch (error) {
|
|
12169
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
12170
|
+
path: v2
|
|
12171
|
+
});
|
|
12172
|
+
}
|
|
12173
|
+
try {
|
|
12174
|
+
let complete = true;
|
|
12175
|
+
for (const [name, digest] of done) {
|
|
12176
|
+
const file = path9.join(v2Tasks, name);
|
|
12177
|
+
if (sha256(fs8.readFileSync(file, "utf8")) !== digest) {
|
|
12178
|
+
complete = false;
|
|
12179
|
+
continue;
|
|
12180
|
+
}
|
|
12181
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
12182
|
+
}
|
|
12183
|
+
if (!complete)
|
|
12184
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
12185
|
+
path: v2
|
|
12186
|
+
});
|
|
12187
|
+
fs8.rmSync(path9.join(v2, "index.json"), { force: true });
|
|
12188
|
+
this.writeTombstone(v2, target.id);
|
|
12189
|
+
} catch (error) {
|
|
12190
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
12191
|
+
path: v2
|
|
12192
|
+
});
|
|
12193
|
+
}
|
|
12194
|
+
reportMigration({
|
|
12195
|
+
from: v2,
|
|
12196
|
+
to: dirs.dir,
|
|
12197
|
+
backup: legacy,
|
|
12198
|
+
tasks: migrated,
|
|
12199
|
+
skipped,
|
|
12200
|
+
workspace: workspaceMoved
|
|
12201
|
+
});
|
|
12202
|
+
return success(null, null, null);
|
|
12203
|
+
}));
|
|
12204
|
+
}
|
|
12205
|
+
adoptLocalStore() {
|
|
12206
|
+
const dirs = this.paths();
|
|
12207
|
+
const marker = path9.join(dirs.v2, "store.json");
|
|
12208
|
+
if (!dirs.shared || !fs8.existsSync(marker))
|
|
12209
|
+
return success(null, null, null);
|
|
12210
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
12211
|
+
if (!fs8.existsSync(marker))
|
|
12212
|
+
return success(null, null, null);
|
|
12213
|
+
let tasks = 0;
|
|
12214
|
+
try {
|
|
12215
|
+
const merge = (from, to) => {
|
|
12216
|
+
if (!fs8.existsSync(to)) {
|
|
12217
|
+
fs8.mkdirSync(path9.dirname(to), { recursive: true });
|
|
12218
|
+
try {
|
|
12219
|
+
fs8.renameSync(from, to);
|
|
12220
|
+
} catch {
|
|
12221
|
+
fs8.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
12222
|
+
fs8.rmSync(from, { recursive: true, force: true });
|
|
12223
|
+
}
|
|
12224
|
+
return;
|
|
12225
|
+
}
|
|
12226
|
+
if (fs8.lstatSync(from).isDirectory() && fs8.lstatSync(to).isDirectory())
|
|
12227
|
+
for (const name of fs8.readdirSync(from))
|
|
12228
|
+
merge(path9.join(from, name), path9.join(to, name));
|
|
12229
|
+
};
|
|
12230
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
12231
|
+
const from = path9.join(dirs.v2, sub);
|
|
12232
|
+
if (!fs8.existsSync(from))
|
|
12233
|
+
continue;
|
|
12234
|
+
if (sub === "tasks") {
|
|
12235
|
+
for (const name of fs8.readdirSync(from))
|
|
12236
|
+
if (validId(name) && !fs8.existsSync(path9.join(dirs.dir, sub, name)))
|
|
12237
|
+
tasks += 1;
|
|
12238
|
+
}
|
|
12239
|
+
merge(from, path9.join(dirs.dir, sub));
|
|
12240
|
+
}
|
|
12241
|
+
const ledger = path9.join(dirs.v2, "ledger");
|
|
12242
|
+
if (fs8.existsSync(ledger) && !fs8.existsSync(path9.join(dirs.dir, "ledger")))
|
|
12243
|
+
merge(ledger, path9.join(dirs.dir, "ledger"));
|
|
12244
|
+
fs8.rmSync(path9.join(dirs.v2, "task-index.json"), { force: true });
|
|
12245
|
+
fs8.renameSync(marker, path9.join(dirs.v2, "store.moved.json"));
|
|
12246
|
+
} catch (error) {
|
|
12247
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
12248
|
+
path: dirs.v2
|
|
12249
|
+
});
|
|
12250
|
+
}
|
|
12251
|
+
reportMigration({
|
|
12252
|
+
from: dirs.v2,
|
|
12253
|
+
to: dirs.dir,
|
|
12254
|
+
backup: dirs.v2,
|
|
12255
|
+
tasks,
|
|
12256
|
+
skipped: 0,
|
|
12257
|
+
workspace: false
|
|
12258
|
+
});
|
|
12259
|
+
return success(null, null, null);
|
|
12260
|
+
});
|
|
12261
|
+
}
|
|
12262
|
+
readV2Workspace(file) {
|
|
12263
|
+
let bytes;
|
|
12264
|
+
try {
|
|
12265
|
+
bytes = fs8.readFileSync(file, "utf8");
|
|
12266
|
+
} catch {
|
|
12267
|
+
return null;
|
|
12268
|
+
}
|
|
12269
|
+
try {
|
|
12270
|
+
const value = JSON.parse(bytes);
|
|
12271
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
12272
|
+
return TOMBSTONE;
|
|
12273
|
+
} catch {}
|
|
12274
|
+
return { bytes };
|
|
12275
|
+
}
|
|
12276
|
+
writeTombstone(v2, workspaceId) {
|
|
12277
|
+
const dirs = this.paths();
|
|
12278
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12279
|
+
const marker = {
|
|
12280
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12281
|
+
id: workspaceId ?? newId(),
|
|
12282
|
+
revision: newRevision(),
|
|
12283
|
+
root: path9.dirname(v2),
|
|
12284
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12285
|
+
writer: null,
|
|
12286
|
+
store: {
|
|
12287
|
+
format: "workit-store",
|
|
12288
|
+
version: STORE_FORMAT,
|
|
12289
|
+
path: dirs.dir,
|
|
12290
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
12291
|
+
},
|
|
12292
|
+
critical: ["store"]
|
|
12293
|
+
};
|
|
12294
|
+
fs8.mkdirSync(v2, { recursive: true });
|
|
12295
|
+
const ignore = path9.join(v2, ".gitignore");
|
|
12296
|
+
if (!fs8.existsSync(ignore))
|
|
12297
|
+
fs8.writeFileSync(ignore, `*
|
|
12298
|
+
`);
|
|
12299
|
+
replaceFile(path9.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
12300
|
+
`, true);
|
|
12301
|
+
}
|
|
12302
|
+
stubFor(taskId) {
|
|
12303
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
12304
|
+
return `${JSON.stringify({
|
|
12305
|
+
schemaVersion: SCHEMA_VERSION,
|
|
12306
|
+
id: taskId,
|
|
12307
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
12308
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
12309
|
+
critical: ["store"]
|
|
12310
|
+
})}
|
|
12311
|
+
`;
|
|
12312
|
+
}
|
|
12313
|
+
isStub(file) {
|
|
12314
|
+
try {
|
|
12315
|
+
const value = JSON.parse(fs8.readFileSync(file, "utf8"));
|
|
12316
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
12317
|
+
} catch {
|
|
12318
|
+
return false;
|
|
12319
|
+
}
|
|
12320
|
+
}
|
|
12321
|
+
writeTombstoneIfMissing() {
|
|
12322
|
+
try {
|
|
12323
|
+
const v2 = path9.join(this.root, ".workit");
|
|
12324
|
+
if (fs8.existsSync(path9.join(v2, "workspace.json")))
|
|
12325
|
+
return;
|
|
12326
|
+
let id = null;
|
|
12327
|
+
try {
|
|
12328
|
+
id = JSON.parse(fs8.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
12329
|
+
} catch {}
|
|
12330
|
+
this.writeTombstone(v2, id);
|
|
12331
|
+
} catch {}
|
|
12332
|
+
}
|
|
12333
|
+
initializeStorage() {
|
|
12334
|
+
const dirs = this.paths();
|
|
12335
|
+
retryTransient(() => fs8.mkdirSync(dirs.checkout, { recursive: true }));
|
|
12336
|
+
retryTransient(() => fs8.mkdirSync(dirs.tasks, { recursive: true }));
|
|
12337
|
+
if (!fs8.existsSync(dirs.marker))
|
|
12338
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
12339
|
+
`, false);
|
|
12340
|
+
if (!dirs.shared) {
|
|
12341
|
+
const ignore = path9.join(dirs.dir, ".gitignore");
|
|
12342
|
+
let current = null;
|
|
12343
|
+
try {
|
|
12344
|
+
current = fs8.readFileSync(ignore, "utf8");
|
|
12345
|
+
} catch {}
|
|
12346
|
+
if (current !== `*
|
|
12347
|
+
`)
|
|
12348
|
+
retryTransient(() => fs8.writeFileSync(ignore, `*
|
|
12349
|
+
`));
|
|
12350
|
+
}
|
|
12351
|
+
this.writeTombstoneIfMissing();
|
|
12352
|
+
}
|
|
12353
|
+
lockOptions(lockPath) {
|
|
12354
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
12355
|
+
return {
|
|
12356
|
+
lockPath,
|
|
12357
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
12358
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
12359
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
12360
|
+
staleRecovery: "remove-if-unchanged",
|
|
12361
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
12362
|
+
let ageMs = null;
|
|
12363
|
+
try {
|
|
12364
|
+
ageMs = nowMs - fs8.lstatSync(lockPath).mtimeMs;
|
|
12365
|
+
} catch {}
|
|
12366
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
12367
|
+
},
|
|
12368
|
+
shouldRemoveStaleLock: () => true,
|
|
12369
|
+
parsePayload: parseMetadataLockOrNull,
|
|
12370
|
+
payload: () => ({
|
|
12371
|
+
pid: process.pid,
|
|
12372
|
+
processStart: processStartOf(process.pid),
|
|
12373
|
+
host: localLockHost(),
|
|
12374
|
+
nonce: randomUUID3()
|
|
12375
|
+
})
|
|
10947
12376
|
};
|
|
10948
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
10949
12377
|
}
|
|
10950
|
-
|
|
12378
|
+
acquire(lockPath, options) {
|
|
12379
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
12380
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
12381
|
+
while (true) {
|
|
12382
|
+
try {
|
|
12383
|
+
return acquireFileLockSync(lockPath, {
|
|
12384
|
+
...options,
|
|
12385
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
12386
|
+
});
|
|
12387
|
+
} catch (error) {
|
|
12388
|
+
const code = error?.code;
|
|
12389
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
12390
|
+
throw error;
|
|
12391
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12392
|
+
throw error;
|
|
12393
|
+
if (code !== "file_lock_stale")
|
|
12394
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
12395
|
+
}
|
|
12396
|
+
}
|
|
12397
|
+
}
|
|
12398
|
+
withLock(lockPath, operation) {
|
|
10951
12399
|
try {
|
|
10952
|
-
this.
|
|
12400
|
+
this.initializeStorage();
|
|
12401
|
+
fs8.mkdirSync(path9.dirname(lockPath), { recursive: true });
|
|
10953
12402
|
} catch (error) {
|
|
10954
12403
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
10955
|
-
path: this.
|
|
12404
|
+
path: this.paths().dir
|
|
10956
12405
|
});
|
|
10957
12406
|
}
|
|
10958
12407
|
let handle;
|
|
10959
|
-
let result = failure2("storage_error", "metadata lock operation did not produce a result");
|
|
10960
12408
|
try {
|
|
10961
|
-
handle = this.
|
|
12409
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
10962
12410
|
} catch (error) {
|
|
10963
|
-
|
|
12411
|
+
return this.lockFailure(lockPath, error);
|
|
10964
12412
|
}
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
result = operation(handle);
|
|
10976
|
-
} catch (error) {
|
|
10977
|
-
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
10978
|
-
}
|
|
12413
|
+
hold(lockPath);
|
|
12414
|
+
let result;
|
|
12415
|
+
try {
|
|
12416
|
+
if (!handle.verifyStillHeld())
|
|
12417
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
12418
|
+
else {
|
|
12419
|
+
try {
|
|
12420
|
+
result = operation();
|
|
12421
|
+
} catch (error) {
|
|
12422
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
10979
12423
|
}
|
|
10980
|
-
} catch (error) {
|
|
10981
|
-
result = this.lockFailure(error);
|
|
10982
12424
|
}
|
|
12425
|
+
} catch (error) {
|
|
12426
|
+
result = this.lockFailure(lockPath, error);
|
|
10983
12427
|
}
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
10992
|
-
}
|
|
12428
|
+
drop(lockPath);
|
|
12429
|
+
try {
|
|
12430
|
+
retryTransient(() => handle.release());
|
|
12431
|
+
} catch (error) {
|
|
12432
|
+
const released = this.lockFailure(lockPath, error);
|
|
12433
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
12434
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
10993
12435
|
}
|
|
10994
12436
|
return result;
|
|
10995
12437
|
}
|
|
10996
|
-
lockDirWritable() {
|
|
10997
|
-
const probe = `${
|
|
12438
|
+
lockDirWritable(lockPath) {
|
|
12439
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
10998
12440
|
try {
|
|
10999
|
-
|
|
12441
|
+
fs8.closeSync(fs8.openSync(probe, "wx", 384));
|
|
11000
12442
|
} catch {
|
|
11001
12443
|
return false;
|
|
11002
12444
|
}
|
|
11003
12445
|
try {
|
|
11004
|
-
|
|
12446
|
+
fs8.rmSync(probe, { force: true });
|
|
11005
12447
|
} catch {}
|
|
11006
12448
|
return true;
|
|
11007
12449
|
}
|
|
11008
|
-
lockHolderPresent() {
|
|
12450
|
+
lockHolderPresent(lockPath) {
|
|
11009
12451
|
try {
|
|
11010
|
-
|
|
12452
|
+
fs8.lstatSync(lockPath);
|
|
11011
12453
|
return true;
|
|
11012
12454
|
} catch (error) {
|
|
11013
12455
|
return error?.code !== "ENOENT";
|
|
11014
12456
|
}
|
|
11015
12457
|
}
|
|
11016
|
-
lockFailure(
|
|
12458
|
+
lockFailure(lockPath, error) {
|
|
11017
12459
|
const value = error;
|
|
11018
12460
|
const code = typeof value?.code === "string" ? value.code : "";
|
|
11019
|
-
if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
11020
|
-
return failure2("storage_error", `cannot create the
|
|
11021
|
-
path:
|
|
11022
|
-
guidance: `Check permissions and the read-only attribute on ${
|
|
12461
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
12462
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
12463
|
+
path: lockPath,
|
|
12464
|
+
guidance: `Check permissions and the read-only attribute on ${path9.dirname(lockPath)}.`
|
|
11023
12465
|
});
|
|
11024
12466
|
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
11025
12467
|
let lock = null;
|
|
11026
12468
|
try {
|
|
11027
|
-
lock = parseMetadataLockOrNull(
|
|
12469
|
+
lock = parseMetadataLockOrNull(fs8.readFileSync(lockPath, "utf8"));
|
|
11028
12470
|
} catch {}
|
|
11029
12471
|
if (lock?.externalAction)
|
|
11030
12472
|
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
11031
12473
|
outcome: "not_started",
|
|
11032
|
-
path:
|
|
11033
|
-
});
|
|
11034
|
-
if (contention === "busy")
|
|
11035
|
-
return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
11036
|
-
outcome: "not_started",
|
|
11037
|
-
path: this.lockPath,
|
|
11038
|
-
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
12474
|
+
path: lockPath
|
|
11039
12475
|
});
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
initializeMutationStorage() {
|
|
11045
|
-
retryTransient(() => fs6.mkdirSync(this.tasksDir, { recursive: true }));
|
|
11046
|
-
retryTransient(() => fs6.mkdirSync(this.recoveryDir, { recursive: true }));
|
|
11047
|
-
let current = null;
|
|
11048
|
-
try {
|
|
11049
|
-
current = retryTransient(() => fs6.readFileSync(this.gitignorePath, "utf8"));
|
|
11050
|
-
} catch {}
|
|
11051
|
-
if (current !== `*
|
|
11052
|
-
`)
|
|
11053
|
-
retryTransient(() => fs6.writeFileSync(this.gitignorePath, `*
|
|
11054
|
-
`));
|
|
11055
|
-
}
|
|
11056
|
-
replaceSnapshot(file, value, previous) {
|
|
11057
|
-
let temporary;
|
|
11058
|
-
try {
|
|
11059
|
-
if (Buffer.isBuffer(previous))
|
|
11060
|
-
this.saveRecovery(file, previous);
|
|
11061
|
-
else if (previous !== null && typeof previous === "object")
|
|
11062
|
-
this.saveRecovery(file, jsonBytes(previous));
|
|
11063
|
-
else if (typeof previous === "string")
|
|
11064
|
-
this.saveRecovery(file, previous);
|
|
11065
|
-
temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11066
|
-
const fd = fs6.openSync(temporary, "wx", 384);
|
|
11067
|
-
try {
|
|
11068
|
-
fs6.writeSync(fd, jsonBytes(value));
|
|
11069
|
-
fs6.fsyncSync(fd);
|
|
11070
|
-
} finally {
|
|
11071
|
-
fs6.closeSync(fd);
|
|
11072
|
-
}
|
|
11073
|
-
retryTransient(() => fs6.renameSync(temporary, file));
|
|
11074
|
-
temporary = undefined;
|
|
11075
|
-
this.fsyncDirectory(path7.dirname(file));
|
|
11076
|
-
if (path7.dirname(file) === this.tasksDir)
|
|
11077
|
-
this.indexTaskWrite(file, value);
|
|
11078
|
-
return success(null, null, value);
|
|
11079
|
-
} catch (error) {
|
|
11080
|
-
return failure2("storage_error", `snapshot replacement failed: ${String(error)}`, {
|
|
11081
|
-
path: file
|
|
12476
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
12477
|
+
outcome: "not_started",
|
|
12478
|
+
path: lockPath,
|
|
12479
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
11082
12480
|
});
|
|
11083
|
-
} finally {
|
|
11084
|
-
if (temporary)
|
|
11085
|
-
try {
|
|
11086
|
-
fs6.unlinkSync(temporary);
|
|
11087
|
-
} catch {}
|
|
11088
|
-
}
|
|
11089
|
-
}
|
|
11090
|
-
readIndex(workspaceId) {
|
|
11091
|
-
try {
|
|
11092
|
-
const value = JSON.parse(fs6.readFileSync(this.indexPath, "utf8"));
|
|
11093
|
-
if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
|
|
11094
|
-
return {};
|
|
11095
|
-
const tasks = {};
|
|
11096
|
-
for (const [id, entry] of Object.entries(value.tasks))
|
|
11097
|
-
if (isObject2(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && Array.isArray(entry.sessions) && isObject2(entry.progress) && isObject2(entry.source))
|
|
11098
|
-
tasks[id] = entry;
|
|
11099
|
-
return tasks;
|
|
11100
|
-
} catch {
|
|
11101
|
-
return {};
|
|
11102
|
-
}
|
|
11103
|
-
}
|
|
11104
|
-
writeIndex(workspaceId, entries) {
|
|
11105
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11106
|
-
try {
|
|
11107
|
-
fs6.writeFileSync(temporary, JSON.stringify({
|
|
11108
|
-
version: INDEX_VERSION,
|
|
11109
|
-
workspaceId,
|
|
11110
|
-
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
11111
|
-
}), { mode: 384, flag: "wx" });
|
|
11112
|
-
retryTransient(() => fs6.renameSync(temporary, this.indexPath));
|
|
11113
|
-
} catch {
|
|
11114
|
-
try {
|
|
11115
|
-
fs6.unlinkSync(temporary);
|
|
11116
|
-
} catch {}
|
|
11117
|
-
}
|
|
11118
|
-
}
|
|
11119
|
-
indexTaskWrite(file, task) {
|
|
11120
|
-
try {
|
|
11121
|
-
const signature = fileSignature(file);
|
|
11122
|
-
if (signature === null)
|
|
11123
|
-
return;
|
|
11124
|
-
const tasks = this.readIndex(task.workspaceId);
|
|
11125
|
-
tasks[task.id] = indexEntry(task, signature);
|
|
11126
|
-
this.writeIndex(task.workspaceId, Object.values(tasks));
|
|
11127
|
-
} catch {}
|
|
11128
|
-
}
|
|
11129
|
-
saveRecovery(file, bytes) {
|
|
11130
|
-
const target = path7.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
11131
|
-
const id = target === "task" ? path7.basename(file, ".json") : "workspace";
|
|
11132
|
-
const destination = path7.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
11133
|
-
if (fs6.existsSync(destination)) {
|
|
11134
|
-
if (digestBytes(retryTransient(() => fs6.readFileSync(destination))) !== digestBytes(bytes))
|
|
11135
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
11136
|
-
const stamp = new Date;
|
|
11137
|
-
retryTransient(() => fs6.utimesSync(destination, stamp, stamp));
|
|
11138
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11139
|
-
return;
|
|
11140
|
-
}
|
|
11141
|
-
let temporary;
|
|
11142
|
-
try {
|
|
11143
|
-
temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11144
|
-
const fd = fs6.openSync(temporary, "wx", 384);
|
|
11145
|
-
try {
|
|
11146
|
-
fs6.writeSync(fd, bytes);
|
|
11147
|
-
fs6.fsyncSync(fd);
|
|
11148
|
-
} finally {
|
|
11149
|
-
fs6.closeSync(fd);
|
|
11150
|
-
}
|
|
11151
|
-
retryTransient(() => fs6.renameSync(temporary, destination));
|
|
11152
|
-
temporary = undefined;
|
|
11153
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
11154
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
11155
|
-
} finally {
|
|
11156
|
-
if (temporary)
|
|
11157
|
-
try {
|
|
11158
|
-
fs6.unlinkSync(temporary);
|
|
11159
|
-
} catch {}
|
|
11160
|
-
}
|
|
11161
|
-
}
|
|
11162
|
-
pruneRecovery(prefix, keep) {
|
|
11163
|
-
try {
|
|
11164
|
-
const copies = this.recoveryCopies(prefix);
|
|
11165
|
-
const surplus = this.surplusCopies(copies, path7.basename(keep));
|
|
11166
|
-
for (const copy of surplus)
|
|
11167
|
-
fs6.rmSync(copy.path, { force: true });
|
|
11168
|
-
} catch {}
|
|
11169
|
-
}
|
|
11170
|
-
recoveryCopies(prefix = "") {
|
|
11171
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
11172
|
-
return [];
|
|
11173
|
-
const copies = [];
|
|
11174
|
-
for (const name of fs6.readdirSync(this.recoveryDir)) {
|
|
11175
|
-
if (!name.startsWith(prefix))
|
|
11176
|
-
continue;
|
|
11177
|
-
const match = RECOVERY_NAME.exec(name);
|
|
11178
|
-
if (!match)
|
|
11179
|
-
continue;
|
|
11180
|
-
const file = path7.join(this.recoveryDir, name);
|
|
11181
|
-
try {
|
|
11182
|
-
const stat = fs6.lstatSync(file, { bigint: true });
|
|
11183
|
-
if (!stat.isFile())
|
|
11184
|
-
continue;
|
|
11185
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
11186
|
-
} catch {}
|
|
11187
|
-
}
|
|
11188
|
-
return copies;
|
|
11189
|
-
}
|
|
11190
|
-
surplusCopies(copies, keep) {
|
|
11191
|
-
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);
|
|
11192
|
-
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
11193
|
-
}
|
|
11194
|
-
collectGarbage(options = {}) {
|
|
11195
|
-
const dryRun = options.dryRun === true;
|
|
11196
|
-
const report = {
|
|
11197
|
-
dryRun,
|
|
11198
|
-
recovery: { removed: 0, removedBytes: 0, kept: 0 },
|
|
11199
|
-
temporary: { removed: 0 },
|
|
11200
|
-
candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
|
|
11201
|
-
};
|
|
11202
|
-
if (!fs6.existsSync(this.workitDir))
|
|
11203
|
-
return success(null, null, report);
|
|
11204
|
-
const sweep = () => {
|
|
11205
|
-
try {
|
|
11206
|
-
const groups = new Map;
|
|
11207
|
-
for (const copy of this.recoveryCopies())
|
|
11208
|
-
groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
|
|
11209
|
-
for (const copies of groups.values()) {
|
|
11210
|
-
const surplus = this.surplusCopies(copies);
|
|
11211
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
11212
|
-
for (const copy of surplus) {
|
|
11213
|
-
report.recovery.removed += 1;
|
|
11214
|
-
try {
|
|
11215
|
-
report.recovery.removedBytes += fs6.statSync(copy.path).size;
|
|
11216
|
-
} catch {}
|
|
11217
|
-
if (!dryRun)
|
|
11218
|
-
fs6.rmSync(copy.path, { force: true });
|
|
11219
|
-
}
|
|
11220
|
-
}
|
|
11221
|
-
const nowMs = Date.now();
|
|
11222
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
11223
|
-
if (!fs6.existsSync(directory))
|
|
11224
|
-
continue;
|
|
11225
|
-
for (const name of fs6.readdirSync(directory)) {
|
|
11226
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
11227
|
-
continue;
|
|
11228
|
-
const file = path7.join(directory, name);
|
|
11229
|
-
const stat = fs6.lstatSync(file);
|
|
11230
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
11231
|
-
continue;
|
|
11232
|
-
report.temporary.removed += 1;
|
|
11233
|
-
if (!dryRun)
|
|
11234
|
-
fs6.rmSync(file, { force: true });
|
|
11235
|
-
}
|
|
11236
|
-
}
|
|
11237
|
-
return success(null, null, null);
|
|
11238
|
-
} catch (error) {
|
|
11239
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
11240
|
-
path: this.recoveryDir
|
|
11241
|
-
});
|
|
11242
|
-
}
|
|
11243
|
-
};
|
|
11244
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
11245
|
-
if (!pruned.ok)
|
|
11246
|
-
return pruned;
|
|
11247
|
-
const tasks = this.listTasks();
|
|
11248
|
-
if (!tasks.ok)
|
|
11249
|
-
return tasks;
|
|
11250
|
-
for (const task of tasks.data) {
|
|
11251
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
11252
|
-
const removed = task.candidates.length - deduped.length;
|
|
11253
|
-
if (removed === 0)
|
|
11254
|
-
continue;
|
|
11255
|
-
if (task.status === "active") {
|
|
11256
|
-
report.candidates.skippedActive.push(task.id);
|
|
11257
|
-
continue;
|
|
11258
|
-
}
|
|
11259
|
-
if (task.status === "closed") {
|
|
11260
|
-
report.candidates.skippedClosed.push(task.id);
|
|
11261
|
-
continue;
|
|
11262
|
-
}
|
|
11263
|
-
if (!dryRun) {
|
|
11264
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
11265
|
-
...current,
|
|
11266
|
-
candidates: dedupeCandidates(current.candidates)
|
|
11267
|
-
}));
|
|
11268
|
-
if (!written.ok) {
|
|
11269
|
-
report.candidates.failed.push(task.id);
|
|
11270
|
-
continue;
|
|
11271
|
-
}
|
|
11272
|
-
}
|
|
11273
|
-
report.candidates.removed += removed;
|
|
11274
|
-
report.candidates.tasks.push(task.id);
|
|
11275
|
-
}
|
|
11276
|
-
return success(null, null, report);
|
|
11277
|
-
}
|
|
11278
|
-
findRecovery(target, taskId, digest, workspace) {
|
|
11279
|
-
if (!fs6.existsSync(this.recoveryDir))
|
|
11280
|
-
return null;
|
|
11281
|
-
const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
|
|
11282
|
-
if (target === "workspace" && !workspaceId)
|
|
11283
|
-
return null;
|
|
11284
|
-
const candidates = this.recoveryCandidates();
|
|
11285
|
-
if (!candidates.ok)
|
|
11286
|
-
return null;
|
|
11287
|
-
for (const candidate of candidates.data) {
|
|
11288
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
11289
|
-
continue;
|
|
11290
|
-
try {
|
|
11291
|
-
if (!fs6.lstatSync(candidate.path).isFile())
|
|
11292
|
-
continue;
|
|
11293
|
-
const bytes = fs6.readFileSync(candidate.path);
|
|
11294
|
-
if (digestBytes(bytes) !== digest)
|
|
11295
|
-
continue;
|
|
11296
|
-
if (target === "workspace") {
|
|
11297
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
11298
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
11299
|
-
return bytes;
|
|
11300
|
-
} else {
|
|
11301
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
11302
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
11303
|
-
return bytes;
|
|
11304
|
-
}
|
|
11305
|
-
} catch {}
|
|
11306
|
-
}
|
|
11307
|
-
return null;
|
|
11308
|
-
}
|
|
11309
|
-
fsyncDirectory(directory) {
|
|
11310
|
-
try {
|
|
11311
|
-
const fd = fs6.openSync(directory, "r");
|
|
11312
|
-
try {
|
|
11313
|
-
fs6.fsyncSync(fd);
|
|
11314
|
-
} finally {
|
|
11315
|
-
fs6.closeSync(fd);
|
|
11316
|
-
}
|
|
11317
|
-
} catch (error) {
|
|
11318
|
-
if (process.platform !== "win32")
|
|
11319
|
-
throw error;
|
|
11320
|
-
}
|
|
11321
|
-
}
|
|
11322
|
-
trustedWorkspaceId() {
|
|
11323
|
-
if (!fs6.existsSync(this.tasksDir))
|
|
11324
|
-
return null;
|
|
11325
|
-
const ids = new Set;
|
|
11326
|
-
try {
|
|
11327
|
-
for (const name of fs6.readdirSync(this.tasksDir)) {
|
|
11328
|
-
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
11329
|
-
return null;
|
|
11330
|
-
const parsed = this.parseBytes(fs6.readFileSync(path7.join(this.tasksDir, name)), taskRecordSchema);
|
|
11331
|
-
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
11332
|
-
return null;
|
|
11333
|
-
ids.add(parsed.data.workspaceId);
|
|
11334
|
-
}
|
|
11335
|
-
} catch {
|
|
11336
|
-
return null;
|
|
11337
|
-
}
|
|
11338
|
-
return ids.size === 1 ? [...ids][0] : null;
|
|
11339
|
-
}
|
|
11340
|
-
snapshotBytes(file) {
|
|
11341
|
-
try {
|
|
11342
|
-
return retryTransient(() => fs6.readFileSync(file));
|
|
11343
|
-
} catch {
|
|
11344
|
-
return null;
|
|
11345
|
-
}
|
|
11346
|
-
}
|
|
11347
|
-
readRecord(file, schema) {
|
|
11348
|
-
try {
|
|
11349
|
-
const bytes = retryTransient(() => fs6.readFileSync(file, "utf8"));
|
|
11350
|
-
return { exists: true, result: this.parseBytes(bytes, schema) };
|
|
11351
|
-
} catch (error) {
|
|
11352
|
-
if (error?.code === "ENOENT")
|
|
11353
|
-
return { exists: false, result: success(null, null, null) };
|
|
11354
|
-
return {
|
|
11355
|
-
exists: true,
|
|
11356
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
11357
|
-
path: file
|
|
11358
|
-
})
|
|
11359
|
-
};
|
|
11360
|
-
}
|
|
11361
|
-
}
|
|
11362
|
-
parseBytes(bytes, schema, mode = "read") {
|
|
11363
|
-
let value;
|
|
11364
|
-
try {
|
|
11365
|
-
value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
|
|
11366
|
-
} catch {
|
|
11367
|
-
return failure2("recovery_required", "snapshot JSON is corrupt");
|
|
11368
12481
|
}
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
const parsed = parseStoredRecord(schema, value);
|
|
11372
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
11373
|
-
return success(null, null, parsed.data);
|
|
11374
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
11375
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
11376
|
-
if (parsed.success)
|
|
11377
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
11378
|
-
if (parsed.critical.length > 0)
|
|
11379
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
11380
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
11381
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
11382
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
11383
|
-
}
|
|
11384
|
-
conflict(expected, actual) {
|
|
11385
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
11386
|
-
expectedRevision: expected,
|
|
11387
|
-
actualRevision: actual
|
|
11388
|
-
});
|
|
11389
|
-
}
|
|
11390
|
-
workspaceConflict(expected, actual) {
|
|
11391
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
11392
|
-
}
|
|
11393
|
-
taskPath(taskId) {
|
|
11394
|
-
return path7.join(this.tasksDir, `${taskId}.json`);
|
|
11395
|
-
}
|
|
11396
|
-
get workitDir() {
|
|
11397
|
-
return path7.join(this.root, ".workit");
|
|
11398
|
-
}
|
|
11399
|
-
get tasksDir() {
|
|
11400
|
-
return path7.join(this.workitDir, "tasks");
|
|
11401
|
-
}
|
|
11402
|
-
get recoveryDir() {
|
|
11403
|
-
return path7.join(this.workitDir, "recovery");
|
|
11404
|
-
}
|
|
11405
|
-
get workspacePath() {
|
|
11406
|
-
return path7.join(this.workitDir, "workspace.json");
|
|
11407
|
-
}
|
|
11408
|
-
get indexPath() {
|
|
11409
|
-
return path7.join(this.workitDir, "index.json");
|
|
11410
|
-
}
|
|
11411
|
-
get lockPath() {
|
|
11412
|
-
return path7.join(this.workitDir, "metadata.lock");
|
|
11413
|
-
}
|
|
11414
|
-
get gitignorePath() {
|
|
11415
|
-
return path7.join(this.workitDir, ".gitignore");
|
|
12482
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
12483
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
11416
12484
|
}
|
|
11417
12485
|
}
|
|
11418
12486
|
|
|
@@ -11489,7 +12557,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
|
|
|
11489
12557
|
var digestOf = (configDirPath) => {
|
|
11490
12558
|
try {
|
|
11491
12559
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
11492
|
-
return
|
|
12560
|
+
return createHash7("sha256").update(raw).digest("hex");
|
|
11493
12561
|
} catch {
|
|
11494
12562
|
return null;
|
|
11495
12563
|
}
|
|
@@ -11533,7 +12601,7 @@ var targetOf = (root, descriptor) => {
|
|
|
11533
12601
|
const cwd = parsed?.payload?.cwd;
|
|
11534
12602
|
if (cwd === undefined)
|
|
11535
12603
|
return root;
|
|
11536
|
-
if (typeof cwd !== "string" || !
|
|
12604
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
11537
12605
|
return null;
|
|
11538
12606
|
return realpathSync3(cwd);
|
|
11539
12607
|
} catch {
|
|
@@ -11758,11 +12826,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
|
11758
12826
|
return [];
|
|
11759
12827
|
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);
|
|
11760
12828
|
}
|
|
11761
|
-
var
|
|
12829
|
+
var digestBytes = (bytes) => createHash8("sha256").update(bytes).digest("hex");
|
|
11762
12830
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
11763
12831
|
let root;
|
|
11764
12832
|
try {
|
|
11765
|
-
root =
|
|
12833
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
11766
12834
|
} catch {
|
|
11767
12835
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
11768
12836
|
}
|
|
@@ -11771,23 +12839,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11771
12839
|
continue;
|
|
11772
12840
|
if (!reference.digest)
|
|
11773
12841
|
return failure2("invalid_input", "document references require a byte digest");
|
|
11774
|
-
const target =
|
|
11775
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
12842
|
+
const target = path11.resolve(root, reference.path);
|
|
12843
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
11776
12844
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11777
12845
|
fields: [
|
|
11778
12846
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
11779
12847
|
]
|
|
11780
12848
|
});
|
|
11781
12849
|
try {
|
|
11782
|
-
const relative =
|
|
12850
|
+
const relative = path11.relative(root, target);
|
|
11783
12851
|
let current = root;
|
|
11784
|
-
for (const segment of relative.split(
|
|
12852
|
+
for (const segment of relative.split(path11.sep)) {
|
|
11785
12853
|
if (!segment)
|
|
11786
12854
|
continue;
|
|
11787
|
-
current =
|
|
11788
|
-
const stat =
|
|
11789
|
-
const real =
|
|
11790
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
12855
|
+
current = path11.join(current, segment);
|
|
12856
|
+
const stat = fs9.lstatSync(current);
|
|
12857
|
+
const real = fs9.realpathSync(current);
|
|
12858
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
11791
12859
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
11792
12860
|
fields: [
|
|
11793
12861
|
{
|
|
@@ -11803,7 +12871,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
11803
12871
|
if (current !== target && !stat.isDirectory())
|
|
11804
12872
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
11805
12873
|
}
|
|
11806
|
-
if (
|
|
12874
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
11807
12875
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
11808
12876
|
} catch {
|
|
11809
12877
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -12494,15 +13562,15 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
12494
13562
|
}
|
|
12495
13563
|
|
|
12496
13564
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12497
|
-
import { createHash as
|
|
12498
|
-
import * as
|
|
12499
|
-
import { spawnSync as
|
|
12500
|
-
import
|
|
13565
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
13566
|
+
import * as fs12 from "node:fs";
|
|
13567
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
13568
|
+
import path14 from "node:path";
|
|
12501
13569
|
|
|
12502
13570
|
// packages/workit-core/src/check-config.ts
|
|
12503
|
-
import { spawnSync as
|
|
12504
|
-
import
|
|
12505
|
-
import
|
|
13571
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
13572
|
+
import fs10 from "node:fs";
|
|
13573
|
+
import path12 from "node:path";
|
|
12506
13574
|
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
12507
13575
|
var CHECKS_FILE = "workit.checks.json";
|
|
12508
13576
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -12544,7 +13612,7 @@ function splitCommand(command) {
|
|
|
12544
13612
|
return argv.length ? argv : new Error("empty command");
|
|
12545
13613
|
}
|
|
12546
13614
|
var git = (cwd, args) => {
|
|
12547
|
-
const run =
|
|
13615
|
+
const run = spawnSync4("git", args, {
|
|
12548
13616
|
cwd,
|
|
12549
13617
|
encoding: "utf8",
|
|
12550
13618
|
timeout: 20000,
|
|
@@ -12555,10 +13623,10 @@ var git = (cwd, args) => {
|
|
|
12555
13623
|
return value || null;
|
|
12556
13624
|
};
|
|
12557
13625
|
function checkRoot(cwd) {
|
|
12558
|
-
return
|
|
13626
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12559
13627
|
}
|
|
12560
13628
|
var packageManager = (root) => {
|
|
12561
|
-
const has = (file) =>
|
|
13629
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
12562
13630
|
if (has("bun.lock") || has("bun.lockb"))
|
|
12563
13631
|
return "bun";
|
|
12564
13632
|
if (has("pnpm-lock.yaml"))
|
|
@@ -12578,7 +13646,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
|
12578
13646
|
function detectedChecks(root) {
|
|
12579
13647
|
const read = (file) => {
|
|
12580
13648
|
try {
|
|
12581
|
-
return
|
|
13649
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
12582
13650
|
} catch {
|
|
12583
13651
|
return null;
|
|
12584
13652
|
}
|
|
@@ -12617,7 +13685,7 @@ function detectedChecks(root) {
|
|
|
12617
13685
|
var GATES = ["testing", "verification"];
|
|
12618
13686
|
function loadCheckConfig(cwd) {
|
|
12619
13687
|
const root = checkRoot(cwd);
|
|
12620
|
-
const file =
|
|
13688
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
12621
13689
|
const none = (error = null) => ({
|
|
12622
13690
|
source: error ? CHECKS_FILE : "none",
|
|
12623
13691
|
root,
|
|
@@ -12627,7 +13695,7 @@ function loadCheckConfig(cwd) {
|
|
|
12627
13695
|
});
|
|
12628
13696
|
let text = null;
|
|
12629
13697
|
try {
|
|
12630
|
-
text =
|
|
13698
|
+
text = fs10.readFileSync(file, "utf8");
|
|
12631
13699
|
} catch (error) {
|
|
12632
13700
|
if (error.code !== "ENOENT")
|
|
12633
13701
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -12690,10 +13758,10 @@ function workitAvailable(env = process.env) {
|
|
|
12690
13758
|
return workitOnPath;
|
|
12691
13759
|
const windows = process.platform === "win32";
|
|
12692
13760
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12693
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
13761
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
12694
13762
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12695
13763
|
try {
|
|
12696
|
-
return
|
|
13764
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
12697
13765
|
} catch {
|
|
12698
13766
|
return false;
|
|
12699
13767
|
}
|
|
@@ -12705,15 +13773,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
12705
13773
|
}
|
|
12706
13774
|
|
|
12707
13775
|
// packages/workit-core/src/git/rev.ts
|
|
12708
|
-
import { spawnSync as
|
|
12709
|
-
import { createHash as
|
|
12710
|
-
import
|
|
13776
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
13777
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
13778
|
+
import fs11 from "node:fs";
|
|
12711
13779
|
import os2 from "node:os";
|
|
12712
|
-
import
|
|
13780
|
+
import path13 from "node:path";
|
|
12713
13781
|
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
12714
13782
|
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
12715
13783
|
var git2 = (cwd, args, options = {}) => {
|
|
12716
|
-
const result =
|
|
13784
|
+
const result = spawnSync5("git", args, {
|
|
12717
13785
|
cwd,
|
|
12718
13786
|
encoding: "utf8",
|
|
12719
13787
|
env: options.env ?? process.env,
|
|
@@ -12743,10 +13811,10 @@ function headSha(cwd) {
|
|
|
12743
13811
|
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
12744
13812
|
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
12745
13813
|
var seedIndex = (from, to) => {
|
|
12746
|
-
|
|
12747
|
-
const { atimeNs, mtimeNs } =
|
|
13814
|
+
fs11.copyFileSync(from, to);
|
|
13815
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
12748
13816
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12749
|
-
|
|
13817
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12750
13818
|
};
|
|
12751
13819
|
function worktreeTree(cwd, options = {}) {
|
|
12752
13820
|
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
@@ -12755,10 +13823,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12755
13823
|
return null;
|
|
12756
13824
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12757
13825
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12758
|
-
const scratch =
|
|
13826
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
12759
13827
|
try {
|
|
12760
|
-
const tempIndex =
|
|
12761
|
-
if (
|
|
13828
|
+
const tempIndex = path13.join(scratch, "index");
|
|
13829
|
+
if (fs11.existsSync(indexPath))
|
|
12762
13830
|
seedIndex(indexPath, tempIndex);
|
|
12763
13831
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12764
13832
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -12770,7 +13838,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12770
13838
|
const skipped = [];
|
|
12771
13839
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12772
13840
|
try {
|
|
12773
|
-
const stat =
|
|
13841
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
12774
13842
|
if (stat.isFile() && stat.size > cap)
|
|
12775
13843
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12776
13844
|
} catch {}
|
|
@@ -12787,10 +13855,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
12787
13855
|
if (!tree)
|
|
12788
13856
|
return null;
|
|
12789
13857
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12790
|
-
const key = skipped.length ? `sha256:${
|
|
13858
|
+
const key = skipped.length ? `sha256:${createHash9("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12791
13859
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12792
13860
|
} finally {
|
|
12793
|
-
|
|
13861
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
12794
13862
|
}
|
|
12795
13863
|
}
|
|
12796
13864
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -12810,16 +13878,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12810
13878
|
});
|
|
12811
13879
|
if (!status.ok)
|
|
12812
13880
|
return null;
|
|
12813
|
-
const hash =
|
|
13881
|
+
const hash = createHash9("sha256");
|
|
12814
13882
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12815
13883
|
for (const entry of status.stdout.split("\x00")) {
|
|
12816
13884
|
if (entry.length < 4)
|
|
12817
13885
|
continue;
|
|
12818
13886
|
const file = entry.slice(3);
|
|
12819
|
-
const absolute =
|
|
13887
|
+
const absolute = path13.join(top, file);
|
|
12820
13888
|
let stat = "-";
|
|
12821
13889
|
try {
|
|
12822
|
-
const info =
|
|
13890
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
12823
13891
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12824
13892
|
if (info.isDirectory())
|
|
12825
13893
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -12830,15 +13898,15 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
12830
13898
|
}
|
|
12831
13899
|
|
|
12832
13900
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12833
|
-
var
|
|
12834
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
12835
|
-
var relative = (root, candidate) =>
|
|
13901
|
+
var digestBytes2 = (value) => createHash10("sha256").update(value).digest("hex");
|
|
13902
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
|
|
13903
|
+
var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
|
|
12836
13904
|
var canonicalPath = (value) => {
|
|
12837
13905
|
if (typeof value !== "string" || !value)
|
|
12838
13906
|
return null;
|
|
12839
13907
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12840
13908
|
return null;
|
|
12841
|
-
const normalized =
|
|
13909
|
+
const normalized = path14.posix.normalize(value);
|
|
12842
13910
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12843
13911
|
return null;
|
|
12844
13912
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -12874,7 +13942,7 @@ var compareCodeUnits3 = (left, right) => {
|
|
|
12874
13942
|
return left.length - right.length;
|
|
12875
13943
|
};
|
|
12876
13944
|
var gitPaths = (root) => {
|
|
12877
|
-
const result =
|
|
13945
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12878
13946
|
cwd: root,
|
|
12879
13947
|
encoding: "buffer"
|
|
12880
13948
|
});
|
|
@@ -12886,21 +13954,21 @@ var gitPaths = (root) => {
|
|
|
12886
13954
|
git: !/not a git repository/i.test(stderr)
|
|
12887
13955
|
};
|
|
12888
13956
|
}
|
|
12889
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
12890
|
-
const stagedDeleted =
|
|
13957
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
13958
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12891
13959
|
if (stagedDeleted.status !== 0) {
|
|
12892
13960
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12893
13961
|
if (!/not a git repository/i.test(stderr))
|
|
12894
13962
|
return { paths, uncertain: true, git: true };
|
|
12895
13963
|
} else if (stagedDeleted.stdout) {
|
|
12896
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
13964
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
12897
13965
|
}
|
|
12898
13966
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12899
13967
|
};
|
|
12900
13968
|
var walk = (root, directory, output) => {
|
|
12901
13969
|
let names;
|
|
12902
13970
|
try {
|
|
12903
|
-
names =
|
|
13971
|
+
names = fs12.readdirSync(directory);
|
|
12904
13972
|
} catch {
|
|
12905
13973
|
return true;
|
|
12906
13974
|
}
|
|
@@ -12908,10 +13976,10 @@ var walk = (root, directory, output) => {
|
|
|
12908
13976
|
for (const name of names) {
|
|
12909
13977
|
if (name === ".git" || name === ".workit")
|
|
12910
13978
|
continue;
|
|
12911
|
-
const target =
|
|
13979
|
+
const target = path14.join(directory, name);
|
|
12912
13980
|
let stat;
|
|
12913
13981
|
try {
|
|
12914
|
-
stat =
|
|
13982
|
+
stat = fs12.lstatSync(target);
|
|
12915
13983
|
} catch {
|
|
12916
13984
|
uncertain = true;
|
|
12917
13985
|
continue;
|
|
@@ -12926,14 +13994,14 @@ var walk = (root, directory, output) => {
|
|
|
12926
13994
|
var scopeRoots = (root, scope) => {
|
|
12927
13995
|
const roots = [];
|
|
12928
13996
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12929
|
-
const target =
|
|
13997
|
+
const target = path14.resolve(root, item);
|
|
12930
13998
|
if (!inside(root, target))
|
|
12931
13999
|
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` }] });
|
|
12932
14000
|
let ancestor = target;
|
|
12933
|
-
while (!
|
|
12934
|
-
ancestor =
|
|
14001
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
14002
|
+
ancestor = path14.dirname(ancestor);
|
|
12935
14003
|
try {
|
|
12936
|
-
if (!inside(root,
|
|
14004
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
12937
14005
|
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` }] });
|
|
12938
14006
|
} catch {
|
|
12939
14007
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -12945,7 +14013,7 @@ var scopeRoots = (root, scope) => {
|
|
|
12945
14013
|
function captureCandidate(root, scope, environment = []) {
|
|
12946
14014
|
let checkout;
|
|
12947
14015
|
try {
|
|
12948
|
-
checkout =
|
|
14016
|
+
checkout = fs12.realpathSync(root);
|
|
12949
14017
|
} catch {
|
|
12950
14018
|
return failure2("invalid_input", "candidate root does not exist");
|
|
12951
14019
|
}
|
|
@@ -12963,11 +14031,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12963
14031
|
if (item === ".git" || item === ".workit")
|
|
12964
14032
|
continue;
|
|
12965
14033
|
names.add(item);
|
|
12966
|
-
if (!
|
|
14034
|
+
if (!fs12.existsSync(target))
|
|
12967
14035
|
continue;
|
|
12968
14036
|
let stat;
|
|
12969
14037
|
try {
|
|
12970
|
-
stat =
|
|
14038
|
+
stat = fs12.lstatSync(target);
|
|
12971
14039
|
} catch {
|
|
12972
14040
|
uncertain = true;
|
|
12973
14041
|
continue;
|
|
@@ -12983,17 +14051,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12983
14051
|
for (const item of [...names].toSorted()) {
|
|
12984
14052
|
if (!scopeMatches(item, normalizedScope))
|
|
12985
14053
|
continue;
|
|
12986
|
-
const target =
|
|
14054
|
+
const target = path14.join(checkout, item);
|
|
12987
14055
|
try {
|
|
12988
|
-
const stat =
|
|
14056
|
+
const stat = fs12.lstatSync(target);
|
|
12989
14057
|
if (stat.isSymbolicLink()) {
|
|
12990
|
-
const link =
|
|
12991
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
14058
|
+
const link = fs12.readlinkSync(target);
|
|
14059
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
12992
14060
|
} else if (stat.isFile()) {
|
|
12993
14061
|
files.push({
|
|
12994
14062
|
path: item,
|
|
12995
14063
|
kind: "file",
|
|
12996
|
-
digest:
|
|
14064
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
12997
14065
|
executable: (stat.mode & 73) !== 0
|
|
12998
14066
|
});
|
|
12999
14067
|
} else if (!stat.isDirectory()) {
|
|
@@ -13018,7 +14086,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
13018
14086
|
const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
13019
14087
|
if (values.some(({ value }) => value === null))
|
|
13020
14088
|
completeness = "uncertain";
|
|
13021
|
-
const headResult =
|
|
14089
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
13022
14090
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
13023
14091
|
completeness = "uncertain";
|
|
13024
14092
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -13480,7 +14548,10 @@ PR readiness, a fresh review, or post-commit assessment/closure paperwork.
|
|
|
13480
14548
|
|
|
13481
14549
|
For tracked work, inspect its current state and use the shared operations for
|
|
13482
14550
|
task, policy, evidence, finding, decision, worker, writer, and state changes.
|
|
13483
|
-
|
|
14551
|
+
Each branch has one implicit task: an operation without taskId applies to it,
|
|
14552
|
+
and the first recording (a note, check, finding or evidence) creates it, so you
|
|
14553
|
+
never manage task ids. Start a record only for an explicit tracked objective;
|
|
14554
|
+
assess or reassess only
|
|
13484
14555
|
when policy selection or changed evidence/constraints requires it. Omitted
|
|
13485
14556
|
expectedRevision and expectedWorkspaceRevision use current values; explicit
|
|
13486
14557
|
values are still concurrency-checked, so never copy revisions between calls.
|
|
@@ -13837,6 +14908,38 @@ var applyWorkerLifecycle = (input) => {
|
|
|
13837
14908
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
13838
14909
|
};
|
|
13839
14910
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
14911
|
+
var TASK_SCOPED_ACTIONS = new Set([
|
|
14912
|
+
"task.inspect",
|
|
14913
|
+
"task.revise",
|
|
14914
|
+
"task.progress",
|
|
14915
|
+
"task.pause",
|
|
14916
|
+
"task.resume",
|
|
14917
|
+
"task.close",
|
|
14918
|
+
"policy.assess",
|
|
14919
|
+
"policy.preview",
|
|
14920
|
+
"policy.explain",
|
|
14921
|
+
"evidence.record",
|
|
14922
|
+
"finding.record",
|
|
14923
|
+
"finding.resolve",
|
|
14924
|
+
"decision.record",
|
|
14925
|
+
"decision.revoke",
|
|
14926
|
+
"worker.assign",
|
|
14927
|
+
"worker.report",
|
|
14928
|
+
"worker.cancel",
|
|
14929
|
+
"writer.acquire",
|
|
14930
|
+
"writer.release",
|
|
14931
|
+
"state.export"
|
|
14932
|
+
]);
|
|
14933
|
+
var CREATING_ACTIONS = new Set([
|
|
14934
|
+
"task.revise",
|
|
14935
|
+
"task.progress",
|
|
14936
|
+
"policy.assess",
|
|
14937
|
+
"evidence.record",
|
|
14938
|
+
"finding.record",
|
|
14939
|
+
"decision.record",
|
|
14940
|
+
"worker.assign",
|
|
14941
|
+
"writer.acquire"
|
|
14942
|
+
]);
|
|
13840
14943
|
var REVISION_RETRY_ATTEMPTS = 8;
|
|
13841
14944
|
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
13842
14945
|
var filledRevisions = (request) => {
|
|
@@ -14088,7 +15191,9 @@ class WorkitCore {
|
|
|
14088
15191
|
}
|
|
14089
15192
|
contextRootError() {
|
|
14090
15193
|
try {
|
|
14091
|
-
|
|
15194
|
+
realpathSync6(this.context.root);
|
|
15195
|
+
const root = checkoutRootOf(this.context.root);
|
|
15196
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
14092
15197
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
14093
15198
|
} catch {
|
|
14094
15199
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -14110,6 +15215,30 @@ class WorkitCore {
|
|
|
14110
15215
|
retryOmittedRevisions(request, run) {
|
|
14111
15216
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
14112
15217
|
}
|
|
15218
|
+
implicitTask(family, request) {
|
|
15219
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
15220
|
+
return success(null, null, request);
|
|
15221
|
+
const value = request;
|
|
15222
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
15223
|
+
return success(null, null, request);
|
|
15224
|
+
const action = `${family}.${value.action}`;
|
|
15225
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
15226
|
+
return success(null, null, request);
|
|
15227
|
+
const root = this.contextRootError();
|
|
15228
|
+
if (!root.ok)
|
|
15229
|
+
return root;
|
|
15230
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
15231
|
+
const found = this.store.implicitTask({
|
|
15232
|
+
provenance: provenance(this.context),
|
|
15233
|
+
create,
|
|
15234
|
+
now: trustedNow(this.context)
|
|
15235
|
+
});
|
|
15236
|
+
if (!found.ok)
|
|
15237
|
+
return found;
|
|
15238
|
+
if (!found.data)
|
|
15239
|
+
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');
|
|
15240
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
15241
|
+
}
|
|
14113
15242
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
14114
15243
|
const workerId = this.context.workerId ?? null;
|
|
14115
15244
|
if (workerId === null)
|
|
@@ -14141,7 +15270,11 @@ class WorkitCore {
|
|
|
14141
15270
|
return null;
|
|
14142
15271
|
}
|
|
14143
15272
|
state(request) {
|
|
14144
|
-
|
|
15273
|
+
const resolved = this.implicitTask("state", request);
|
|
15274
|
+
if (!resolved.ok)
|
|
15275
|
+
return resolved;
|
|
15276
|
+
const value = resolved.data;
|
|
15277
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
14145
15278
|
}
|
|
14146
15279
|
stateOnce(request) {
|
|
14147
15280
|
const root = this.contextRootError();
|
|
@@ -14208,44 +15341,14 @@ class WorkitCore {
|
|
|
14208
15341
|
return imported;
|
|
14209
15342
|
return this.summary(imported.data);
|
|
14210
15343
|
}
|
|
14211
|
-
|
|
14212
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
14213
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
14214
|
-
lock,
|
|
14215
|
-
writer,
|
|
14216
|
-
reason: input.reason,
|
|
14217
|
-
authorityRefs: input.authorityRefs
|
|
14218
|
-
});
|
|
14219
|
-
if (input.target === "workspace")
|
|
14220
|
-
return this.store.recoverWorkspace({
|
|
14221
|
-
expectedBytes: input.expectedBytes,
|
|
14222
|
-
snapshotDigest: input.snapshotDigest,
|
|
14223
|
-
reason: input.reason,
|
|
14224
|
-
authorityRefs: input.authorityRefs,
|
|
14225
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14226
|
-
processEvidence
|
|
14227
|
-
});
|
|
14228
|
-
const workspace = this.store.readWorkspace();
|
|
14229
|
-
if (!workspace.ok)
|
|
14230
|
-
return workspace;
|
|
14231
|
-
if (!workspace.data)
|
|
14232
|
-
return failure2("not_found", "workspace not found");
|
|
14233
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
14234
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
14235
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
14236
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
14237
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
14238
|
-
return this.store.recoverTask(input.taskId, {
|
|
14239
|
-
expectedBytes: input.expectedBytes,
|
|
14240
|
-
snapshotDigest: input.snapshotDigest,
|
|
14241
|
-
reason: input.reason,
|
|
14242
|
-
authorityRefs: input.authorityRefs,
|
|
14243
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
14244
|
-
processEvidence
|
|
14245
|
-
});
|
|
15344
|
+
return failure2("invalid_input", "unknown state action");
|
|
14246
15345
|
}
|
|
14247
15346
|
task(request) {
|
|
14248
|
-
|
|
15347
|
+
const resolved = this.implicitTask("task", request);
|
|
15348
|
+
if (!resolved.ok)
|
|
15349
|
+
return resolved;
|
|
15350
|
+
const value = resolved.data;
|
|
15351
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
14249
15352
|
}
|
|
14250
15353
|
taskOnce(request) {
|
|
14251
15354
|
const root = this.contextRootError();
|
|
@@ -14347,7 +15450,11 @@ class WorkitCore {
|
|
|
14347
15450
|
}
|
|
14348
15451
|
}
|
|
14349
15452
|
policy(request) {
|
|
14350
|
-
|
|
15453
|
+
const resolved = this.implicitTask("policy", request);
|
|
15454
|
+
if (!resolved.ok)
|
|
15455
|
+
return resolved;
|
|
15456
|
+
const value = resolved.data;
|
|
15457
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
14351
15458
|
}
|
|
14352
15459
|
policyOnce(request) {
|
|
14353
15460
|
const root = this.contextRootError();
|
|
@@ -14396,7 +15503,11 @@ class WorkitCore {
|
|
|
14396
15503
|
return success(changed.data.revision, null, changed.data.policy);
|
|
14397
15504
|
}
|
|
14398
15505
|
evidence(request) {
|
|
14399
|
-
|
|
15506
|
+
const resolved = this.implicitTask("evidence", request);
|
|
15507
|
+
if (!resolved.ok)
|
|
15508
|
+
return resolved;
|
|
15509
|
+
const value = resolved.data;
|
|
15510
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
14400
15511
|
}
|
|
14401
15512
|
evidenceOnce(request) {
|
|
14402
15513
|
const root = this.contextRootError();
|
|
@@ -14537,13 +15648,16 @@ class WorkitCore {
|
|
|
14537
15648
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14538
15649
|
}
|
|
14539
15650
|
decision(request) {
|
|
14540
|
-
|
|
15651
|
+
const resolved = this.implicitTask("decision", request);
|
|
15652
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
14541
15653
|
}
|
|
14542
15654
|
observeDecision(request, observation) {
|
|
14543
|
-
|
|
15655
|
+
const resolved = this.implicitTask("decision", request);
|
|
15656
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
14544
15657
|
}
|
|
14545
15658
|
observeStandingDecision(request) {
|
|
14546
|
-
|
|
15659
|
+
const resolved = this.implicitTask("decision", request);
|
|
15660
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
14547
15661
|
}
|
|
14548
15662
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
14549
15663
|
let revision = expected;
|
|
@@ -14705,7 +15819,11 @@ class WorkitCore {
|
|
|
14705
15819
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
14706
15820
|
}
|
|
14707
15821
|
finding(request) {
|
|
14708
|
-
|
|
15822
|
+
const resolved = this.implicitTask("finding", request);
|
|
15823
|
+
if (!resolved.ok)
|
|
15824
|
+
return resolved;
|
|
15825
|
+
const value = resolved.data;
|
|
15826
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
14709
15827
|
}
|
|
14710
15828
|
findingOnce(request) {
|
|
14711
15829
|
const root = this.contextRootError();
|
|
@@ -14831,7 +15949,11 @@ class WorkitCore {
|
|
|
14831
15949
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
14832
15950
|
}
|
|
14833
15951
|
worker(request) {
|
|
14834
|
-
|
|
15952
|
+
const resolved = this.implicitTask("worker", request);
|
|
15953
|
+
if (!resolved.ok)
|
|
15954
|
+
return resolved;
|
|
15955
|
+
const value = resolved.data;
|
|
15956
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
14835
15957
|
}
|
|
14836
15958
|
workerOnce(request) {
|
|
14837
15959
|
const root = this.contextRootError();
|
|
@@ -15181,7 +16303,11 @@ class WorkitCore {
|
|
|
15181
16303
|
return applied;
|
|
15182
16304
|
}
|
|
15183
16305
|
writer(request) {
|
|
15184
|
-
|
|
16306
|
+
const resolved = this.implicitTask("writer", request);
|
|
16307
|
+
if (!resolved.ok)
|
|
16308
|
+
return resolved;
|
|
16309
|
+
const value = resolved.data;
|
|
16310
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
15185
16311
|
}
|
|
15186
16312
|
writerOnce(request) {
|
|
15187
16313
|
const root = this.contextRootError();
|
|
@@ -15814,7 +16940,9 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
15814
16940
|
};
|
|
15815
16941
|
});
|
|
15816
16942
|
// packages/workit-core/src/hooks/context.ts
|
|
15817
|
-
import
|
|
16943
|
+
import path15 from "node:path";
|
|
16944
|
+
var MIGRATION_PENDING_NOTE = "workit migration pending — run `workit task status`";
|
|
16945
|
+
var migrationPending = (result) => !result.ok && result.code === "needs_input" && (result.error ?? "").startsWith(MIGRATION_PENDING);
|
|
15818
16946
|
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
|
|
15819
16947
|
var newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt);
|
|
15820
16948
|
var sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null;
|
|
@@ -15825,6 +16953,15 @@ var currentTaskEntry = (entries, session, selection) => {
|
|
|
15825
16953
|
const active = entries.filter((entry) => entry.status === "active");
|
|
15826
16954
|
return active.length === 1 ? active[0] : null;
|
|
15827
16955
|
};
|
|
16956
|
+
var implicitTaskEntry = (store, entries, session) => {
|
|
16957
|
+
const open = entries.filter((entry) => entry.key !== null && entry.status !== "closed" && !entry.sessions.some((item) => item.host !== "workit_cli" && !(item.host === session.host && item.handle === session.handle)));
|
|
16958
|
+
if (open.length === 0)
|
|
16959
|
+
return null;
|
|
16960
|
+
const key = store.currentKey();
|
|
16961
|
+
if (!key.ok)
|
|
16962
|
+
return null;
|
|
16963
|
+
return open.filter((entry) => entry.key === key.data.key).toSorted(newestFirst)[0] ?? null;
|
|
16964
|
+
};
|
|
15828
16965
|
var unfinishedTaskOffer = (entries, session, excludeTaskId = null) => {
|
|
15829
16966
|
const tasks = unboundOpenTaskEntries(entries.filter((entry) => entry.id !== excludeTaskId), session);
|
|
15830
16967
|
if (tasks.length === 0)
|
|
@@ -15856,8 +16993,8 @@ var cache = new Map;
|
|
|
15856
16993
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15857
16994
|
const listed = store.listTaskIndex();
|
|
15858
16995
|
if (!listed.ok)
|
|
15859
|
-
return null;
|
|
15860
|
-
const entry = currentTaskEntry(listed.data, session, selection);
|
|
16996
|
+
return migrationPending(listed) ? MIGRATION_PENDING_NOTE : null;
|
|
16997
|
+
const entry = currentTaskEntry(listed.data, session, selection) ?? implicitTaskEntry(store, listed.data, session);
|
|
15861
16998
|
return entry ? entryCompactContext(store, entry, session, context) : null;
|
|
15862
16999
|
}
|
|
15863
17000
|
function entryCompactContext(store, entry, session, context) {
|
|
@@ -15882,7 +17019,7 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15882
17019
|
return null;
|
|
15883
17020
|
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15884
17021
|
const observed = task.data.evidence.some(isObservedCheck);
|
|
15885
|
-
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [
|
|
17022
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path15.resolve(store.root, ref.path)] : []));
|
|
15886
17023
|
const observedFiles = filesKey(files);
|
|
15887
17024
|
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15888
17025
|
if (!compact.ok)
|
|
@@ -15915,9 +17052,15 @@ var sessionContextText = (input, descriptor, options) => {
|
|
|
15915
17052
|
try {
|
|
15916
17053
|
const store = new TaskStore(input.cwd);
|
|
15917
17054
|
const listed = store.listTaskIndex();
|
|
17055
|
+
if (migrationPending(listed))
|
|
17056
|
+
return `<workit-contract>
|
|
17057
|
+
${invariantBootstrap()}
|
|
17058
|
+
${MIGRATION_PENDING_NOTE}${options.addendum ? `
|
|
17059
|
+
${options.addendum}` : ""}
|
|
17060
|
+
</workit-contract>`;
|
|
15918
17061
|
if (!listed.ok)
|
|
15919
17062
|
throw new Error(listed.error);
|
|
15920
|
-
const entry = currentTaskEntry(listed.data, session, descriptor.context.task);
|
|
17063
|
+
const entry = currentTaskEntry(listed.data, session, descriptor.context.task) ?? implicitTaskEntry(store, listed.data, session);
|
|
15921
17064
|
const text = entry ? entryCompactContext(store, entry, session, hookOperationContext(input, descriptor)) : null;
|
|
15922
17065
|
if (text)
|
|
15923
17066
|
compact = `
|
|
@@ -15937,6 +17080,8 @@ ${options.addendum}` : ""}
|
|
|
15937
17080
|
var turnContextText = (input, descriptor) => {
|
|
15938
17081
|
try {
|
|
15939
17082
|
const text = sessionCompactContext(new TaskStore(input.cwd), { host: input.host, handle: input.session.id }, hookOperationContext(input, descriptor), descriptor.context.task);
|
|
17083
|
+
if (text === MIGRATION_PENDING_NOTE)
|
|
17084
|
+
return text;
|
|
15940
17085
|
return text ? `<workit-task-context>${text}</workit-task-context>` : null;
|
|
15941
17086
|
} catch {
|
|
15942
17087
|
return null;
|
|
@@ -16039,12 +17184,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
16039
17184
|
};
|
|
16040
17185
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
16041
17186
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
16042
|
-
import
|
|
17187
|
+
import path16 from "node:path";
|
|
16043
17188
|
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16044
17189
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
16045
17190
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
16046
17191
|
var existingDirectory = (value) => {
|
|
16047
|
-
if (!nonEmpty2(value) || !
|
|
17192
|
+
if (!nonEmpty2(value) || !path16.isAbsolute(value) || !existsSync5(value))
|
|
16048
17193
|
return null;
|
|
16049
17194
|
try {
|
|
16050
17195
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -16551,7 +17696,7 @@ var codexAdapter = {
|
|
|
16551
17696
|
};
|
|
16552
17697
|
|
|
16553
17698
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16554
|
-
import
|
|
17699
|
+
import path17 from "node:path";
|
|
16555
17700
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16556
17701
|
var none = { support: "none", native: null };
|
|
16557
17702
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16734,7 +17879,7 @@ var protocolEvent2 = (input) => {
|
|
|
16734
17879
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16735
17880
|
}
|
|
16736
17881
|
};
|
|
16737
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
17882
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path17.sep) ? root : `${root}${path17.sep}`);
|
|
16738
17883
|
var hookCwd = (input) => {
|
|
16739
17884
|
const cwd = existingDirectory(input.cwd);
|
|
16740
17885
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|